跳转到主要内容

这是本节的多页打印视图。 .

返回本页常规视图.

命令

SOW CLI 的完整语法、参数、行为、输出与退出码。

每条顶层命令单独成页;configrepodistretainexportlog 等命令组在同一页说明其子命令。

二进制内置的 sow help 是语法权威。本手册在此基础上补充选择规则、状态变化、输出契约、 失败行为与可直接使用的示例。

命令索引

sow create 是 Plain 模式的仓库命令,直接作用于目录。sow init 用于启动 Managed 模式, 必要时会创建 sow.yml;其余有状态命令发现既有工作区。helpversion 是工具命令, 不需要进入任何模式。

命令 模式 用途
sow create [DIR] Plain 就地生成平面 RPM/DEB 仓库
sow init [DIR] Managed 初始化工作区并收敛已声明的 Repository/Dist
sow config check|show Managed 校验配置或打印有效配置
sow repo ls|new|show|migrate|rm Managed 管理 Repository;migrate 是专用维护命令
sow dist ls|new|show|rm Managed 管理 Dist
sow add PATH... Managed 将软件包加入期望成员集
sow rm PACKAGE... Managed 从期望成员集中移除软件包
sow ls Managed 列出期望成员与已构建成员
sow show PACKAGE Managed 查看一个 Package Object
sow where PACKAGE Managed 在整个工作区定位 Package Object
sow status Managed 快速读取 Repository 状态
sow build Managed 将 Desired 状态收敛为 Built Generation
sow check Managed 校验配置、状态、包体、视图、签名与清单
sow changes [BASE_GENERATION] Managed 将 Generation 差异输出为文件交付计划
sow publish TARGET Managed 将已验证 Generation 发布到配置目标
sow retain add|ls|rm Managed 管理显式保留的 Generation 根
sow gc [TARGET] Managed 回收本地不可达包体,或维护发布目标
sow export rpm-leaf Managed 生成独立的 RPM 兼容 leaf
sow log [OPERATION] Managed 查询、导出与裁剪 Operation 审计账本

全局语法

sow [OPTIONS] COMMAND [ARGS]

不带参数运行 sow 会打印命令列表并退出 0。用 sow help COMMANDsow help COMMAND SUBCOMMAND 查看内置帮助。sow versionsow --version 打印二进制身份。

SOW 没有全局 --format--yes--dry-run-q-v--config。未知参数直接按 用法错误处理。

工作区发现

Managed 命令按以下规则寻找最近的 sow.yml

  1. -C/--workdir DIR 时从 DIR 开始,否则从当前目录开始。
  2. 逐级向上查找,在第一个 sow.yml 停止。
  3. 首次查找失败且设置了 SOW_DIR 时,再从该目录查找。显式 -C 会取代当前目录候选, 但不会禁用 SOW_DIR 回退。
  4. 仍未发现工作区则退出 2

--workdir 只改变发现起点,不会切换进程工作目录;相对位置参数仍相对于真实当前目录解析。 sow create 完全不参与工作区发现。

Repository 选择

需要唯一 Repository 的命令按以下顺序选择:

  1. 显式 -r/--repo NAME
  2. 发现起点所在的 Repository;
  3. 工作区中唯一的 Repository;
  4. 否则退出 2 并列出候选项。

repo newrepo rm 用位置参数接收 NAME,不接受 -rsow where 默认搜索所有 Repository,-r 只用于收窄范围。发布目标自身绑定 Repository,因此 publish TARGETgc TARGET 不再接受额外的 Repository 选择。

Dist 选择

addrmls 要求明确的 Dist 集合,并按以下顺序选择:

  1. 一个或多个 -d/--dist NAME
  2. 发现起点所在的 Dist;
  3. 所选 Repository 中唯一的 Dist;
  4. 否则退出 2 并列出候选项。

其他命令有意采用不同规则:

  • 未指定 -d 时,buildcheckstatus 默认作用于全部 Dist;
  • show 默认搜索所选 Repository,-d 只用于收窄;
  • where 默认跨工作区搜索全部匹配 Dist,-r/-d 用于收窄;
  • changes 作用于整个 Repository,明确拒绝 -d

init 外,写命令接受 -T/--timeout DUR-N/--no-waitinit 获取 Workspace 锁,且不提供 命令行超时覆盖;其他锁均为 Repository 级,但 repo newrepo rm 同样使用 Workspace 锁。 --timeout 0 表示无限等待;正数使用 Go duration,例如 500ms30s5m--no-wait 立即失败;它与正数 timeout 互斥。获取锁失败退出 4

只读命令不获取写锁;status 仍会报告 Repository 是否正被写者持锁。

并发

只有需要解析软件包、哈希包体、渲染索引或执行校验的命令才接受 -j/--jobs Ncreateaddrmbuildcheckrepo migrate。默认值是逻辑 CPU 数,且不得小于 1

JSON 输出

支持 --json 的命令在 stdout 输出一个带版本的 Envelope,诊断信息仍写入 stderr:

{
  "schema": "sow.cli/v1",
  "command": "add",
  "ok": true,
  "repository": "demo",
  "operation": "1430722512865805553",
  "result": {},
  "errors": []
}

任何非零退出都会令 ok 为 false;部分成功的批处理仍会返回已提交项与失败项。完整结果结构见 JSON 输出

不带 --json 时,每条 Managed 命令都有稳定的人类可读 renderer,适合交互使用,但不属于机器 协议。需要结构化字段的脚本应始终使用 --json;它也是唯一受支持的机器接口。

退出码

代码 含义
0 成功或幂等空操作
1 运行时 I/O、解析、渲染、签名或传输错误
2 用法、工作区发现或配置错误
3 批处理部分成功
4 写锁不可用
5 完整性/恢复失败,或 check 判定目录不可交付
6 可预期拒绝:冲突、受保护对象、无匹配或架构不兼容

各命令的精确触发条件见退出码

1 - sow create

在普通目录中就地生成平面 RPM/DEB 仓库 —— Plain 平面模式的唯一入口。

sow create 把一个已经放着 .rpm / .deb 的目录变成平面仓库(flat repository):在包旁边写出索引 文件。它就是 Plain 平面模式的全部——没有 sow.yml、没有 SQLite、不做工作区发现。本页讲清单遍扫描 契约、--pigsty 完成门禁,以及 --sign-with 的 RPM 包签名。

语法

sow create [DIR] [-j N] [--pigsty] [-S KEY [--overwrite]] [-T DUR | -N] [--json]

DIR 默认为当前目录。

说明

create 读取 DIR 顶层的普通文件,按发现的内容渲染对应索引:有 RPM 就生成 repodata/,有 DEB 就 生成 PackagesPackages.gz,混合目录两套一起生成。架构全部来自包头——Plain 模式没有架构参数, 也没有架构许可表。

平面元数据只引用同目录的包:RPM 的 location 是裸 basename,DEB 的 Filename./<basename>。无论目录作为 file:// 源还是 HTTP 根暴露,两者都保持相对引用。

默认情况下 create 不删除、不移动、不重命名、不重签、不改写任何一个包字节。它只替换自己拥有的索引 路径,未知文件原样保留。

参数

参数 说明 默认
-j, --jobs N 唯一一次包哈希/解析扫描的并发 worker 数 逻辑 CPU 数
--pigsty 启用 Pigsty 兼容清理与完成 marker 关闭
-S, --sign-with KEY 用 16/40/64 位十六进制 GPG key ID 给未签名 RPM 补签 关闭
--overwrite 重签全部 RPM;必须与 --sign-with 同用 关闭
-T, --timeout DUR 等待锁的最长时间;0 表示无限等待 0
-N, --no-wait 锁被占用时立即失败 false
--json 输出版本化 JSON envelope false
-h, --help 显示帮助

扫描规则

  • 只考虑顶层、以 .rpm.deb 结尾的普通文件。
  • 不递归、不跟随符号链接、不读工作区配置。
  • 所有有效版本都进入索引。两个文件对应同一逻辑坐标但内容不同时,硬失败。
  • 默认模式下没有受支持包会被拒绝;--pigsty 接受空权威集合,以便中断的“删除全部包”清理能够收敛并写 marker。
sow create /srv/empty
plain: scan /srv/empty: no supported top-level regular RPM or DEB packages

包 I/O 与最终校验

默认未签名路径中,每个选中包恰好只有一次完整内容扫描。worker 打开包、计算一次 SHA-256、解析 header/control,并保留完整解析结果。RPM XML 与 DEB Packages 都从该结果渲染;渲染和生成元数据 校验都不会重新打开包体。--jobs 并行化这一次扫描,规范结果顺序保证 worker 调度不改变输出字节。

发布前,create 重新列出顶层包集合,把文件 identity、类型/mode、size 与 mtime 同扫描后快照比较。 这是便宜的 stat 校验,不是第二次哈希。集合或 stat 变化会在任何 stage 输出发布前以完整性错误 5 退出。原地改字节同时刻意保持 inode、size、mtime 不变,不属于本机协作写者契约。

显式 RPM 签名是例外:复制、签名、签名验证以及解析最终签后 RPM,会对实际修改的包增加必要读取。

确定性输出与幂等

对给定输入集,渲染出的元数据是字节稳定的:gzip 输出确定,repomd.xml<revision>0</revision> 与 timestamp 0。对未变化的目录重跑 create 不写任何字节,报告 noop=true

sow create /srv/flat
created /srv/flat: rpm=3 deb=1 signed=0 removed=0 marker=false noop=false recovered=false

sow create /srv/flat
created /srv/flat: rpm=3 deb=1 signed=0 removed=0 marker=false noop=true recovered=false

repo_complete 门禁

默认模式永不生成 repo_complete。如果 marker 已经存在,create 宁可拒绝写索引,也不留下一个内容 已过期却仍宣称"完成"的旧 marker:

sow create /srv/pigsty
plain: marker gate /srv/pigsty/repo_complete: repo_complete exists; use --pigsty or remove it explicitly before rebuilding

要么加 --pigsty 重跑(由它按文档顺序撤下并重新发布 marker),要么自己先把 marker 移走。

–pigsty

--pigsty 在一次调用中同时启用三项相互关联的兼容动作。发布顺序受 marker 门禁保护,但中断后是 重新扫描重建,不会从 journal 恢复:

  1. 删除解析架构为 i386 的 DEB;RPM 不会仅因为架构是 i386/i486/i586/i686 而被删除。
  2. 删除二进制包名恰为 patroni 且 upstream 版本恰为 3.0.4 的 RPM/DEB。RPM 比较 VERSION,忽略 epoch 与 release;DEB 先剥掉 epoch 与 Debian revision 再比。3.0.4+foo 不算命中。
  3. 全部索引渲染成功后写出 repo_complete:剩余顶层 RPM/DEB 的 SHA-256,按 basename 字节序排序, 格式为 <sha256><两个空格><basename>
sow create /srv/pigsty --pigsty
created /srv/pigsty: rpm=2 deb=0 signed=0 removed=2 marker=true noop=false recovered=false
cat /srv/pigsty/repo_complete
b4111ef2a51542eacc9bd1ebd080da02e53d400f9d172530c75a1e4ac06e7ead  centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm
d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab  epel-release-7-5.noarch.rpm

清理只触碰解析成功且命中规则的顶层普通包文件,绝不按宽泛 glob 删目录或未知文件。

发布顺序对以 marker 为门禁的调用方很关键:先撤下已有的 repo_complete,再切换索引,只在替换 元数据安装后删除命中包,最后才写入新 marker。调用方必须把 marker 缺失视为尚未完成。

Marker 语义

repo_complete 缺失当作"构建进行中"。这正是 --pigsty 设计围绕的契约。

RPM 包签名

-S/--sign-with KEY 是修改 RPM 字节的显式授权。KEY 必须是恰好 16、40 或 64 位十六进制 GPG key ID/fingerprint,不接受 0x 前缀。SOW 将其规范化为大写,通过 _gpg_name macro 传给环境中的 rpm --addsign。私钥、passphrase、GPG home、pinentry 以及额外 RPM macro 都由你的运行环境提供—— SOW 不接收、不持久化、不回显任何秘密。

  • 默认只给没有可解析嵌入 OpenPGP 签名的 RPM 补签;已有签名的包保持原字节。
  • --overwrite 必须与 --sign-with 同用,改为对全部保留 RPM 执行 rpm --resign
  • 签名发生在同文件系统的私有 stage 副本上。每个结果都会重新解析以确认嵌入签名存在、 signature-neutral digest 与 NEVRA 未变,并以最终完整字节生成 rpm-md。
  • --pigsty 清理后至少要保留一个顶层 RPM,且 PATH 中要有 rpm
