JSON 输出

sow.cli/v1 信封结构、各字段含义,以及每条命令的 result 形态。

所有产出数据的命令都接受 --json。输出是 stdout 上的一行版本化信封 —— 不管是哪条命令产生的,都能直接管道给 jq

sow status --json
{"schema":"sow.cli/v1","command":"status","ok":true,"repository":"pigsty","operation":null,
 "result":{"repository":"pigsty","status":"clean","ready_to_copy":true,"desired_revision":4,
 "built_generation":4,"dirty_dists":[],"dirty_reasons":[],"pending":{"count":0,"bytes":0},
 "recent_operation":{"id":"8632724976452398569","kind":"add","state":"done",
 "created_at":"2026-08-04T04:07:17.665377Z","updated_at":"2026-08-04T04:07:18.293848Z"},
 "repository_locked":false},"errors":[]}

(此处为便于阅读做了折行,实际输出是一行。)

信封结构

字段类型含义
schemastring恒为 sow.cli/v1。解析其他字段前先检查它。
commandstring实际调用的命令,含子命令:addrepo lsconfig show
okboolerrors 为空时为 true,等价于退出码 0
repositorystring 或 null选定的仓库;工作区级与 Plain 模式命令为 null
operationstring 或 null写命令的 Operation ID,只读命令为 null
resultobject 或 null命令专属载荷,详见下文。
errorsarray零个或多个 {code, class, message} 对象。

七个字段永远存在。只有命令在产出任何东西之前就失败时(比如未知参数), result 才是 null

errors

"errors":[{"code":3,"class":"partial","message":"managed: batch partially succeeded"}]
字段含义
code进程退出码 —— 16
classruntimeusagepartiallockintegrityrejected 之一。
message与写到 stderr 的文本相同。

请对 class 做分支判断,不要匹配 message 文本。message 里含路径和包名,会变;class 不会。

Operation ID 是字符串

"operation":"8632724976452398569"

Operation ID 是 64 位值,序列化为十进制字符串,因为它经常超出 IEEE 754 双精度 能精确表示的范围。在 JavaScript 里,JSON.parse 处理裸数字会静默损坏它们。 请保持字符串形态;jq 原样处理即可。

stdout 与 stderr

结果和 JSON 信封写 stdout;警告与错误诊断写 stderr,同时也出现在 errors 数组里。 所以这样写是可行的:

sow check --json 2>/dev/null | jq -e '.ok'

各命令的 result 形态

create

sow create /srv/offline --json
{"schema":"sow.cli/v1","command":"create","ok":true,"repository":null,"operation":null,
 "result":{"dir":"/srv/offline","rpm":4,"deb":3,
 "kept":["blackbox_exporter-0.28.0-1.aarch64.rpm","blackbox_exporter-0.28.0-1.x86_64.rpm",
 "libpq5_18.2-1.pgdg12+1_amd64.deb","pev2-1.23.0-1.noarch.rpm"],
 "removed":[],"marker":false,"noop":true,"recovered":false},"errors":[]}
字段含义
dir被索引的绝对目录
rpmdeb各格式的包数量
kept进入索引的文件名,已排序
removed--pigsty 清理删除的包;否则为空
marker是否写入了 repo_complete
marker_sha256marker 文件的摘要;仅 --pigsty 时出现
noop索引本来就正确、什么都没改时为 true
recovered本次运行完成了上一次被中断的操作时为 true
signed被重签的文件名;仅 --sign-with 时出现

init

"result":{"workspace":"/srv/repo","config_created":true,
 "repositories_initialized":0,"dists_initialized":0,"existing":[]}

对已存在的工作区重跑时,计数为 0,existing 说明找到了什么:

"result":{"workspace":"/srv/repo","config_created":false,
 "repositories_initialized":0,"dists_initialized":0,"existing":["sow.yml"]}

config check 与 config show

"result":{"workspace":"/srv/repo","repositories":1,"dists":2}

config show 返回有效配置本身,形态与规范化之后的 sow.yml 一致:

"result":{"schema":"sow/v2","architectures":["x86_64","aarch64"],
 "repos":{"pigsty":{"protected":false,
 "signing":{"rpm":{"packages":{"mode":"never"}}},
 "dists":{"el9":{"format":"rpm","architectures":["x86_64","aarch64"],"limit":0,"exclude":null},
          "trixie":{"format":"deb","architectures":["x86_64","aarch64"],"limit":0,"exclude":null}}}}}

