sow build / status / check / changes

把期望状态收敛成新的已构建代,以及告诉你当前处境的三条命令。

一个概念一条命令。status 是便宜的读,check 是完整校验,build 是唯一会改变公开树的命令, changes 给出两代之间的物理文件差分。四条放在一页,因为你几乎总是一起用它们。

语法

sow status [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [--json]
sow build [-j|--jobs N] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [-T|--timeout DUR | -N|--no-wait] [--json]
sow check [-j|--jobs N] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [--json]
sow changes [BASE_GENERATION] [-C|--workdir DIR] [-r|--repo NAME] [--json]

状态模型

每个仓库同时维护两样东西:SQLite 中的 Desired Revision(期望修订),以及磁盘上 dists/ 树所 对应的 Built Generation(已构建代)

状态含义客户端看到什么
cleanDesired 与 Built 一致每个视图都是最新且完整的
dirtyDesired 领先——来自 --skip 或配置变化旧的 Built 视图,仍然完整
recovering存在未完成的 Operation,下一条写命令必须先恢复最后一次完成的协议指针
error自动恢复无法安全判定,需要人工介入最后一次完成的视图,永不被覆盖

dirty 从不意味着索引写了一半。客户端永远沿协议指针读到一个完整的旧视图或完整的新视图。

sow status

便宜、只读、不做哈希。报告仓库状态、Desired Revision、Built Generation、dirty 的 Dist、pending 载荷的数量与字节数、最近一条 Operation,以及锁状态。

sow status
repository=pigsty status=clean ready_to_copy=true revision=11 generation=12 dirty_dists= pending=0/0 locked=false
sow status -r pgsql
repository=pgsql status=dirty ready_to_copy=false revision=4 generation=3 dirty_dists=trixie pending=4/2326 locked=false

ready_to_copy 是同步脚本唯一需要读的字段:它告诉你此刻 pool/ + dists/ 能不能原样 rsync 出去。

只要状态可读,status 在任何状态下都返回 0——clean、dirty、recovering、error 一视同仁——好让 脚本消费结构化状态而不是解析错误。只有状态数据库无法读取或解析时才返回非零(完整性错误)。需要 硬门禁时用 sow check

sow status -r demo --json
{"schema":"sow.cli/v1","command":"status","ok":true,"repository":"demo","operation":null,"result":{"repository":"demo","status":"dirty","ready_to_copy":false,"desired_revision":5,"built_generation":4,"dirty_dists":["el9"],"dirty_reasons":["dist el9 Desired and Built membership sets differ","one or more dists differ from their built projections"],"pending":{"count":1,"bytes":19776},"recent_operation":{"id":"3329269325810066022","kind":"add","state":"done_dirty","created_at":"2026-08-04T04:10:22.481991Z","updated_at":"2026-08-04T04:10:22.553516Z"},"repository_locked":false},"errors":[]}

status 从不执行恢复,也绝不把"存在一个旧但自洽的 Generation"误报为索引损坏。

sow build

取得仓库写锁,先恢复任何未完成的 Operation,再把当前期望状态收敛成新的 Built Generation。

sow build -r pgsql -d el9
{"operation":"4262183287563704350","repository":"pgsql","dists":["el9"],"desired_revision":6,"built_generation":6,"noop":false,"dirty":false}
参数说明默认
-j, --jobs N并发 worker 数逻辑 CPU 数
-C, --workdir DIR工作区发现的起始目录当前目录
-r, --repo NAME选择一个仓库按选择规则
-d, --dist NAME选择一个 Dist;可重复全部 Dist
-T, --timeout DUR等待锁的最长时间;0 无限等待0
-N, --no-wait锁被占用时立即失败false
--json输出版本化 JSON envelopefalse

不给 -d 时,build 收敛该仓库全部受影响的 Dist;给了 -d 则只收敛选中的,其余保持 dirty。

rmshow 一样,build 即使不加 --json 也在 stdout 打印结构化 JSON。

空操作构建

输入与 renderer 配置都没变时,build 什么也不做,也不推进 Generation:

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

策略收敛是单向的

build 会重新执行当前策略,因此改完 sow.yml 里的 limitexclude 后跑 build 就是应用它们 的标准做法。收紧策略会移除成员;放宽策略不会从 pool 里残留的字节反推出历史成员——请重新执行 sow add

提交顺序

所有元数据先在同一文件系统上 stage、验证、签名,然后才切换。协议指针——RPM 的 repomd.xml、APT 的 Release/InRelease——最后替换;checksum 命名的元数据配合 APT by-hash,保证新旧客户端都不会取到 悬空引用。

一个 build Operation 可以覆盖多个 Dist。SOW 不承诺并发读者在同一瞬间看到所有 Dist 一起翻代;它承诺 的是每个协议视图始终自洽,且命令返回后全部目标属于同一个 Built Generation。

恢复

build 是唯一显式的前滚恢复入口。它会先尝试完成或回滚可判定的非终态 Operation。error 状态专指 journal、数据库与文件证据互相矛盾、工具无法安全选择的情况——此时 build 拒绝覆盖,你需要从备份恢复 后再跑 check/build。没有可能猜错的 repair --force

元数据签名

Managed 模式的元数据签名只由 sow.yml 控制,命令行没有覆盖开关。RPM 架构视图始终生成 repodata/repomd.xml;配置了 signing.rpm.metadata.key 时同时生成 ASCII-armored 的 repodata/repomd.xml.asc。DEB Dist 始终生成 Release;配置了 signing.deb.metadata.key 时同时 生成 clearsigned 的 InRelease 与分离式的 Release.gpg。改动 key 引用或 fingerprint 会让相关 Dist 变 dirty,下一次 build 重新签名并产生新的 Generation。

sow check

对选定仓库与 Dist 做完整只读校验,分八层报告。

sow check
repository=pigsty status=clean ready_to_copy=true revision=5 generation=5
config	ok=true	checked=5
state	ok=true	checked=1
public-modes	ok=true	checked=67
package-bytes	ok=true	checked=8
desired-membership	ok=true	checked=8
index	ok=true	checked=2
signature	ok=true	checked=9
generation-manifest	ok=true	checked=5
校验内容checked 计的是
config该仓库的 sow.yml 可解析且通过校验配置对象数
stateSQLite quick_check、外键,以及 journal/恢复证据恒为 1
public-modes对外服务目录树的文件与目录权限检查的路径数
package-bytes每个 pool 与 pending 载荷的 SHA-256包对象数
desired-membershipMembership 行在当前策略下能解析到真实对象成员数
index渲染出的索引与其声称的成员集一致Dist 数
signature所有声明的签名都能验证通过签名数
generation-manifest已构建代的 manifest 与磁盘文件一致Generation 号
参数说明默认
-j, --jobs N并发 worker 数逻辑 CPU 数
-C, --workdir DIR工作区发现的起始目录当前目录
-r, --repo NAME选择一个仓库按选择规则
-d, --dist NAME选择一个 Dist;可重复全部 Dist
--json输出版本化 JSON envelopefalse

check 从不修复、从不构建、从不恢复 Operation。

dirty 就是校验失败

仓库 dirty 时,check 会分别校验期望状态与旧的已构建代——然后判定该树尚不可交付,退出 5

sow check
repository=pigsty status=dirty ready_to_copy=false revision=6 generation=5
config	ok=true	checked=5
state	ok=true	checked=1
public-modes	ok=true	checked=67
package-bytes	ok=true	checked=8
desired-membership	ok=true	checked=7
index	ok=true	checked=2
signature	ok=true	checked=9
generation-manifest	ok=true	checked=5
integrity or recovery error: managed: repository is not ready to copy: repository status is dirty

八层全过。这里的退出码 5 意思是"旧树完好,但它不是你要的东西"——去跑 build。这正是发布流水线 里应该有的门禁。

sow changes

以交付计划的形式,打印两个已构建代之间的物理文件变化。

sow changes
base=4 generation=5 dirty=false
add	payload	dists/el9/x86_64/pool/c/centos-release/centos-release-6-0.el6.centos.5.x86_64.rpm	19776	ffd9e7bdaa4884831a6c055ada01dac96b84c50a8d518dac409b445af5dadc16
add	payload	pool/c/centos-release/centos-release-6-0.el6.centos.5.x86_64.rpm	19776	ffd9e7bdaa4884831a6c055ada01dac96b84c50a8d518dac409b445af5dadc16
add	metadata	dists/el9/x86_64/repodata/5bc463cb00bec4d6185ea593a6fa8f180f24d3251b498f5bbeb14875581c33cc-primary.xml.gz	1460	5bc463cb00bec4d6185ea593a6fa8f180f24d3251b498f5bbeb14875581c33cc
update	pointer	dists/el9/x86_64/repodata/repomd.xml	1514	05d3d5bf0f9236626b22a8ae9c92853277fff506f5773fbc33316ea12683cf0b
delete	delete	dists/el9/x86_64/repodata/0df96f0b046b6c098398194f908cc99d90bf3af8c5f66d262b2e6d43a658a58f-primary.xml.gz	0

各列依次是 opphase、相对仓库根的路径、大小与 SHA-256。opadd/update/deletephasepayload/metadata/pointer/delete按 phase 顺序应用——payload 最先,pointer 最后,删除放在最末——任何客户端都不会看到悬空引用。

参数说明默认
-C, --workdir DIR工作区发现的起始目录当前目录
-r, --repo NAME选择一个仓库按选择规则
--json输出版本化 JSON envelopefalse

BASE_GENERATION

不带参数时,changes 比较最近一个已构建代与它的前一代。

changes 0 给出当前已构建代的完整交付清单——pool/dists/ 下的每个文件,不含 sow.yml.sow/

sow changes 0
base=0 generation=2 dirty=false
add	payload	dists/el9/aarch64/pool/e/epel-release/epel-release-7-5.noarch.rpm	14524	d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab
add	payload	dists/el9/x86_64/pool/e/epel-release/epel-release-7-5.noarch.rpm	14524	d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab
add	payload	pool/e/epel-release/epel-release-7-5.noarch.rpm	14524	d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab
add	metadata	dists/el9/aarch64/repodata/fb3777fe0da404b2ac78b26566e1eec95a4fc90f04b322e52925fc9baebb2764-primary.xml.gz	797	fb3777fe0da404b2ac78b26566e1eec95a4fc90f04b322e52925fc9baebb2764
add	pointer	dists/el9/x86_64/repodata/repomd.xml	1511	16d334bc2b1c20c27aac9f3a353b97018a994e55ef45acc90fa50dcf5b8268a4

超出范围的 base 会被拒绝:

sow changes 99
operation rejected: managed: operation rejected: base generation 99 is outside 0..2

从未构建过任何东西的仓库输出空计划:

sow changes -r empty
base=0 generation=0 dirty=false

只作用于仓库级

changes 是仓库级的 Generation 输出,拒绝 -d。只要某个 Dist 的话,按相对仓库根的路径过滤:

sow changes -d el9
usage error: --dist is not allowed for changes

dirty 与 recovering

dirty 的期望状态不进入 changes。输出会标记 dirty=true,终点仍然是当前的已构建代——私有 pending 载荷在这里不可见,因为它们还不属于可交付树:

sow changes -r demo
base=3 generation=4 dirty=true
add	metadata	dists/el9/aarch64/repodata/0df96f0b046b6c098398194f908cc99d90bf3af8c5f66d262b2e6d43a658a58f-primary.xml.gz	140	0df96f0b046b6c098398194f908cc99d90bf3af8c5f66d262b2e6d43a658a58f

仓库处于 recoveringerror 时,changes 直接拒绝输出同步计划:未决的文件动作绝不能被当成一 个 Generation。

示例

标准的批量导入循环:

sow add /srv/build/ -R -r pgsql -d el9 --skip
sow status -r pgsql
sow build -r pgsql -j 12
sow check -r pgsql

用完整校验为发布把关:

sow check -r pgsql || { echo "不可交付"; exit 1; }
rsync -a --delete /srv/repo/pgsql/ mirror:/srv/www/pgsql/

把增量计划交给外部同步工具:

sow changes 41 -r pgsql --json > changes-41-current.json

最多等另一个写者 30 秒,然后放弃:

sow build -r pgsql -T 30s

退出码

命令触发条件
status0状态可读——cleandirtyrecoveringerror 一视同仁
status2工作区未找到或选择有歧义
status5状态数据库无法读取或解析
build0收敛成功,或无事可做
build1渲染、签名或 I/O 失败
build2用法错误或选择有歧义
build4锁不可用
build5恢复无法安全完成,或仓库处于 error
build6配置拒绝当前状态,例如仍在使用的架构被从许可表中删除
check0八层全过且可交付
check1校验过程中的 I/O 失败
check2用法错误或选择有歧义
check5某一层失败,或仓库 dirty 因而不可交付
changes0计划已打印,包括空计划
changes2给了 -d、选择有歧义,或工作区未找到
changes5仓库处于 recoveringerror
changes6BASE_GENERATION 超出有效范围

参见

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