sow create /srv/flat -S 0123456789ABCDEF --overwrite
plain: sign rpm epel-release-7-5.noarch.rpm: rpm executable is required for --sign-with
sow create /srv/deb-only -S 0123456789ABCDEF
plain: sign rpm: --sign-with requires at least one retained top-level RPM package
sow create /srv/flat --overwrite
usage error: --overwrite requires --sign-with
sow create /srv/flat -S ZZZZ
usage error: --sign-with must be a 16, 40, or 64 hexadecimal GPG key ID/fingerprint

锁、staging 与覆盖重建

create 对目标目录取写锁,服从 --timeout/--no-wait。全部元数据先写入私有 stage 并验证,之后 才开始发布。锁协调本机 SOW 写者;任意外部进程同时修改包不属于受支持负载。

Plain create 不创建持久操作 journal、回滚 pre-image 或 recovery trash。发布由多个单文件 rename 组成, 因此崩溃可能留下部分替换的派生文件。使用你当前想要的参数重新执行 sow create:它丢弃保留命名空间 中的陈旧 Plain 临时状态,再按现在仍存在的包重建全部索引。recovered 始终为 false; 重跑是一次全新覆盖构建,不是事务重放。

平面目录没有整个仓库的 generation 指针,RPM 与 DEB 入口也无法用一次 POSIX rename 同时切换。因此 Plain 不承诺跨文件瞬时原子性。--pigstyrepo_complete 做门禁;需要事务恢复时使用 Managed。

示例

给混合目录建索引:

sow create /srv/flat
created /srv/flat: rpm=3 deb=1 signed=0 removed=0 marker=false noop=false recovered=false
ls /srv/flat
centos-release-6-0.el6.centos.5.x86_64.rpm
centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm
epel-release-7-5.noarch.rpm
libpq5_18.3-1_amd64.deb
Packages
Packages.gz
repodata

机器可读结果:

sow create /srv/flat --json
{"schema":"sow.cli/v1","command":"create","ok":true,"repository":null,"operation":null,"result":{"dir":"/srv/flat","rpm":3,"deb":1,"kept":["centos-release-6-0.el6.centos.5.x86_64.rpm","centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm","epel-release-7-5.noarch.rpm","libpq5_18.3-1_amd64.deb"],"removed":[],"marker":false,"noop":true,"recovered":false},"errors":[]}

用八个 worker 替换 Pigsty 现有的平面构建:

sow create /www/pigsty -j 8 --pigsty

失败时的 envelope:

sow create /srv/empty --json
{"schema":"sow.cli/v1","command":"create","ok":false,"repository":null,"operation":null,"result":{"dir":"","rpm":0,"deb":0,"kept":null,"removed":null,"marker":false,"noop":false,"recovered":false},"errors":[{"code":6,"class":"rejected","message":"operation rejected: plain: scan /srv/empty: no supported top-level regular RPM or DEB packages"}]}

退出码

触发条件
0 索引写出成功,或输入未变化产生 no-op
1 目录不可读或不存在、包解析失败、渲染失败、签名工具失败
2 用法错误——--overwrite 未配 --sign-with、key 格式非法、--no-wait 与非零 --timeout 同用
4 目录写锁被占用,且给了 --no-wait--timeout 到期
5 发布前输入集合/stat 变化,或受控输出路径未通过完整性检查
6 未找到受支持的包、撞上 repo_complete 门禁、对 DEB-only 目录用 --sign-with、坐标冲突

参见

2 - sow init

创建工作区,并收敛 sow.yml 中已声明的 Repository 与 Dist。

sow init 创建根级 sow.yml 与私有状态目录 .sow/,这两样东西让一个目录成为工作区(Workspace)。 它同时也是手写配置的收敛命令:如果 sow.yml 里已经声明了 Repository 与 Dist,init 会把还不存在 的那些实体化出来,已完成的原样跳过。

语法

sow init [DIR] [--json]

DIR 默认为当前目录。init 不接受 -C/--workdir——位置参数已经明确指定了目标。

说明

首次 init 写出最小配置与私有状态目录:

sow init .
initialized /srv/repo: config_created=true repositories_initialized=0 dists_initialized=0
cat sow.yml
schema: sow/v3
architectures:
  - x86_64
  - aarch64
ls -a /srv/repo
.  ..  .sow  sow.yml

.sow/ 里放着 workspace.lock、工作区生命周期命令使用的持久文件 journal workspace-ops/repo-locks/,以及后续每个 Repository 一个的 SQLite 数据库。它的权限是 0700,绝不能对外提供 HTTP 访问。

参数

参数 说明 默认
--json 输出版本化 JSON envelope false
-h, --help 显示帮助

幂等规则

init 被设计成可以反复运行——无论是在 provisioning 脚本里还是手工执行:

  1. 创建新配置时写入 schema: sow/v3 与默认 architectures: [x86_64, aarch64]

  2. 它从不自动创建 Repository。请用 sow repo new,或先在 sow.yml 中声明。

  3. 它从不覆盖已存在的 sow.yml。重复运行只报告现状,并列出发现了什么:

    sow init .
    initialized /srv/repo: config_created=false repositories_initialized=0 dists_initialized=0
    
  4. 非空目录可以初始化,但若已有文件与 SOW 保留路径冲突则失败。

收敛已声明的配置

如果 sow.yml 里已经描述了 Repository 与 Dist,init 会为它们补齐缺失的目录树、SQLite 数据库与 空索引。已初始化的对象直接跳过,因此计数器准确反映本次运行做了什么。

schema: sow/v3
architectures: [x86_64, aarch64]

repos:
  pgsql:
    dists:
      el9:
        format: rpm
        limit: 1
        exclude:
          - kind: [debuginfo, debugsource]
      trixie:
        format: deb
  infra:
    protected: true
    dists:
      el9:
        format: rpm
sow init .
initialized /srv/repo: config_created=false repositories_initialized=2 dists_initialized=3
sow repo ls
NAME	PROTECTED	DISTS	GENERATION	STATUS	PACKAGES	MEMBERSHIPS
infra	true	1	1	clean	0	0
pgsql	false	2	2	clean	0	0

这样创建出来的 Dist 立刻具备协议完整的空发布面:RPM Dist 每个架构视图有一份空 repodata/,DEB Dist 有空的 Packages/Packages.gzby-hashRelease

再跑一次什么都不会变:

sow init . --json
{"schema":"sow.cli/v1","command":"init","ok":true,"repository":null,"operation":null,"result":{"workspace":"/srv/repo","config_created":false,"repositories_initialized":0,"dists_initialized":0,"existing":["sow.yml"]},"errors":[]}

锁与恢复

工作区生命周期命令——initrepo newrepo rm——运行在目标 Repository 数据库存在之前或被删除 之后,因此它们使用 .sow/workspace.lock.sow/workspace-ops/ 里的持久文件 journal,而不是 SQLite Operation Journal。被中断的 init 会由下一条工作区生命周期命令前滚完成或回滚。

示例

建好工作区后手工添加 Repository:

mkdir -p /srv/repo && cd /srv/repo
sow init
sow repo new infra
sow repo new pgsql
sow dist new el9 --format rpm -r pgsql
sow dist new trixie --format deb -r pgsql

初始化当前目录之外的目录:

sow init /srv/repo

从版本控制中的配置文件 provision:

install -m 0644 sow.yml /srv/repo/sow.yml
sow init /srv/repo
sow config check -C /srv/repo

退出码

触发条件
0 工作区创建成功,或已收敛(no-op)
1 写配置或状态目录时的运行时 I/O 错误
2 用法错误,或已存在的 sow.yml 解析/校验不通过
3 部分成功——部分声明的 Repository/Dist 已提交,至少一个失败
5 工作区 journal 无法恢复到终态
6 已有文件与 SOW 保留路径冲突

参见

3 - sow config

只读校验 sow.yml,并打印任意作用域的有效配置。

sow config 有两个只读子命令。config check 是对 sow.yml 的全量预检——每次手工改完配置以及在 CI 里都该跑一遍。config show 打印 SOW 实际算出来的配置,用它确认默认值、继承的架构与规范化别名 是不是按你预期解析的。

两个子命令都不创建目录、不碰数据库、不自动修正你的文件。

语法

sow config check [-C DIR] [--json]
sow config show [--all] [-C DIR] [-r NAME] [-d NAME]... [--json]

sow help config 会列出两者。

sow config check

解析并校验完整的 sow.yml:schema 版本、名称、路径冲突、架构许可表、Dist 格式、成员策略与签名 key 引用。它会回报解析到的工作区以及校验了多少对象。

sow config check
configuration valid: /srv/repo repositories=1 dists=2
sow config check --json
{"schema":"sow.cli/v1","command":"config check","ok":true,"repository":null,"operation":null,"result":{"workspace":"/srv/repo","repositories":1,"dists":2},"errors":[]}

参数

参数 说明 默认
-C, --workdir DIR 工作区发现的起始目录 当前目录
--json 输出版本化 JSON envelope false
-h, --help 显示帮助

严格拒绝未知字段

未知键是错误,不是警告。一个拼写错误不会静默地让某条策略失效:

sow config check
configuration error: load config "/srv/repo/sow.yml": parse sow.yml: yaml: unmarshal errors:
  line 8: field bogus_field not found in type config.DistConfig

schema 版本被钉死:

sow config check
configuration error: load config "/srv/repo/sow.yml": config schema must be "sow/v3", got "invalid"

唯一有效值是 schema: sow/v3。不要靠修改 Schema 字符串绕过校验错误。

check 还会验证声明的每个签名 key 引用可解析且适用于签名——过程中绝不打印密钥材料。如果你从许可表 里删掉一个架构,而仍有 Dist 配置、Membership 或已构建代在用它,config check 会拒绝该配置。

sow config show

以 YAML 打印当前选定作用域的有效配置。

sow config show
schema: sow/v3
architectures:
  - x86_64
  - aarch64
repos:
  pigsty:
    protected: false
    signing:
      rpm:
        packages:
          mode: never
    dists:
      el9:
        format: rpm
        architectures:
          - x86_64
          - aarch64
        limit: 0
        exclude: []
      trixie:
        format: deb
        architectures:
          - x86_64
          - aarch64
        limit: 0
        exclude: []

对比磁盘上的文件——里面只有你写的内容:

cat sow.yml
schema: sow/v3
architectures:
  - x86_64
  - aarch64
repos:
  pigsty:
    signing:
      rpm:
        packages:
          mode: never
    dists:
      el9:
        format: rpm
      trixie:
        format: deb

show 补上了 protected: false、每个 Dist 继承来的 architectureslimit: 0 与空的 exclude 列表。架构一律以规范化 family(x86_64aarch64)打印,绝不用生态别名——amd64arm64 只是 同两个 family 的 DEB 写法。

参数

参数 说明 默认
--all 展开整个工作区的默认值与规范化架构 关闭
-C, --workdir DIR 工作区发现的起始目录 当前目录
-r, --repo NAME 选择一个仓库 按选择规则
-d, --dist NAME 选择一个 Dist;可重复 按选择规则
--json 输出版本化 JSON envelope false
-h, --help 显示帮助

用 -r/-d 做作用域投影

-r-d 把输出收窄到选中的对象。要回答"这一个 Dist 上实际生效的策略是什么",这是最快的方式:

sow config show -r pigsty -d el9
schema: sow/v3
architectures:
  - x86_64
  - aarch64
repos:
  pigsty:
    protected: false
    signing:
      rpm:
        packages:
          mode: never
    dists:
      el9:
        format: rpm
        architectures:
          - x86_64
          - aarch64
        limit: 0
        exclude: []

--all 方向相反:无论你站在哪里,它都展开整个工作区。

秘密永不输出