--all 时,签名条目会额外携带 key_fingerprint。私钥内容与口令永不出现。

repo ls / repo new / repo show

repo ls 返回数组;repo newrepo show 返回同一形态的单个对象。

"result":{"repositories":[{"name":"pigsty","path":"/srv/repo/pigsty","protected":false,
 "dists":2,"generation":4,"desired_revision":4,"status":"clean","packages":7,"memberships":7,
 "recent_operation":{"id":"8632724976452398569","kind":"add","state":"done",
  "created_at":"2026-08-04T04:07:17.665377Z","updated_at":"2026-08-04T04:07:18.293848Z"},
 "config":{"protected":false,"signing":{...},"dists":{...}}}]}

packages 统计包池中不同的包对象数;memberships 统计 Dist 成员关系数 —— 同一个包出现在两个 Dist 里,前者计一次,后者计两次。

repo rm 只返回结果:

"result":{"name":"demo","noop":false,"removed":true}

dist ls / dist new / dist show

"result":{"dists":[{"name":"el9","format":"rpm",
 "architectures":[{"family":"x86_64","ecosystem_arch":"x86_64"},
                  {"family":"aarch64","ecosystem_arch":"aarch64"}],
 "desired_members":4,"built_members":4,"generation":3,"dirty":false,"status":"clean",
 "effective_config_sha256":"39913af601d10d4d4033b0c29e8d66df385f8a6eb22f45219773a7fc170d4243",
 "config":{"format":"rpm","architectures":["x86_64","aarch64"],"limit":0,"exclude":null}}]}

每个架构条目同时给出两个名字:family 是配置里用的规范名, ecosystem_arch 是发布树里出现的名字 —— RPM 两者相同,DEB 分别是 amd64/arm64

desired_members 大于 built_members,或 dirty: true,都表示还欠一次 sow buildeffective_config_sha256 是所有喂给渲染器的输入的摘要;它一变,该 Dist 就变 dirty。

dist rmrepo rm 一致:{"name":"el9","noop":false,"removed":true}

add

"result":{"operation":"320653458389425222","repository":"demo",
 "desired_revision":2,"built_generation":2,"dirty":false,
 "accepted":1,"failed":0,"memberships_added":1,"memberships_removed":0,
 "items":[{"input":"/incoming/pev2-1.23.0-1.noarch.rpm","status":"accepted","format":"rpm",
 "coordinate":"pev2-0:1.23.0-1.noarch",
 "sha256":"d06d7f23b9cfc6aedaab7b60c8e890cda020efe84f1f246243414862b98b1229",
 "dists":{"el9":"accepted"}}]}

每个输入路径对应一条 items,顺序稳定。status 是该项的总体结果, dists 给出逐 Dist 的裁决:

status含义
accepted新包对象,已建立成员关系
reused完全相同的对象已存在;可能仍为其他 Dist 新增成员关系
excluded被策略挡下 —— 看 dists 区分是 excluded 还是 limited
failed未被接纳;error 给出原因

逐 Dist 的取值是 acceptedexcludedlimited。同一条命令里, 一个包可以被某个 Dist 接受、被另一个 Dist 限流:

"items":[{"input":".../libpq5_18.2-1.pgdg12+1_amd64.deb","status":"excluded","format":"deb",
 "coordinate":"libpq5=18.2-1.pgdg12+1:amd64","sha256":"310611d0...","dists":{"trixie":"limited"}}]

失败项携带 error,不带包字段:

{"input":"/incoming/broken-1.0-1.x86_64.rpm","status":"failed",
 "error":"invalid RPM package: parse RPM reader: unexpected EOF"}

memberships_addedmemberships_removed 双向计数,因为 limit 可能在接纳新版本的 同一个操作里淘汰旧版本。

rm

"result":{"operation":"3422380511083828695","repository":"pigsty",
 "desired_revision":5,"built_generation":5,"dirty":false,"check":false,
 "removed":[{"dist":"el9","sha256":"45171966...",
   "coordinate":"rpm:pgbouncer_fdw_18-0:1.4.0-1PGDG.rhel9.8.x86_64","name":"pgbouncer_fdw_18"}],
 "dists":["el9"],
 "changes":[{"op":"add","path":"dists/el9/x86_64/repodata/1a57aa2f...-filelists.xml.gz",
   "phase":"metadata","size":382,"sha256":"1a57aa2f..."},
  {"op":"update","path":"dists/el9/x86_64/repodata/repomd.xml","phase":"pointer",
   "size":1510,"sha256":"f28ffe14..."},
  {"op":"delete","path":"dists/el9/x86_64/repodata/0df96f0b...-primary.xml.gz","phase":"delete"}]}

-c/--check 运行时 checktrue,此时什么都没写,changes 是一份预测。 注意 removed 只列出成员关系的移除 —— rm 永远不删除包池字节。

build

"result":{"operation":"3701044631565986409","repository":"pigsty",
 "dists":["el9","trixie"],"desired_revision":5,"built_generation":5,
 "noop":true,"dirty":false}

noop: true 表示期望状态已经与已构建的树一致,没有产生新的代。 dists 列的是被纳入考量的 Dist,不一定是真正重建了的那些。

status

"result":{"repository":"pigsty","status":"clean","ready_to_copy":true,
 "desired_revision":4,"built_generation":4,"dirty_dists":[],"dirty_reasons":[],
 "pending":{"count":0,"bytes":0},
 "recent_operation":{"id":"8632724976452398569","kind":"add","state":"done",
  "created_at":"...","updated_at":"..."},
 "repository_locked":false}

status 取值为 cleandirtyrecoveringerror。部署脚本该读的字段是 ready_to_copy —— 但记住 status 在任何状态下都返回 0,所以要判断字段,不是退出码:

sow status --json | jq -e '.result.ready_to_copy' >/dev/null || exit 1

pending 统计 add --skip 之后私有保存、尚未发布的包体。 repository_locked 报告当前是否有其他进程持有写锁。

check

"result":{"repository":"pigsty","status":"clean","ready_to_copy":true,
 "built_generation":4,"desired_revision":4,
 "layers":[{"name":"config","ok":true,"checked":5,"issues":[]},
  {"name":"state","ok":true,"checked":1,"issues":[]},
  {"name":"public-modes","ok":true,"checked":72,"issues":[]},
  {"name":"package-bytes","ok":true,"checked":7,"issues":[]},
  {"name":"desired-membership","ok":true,"checked":7,"issues":[]},
  {"name":"index","ok":true,"checked":2,"issues":[]},
  {"name":"signature","ok":true,"checked":11,"issues":[]},
  {"name":"generation-manifest","ok":true,"checked":4,"issues":[]}]}

八层校验,顺序固定,每层给出检查了多少项以及发现的问题。dirty 仓库会八层全部 ok: true,但仍以退出码 5 失败 —— 因为层校验的是自洽性, 而 ready_to_copy 报告的是时效性:

{...,"ok":false,"result":{"status":"dirty","ready_to_copy":false,...},
 "errors":[{"code":5,"class":"integrity",
  "message":"integrity or recovery error: managed: repository is not ready to copy: repository status is dirty"}]}

changes

"result":{"repository":"pigsty","base":4,"generation":5,"dirty":false,
 "changes":[{"op":"add","path":"dists/el9/x86_64/repodata/1a57aa2f...-filelists.xml.gz",
   "phase":"metadata","size":382,"sha256":"1a57aa2f..."},
  {"op":"update","path":"dists/el9/x86_64/repodata/repomd.xml","phase":"pointer",
   "size":1510,"sha256":"f28ffe14..."},
  {"op":"delete","path":"dists/el9/x86_64/repodata/0df96f0b...-primary.xml.gz","phase":"delete"}]}
字段取值
opaddupdatedelete
phasepayloadmetadatapointerdelete
path永远相对仓库根,永远用 / 分隔
sizesha256addupdate 有;delete 没有

按这个 phase 顺序施加变更,客户端永远不会取到悬空引用:先包体, 再校验和命名的元数据,然后是协议指针(repomd.xmlRelease),最后才删除被取代的文件。

sow changes 0 把当前整棵树作为一个 add 集合给出 —— 也就是一份完整交付清单。

ls / show / where

ls 返回包对象数组;showpackage 下返回恰好一个。