密钥材料与 passphrase 不会出现在 config show、JSON、操作日志或错误文本中。只显示引用形态 (file://…env://…agent://…)与 fingerprint。

示例

在 CI 里先校验再构建:

sow config check -C /srv/repo || exit 1
sow build -r pgsql

比较两个 Dist 的有效策略:

sow config show -r pgsql -d el9 > /tmp/el9.yml
sow config show -r pgsql -d el9-beta > /tmp/beta.yml
diff -u /tmp/el9.yml /tmp/beta.yml

退出码

触发条件
0 配置合法,或输出成功打印
1 读取配置文件时的运行时 I/O 错误
2 用法错误、工作区未找到、未知字段、schema 不符,或任何校验失败
6 指定的仓库或 Dist 不存在

config check 把校验失败报为退出码 2 而不是 6:非法的 sow.yml 属于配置错误,不是被拒绝的 操作。

参见

4 - sow repo

列出、创建、查看与删除仓库 —— 锁、事务与 Generation 的边界。

一个仓库(Repository)独占一份 pool/、一份 dists/、一个 SQLite 数据库与一个私有状态目录。它是 锁、事务恢复、Generation 编号与 Changeset 的边界——跨仓库不去重,也不承诺跨仓库原子提交。 sow repo 管理的就是这条边界。

语法

sow repo ls [-C DIR] [--json]
sow repo new NAME [-C DIR] [-T DUR | -N] [--json]
sow repo show [NAME] [-C DIR] [-r NAME] [--json]
sow repo migrate [NAME] [--abort] [-j N] [-C DIR] [-r NAME] [-T DUR | -N] [--json]
sow repo rm NAME [-f|--force] [-C DIR] [-T DUR | -N] [--json]

命名

仓库名必须匹配 [a-z0-9][a-z0-9._-]*,且不能是 ....sowpooldists,也不能与工作区 保留文件冲突。

sow repo new .sow
operation rejected: managed: operation rejected: name ".sow" must match [a-z0-9][a-z0-9._-]*

路径不可指定。仓库永远位于 <workspace>/<NAME>/

sow repo ls

只读列出工作区里的全部仓库。

sow repo ls
NAME	PROTECTED	DISTS	GENERATION	STATUS	PACKAGES	MEMBERSHIPS
infra	true	1	1	clean	0	0
pgsql	false	2	2	clean	0	0
参数 说明 默认
-C, --workdir DIR 工作区发现的起始目录 当前目录
--json 输出版本化 JSON envelope false

STATUS 取值为 cleandirtyrecoveringerror。各状态对客户端意味着什么,见 事务与恢复

sow repo new

原子更新 sow.yml,然后创建 <workspace>/<NAME>/{pool,dists}、SQLite 数据库与私有状态目录。新仓库 处于 Generation 0、clean 状态。

sow repo new pigsty
created pigsty: path=/srv/repo/pigsty protected=false dists=0 generation=0 status=clean packages=0 memberships=0
参数 说明 默认
-C, --workdir DIR 工作区发现的起始目录 当前目录
-T, --timeout DUR 等待锁的最长时间;0 无限等待 0
-N, --no-wait 锁被占用时立即失败 false
--json 输出版本化 JSON envelope false

repo new 取的是工作区锁而不是仓库锁——此时仓库数据库还不存在。它不接受 -r,位置参数已经指明了 目标。

对已存在的仓库再跑一次是收敛型 no-op,只报告当前状态,因此在 provisioning 脚本里是安全的。

sow repo show

只读显示一个仓库的细节。省略 NAME 时按 CLI 全局约定 中的仓库选择规则 解析。

sow repo show pigsty
repository pigsty:
  path: /srv/repo/pigsty
  protected: false
  dists: 2
  generation: 6
  desired_revision: 6
  status: clean
  packages: 5
  memberships: 8
  config: {"protected":false,"signing":{"rpm":{"packages":{"mode":"never"}}},"dists":{"el9":{"format":"rpm","architectures":["x86_64","aarch64"],"limit":1,"exclude":[{"kind":["debuginfo","debugsource"]}]},"trixie":{"format":"deb","architectures":["x86_64","aarch64"],"limit":0,"exclude":null}}}
  dirty_reasons: []
  recent_operation: id=4142220455201181493 kind=add state=done error_class= created_at=2026-08-04T04:09:24.995538Z updated_at=2026-08-04T04:09:25.332772Z
参数 说明 默认
-C, --workdir DIR 工作区发现的起始目录 当前目录
-r, --repo NAME 省略 NAME 时用它选择仓库 按选择规则
--json 输出版本化 JSON envelope false

同时给出 NAME-r 时两者必须一致;不一致会在读取任何状态前失败:

sow repo show demo -r empty
operation rejected: repo show NAME "demo" and --repo "empty" select different repositories

sow repo migrate

这是专用维护命令,不属于全新的 0.4 Managed 工作流。由 SOW 0.4 创建的 Repository 已经使用 当前单包体布局与 Schema。

但从既有 v0.3 Workspace 升级时,迁移是强制步骤:先停止全部 Workspace 写入并完成备份,再在 执行普通读写之前逐个迁移所有已配置 Repository。

cp -a /srv/sow /srv/sow.backup-before-0.4.0
sow repo migrate pigsty -C /srv/sow
sow repo migrate pgsql -C /srv/sow

0.4 Transition 会安装 Schema v11 与 v12:按全部 Dist 重新派生 Repository 状态;在不猜测缺失 历史签名者的前提下修复 Publication 与 Generation Signer Projection;移除陈旧 abandoned-object evidence;并回填 append-only publication-target binding ledger 的 Revision 1。v0.3 未记录的历史 Signer 保持显式未验证,不能进入 Current Head,也不能成为 retained trust assertion。

Schema Transition 完成后不可逆,不要再用 SOW 0.3 打开数据库,也不要手工修改 PRAGMA user_version--abort 只适用于诊断出的 pre-commit layout-maintenance attempt,不能 撤销已经完成的 Schema Migration。除升级或 SOW 明确诊断外,不要试探性执行 migrate。

参数 含义 默认值
-j, --jobs N 并行校验/渲染 worker 逻辑 CPU 数
--abort 在提交决策前放弃维护尝试 false
-C, --workdir DIR 工作区发现起点 当前目录
-r, --repo NAME 省略 NAME 时选择仓库 选择规则
-T, --timeout DUR 最长锁等待;0 无限等待 0
-N, --no-wait 锁被占用时立即失败 false
--json 输出版本化 JSON envelope false

sow repo rm

删除一个仓库:它在 sow.yml 中的条目、数据库、pool/dists/ 与私有状态。绝不跟随符号链接, 也绝不越出固定的仓库路径。

不加 -f 时,只能删除空仓库——没有 Dist、没有 Membership、没有 Package Object:

sow repo rm infra
removed repository infra
sow repo rm pgsql
operation rejected: managed: operation rejected: repository "pgsql" is not empty; use --force
sow repo rm pgsql -f
removed repository pgsql
参数 说明 默认
-f, --force 删除非空的、未 protected 的仓库 false
-C, --workdir DIR 工作区发现的起始目录 当前目录
-T, --timeout DUR 等待锁的最长时间;0 无限等待 0
-N, --no-wait 锁被占用时立即失败 false
--json 输出版本化 JSON envelope false

-f 到底降级了什么

-f 只放宽为空这一个前置条件。它不绕过路径安全检查、不绕过符号链接拒绝、也不绕过 protected 门禁。

protected

sow.yml 中的 protected: true 直接封死仓库删除,加 -f 也不行:

sow repo rm alpha -f
operation rejected: managed: operation rejected: repository "alpha" is protected

要删除受保护的仓库,必须先改 sow.yml,通过 sow config check,再重试。没有 --yes,也没有临时覆盖开关。

protected 只作用于仓库删除。受保护仓库上的包级操作不受影响——addrmbuild,乃至 dist rm 都照常工作:

sow dist rm el9 -r alpha -f
removed dist el9 from alpha

示例

为两层结构创建仓库:

sow repo new infra
sow repo new pgsql

在 cron 任务中快速失败,而不是排队等另一个写者:

sow repo new nightly -N || echo "另一个写者持有工作区锁"

一行一个仓库地做审计:

sow repo ls --json | jq -r '.result.repositories[] | "\(.name)\t\(.status)\tgen=\(.generation)"'

退出码

触发条件
0 列出、创建、显示、迁移、放弃 pre-commit transition 或删除成功;或 repo new 收敛了已存在的仓库
1 创建或删除目录树时的运行时 I/O 错误
2 用法错误、工作区未找到,或仓库选择有歧义
4 工作区锁被占用,且给了 --no-wait--timeout 到期
5 工作区 journal 的完整性或恢复错误
6 名称非法、仓库不存在、非空但未给 -fprotected,或 NAME-r 冲突

参见

5 - sow dist

列出、创建、查看与删除 Dist —— 客户端真正消费的、单一格式的具名成员集。

Dist 是一个仓库内、单一格式(rpmdeb)的具名包集合。客户端指向的就是它。一个仓库可以同时拥有 RPM Dist 与 DEB Dist,两者共用一份 pool/,但渲染进完全独立的 dists/ 子树。

语法

sow dist ls [-C DIR] [-r NAME] [--json]
sow dist new NAME --format rpm|deb [-C DIR] [-r NAME] [-T DUR | -N] [--json]
sow dist show NAME [-C DIR] [-r NAME] [--json]
sow dist rm NAME [-f|--force] [-C DIR] [-r NAME] [-T DUR | -N] [--json]

命名

Dist 名与仓库名规则相同:[a-z0-9][a-z0-9._-]*,排除 ....sowpooldists

对 SOW 而言这个名字是不透明字符串。el9trixieel9-betacustomer-acme2026-07-31 都只是 名字——beta 频道、按客户切分的视图、快照,都是你自己施加的命名约定,不是 SOW 建模的功能。

sow dist ls

只读平铺列出选定仓库的全部 Dist。

sow dist ls -r pigsty
NAME	FORMAT	ARCHITECTURES	DESIRED	BUILT	GENERATION	DIRTY	DIRTY_REASONS
el9	rpm	x86_64,aarch64	0	0	1	false	[]
trixie	deb	x86_64,aarch64	0	0	2	false	[]

DESIREDBUILT 是成员计数。两者不一致时,DIRTY_REASONS 会说明原因:

sow dist ls -r demo
NAME	FORMAT	ARCHITECTURES	DESIRED	BUILT	GENERATION	DIRTY	DIRTY_REASONS
el9	rpm	x86_64,aarch64	2	1	4	true	["Desired and Built membership sets differ"]
参数 说明 默认
-C, --workdir DIR 工作区发现的起始目录 当前目录
-r, --repo NAME 选择一个仓库 按选择规则
--json 输出版本化 JSON envelope false

架构按规范化 family 打印。JSON 输出同时给出两种写法,用它可以确认 DEB Dist 渲染的是 binary-amd64binary-arm64

"architectures":[{"family":"x86_64","ecosystem_arch":"amd64"},{"family":"aarch64","ecosystem_arch":"arm64"}]

sow dist new

创建一个普通的、后续可继续修改的 Dist。唯一的业务参数是 --format

sow dist new el9 --format rpm -r pigsty
created el9: format=rpm architectures=x86_64,aarch64 members=0/0 generation=1 dirty=false
sow dist new trixie --format deb -r pigsty
created trixie: format=deb architectures=x86_64,aarch64 members=0/0 generation=2 dirty=false
参数 说明 默认
--format FORMAT 必填;rpmdeb
-C, --workdir DIR 工作区发现的起始目录 当前目录
-r, --repo NAME 选择一个仓库 按选择规则
-T, --timeout DUR 等待锁的最长时间;0 无限等待 0
-N, --no-wait 锁被占用时立即失败 false
--json 输出版本化 JSON envelope false

--format 必填且取值封闭:

sow dist new x -r alpha
usage error: dist new requires --format rpm|deb
sow dist new x --format zip -r alpha
usage error: --format must be rpm or deb

没有 --arch。架构从工作区许可表继承;高级用户在 sow.yml 里为某个 Dist 声明子集来收窄。策略 (limitexclude)同样只在 sow.yml 中配置,绝不在命令行上重复建模。

用相同名称与相同格式重跑 dist new 是收敛操作,只报告当前状态。同名但格式不同会被拒绝:

sow dist new el9 --format deb -r alpha
operation rejected: managed: operation rejected: dist "el9" already exists with format rpm

三方事务

dist new 要在三个地方同时提交:sow.yml 条目、仓库数据库、磁盘目录树。它走 SQLite Operation Journal(此时仓库数据库已存在,与 repo new 不同),并产生一个带空索引的新 Built Generation。

因此新建的 Dist 立刻具备协议完整的空发布面。RPM Dist 在每个架构视图下有一份空 repodata/;DEB Dist 有空的 PackagesPackages.gzby-hash/SHA256/ 条目, 以及 Release;配置签名时再生成 InReleaseRelease.gpg

sow dist show

只读显示一个 Dist 的细节。

sow dist show trixielim -r pgsql
dist trixielim:
  format: deb
  architectures: x86_64,aarch64
  desired_members: 3
  built_members: 3
  generation: 6
  status: clean
  dirty: false
  dirty_reasons: []
参数 说明 默认
-C, --workdir DIR 工作区发现的起始目录 当前目录
-r, --repo NAME 选择一个仓库 按选择规则
--json 输出版本化 JSON envelope false

JSON 形态额外给出 effective_config_sha256,即解析后 Dist 配置的摘要。当你改动 limitexclude 或签名 key 时,正是这个摘要让 Dist 变 dirty——配置身份变了,已构建代就不再等于期望状态。

sow dist show el9 -r pgsql --json
{"schema":"sow.cli/v1","command":"dist show","ok":true,"repository":"pgsql","operation":null,"result":{"name":"el9","format":"rpm","architectures":[{"family":"x86_64","ecosystem_arch":"x86_64"},{"family":"aarch64","ecosystem_arch":"aarch64"}],"desired_members":0,"built_members":0,"generation":"00000000000000000001","dirty":false,"status":"clean","effective_config_sha256":"a0b3ae2f943bc4fce951aaadda0fc8fb146ccf7944b0193a0dcc2b86ddc7ce7e","config":{"format":"rpm","architectures":["x86_64","aarch64"],"limit":1,"exclude":[{"kind":["debuginfo","debugsource"]}]}},"errors":[]}
sow dist show nope -r demo
operation rejected: managed: operation rejected: dist "nope" does not exist

sow dist rm

删除一个 Dist 的 Membership 与衍生索引。

sow dist rm el9 -r pgsql
operation rejected: managed: operation rejected: dist "el9" is not empty; use --force
sow dist rm el9 -r pgsql -f
removed dist el9 from pgsql
参数 说明 默认
-f, --force 删除成员与索引,但保留 pool 中的包 false
-C, --workdir DIR 工作区发现的起始目录 当前目录
-r, --repo NAME 选择一个仓库 按选择规则
-T, --timeout DUR 等待锁的最长时间;0 无限等待 0
-N, --no-wait 锁被占用时立即失败 false
--json 输出版本化 JSON envelope false

删除 Dist 不会删除 pool 字节

删除 Dist 绝不会从 pool/ 删包。整个 Dist 目录被移入恢复区后原子移除,包池完全不受影响:

sow dist rm el9 -r pgsql -f
removed dist el9 from pgsql

find pgsql -type f
pgsql/pool/e/epel-release/epel-release-7-5.noarch.rpm

失去引用的 Pool 对象会继续保留,直到 sow gc 证明它不再被当前、保留、恢复、发布以及 活动维护操作等任何安全根引用。

仓库的 protected: true 只封死仓库删除;受保护仓库上的常规 Dist 维护照常进行。

示例

给一个仓库同时配上 RPM 与 DEB 两副面孔:

sow dist new el9 --format rpm -r pgsql
sow dist new trixie --format deb -r pgsql

加一个带独立保留策略的 beta 频道——先建 Dist,再在 sow.yml 里写策略并收敛:

sow dist new el9-beta --format rpm -r pgsql
$EDITOR sow.yml          # el9-beta: { limit: 0 }
sow config check
sow build -r pgsql -d el9-beta

哪些 Dist 落后于期望状态:

sow dist ls -r pgsql --json | jq -r '.result.dists[] | select(.dirty) | .name'

退出码

触发条件
0 列出、创建、显示或删除成功;或 dist new 收敛了已存在的 Dist
1 创建空索引时的运行时 I/O 或渲染错误
2 用法错误——--format 缺失或非法、工作区未找到、仓库选择有歧义
4 仓库锁被占用,且给了 --no-wait--timeout 到期
5 Operation Journal 的完整性或恢复错误
6 名称非法、Dist 不存在、同名不同格式冲突、非空但未给 -f

参见

6 - sow add

把包加入期望成员集,执行成员策略,并重建受影响的索引。

sow add 是主要的写入路径。它解析你指定的包,从包头推导格式与架构,执行 Dist 的成员策略,并且—— 除非你加 --skip——在返回前重建全部受影响的索引。命令退出码为 0 时,客户端已经能看到新包了。

语法

sow add PATH... [-R|--recursive] [--skip] [-j|--jobs N] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [-T|--timeout DUR | -N|--no-wait] [--json]

参数

参数 说明 默认
-R, --recursive 递归进入 PATH 目录的子目录 关闭(只扫顶层)
--skip 只更新期望状态,不构建 关闭
-j, --jobs N 解析、哈希与渲染的并发 worker 数 逻辑 CPU 数
-C, --workdir DIR 工作区发现的起始目录 当前目录
-r, --repo NAME 选择一个仓库 按选择规则
-d, --dist NAME 选择一个 Dist;可重复 按选择规则
-T, --timeout DUR 等待锁的最长时间;0 无限等待 0
-N, --no-wait 锁被占用时立即失败 false
--json 输出版本化 JSON envelope false

输入与目标

PATH 可以是文件或目录。目录默认只扫描顶层,除非你加 -R

最终必须确定恰好一个仓库与至少一个目标 Dist——见选择规则。RPM 与 DEB 混合批次是允许的:每个包只会被考虑放进格式相同的目标 Dist;一个包如果没有任何兼容目标,则该包失败。

SOW 绝不从 manifest、目录名或宿主机 OS 推断目标。

sow add /srv/pkg/centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm /srv/pkg/epel-release-7-5.noarch.rpm -r pigsty -d el9
add repository=pigsty operation=8677129233475584643 accepted=2 failed=0 memberships=+2/-0 revision=3 generation=3 dirty=false
item input="/srv/pkg/centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm" status=accepted format=rpm coordinate="centos-release-0:7-2.1511.el7.centos.2.10.x86_64" sha256:b4111ef2a51542eacc9bd1ebd080da02e53d400f9d172530c75a1e4ac06e7ead dists=el9:accepted
item input="/srv/pkg/epel-release-7-5.noarch.rpm" status=accepted format=rpm coordinate="epel-release-0:7-5.noarch" sha256:d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab dists=el9:accepted

汇总行给出 Operation ID、逐项计数、成员增减、新的 Desired Revision、Built Generation,以及仓库是否 留在 dirty 状态。随后是每个输入一行 item,顺序稳定。

逐项状态

每行 item 带一个总体 status,以及 dists= 中的逐 Dist 判定。

状态 含义
accepted 新建 Package Object,且至少增加一条 Membership
reused 内容已存在于本仓库;可能只是新增了 Membership 引用
excluded 策略把它从所有目标 Dist 中移除——看 dists= 区分是 excluded 还是 limited
failed 该包被拒绝,error= 字段说明原因

reused 表示内容幂等:同一个文件加两次绝不会产生第二个对象或重复 Membership。默认重复执行 add 时还会收敛所选 Dist;Dist 已经最新时 Generation 不变,先前 --skip 或配置变更留下 dirty 状态时,则会补做构建并可能推进 Generation:

sow add /srv/pkg/epel-release-7-5.noarch.rpm -r pigsty -d el9
add repository=pigsty operation=656950149626836753 accepted=1 failed=0 memberships=+0/-0 revision=4 generation=4 dirty=false
item input="/srv/pkg/epel-release-7-5.noarch.rpm" status=reused format=rpm coordinate="epel-release-0:7-5.noarch" sha256:d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab dists=el9:accepted

把同一个对象加进第二个 Dist 同样是 reused——包池只保留一份,只是多了一条 Membership。 如果仍想保留 dirty 批次,应再次显式使用 --skip

架构是读出来的,不是猜的

add 从包头读取格式与原生架构,再对照工作区许可表。不在许可表中的架构会让该包失败,并明确告诉你 要改什么:

sow add /srv/pkg/centos-release-3.1-1.i386.rpm -r pigsty -d el9
item input="/srv/pkg/centos-release-3.1-1.i386.rpm" status=failed error="managed: operation rejected: unknown rpm package architecture \"i386\"; supported rpm package architectures are [x86_64, aarch64, noarch] (canonical families [x86_64, aarch64, neutral]); use a supported package or update only supported architecture families in sow.yml"

它不会创建目录,也不会修改 sow.yml

RPM 的 noarch 与 DEB 的 all 是架构中性(neutral)的。它们只产生一个 Package Object 与一条 Membership,但会渲染进目标 Dist 的每个有效架构视图。它们不会自动扩散到你没有用 -d 选中的 Dist。

策略:exclude 与 limit

合并进目标 Membership 之后,SOW 会在完整的 Dist 候选集上重新求值 exclude,再求值 limit。被策略 移除的包会被明确报告,不算解析失败。

sow add /srv/pkg/debs -r pgsql -d trixielim
add repository=pgsql operation=4142220455201181493 accepted=3 failed=0 memberships=+3/-0 revision=6 generation=6 dirty=false
item input="/srv/pkg/debs/libpq5-dbgsym_18.3-1_amd64.deb" status=excluded format=deb coordinate="libpq5-dbgsym=18.3-1:amd64" sha256:cf491b9d9b218fa49ad2b41b4740d62cd972e1b515bf33677c2c3ead75acc60a dists=trixielim:excluded
item input="/srv/pkg/debs/libpq5_18.2-1_amd64.deb" status=excluded format=deb coordinate="libpq5=18.2-1:amd64" sha256:fa84dc641b7c686be2f9b512311ad0b74eac03e2afc9eff7e9af75b82b68ff41 dists=trixielim:limited
item input="/srv/pkg/debs/libpq5_18.3-1_amd64.deb" status=reused format=deb coordinate="libpq5=18.3-1:amd64" sha256:491992c502113627d44d0d66a2b189cdaa8accff293ebaf84fe10ccbc9da574c dists=trixielim:accepted
item input="/srv/pkg/debs/libpq5_18.3-1_arm64.deb" status=reused format=deb coordinate="libpq5=18.3-1:arm64" sha256:3a2f7ef7cddfa3dc06280ef59eda1dab9724d57499931ee80758b11531c1f40c dists=trixielim:accepted
item input="/srv/pkg/debs/pg-sample_1.17-1_all.deb" status=reused format=deb coordinate="pg-sample=1.17-1:all" sha256:f23581c5164a143e5e902232589adf1d30b73ba3857a692a11da607f246aacc3 dists=trixielim:accepted

这里 trixielim 配了 exclude: [{kind: [dbgsym]}]limit: 1。dbgsym 包被规则排除; libpq5 18.2-1 在版本上限下输给了 18.3-1,报告为 limited。两者的顶层状态都是 excluded, 靠 dists= 字段区分。

limit 按 (二进制包名, 原生架构) 分组,因此 18.3-1:amd6418.3-1:arm64limit: 1 下都能 留下。同一次运行中,一个包可以被某个 Dist 接受、被另一个 Dist 跳过。

被策略移除的成员不会复活

excludelimit 移除的是真实的期望成员。之后放宽策略不会把它们变回来——pool/ 里残留的字节 不构成候选集。请重新执行 sow add

部分成功的批次

即使同批有失败项,合法且无冲突的包依然会提交。失败的输入原地不动,各自带自己的错误信息,命令退出 3

sow add /srv/pkg/centos-release-3.1-1.i386.rpm /srv/pkg/centos-release-6-0.el6.centos.5.x86_64.rpm -r pigsty -d el9
add repository=pigsty operation=4623871845694427260 accepted=1 failed=1 memberships=+1/-0 revision=5 generation=5 dirty=false
item input="/srv/pkg/centos-release-3.1-1.i386.rpm" status=failed error="managed: operation rejected: unknown rpm package architecture \"i386\"; ..."
item input="/srv/pkg/centos-release-6-0.el6.centos.5.x86_64.rpm" status=accepted format=rpm coordinate="centos-release-0:6-0.el6.centos.5.x86_64" sha256:ffd9e7bdaa4884831a6c055ada01dac96b84c50a8d518dac409b445af5dadc16 dists=el9:accepted
managed: batch partially succeeded

如果一个都没被接受,整个操作以退出码 6 被拒绝,仓库保持原样:

sow add /srv/pkg/centos-release-3.1-1.i386.rpm -r pigsty -d el9
operation rejected: managed: operation rejected: no input package was accepted

没有 rejected/隔离目录。

–skip

--skip 在期望状态提交后就停下。公开的 pool/dists/ 字节不变,Built Generation 保持原位, 仓库变为 dirty。新包字节被持久保存在私有 pending 存储中,直到下一次 build 才发布。

sow add /srv/pkg/tree -R --skip -r pgsql -d trixie
add repository=pgsql operation=8405631664133415270 accepted=6 failed=0 memberships=+4/-0 revision=4 generation=3 dirty=true
sow status -r pgsql
repository=pgsql status=dirty ready_to_copy=false revision=4 generation=3 dirty_dists=trixie pending=4/2326 locked=false

pending=4/2326 表示私有存储里有 4 个对象、共 2326 字节在等待。它们不会出现在 sow changes 中——只有成功的 build 才会把它们提升进可交付树。

批量导入时用 --skip,最后一次性收敛:

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

处理顺序

一次 add 的执行顺序如下:

  1. 取得仓库写锁,并恢复任何未完成的 Operation。
  2. 在 SQLite 中提交一条 planned Operation。
  3. 只读解析输入,计算逻辑坐标与输入字节 SHA-256(RPM 还会计算 signature-neutral payload digest)。
  4. 校验架构许可表,并查询已有坐标。
  5. 只对确实全新的坐标,在 stage 副本上执行可选的 RPM 签名并计算最终 SHA-256,再校验内容与路径唯一 性。
  6. 合并目标 Membership,然后在完整 Dist 集合上执行 excludelimit
  7. 提交期望状态;新字节写入私有 pending 内容存储。
  8. 除非给了 --skip,把仍被需要的 pending 对象发布进 pool/ 并渲染索引——一次命令中每个 Dist 最多 构建一次。

任何模式下,输入文件都不会被修改、移动或删除。

RPM 签名模式

Managed 模式的 RPM 包签名在 sow.ymlsigning.rpm.packages.mode 中配置,命令行没有覆盖开关。

模式 行为
never 完整保留输入字节
fill 无签名或签名不受信任时用配置 key 签名;已有能被 trusted_keys 验证的签名则保持字节。配置了 key 时的默认值
always 确保最终包由配置 key 有效签名;否则对 stage 副本重签

没有配置 key 时只能用 never

由于签名包含非确定字段,SOW 无法先重签再比较最终哈希。重试幂等因此建立在坐标上:输入字节完全相同 则直接复用;RPM signature-neutral digest 相同、且既有对象满足当前策略时也复用。payload digest 不同, 或既有对象已不满足策略,则是硬冲突——add 不会静默地对同一坐标原地重签。

退出码

触发条件
0 全部输入被接受或复用;索引已重建(或因 --skip 跳过)
1 运行时 I/O、解析器、渲染器或签名失败
2 用法错误、工作区未找到,或仓库/Dist 选择有歧义
3 部分批次——至少一项已提交,至少一项失败
4 仓库锁被占用,且给了 --no-wait--timeout 到期
5 完整性或恢复错误,包括在 applied 之后构建失败
6 一个都没接受——架构不受支持、没有兼容的目标 Dist,或坐标冲突

参见

7 - sow rm

从选定 Dist 中移除期望成员,并提供不写盘的预览模式。

sow rm 把包从你选定的 Dist 的期望成员集中拿掉,并默认立即重建受影响的索引。它不会从 pool/ 删除字节——成员关系与内容是两个概念,回收由独立的保守操作 sow gc 完成。

语法

sow rm PACKAGE... [-c|--check] [--skip] [-j|--jobs N] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [-T|--timeout DUR | -N|--no-wait] [--json]

参数

参数 说明 默认
-c, --check 只预览:计算并打印方案,不写任何东西 关闭
--skip 只更新期望状态,不构建 关闭
-j, --jobs N 并发 worker 数 逻辑 CPU 数
-C, --workdir DIR 工作区发现的起始目录 当前目录
-r, --repo NAME 选择一个仓库 按选择规则
-d, --dist NAME 选择一个 Dist;可重复 按选择规则
-T, --timeout DUR 等待锁的最长时间;0 无限等待 0
-N, --no-wait 锁被占用时立即失败 false
--json 输出版本化 JSON envelope false

--check--skip 互斥:

sow rm epel-release -c --skip
usage error: --check and --skip are mutually exclusive

包引用

PACKAGE 接受五种形态。完整文法与歧义规则见包引用,简版如下:

形态 例子
内容哈希 sha256:d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab
RPM 坐标 rpm:epel-release-0:7-5.noarch
DEB 坐标 deb:libpq5=18.3-1:amd64
完整文件名 epel-release-7-5.noarch.rpm
裸二进制包名 epel-release

裸名表示选定 Dist 中该名称的全部版本与原生架构——正因如此,sow rm patroni 才是一条好用的下架 命令。非裸名的模糊短引用会失败并列出候选,而不是替你猜。

sow ls 会直接打印精确的 sha256: 引用与规范化坐标,你不需要手工 拼接。

引用匹配不到任何东西属于拒绝,不是静默成功:

sow rm nosuch -r pigsty -d el9
operation rejected: managed: operation rejected: package reference not found: package reference "nosuch" matches no Desired Membership

没有 --allow-empty、没有 --all、没有 --yes、没有 --source-list

用 –check 预览

-c/--check 精确算出会移除什么、策略随后会怎么判定、以及立即构建会触碰哪些文件——并且什么都不写。

sow rm centos-release -r demo -d el9 -c
preview repository=demo operation= dists=el9 memberships=2 revision=2 generation=00000000000000000003 dirty=false changes=2
membership dist=el9 name="centos-release" coordinate="rpm:centos-release-0:6-0.el6.centos.5.x86_64" sha256:ffd9e7bdaa4884831a6c055ada01dac96b84c50a8d518dac409b445af5dadc16
membership dist=el9 name="centos-release" coordinate="rpm:centos-release-0:7-2.1511.el7.centos.2.10.x86_64" sha256:b4111ef2a51542eacc9bd1ebd080da02e53d400f9d172530c75a1e4ac06e7ead
change op=update phase=pointer path="dists/el9/aarch64/repodata/repomd.xml" size=1509 sha256:1cfe38698967d11384f1a985618d75f5e690d1284accf951262fc663fa9afc81
change op=update phase=pointer path="dists/el9/x86_64/repodata/repomd.xml" size=1509 sha256:1cfe38698967d11384f1a985618d75f5e690d1284accf951262fc663fa9afc81

注意两个 centos-release 版本都被裸名命中了。change 行是一份真实交付计划,按 payload → metadata → pointer → delete 排列。其他程序需要对应的 removed[]changes[] 数组时应使用 --json

预览与写操作使用同一套候选配置和完整性预检;预览未通过门禁时,不能据此认为实际写入会成功。

--check 有意不取写锁。把它与锁参数一起用是用法错误,免得有人以为预览会排队等待写事务:

sow rm centos-release -r pigsty -d el9 -c -T 5s
usage error: rm --check does not accept --timeout or --no-wait

默认行为:移除并重建

不带 --check--skip 时,rm 提交期望状态变更,并在返回前重建每个受影响的 Dist。pool 对象 留在磁盘上。

sow rm 'rpm:centos-release-0:6-0.el6.centos.5.x86_64' -r demo -d el9
removed repository=demo operation=2283442100870457321 dists=el9 memberships=1 revision=2 generation=00000000000000000003 dirty=false changes=8
membership dist=el9 name="centos-release" coordinate="rpm:centos-release-0:6-0.el6.centos.5.x86_64" sha256:ffd9e7bdaa4884831a6c055ada01dac96b84c50a8d518dac409b445af5dadc16

汇总之后会为每个受影响文件输出一行 change(本次运行共八行)。加上 --json 后,同一结果 以稳定的标准 Envelope 返回。

移除一个 Dist 的最后一个成员是允许的。SOW 仍会渲染合法的空索引(配了 key 就带签名)——空的 Packages 配可验签的 InRelease,或每架构的空 repodata/

–skip

--skip 提交期望状态变更并把仓库标为 dirty,不触碰公开树。旧的 Built Generation 对客户端依然完全 自洽。

sow rm 'rpm:centos-release-0:7-2.1511.el7.centos.2.10.x86_64' --skip -r demo -d el9
removed repository=demo operation=314678479940914827 dists=el9 memberships=1 revision=4 generation=00000000000000000004 dirty=true changes=0
membership dist=el9 name="centos-release" coordinate="rpm:centos-release-0:7-2.1511.el7.centos.2.10.x86_64" sha256:b4111ef2a51542eacc9bd1ebd080da02e53d400f9d172530c75a1e4ac06e7ead
sow status -r pigsty
repository=pigsty status=dirty ready_to_copy=false revision=6 generation=5 dirty_dists=el9 pending=0/0 locked=false

changes 为空是因为什么都没构建。执行 sow build 收敛。

与策略的交互

移除属于期望状态编辑,因此策略会在新的候选集上重新求值——移除操作绝不会让先前被 limit 挤掉的包 复活。如果你从一个 limit: 1 的 Dist 里删掉 libpq5 18.3-118.2-1 不会回来;需要重新显式 add。

示例

安全下架——先预览,再执行:

sow rm patroni -r pgsql -d el9 -c
sow rm patroni -r pgsql -d el9

一次从两个 Dist 中移除同一个精确对象:

sow rm sha256:d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab -r pgsql -d el9 -d el9-beta

批量移除后只重建一次:

sow rm old-tool legacy-agent -r pgsql -d el9 --skip
sow build -r pgsql -d el9
sow check -r pgsql

把预览计划喂给其他工具:

sow rm patroni -r pgsql -d el9 -c --json | jq -r '.result.changes[] | "\(.phase)\t\(.op)\t\(.path)"'

退出码

触发条件
0 成员已移除并重建,或 --check 预览已打印
1 运行时 I/O 或渲染失败
2 用法错误——--check--skip 同用、--check 与锁参数同用、选择有歧义、工作区未找到
3 部分批次——至少一个引用被移除,至少一个失败
4 仓库锁被占用,且给了 --no-wait--timeout 到期
5 完整性或恢复错误
6 引用无匹配,或非裸名的引用有歧义

参见

8 - sow ls

列出所选 Dist 的期望成员与已构建成员。

sow ls 是针对 Package Object 与 Dist Membership 的只读查询。它显示所选 Dist 应包含哪些包, 以及这些成员是否已经进入当前 Built Generation。

语法

sow ls [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [--json]
参数 含义 默认值
-C, --workdir DIR 工作区发现起点 当前目录
-r, --repo NAME 选择 Repository 选择规则
-d, --dist NAME 选择 Dist;可重复 选择规则
--json 输出 sow.cli/v1 Envelope false

该命令没有 --pool--match 或输出格式参数。

输出

sow ls -r pigsty -d el9
repository=pigsty dists=el9 dirty=false
SHA256	COORDINATE	DISTS	BUILT_DISTS	POOL_PATH
sha256:d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab	rpm:epel-release-0:7-5.noarch	el9	el9	pool/e/epel-release/epel-release-7-5.noarch.rpm
含义
SHA256 不可变内容身份,可直接传给 showrm
COORDINATE 规范的 rpm:deb: 包引用
DISTS 所选范围内的 Desired Membership
BUILT_DISTS 当前 Built Generation 中的成员关系
POOL_PATH Repository 内的不可变包体路径

Desired 与 Built 不一致时,首行显示 dirty=trueBUILT_DISTS 为空表示该包已进入期望状态, 但客户端尚不可见;运行 sow build 完成收敛。

多个所选 Dist 共享同一对象时,只输出一行,成员列表用逗号分隔。空 Dist 只有表头、没有包行, 仍然是成功结果。

选择范围

ls 要求 Dist 集合无歧义。Repository 包含多个 Dist 时,应传入一个或多个 -d,或从 <repo>/dists/<dist>/ 内运行。

sow ls -r pigsty
workspace discovery error: managed: workspace discovery or configuration error: repository "pigsty" has multiple Dists (el9, trixie); select one or more with --dist

该命令不获取写锁,也不重新哈希包文件。--jsonresult.packages 中返回同一批记录。

示例

列出尚未构建对象的精确引用:

sow ls -r pgsql -d el9 --json |
  jq -r '.result.packages[] | select(.built_dists | length == 0) | .sha256'

按路径列出包体:

sow ls -r pgsql -d el9 --json | jq -r '.result.packages[].pool_path' | sort

退出码

代码 触发条件
0 已输出成员列表,包括空列表
1 运行时 I/O 错误
2 用法错误、未发现工作区或隐式 Repository/Dist 选择有歧义
5 Repository 状态库不可读或不一致
6 显式指定的 Repository 或 Dist 未配置

参见

  • sow show —— 查看一个已列出的对象
  • sow where —— 在工作区中定位对象
  • sow rm —— 从期望成员集中移除引用
  • 包引用 —— 可接受的身份写法

9 - sow show

查看一个 Package Object 的身份、标准化事实、存储、签名与成员关系。

sow show 在所选 Repository 中解析一个包引用,并打印完整 Package Object。该命令只读, 不获取写锁。

语法

sow show PACKAGE [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [--json]
参数 含义 默认值
-C, --workdir DIR 工作区发现起点 当前目录
-r, --repo NAME 选择 Repository 选择规则
-d, --dist NAME 将候选项收窄到指定 Dist;可重复 整个 Repository
--json 将结果包装进 sow.cli/v1 Envelope false

包引用

PACKAGE 可使用 sha256:<hex> 内容身份、规范的 rpm:<NEVRA>deb:<name>=<version>:<arch> 坐标、完整包文件名或裸二进制包名。精确文法见 包引用

裸包名必须在所选范围内唯一。sow rm foo 会移除所有匹配版本,而 sow show foo 只允许返回 一个对象;有歧义时会列出候选项:

sow show libpq5 -r pgsql -d trixie
operation rejected: managed: operation rejected: package reference "libpq5" is ambiguous: deb:libpq5=18.2-1:amd64 sha256:fa84dc64..., deb:libpq5=18.3-1:amd64 sha256:491992c5..., deb:libpq5=18.3-1:arm64 sha256:3a2f7ef7...

从错误信息或 sow ls 复制精确坐标/SHA-256 后重试。

输出

不带 --json 时,show 以紧凑的人类可读格式输出身份、存储路径与 Desired/Built 位置:

sow show centos-release-6-0.el6.centos.5.x86_64.rpm -r demo -d el9
package repository=demo coordinate="centos-release-0:6-0.el6.centos.5.x86_64" sha256:ffd9e7bdaa4884831a6c055ada01dac96b84c50a8d518dac409b445af5dadc16 format=rpm architecture=x86_64 size=19776 storage=pool
pool=pool/c/centos-release/centos-release-6-0.el6.centos.5.x86_64.rpm
dists=el9 built_dists=el9

加上 --json 后,标准 Envelope 的 result 会返回完整 Package Object,包括下列规范化字段。

字段 含义
canonical_arch x86_64aarch64,或 RPM noarch / DEB all 对应的 neutral
kind 策略分类:maindebuginfodebugsourcellvmjitdbgsymdbg
source 标准化源码包名
payload_sha256 RPM 去签名摘要,用于保证重签名幂等
signature_key 包内签名的 Key ID(如有)
storage 构建前为 pending,进入仓库树后为 pool
dists / built_dists Desired 与当前 Built Membership

-d 只收窄候选解析范围,不改变包身份。

退出码

代码 触发条件
0 已输出一个 Package Object
1 运行时 I/O 错误
2 用法错误、未发现工作区或隐式 Repository 选择有歧义
5 Repository 状态库不可读或不一致
6 显式范围未配置,或引用没有匹配/匹配多个对象

参见

  • sow ls —— 从 Dist Membership 获取精确身份
  • sow where —— 跨 Repository 搜索
  • sow rm —— 移除匹配的 Desired Membership
  • JSON 输出 —— 完整结果结构

10 - sow where

在工作区的 Repository 与 Dist 中定位一个 Package Object。

sow where 用于回答工作区中哪些 Dist 仍包含某个 Package Object。它默认搜索全部 Repository, 只读且不获取写锁。

语法

sow where PACKAGE [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [--json]
参数 含义 默认值
-C, --workdir DIR 工作区发现起点 当前目录
-r, --repo NAME 将搜索限制到一个 Repository 全部 Repository
-d, --dist NAME 将搜索限制到指定 Dist;可重复 全部 Dist
--json 输出 sow.cli/v1 Envelope false

引用解析

PACKAGEsow show 使用相同文法:SHA-256、规范 RPM/DEB 坐标、 完整文件名或裸包名。

解析范围是完整的所选工作区范围。裸包名必须标识唯一 Package Object;即使同名对象位于不同 Repository,也会产生歧义。使用 -r/-d 收窄范围,或提供精确坐标/SHA-256。

输出

不带 --json 时,where 先输出摘要,再为每个位置输出一行:

sow where 'rpm:centos-release-0:6-0.el6.centos.5.x86_64'
reference="rpm:centos-release-0:6-0.el6.centos.5.x86_64" locations=1
repository=demo coordinate="rpm:centos-release-0:6-0.el6.centos.5.x86_64" sha256:ffd9e7bdaa4884831a6c055ada01dac96b84c50a8d518dac409b445af5dadc16 dists=el9 built_dists=el9

每个位置同时给出 Desired dists 与当前 built_dists,可用于确认已移除或已替换版本是否仍对 客户端可见。

加上 --json 后,同一对象位于 result 下。引用不存在属于明确拒绝,而不是空成功:

sow where nosuchpkg
operation rejected: managed: operation rejected: package reference "nosuchpkg" was not found in the selected Workspace scope

示例

列出仍在提供某个精确版本的全部位置:

sow where 'rpm:patroni-0:3.0.4-1.noarch' --json |
  jq -r '.result.locations[] | "\(.repository)/\(.dists | join(","))"'

退出码

代码 触发条件
0 已输出一个解析后的 Package Object 及其位置
1 运行时 I/O 错误
2 用法错误或未发现工作区
5 某个 Repository 状态库不可读或不一致
6 显式 Repository/Dist 未配置,或引用没有匹配/在所选范围内有歧义

参见

  • sow show —— 查看解析后的 Package Object
  • sow ls —— 列出一个 Dist 集合
  • 包引用 —— 精确文法与歧义规则

11 - sow status

快速读取 Repository 的收敛、可交付、待处理包体、最近 Operation 与锁状态。

sow status 是低成本的 Repository 状态查询。它读取状态,但不哈希文件、不验签、不恢复 Operation、不构建元数据,也不获取写锁。

语法

sow status [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [--json]
参数 含义 默认值
-C, --workdir DIR 工作区发现起点 当前目录
-r, --repo NAME 选择 Repository 选择规则
-d, --dist NAME 只查看指定 Dist;可重复 全部 Dist
--json 输出 sow.cli/v1 Envelope false

Repository 状态

每个 Repository 同时跟踪 SQLite 中的 Desired Revision,以及公开 dists/ 树对应的 Built Generation。

状态 含义 公开视图
clean Desired 与 Built 一致 当前且完整的 Generation
dirty Desired 已领先,常见于 --skip 或配置变化之后 上一个完整 Generation
recovering 存在非终态 Operation,下一条写命令必须先恢复 上一个已完成的协议指针
error 自动恢复无法安全裁决 保留上一个完整视图,不尝试覆盖

dirty 不代表仓库只写了一半。协议指针最后切换,因此读者看到的始终是完整旧视图或完整新视图。

输出

sow status -r pgsql
repository=pgsql status=dirty ready_to_copy=false revision=4 generation=3 dirty_dists=trixie pending=4/2326 locked=false

人类可读输出包含 Repository 状态、ready_to_copy、Desired Revision、Built Generation、 受影响 Dist、待处理对象数量/字节数与写锁状态。

JSON 结果还包含 dirty_reasons 与最近一次 Operation:

{
  "repository": "demo",
  "status": "dirty",
  "ready_to_copy": false,
  "desired_revision": 5,
  "built_generation": "00000000000000000004",
  "dirty_dists": ["el9"],
  "dirty_reasons": ["dist el9 Desired and Built membership sets differ"],
  "pending": {"count": 1, "bytes": 19776},
  "repository_locked": false
}

ready_to_copy=false 是明确警告;true 只是廉价状态判断,并非字节级完整性证明。交付前应运行 sow check

只读契约

status 不迁移也不修复状态。Repository 数据库无法安全读取时,命令退出 5;请先执行 诊断信息明确指出的维护命令,再重新查询。尤其是 v0.3 Repository,使用 0.4 读取表面前必须先 备份,并逐个执行 sow repo migrate

退出行为

只要状态可读,statuscleandirtyrecoveringerror 四种状态下都返回 0。 脚本应读取结构化状态,而不是把后三者当作命令执行失败。

代码 触发条件
0 Repository 状态可读
1 运行时 I/O 错误
2 用法错误、未发现工作区或隐式 Repository 选择有歧义
5 状态库不可读或不一致
6 显式指定的 Repository 或 Dist 未配置

参见

12 - sow build

将 Desired Membership 与渲染配置收敛为完整 Built Generation。

sow build 是显式的 Desired-to-Built 收敛命令。它获取 Repository 写锁,恢复任何可裁决的 未完成 Operation,渲染并验证完整 Generation,最后切换协议指针。

语法

sow build [-j|--jobs N] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [-T|--timeout DUR | -N|--no-wait] [--json]
参数 含义 默认值
-j, --jobs N 并行 Worker 数,不得小于 1 逻辑 CPU 数
-C, --workdir DIR 工作区发现起点 当前目录
-r, --repo NAME 选择 Repository 选择规则
-d, --dist NAME 构建指定 Dist;可重复 全部受影响 Dist
-T, --timeout DUR 最长等锁时间;0 表示无限等待 0
-N, --no-wait 锁被占用时立即失败 false
--json 将结果包装进 sow.cli/v1 Envelope false

不带 -d 时,SOW 收敛所选 Repository 中全部受影响 Dist;带 -d 时只收敛指定 Dist, 未选择的变化继续保持 dirty。

结果

不带 --json 时,build 输出一行人类可读摘要:

sow build -r demo -d el9
built repository=demo operation=2769214987359113555 dists=el9 revision=4 generation=00000000000000000005 dirty=false

需要标准 Envelope 中的命令专属对象时使用 --json

空操作构建

成员关系、相关策略、渲染设置与签名配置均未变化时,build 是幂等空操作,不增加 Generation:

sow build -r demo -d el9
build repository=demo dists=el9 already current (noop) revision=4 generation=00000000000000000005 dirty=false

策略收敛

build 会重新执行当前 excludelimit 策略。收紧策略可能移除 Desired Membership; 放宽策略不会从残留包池字节恢复历史成员,需要重新运行 sow add

提交与恢复

SOW 在同一文件系统暂存新元数据,验证完成后再切换可变协议指针。RPM 校验和命名元数据与 APT by-hash 确保新旧读者看到的视图始终自洽。

Pending 包体提升采用有界单写者 group commit。每批最多 512 个对象或 1 GiB:先创建 Pool 链接并持久化所有不同的目标父目录,再删除 pending 名称并持久化共享 pending 目录。中断只会 留下 pending-only、指向同一 inode 的双链接或 Pool-only 状态,都能按 journal 恢复;不会 持久地同时丢失两个名称。

一个 Operation 可以覆盖多个 Dist。每个 Dist 始终暴露完整视图;build 返回时,本次包含的所有 Dist 属于同一个 Built Generation。

开始新工作前,build 会尝试前向恢复或安全回滚非终态 Operation。如果日志、数据库与文件系统 证据互相矛盾,Repository 进入 errorbuild 拒绝猜测;不存在强制修复参数。

进度事件

耗时较长的构建会向 Operation Log 追加结构化 build_progress 记录。每条事件包含 phasecompletedtotaljobs。当前阶段为:

  • rendering
  • promoting_payload
  • publishing_dists
  • normalizing_public_tree
  • finalizing

这些事件不会推进 Operation 状态,也不会在每次更新后 checkpoint SQLite;它们只用于审计 与可观测性,不参与恢复决策。使用 sow log OPERATION 查看明细。

元数据签名

Managed 元数据签名只从 sow.yml 读取,没有命令行 Key 覆盖。配置的 Key 引用或指纹改变时, 相关 Dist 变为 dirty,下一次 build 重新签名。

  • RPM:总是生成 repodata/repomd.xml;配置签名后额外生成 repomd.xml.asc
  • DEB:总是生成 Release;配置签名后额外生成 InReleaseRelease.gpg

退出码

代码 触发条件
0 收敛成功或无需操作
1 渲染、签名或文件系统错误
2 用法错误、未发现工作区或隐式 Repository 选择有歧义
4 Repository 写锁不可用
5 无法安全完成恢复,或 Repository 处于 error
6 显式范围未配置,或当前配置拒绝既有状态

参见

13 - sow check

执行完整的只读完整性与可交付校验流水线。

sow check 是 Managed Repository 的深度只读门禁。它哈希包体、校验状态、重建期望视图并验证 已声明签名;不会修复、构建、恢复 Operation,也不会获取写锁。

语法

sow check [-j|--jobs N] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]... [--json]
参数 含义 默认值
-j, --jobs N 并行校验 Worker 数,不得小于 1 逻辑 CPU 数
-C, --workdir DIR 工作区发现起点 当前目录
-r, --repo NAME 选择 Repository 选择规则
-d, --dist NAME 校验指定 Dist;可重复 全部 Dist
--json 输出 sow.cli/v1 Envelope false

校验层

稳态下,checker 按顺序报告九层校验:

校验内容 checked 计数
config sow.yml 能否针对该 Repository 解析并通过校验 配置对象
state SQLite quick_check、外键、日志与恢复证据 一个状态库
public-modes 服务目录中所有文件与目录权限 已检查路径
retained 显式保留记录与冻结 Generation Manifest 保留记录
package-bytes 包池与私有 pending 包体的 SHA-256 Package Object
desired-membership Membership 能否在当前策略下解析到真实对象 成员关系
index 渲染索引是否与其声明的成员关系一致 Dist
signature 所有已声明元数据/软件包签名是否有效 签名
generation-manifest Built Generation Manifest 是否与磁盘文件一致 一个 Manifest

Repository 处于未完成布局迁移时,check 改为依次报告 configstatepublic-modes 与条件 层 layout-transition,随后停止,并在诊断指定的 repo migrate 完成或在 commit 前中止前返回不可交付。

物理证据与 I/O 契约

package-bytes 绝不会把缓存指纹当作真实性证明。每次运行都会对每个唯一物理包体执行恰好一次 哈希;证据绑定设备号、inode、size、mtime、ctime 与真正读取的文件描述符。同 inode 的硬链接 共享证明;Retained Generation、最终 Manifest 遍历与 changes 复用它,不再扫描包体。 checked 列统计逻辑对象,不代表全文流数量。

DEB 或无签名 RPM 只需一遍完整包体流。带签名 RPM 最多再用一遍从主 Header 到 EOF 的流, 对全部签名 Packet 与候选 Trust Ring 验证;成本不会随 Dist、Retained Generation 或 Trust Ring 数量增加。伪造或并发替换文件会让描述符证据失效并失败关闭。

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
retained	ok=true	checked=0
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=1

dirty 不可交付

dirty Repository 的九层校验可以分别成立:旧 Built Generation 完整,新 Desired 状态也有效; 但二者不一致,因此整体仍未通过交付门禁:

sow check
repository=pigsty status=dirty ready_to_copy=false revision=6 generation=5
...
integrity or recovery error: managed: repository is not ready to copy: repository status is dirty

此时退出 5。运行 sow build 后重新校验,不应让发布流水线放行该状态。

退出码

代码 触发条件
0 全部校验层通过,Repository 可复制交付
1 校验期间发生 I/O 错误
2 用法错误、未发现工作区或隐式 Repository 选择有歧义
5 某个校验层失败,或 Repository 不可交付
6 显式指定的 Repository 或 Dist 未配置

参见

14 - sow changes

将 Built Generation 差异输出为确定性的 Repository 相对文件交付计划。

sow changes 比较 Built Generation,输出物理的 Repository 相对文件差异。它不显示尚未构建的 Desired 变化,也不是远端事务协议。

语法

sow changes [BASE_GENERATION] [-C|--workdir DIR] [-r|--repo NAME] [--json]
参数 含义 默认值
-C, --workdir DIR 工作区发现起点 当前目录
-r, --repo NAME 选择 Repository 选择规则
--json 输出 sow.cli/v1 Envelope false

该命令作用于整个 Repository,明确拒绝 -d/--dist

输出

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

各列依次为操作、阶段、Repository 相对路径、大小、SHA-256。

字段 取值
操作 addupdatedelete
阶段 payloadmetadatapointerdelete

阶段描述 SOW 如何构建本地 Generation。不要把单行直接重放到线上目录;应使用 sow publish,或先暂存完整副本再原子切换。

Base Generation

不带参数时,SOW 比较当前 Built Generation 与它的前一代。

BASE_GENERATION0..当前代 范围内的十进制整数。Base 0 输出当前 Generation 的完整 交付清单,但不包含私有 sow.yml.sow/。Base 等于当前代时输出空计划;从未构建的 Repository 同样输出空的 0 -> 0 计划。

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

dirty 与恢复状态

Desired 为 dirty 时,首行显示 dirty=true,但计划仍以当前 Built Generation 结束。私有 pending 包体尚不可交付,不会出现在结果中。

Repository 为 recoveringerror 时,changes 拒绝输出计划,避免把待定文件动作误认为已完成 Generation。

示例

输出当前完整清单:

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

生成 Repository 级计划后按路径筛选一个 Dist:

sow changes -r pgsql --json |
  jq '.result.changes[] | select(.path | startswith("dists/el9/"))'

退出码

代码 触发条件
0 已输出计划,包括空计划
1 运行时 I/O 错误
2 用法错误、传入 -d、未发现工作区或隐式 Repository 选择有歧义
5 Repository 处于 recovering/error,或状态证据不一致
6 显式 Repository 未配置,或 Base Generation 超出有效范围

参见

15 - sow publish

将当前已验证 Generation 发布到配置的 filesystem 或 R2 目标。

sow publish 将某个 Repository 的当前 Built Generation 交付到 sow.ymltargets: 中指定的 目标。目标已经绑定 Repository 与 Provider,因此命令不接受 --repo--dist

语法

sow publish TARGET [--abort | --rebind] [-C|--workdir DIR] [-T|--timeout DUR | -N|--no-wait] [--json]
参数 含义 默认值
--abort 放弃已对账、但尚未写入持久 commit intent 的尝试 false
--rebind 确认并记录允许修改的目标名称、公共端点或缓存 TTL false
-C, --workdir DIR 工作区发现起点 当前目录
-T, --timeout DUR 最长 Repository 等锁时间;0 表示无限等待 0
-N, --no-wait 锁被占用时立即失败 false
--json 输出 sow.cli/v1 Envelope false

TARGET 必须是已配置的 filesystemr2 发布目标。--abort--rebind 互斥。

发布协议

交付前,SOW 要求存在已完成的 Built Generation,并验证公开树与冻结 Generation Manifest 精确 一致;然后按以下顺序规划并写入对象:

  1. 不可变包体;
  2. 校验和寻址元数据;
  3. 可变协议指针;
  4. 验证并持久化 Checkpoint。

精确对象集合、Receipt、阶段与 commit intent 都会落盘,确保中断后可以对账恢复。目标已经位于 当前 Generation 时,重复发布是幂等空操作。

sow publish local
published demo generation=00000000000000000005 to local (filesystem): phase=grace objects=14
sow publish local
publication demo generation=00000000000000000005 to local is already current (noop)

重绑定可变目标配置

首次成功 publish 会持久绑定 Repository、存储命名空间与 Target Identity。之后配置发生漂移时, SOW 会拒绝静默接受。只有诊断信息明确提示 --rebind,并且你已经复核变更后,才执行:

sow publish prod --rebind

如果改的是 targets: Map Key,请使用新目标名。Rebind 保留 active attempt 与 checkpoint identity, 并追加不可变、由操作者确认的 binding revision。

可以修改 不可变;必须配置新目标
目标名称 Repository Identity
public_endpoint Provider、存储 endpoint 或 region
max_cache_ttl bucket 或 prefix

Rebind 与发布使用同一组 Workspace/Repository 锁,并在数据库事务内重新核对不可变字段;它可以 继续前滚 active commit-intent attempt。Target Maintenance 未完成时禁止改变 TTL;filesystem 正在进行 conditional-delete 维护时禁止改变 public_endpoint。首次绑定必须运行普通 publish, 不能使用 --rebind

Abort 与恢复

--abort 只允许在持久 commit intent 之前使用。SOW 会对账已经创建的对象,保留后续安全判断所需 证据,并在不继续复制或删除远端对象的前提下放弃本次尝试。

写入 commit intent 后只能前向恢复:重新运行 sow publish TARGET,不能使用 --abort

公共可见性校验

Provider 存储写入成功还不够:写入 Checkpoint 之前,发布流程还要校验 canonical public_endpoint。HTTP(S) 目标以普通 GET 为最终权威;no-cache Probe 只能促进 Revalidation, 必须由之后的普通 GET 才能通过。陈旧内容与缺失对象按 max_cache_ttl 重试;408、425、429 与 5xx 使用较短的有界重试窗口。等待 Header 与 Body 空闲进度分别计时;响应超长时失败关闭。

Filesystem 可使用 file:// 或 HTTP(S) 公共端点。执行条件删除时,file:// 需要精确文件身份 缺失,HTTP(S) 则需要 canonical 404/410。R2 必须使用 HTTP(S) 公共端点;R2 Target GC 仍然 只报告候选,不执行远端删除。

安全边界

  • SOW 只发布到配置目标,不接受任意目标路径。
  • 尚未构建的 Desired 变化不会进入发布。dirty Repository 因而可以发布上一个完整 Built Generation;如果目标必须反映当前 Desired 状态,应先运行 build
  • 布局迁移与相互矛盾的恢复证据会阻止发布;可裁决的未完成 Dist 操作会在选择源 Generation 之前恢复。
  • 对象顺序保证包管理器指针不会引用尚不存在的内容。
  • 发布命令不负责外部 Web Server、Bucket Policy、DNS 路由或缓存配置。

退出行为

代码 触发条件
0 发布完成,或目标已经是当前 Generation
1 文件系统、Provider、网络、验证或绑定冲突(包括必须 rebind)
2 用法、工作区发现或 sow.yml 无效
4 Repository 写锁不可用
5 本地/发布恢复证据不一致,或源不可交付
6 目标不存在/不安全,或其他安全前置条件拒绝 Publish/Abort/Rebind

参见

16 - sow retain

添加、列出与移除供本地垃圾回收使用的显式 Generation 保留根。

sow retain 管理显式的本地 Generation 根。retain add 只能冻结当前 Built Generation;后续 构建使它成为历史版本后,该代所需的软件包体仍受保护。

语法

sow retain add GENERATION [-C|--workdir DIR] [-r|--repo NAME] [-T|--timeout DUR | -N|--no-wait] [--json]
sow retain ls             [-C|--workdir DIR] [-r|--repo NAME] [--json]
sow retain rm GENERATION  [-C|--workdir DIR] [-r|--repo NAME] [-T|--timeout DUR | -N|--no-wait] [--json]

GENERATION 必须是大于零的十进制整数。

retain add

要求 GENERATION 等于当前 Built Generation,校验后将其 Manifest 冻结到工作区私有状态中, 并添加显式 GC 根。不能在事后用 retain add 重建一个更老的 Generation。

sow retain add 12 -r pgsql
retained generation 00000000000000000012: /srv/sow/.sow/pgsql/retained/00000000000000000012

保留记录只保护包体,不切换当前视图,也不执行发布。重复添加同一 Generation 时,只有已验证记录 与当前证据一致才可视为幂等。

retain ls

列出显式保留记录。它是只读命令,因此不接受锁参数或 --dist

sow retain ls -r pgsql
GENERATION	RECORD_IDENTITY	PATH
00000000000000000012	678beeae...	/srv/sow/.sow/pgsql/retained/00000000000000000012

空列表也是成功结果。

retain rm

只移除显式保留根:

sow retain rm 12 -r pgsql
removed retained generation 00000000000000000012

该命令不删除软件包体。移除一个未被保留的 Generation 是幂等空操作。只有在其他安全根也无法 到达这些包体时,后续本地 sow gc 才可能回收。

参数

参数 适用命令 含义
-C, --workdir DIR 全部 工作区发现起点
-r, --repo NAME 全部 选择 Repository
-T, --timeout DUR addrm 最长写锁等待时间
-N, --no-wait addrm 锁被占用时立即失败
--json 全部 输出 sow.cli/v1 Envelope

退出行为

代码 触发条件
0 操作完成,包括空列表
1 文件系统或运行时 I/O 错误
2 Generation 语法无效、发现错误或隐式 Repository 选择有歧义
4 add/rm 无法获取写锁
5 Generation Manifest 或 Repository 状态不一致
6 显式 Repository 未配置、retain add 不是当前 Built Generation,或其他安全规则拒绝请求

参见

17 - sow gc

回收本地不可达包体,或对一个发布目标执行保守维护。

sow gc 有两种严格分离的模式:不带位置目标时,回收本地不可达包体;带 TARGET 时,维护一个 已配置发布目标。

语法

sow gc          [-C|--workdir DIR] [-r|--repo NAME] [-T|--timeout DUR | -N|--no-wait] [--json]
sow gc TARGET   [-C|--workdir DIR]                  [-T|--timeout DUR | -N|--no-wait] [--json]
参数 含义 默认值
-C, --workdir DIR 工作区发现起点 当前目录
-r, --repo NAME 仅用于选择本地 GC 的 Repository 选择规则
-T, --timeout DUR 最长 Repository 等锁时间;0 表示无限等待 0
-N, --no-wait 锁被占用时立即失败 false
--json 输出 sow.cli/v1 Envelope false

目标自身已绑定 Repository,因此 gc TARGET -r NAME 属于用法错误。两种模式都不接受 --dist

本地 GC

本地 GC 只删除所有安全根都无法到达的包池对象。安全根包括:

  • 当前 Built Generation;
  • 显式 retain 记录;
  • 恢复状态与非终态 Operation;
  • 发布尝试及其证据;
  • 活跃维护操作。

操作会记入日志。实际删除包体时,Repository 前进到新 Generation;没有合格对象时为幂等空操作。

sow gc -r pgsql
local gc pgsql: generation=00000000000000000013 objects=4 bytes=1834200

目标 GC

目标维护使用发布 Checkpoint、不存在性证据与配置的 Cache Grace,具体行为取决于 Provider:

Provider 行为
filesystem 仅在 Grace 到期且已有存储/公开不存在性记录后,条件删除合格对象
r2 持久化精确的只报告候选集合;绝不发送对象删除请求
sow gc prod
target gc pgsql/prod (filesystem): phase=done candidates=14 deleted=8 retained=6 pending=0

空操作表示当前没有到期维护任务,并不代表目标已经做过穷尽式重新验证。

退出行为

代码 触发条件
0 GC 完成或没有合格对象
1 文件系统、Provider、网络或其他运行时错误
2 用法、工作区发现、sow.yml 无效或隐式 Repository 选择有歧义
4 Repository 写锁不可用
5 恢复、状态、Receipt 或 Manifest 证据不一致
6 显式 Repository/目标未配置或不安全,或删除被安全前置条件拒绝

参见

18 - sow export

将一个已构建 RPM Dist 架构导出为独立兼容仓库。

SOW 提供一个导出子命令:sow export rpm-leaf。它创建外部、独立的 RPM 仓库, repodata 使用本地 pool/... href。

语法

sow export rpm-leaf DIST ARCH DIR [--hardlink] [-C|--workdir DIR] [-r|--repo NAME] [--json]
参数 要求
DIST 已配置的规范 RPM Dist 名称
ARCH x86_64aarch64
DIR 不存在或为空,且不与 Repository、私有状态、filesystem 目标根重叠的目录
选项 含义 默认值
--hardlink 对可信、同文件系统、只读目标使用硬链接 复制文件
-C, --workdir DIR 工作区发现起点 当前目录
-r, --repo NAME 选择 Repository 选择规则
--json 输出 sow.cli/v1 Envelope false

该命令不接受 --dist、jobs、timeout 或锁参数。

输出

sow export rpm-leaf el9 x86_64 /srv/export/el9-x86_64
exported RPM leaf el9/x86_64 generation=00000000000000000012 method=copy packages=84 to /srv/export/el9-x86_64

目标目录包含:

  • 使用本地包体 href 重写的 RPM repodata;
  • 所需软件包目录树;
  • 导出 Manifest;
  • .sow-export.json 来源记录。

源必须是已完成的 Built Generation。导出物是独立制品,不属于 Desired Membership、 Built Generation、发布输入或 GC 根。

复制与硬链接

复制是安全默认值。--hardlink 只适用于同一文件系统、且消费者无法修改的可信只读目标。硬链接 包体与 SOW 包池共享 inode,不能用于可写或不可信目标。

SOW 会拒绝与已配置 filesystem 发布根重叠的输出,避免导出物被误认为或修改 Managed 发布目标。

退出行为

代码 触发条件
0 独立 RPM leaf 导出完成
1 文件系统、复制、硬链接或元数据写入错误
2 命令语法、Dist/架构 Token 无效,或发现/隐式 Repository 选择有歧义
5 源 Generation 或 Repository 状态不一致
6 显式 Repository 未配置、Dist 不是 RPM、视图/签名者不可用,或目标不安全/非空/重叠

参见

19 - sow log

读取操作审计账本、导出为 JSONL,并清理符合条件的终态记录。

仓库内的每条写命令,都会在该仓库的 SQLite 中提交一条应用级 Operation,然后才产生任何外部文件 副作用。这条记录让崩溃恢复成为可能——而当 Operation 进入终态之后,同一条记录就是你的审计轨迹。 sow log 读的就是它。

语法

sow log [OPERATION] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME] [--json]
sow log export [FILE] [-C|--workdir DIR] [-r|--repo NAME] [-d|--dist NAME]
sow log prune BEFORE [-C|--workdir DIR] [-r|--repo NAME] [-T|--timeout DUR | -N|--no-wait] [--json]

Operation 生命周期

读懂 state 字段,日志就读懂了一大半:

planned → staged → applied → built → done
                        └────────→ done_dirty
   any nonterminal → recovering → built / rolled_back
   pre-apply error  → failed
状态 含义
planned 命令、参数、目标与预期动作已持久化
staged 新包/元数据已写入临时位置并校验通过
applied 期望状态与所需的私有 pending 载荷已提交
built 完整的静态 Generation 已切换
done 终态——一次正常成功的命令
done_dirty 终态——给了 --skip,公开树被有意保留在旧代
failed 终态——在 applied 之前失败,什么都没提交
rolled_back 终态——applied 之后失败,但进程安全地回滚了
recovering 非终态;下一条写命令必须先完成或回滚它

工作区生命周期命令(initrepo newrepo rm)走的是工作区文件 journal,不会出现在仓库的 SQLite 日志中。dist new/dist rm 会出现——那时仓库数据库已经存在。

sow log

不带参数时,按由新到旧打印最近 50 条 Operation。

sow log -r pigsty

输出节选,operations 数组中的一个 Operation 对象:

{
  "id": "4262183287563704350",
  "kind": "build",
  "state": "done",
  "payload_json": "{\"version\":2,\"repository\":\"pigsty\",\"kind\":\"build\",\"config_sha256\":\"37eb6dcf...\",\"skip\":false,\"dists\":[\"el9\"],\"build_dists\":[\"el9\"],\"manifest_sha256\":\"678beeae...\"}",
  "result_json": "{\"dists\":1,\"dropped_pending\":[]}",
  "created_at": "2026-08-04T04:07:40.334787Z",
  "updated_at": "2026-08-04T04:07:40.907125Z"
}

payload_json 记录意图——包括当时生效配置的摘要 config_sha256,以及结果 Generation 的 manifest_sha256result_json 记录结果。失败的 Operation 还会带 error_classerror_message

{
  "id": "5995346754219751025",
  "kind": "add",
  "state": "failed",
  "result_json": "{\"accepted\":0,\"failed\":1}",
  "error_class": "rejected",
  "error_message": "no input package was accepted"
}
参数 说明 默认
-C, --workdir DIR 工作区发现的起始目录 当前目录
-r, --repo NAME 选择一个仓库 按选择规则
-d, --dist NAME 只显示触及该 Dist 的 Operation 全部
--json 输出版本化 JSON envelope false

查看单条 Operation

给出 Operation ID,就能得到它的完整状态迁移、耗时、包、成员与文件动作。

sow log 4262183287563704350 -r pigsty

输出节选:

{
  "duration_ms": 572,
  "events": [
    {"sequence": 0, "state": "planned",  "occurred_at": "2026-08-04T04:07:40.334787Z"},
    {"sequence": 1, "state": "staged",   "occurred_at": "2026-08-04T04:07:40.380963Z"},
    {"sequence": 2, "state": "applied",  "occurred_at": "2026-08-04T04:07:40.386186Z"},
    {"sequence": 3, "state": "built",    "occurred_at": "2026-08-04T04:07:40.904730Z"},
    {"sequence": 4, "state": "done",     "occurred_at": "2026-08-04T04:07:40.907125Z"}
  ],
  "packages": [],
  "memberships": [],
  "files": [
    {"sequence": 0, "action": "update", "phase": "pointer", "path": "dists/el9/aarch64/repodata/repomd.xml", "size": 1511, "sha256": "ef071821e06c9e86ab4f6d2a56906d82bb66df251e79d1086cfd44dc8395513e"},
    {"sequence": 1, "action": "update", "phase": "pointer", "path": "dists/el9/x86_64/repodata/repomd.xml",  "size": 1514, "sha256": "a31e90ec39169f0373b108458908333c96c5f600f3c63a50c44257856f0d2d55"}
  ]
}

files 数组使用与 sow changes 相同的 phase 词表:payloadmetadatapointerdelete

Build Operation 还会包含进度事件。它们保持当前 state,并把版本化对象放入 detail_json

{
  "state": "applied",
  "detail_json": "{\"version\":1,\"kind\":\"build_progress\",\"phase\":\"rendering\",\"completed\":1,\"total\":2,\"jobs\":8}"
}

阶段包括 renderingpromoting_payloadpublishing_distsnormalizing_public_treefinalizing。进度行是持久审计数据,但不会推进恢复状态机,也不会 单独触发 SQLite checkpoint。

按 Dist 过滤

-d 把列表限制为触及该 Dist 的 Operation——一个仓库服务多个发行版时很有用:

sow log -d trixie -r pigsty

sow log export

把终态 Operation 以 JSONL 写出——每行一条完整的 Operation 明细记录——用于归档或送入日志管道。

sow log export /srv/audit/pigsty-ops.jsonl -r pigsty
exported 12 operations to /srv/audit/pigsty-ops.jsonl

省略 FILE 或传 - 则写到 stdout:

sow log export - -r pigsty | gzip > pigsty-ops-$(date +%F).jsonl.gz
参数 说明 默认
-C, --workdir DIR 工作区发现的起始目录 当前目录
-r, --repo NAME 选择一个仓库 按选择规则
-d, --dist NAME 只导出触及该 Dist 的 Operation 全部

export 没有 --json——JSONL 就是它的输出格式。

它拒绝覆盖

目标已存在属于拒绝,绝不覆盖——审计导出不能静默毁掉上一份:

sow log export /srv/audit/pigsty-ops.jsonl -r pigsty
operation rejected: export target already exists: /srv/audit/pigsty-ops.jsonl

export 同样拒绝父目录不是真实目录的目标——符号链接,或根本不存在的目录:

sow log export /tmp/pigsty-ops.jsonl -r pigsty
log export parent is not a real directory

macOS 上 /tmp 是指向 /private/tmp 的符号链接,所以在那里会触发这条拒绝。请写到明确的真实路径。

sow log prune

删除早于 BEFORE 且符合条件的终态审计记录,并安全压缩数据库。

sow log prune 2027-01-01 -r pigsty
{"operation":"8150803833883584722","repository":"pigsty","before":"2027-01-01T00:00:00+08:00","pruned":1}

绝对时间戳会被回显,这样本地时区的解释永远不含糊。

参数 说明 默认
-C, --workdir DIR 工作区发现的起始目录 当前目录
-r, --repo NAME 选择一个仓库 按选择规则
-T, --timeout DUR 等待锁的最长时间;0 无限等待 0
-N, --no-wait 锁被占用时立即失败 false
--json 输出版本化 JSON envelope false

prune 在仓库级工作,不接受 -d——清掉半条 Operation 只会留下毫无意义的记录。

BEFORE 语法

BEFORE 是 ISO-8601 日期 YYYY-MM-DD(按本地时区零点解释),或带时区的 RFC 3339 时间戳。

sow log prune yesterday -r pigsty
usage error: BEFORE must be YYYY-MM-DD or an RFC 3339 timestamp with timezone

prune 永不删除什么

prune 在设计上是保守的。它绝不会删除:

  • 非终态的 Operation;
  • 当前恢复仍需要的记录;
  • 当前的 Package 或 Membership 状态;
  • Built Generation 或其 Changeset。

pruned 计数准确告诉你有多少条记录符合条件——通常少于截止时间之前的 Operation 总数。日志与 Changeset 位于同一个 SQLite 数据库,但保留规则不同。

示例

排查最近一次写入:

sow log -r pgsql --json | jq -r '.result.operations[0] | "\(.id)\t\(.kind)\t\(.state)"'

列出所有失败:

sow log -r pgsql --json | jq -r '.result.operations[] | select(.state=="failed") | "\(.id)\t\(.error_class)\t\(.error_message)"'

每月归档并收缩:

sow log export /srv/audit/pgsql-$(date +%Y%m).jsonl -r pgsql
sow log prune 2026-05-01 -r pgsql

哪条 Operation 最后触碰了某个 Dist:

sow log -d el9 -r pgsql --json | jq -r '.result.operations[0].id'

退出码

命令 触发条件
log 0 记录已打印,包括空账本
log 2 用法错误(包括非数字的 Operation ID)、工作区未找到,或选择有歧义
log 5 状态数据库不可读
log 6 给定的 Operation ID 不存在
log export 0 导出成功
log export 1 写目标时 I/O 失败,或父目录不是真实目录
log export 2 用法错误或选择有歧义
log export 6 目标已存在
log prune 0 清理完成,包括一条都没清
log prune 2 BEFORE 格式非法、给了 -d,或选择有歧义
log prune 4 仓库锁被占用,且给了 --no-wait--timeout 到期
log prune 5 完整性或恢复错误

参见