"result":{"repository":"pigsty","dists":["el9"],"dirty":false,
 "packages":[{"sha256":"d06d7f23...","format":"rpm","coordinate":"pev2-0:1.23.0-1.noarch",
 "architecture":"noarch","canonical_arch":"neutral",
 "pool_path":"pool/p/pev2/pev2-1.23.0-1.noarch.rpm","filename":"pev2-1.23.0-1.noarch.rpm",
 "size":316372,"name":"pev2","source":"pev2","version":"1.23.0","epoch":"0","release":"1",
 "kind":"main","payload_sha256":"0413d629...","signature_key":"E7935D8DB9BD8B20",
 "storage":"pool","created_revision":3,"dists":["el9"],"built_dists":["el9"]}]}

值得关注的字段:

字段含义
architecture包头里的原始写法:x86_64noarchamd64all
canonical_archSOW 用于分组的族:x86_64aarch64neutral
payload_sha256仅 RPM —— 签名无关摘要,用于识别同一包的重签副本
signature_key包内嵌签名的 key ID(包带签名时)
storage已发布为 pool,--skip 加入的为 pending
dists / built_dists期望成员集 与 上一次构建实际发布的集合

distsbuilt_dists 长,是判断"还欠一次 build"的另一种方式。

where 搜索整个工作区,返回位置而不是完整对象:

"result":{"reference":"pev2","locations":[{"repository":"pigsty","dists":["el9"],
 "built_dists":["el9"],"sha256":"d06d7f23...","coordinate":"rpm:pev2-0:1.23.0-1.noarch"}]}

log

sow log 返回操作账本,由新到旧:

"result":{"repository":"pigsty","operations":[{"id":"3701044631565986409","kind":"build",
 "state":"done",
 "payload_json":"{\"version\":2,\"repository\":\"pigsty\",\"kind\":\"build\",\"config_sha256\":\"37eb6dcf...\",\"skip\":false,\"noop\":true,\"dists\":[\"el9\",\"trixie\"],\"build_dists\":[],\"manifest_sha256\":\"125d7266...\"}",
 "result_json":"{\"dists\":2,\"dropped_pending\":[]}",
 "created_at":"2026-08-04T04:08:08.691678Z","updated_at":"2026-08-04T04:08:08.763019Z"}]}

payload_jsonresult_json内含 JSON 的字符串,不是对象。 它们原样保存以保证审计记录字节稳定;需要二次解析:

sow log --json | jq -r '.result.operations[] | .payload_json | fromjson | .config_sha256'

传入 Operation ID 会返回完整细节 —— 状态迁移、包、成员关系与每一个文件动作:

"result":{"repository":"pigsty","detail":{"operation":{...},"duration_ms":598,
 "events":[{"sequence":0,"state":"planned","detail_json":"{}","occurred_at":"..."},
  {"sequence":1,"state":"staged",...},{"sequence":2,"state":"applied",...},
  {"sequence":3,"state":"built",...},{"sequence":4,"state":"done",...}],
 "packages":[{"sequence":0,"input_path":"pgbouncer_fdw_18","package_sha256":"45171966...",
  "coordinate":"rpm:pgbouncer_fdw_18-0:1.4.0-1PGDG.rhel9.8.x86_64","disposition":"removed"}],
 "memberships":[{"sequence":0,"dist":"el9","package_sha256":"45171966...","action":"remove"}],
 "files":[{"sequence":0,"action":"add","phase":"metadata","path":"dists/el9/x86_64/repodata/1a57aa2f...-filelists.xml.gz","size":382,"sha256":"1a57aa2f..."}]}}

sow log prune 返回它清理了什么:

"result":{"operation":"7140280533435786353","repository":"demo",
 "before":"2026-01-01T00:00:00+08:00","pruned":0}

注意 before 会回显裸日期在本地时区解析出的绝对时间戳。

log export 不是信封

sow log export 输出 JSON Lines —— 每行一条完整的 Operation 记录, 没有信封,也没有 --json 参数。它是给归档用的,不是给单条命令脚本用的:

sow log export - | head -1
sow log export operations.jsonl

它拒绝覆盖已存在的文件,也拒绝父目录是符号链接的目标。

一个完整例子

仓库既自洽又最新时才允许部署,然后列出该复制哪些文件:

#!/usr/bin/env bash
set -euo pipefail

if ! sow check -r pigsty --json 2>/dev/null | jq -e '.ok' >/dev/null; then
  echo "仓库不可交付" >&2
  exit 1
fi

# 当前发布树的完整清单,按交付顺序排列
sow changes 0 -r pigsty --json \
  | jq -r '.result.changes[] | [.phase, .op, .path] | @tsv'

延伸阅读

最后修改:2026-08-08: init commit (fe725aa)