完整配置 schema:工作区、仓库、Dist、成员策略、签名与发布目标。
这是本节的多页打印视图。 .
参考
这一部分记录配置字段、包引用、路径、退出码、JSON、平台与集成等稳定契约。CLI 语法和状态变化 见命令,使用模型见上手。
输出示例只说明形态;标识符、路径、哈希、时间戳与计数会随工作区变化。二进制自带的
sow help 始终是精确语法权威。
命令行上指代一个软件包的五种写法、歧义如何裁决,以及 rm / show / where 各自接受哪些形态。
Plain 与 Managed 两种模式下 SOW 创建的每一条路径、包池分组规则、名称约束, 以及绝对不能通过 HTTP 暴露的目录。
七个退出码分别代表什么。
sow.cli/v1 Envelope、各顶层字段含义与主要命令族的 Result 形态。
Release 目标、文件系统要求、仓库客户端检查、发布 Provider,以及各项自动化集成的确切范围。
约定
命令示例不带 $ 提示符,方便整块复制。输出块只代表结构,可变值与长结构会在标注处省略。
二进制自带的 sow help 始终是精确语法权威。
语法块中占位符用大写(NAME、DIR、PACKAGE),字面量用小写。方括号表示可选参数,
... 表示可重复,竖线分隔互斥项 —— 与 sow help 的写法一致。
1 - sow.yml 配置参考
sow.yml 是 Managed 托管工作区(Workspace)唯一的配置文件。它位于工作区根目录,
声明存在哪些仓库(Repository)与 Dist,并保存每次构建都要执行的成员策略与签名设置。
Plain 平面模式(sow create)完全不读它。
本页列出解析器接受的每一个字段。没有列在这里的字段一律拒绝 —— 不存在未公开的 键,也没有为将来预留的键。
文件是怎么读的
SOW 用严格 YAML 解析器读取 sow.yml。具体表现是:
- 未知字段是错误,不是警告。 把
repos:写成repositories:会直接失败(退出码2), 并指出出问题的行号。 - 只允许一个 YAML 文档。 用
---引入第二个文档会报错。 - 只接受普通文件。
sow.yml是符号链接,或者大于 16 MiB,在解析前就被拒绝。 - 默认值在解析时补齐,不写回磁盘。 想看完全展开后的形态,用
sow config show --all。
文件的一部分是机器维护的:sow init、sow repo new、sow repo rm、sow dist new、
sow dist rm 会作为各自事务的一部分原子改写 sow.yml。成员策略与签名则由你手工编辑 ——
没有对应的命令行参数。
任何手工修改之后,跑一次 sow config check。它解析文件、与每个已初始化仓库的 SQLite
状态交叉核对、并解析每一个签名 key 引用,全程只读:
根级字段
| 字段 | 类型 | 必填 | 默认值 | 含义 |
|---|---|---|---|---|
schema |
string | 是 | — | 必须恰好是 sow/v3,其他值一律是配置错误。 |
architectures |
字符串列表 | 否 | [x86_64, aarch64] |
本工作区允许管理的 CPU 架构族。 |
repos |
map | 否 | 空 | 仓库名到仓库配置的映射。 |
targets |
map | 否 | 空 | 发布目标名到目标配置的映射。 |
schema 配置值必须恰好是 sow/v3。
architectures
这是 上限,不是目标。它声明 SOW 最多可以接纳哪些架构;各 Dist 默认继承整张表, 除非自己再收窄。
目前只支持两个规范族(canonical family):x86_64 与 aarch64。DEB 生态名作为输入别名
被接受,并在解析边界规范化:
| 你可以写 | 存储与展示为 |
|---|---|
x86_64、amd64 |
x86_64 |
aarch64、arm64 |
aarch64 |
所以 architectures: [amd64, arm64] 与 architectures: [x86_64, aarch64] 是同一份配置。
把同一族的两个别名都写上 —— [amd64, x86_64] —— 属于重复,会失败:
noarch(RPM)与 all(DEB)不是 这里的架构。它们是中性(neutral)包,构建时投影进
每个适用视图,解析器拒绝把它们写进这个列表。不支持的值(如 riscv64)立即失败:
这个列表可以整体省略,但不能写成空列表。
Repository 仓库
| 字段 | 类型 | 必填 | 默认值 | 含义 |
|---|---|---|---|---|
protected |
bool | 否 | false |
为真时 sow repo rm 拒绝删除该仓库,-f 也不行。 |
signing |
map | 否 | 无 | 包体与元数据签名设置,见签名。 |
dists |
map | 否 | 空 | Dist 名到 Dist 配置的映射。 |
protected
protected: true 是防止误删整个仓库的闸门,它只拦一件事 —— 仓库删除:
这是退出码 6。其余一切照常:add、rm、build、建/删 Dist 都不受影响。
要真的删掉一个 protected 仓库,先把 sow.yml 改成 protected: false,
用 sow config check 确认,再执行 sow repo rm。
名称约束
仓库名与 Dist 名共用一套文法:必须匹配 [a-z0-9][a-z0-9._-]* —— 小写字母、数字、
点、下划线、连字符,且以字母或数字开头。大写被拒绝,因为名称会变成目录名,
必须在大小写敏感的 Linux 与默认大小写不敏感的 macOS 文件系统上表现一致:
下列名称是保留名,一律拒绝:.、..、.sow、pool、dists、sow.yml、
workspace.lock、workspace-ops、repo-locks。两个会在状态目录里撞车的仓库名
(比如 db 与 db.db)也会被拒绝:
原因见仓库布局。
Dist
| 字段 | 类型 | 必填 | 默认值 | 含义 |
|---|---|---|---|---|
format |
string | 是 | — | rpm 或 deb。一个 Dist 只承载一种格式。 |
architectures |
字符串列表 | 否 | 继承工作区列表 | 把该 Dist 收窄到工作区架构的一个子集。 |
limit |
integer | 否 | 0 |
同一包名 + 架构最多保留几个版本;0 表示全留。 |
exclude |
规则列表 | 否 | 空 | 把命中的包挡在该 Dist 之外的规则。 |
format
format 是 sow dist new 唯一从命令行接受的业务参数,并且创建之后不可更改 ——
RPM Dist 永远不会变成 DEB Dist。格式不匹配的包根本不会成为该 Dist 的候选:
architectures
省略这个字段,Dist 继承工作区列表 —— 绝大多数情况下这就是你要的。
只有需要 收窄 时才声明:比如双架构工作区里,某个 el9 Dist 只做 x86。
列表必须是工作区列表的子集,且不能为空:
在这里新增一个架构族会让该 Dist 变为待构建(dirty),下一次 sow build 渲染新视图。
移除一个仍被现有成员关系或已构建代引用的族,config check 与所有写命令都会拒绝。
limit
limit 限定该 Dist 中同一个包保留几个版本。分组键是 (二进制包名, 原生架构),
所以同一个包的 x86_64 与 aarch64 构建各自计数,noarch/all 包自成一组。
0(默认)保留全部版本。N > 0保留最新的N个,RPM 按 EVR 比较,DEB 按 Debian version 规则比较。- 负数是配置错误:
limit: 1 时,把旧版本和新版本一起加入,旧版本会被报告为 limited 且不建立成员关系:
事后调大 limit 不会 复活曾被策略移出的版本。包体字节可能还留在包池里,
但成员关系已经没了;要拿回来就重新 add 一次。理由见成员策略。
exclude
exclude 是规则列表。每条规则是若干字段的集合:规则内字段之间是 AND,
同一字段的多个 pattern 之间是 OR,规则与规则之间是 OR。任一规则命中即排除。
读作:丢掉所有 debug 类包;另外,丢掉名字以 test- 开头或以 -experimental 结尾的
aarch64 包。
允许五个字段:
| 字段 | 匹配对象 |
|---|---|
name |
二进制包名 |
source |
规范化后的 source 名(RPM 取 SOURCERPM,DEB 取 Source) |
arch |
x86_64、aarch64 或 neutral |
kind |
见下表分类 |
format |
rpm 或 deb |
kind 由二进制包名的后缀决定,取最具体的一个:
| 格式 | 名称后缀 | kind |
|---|---|---|
| RPM | -debuginfo |
debuginfo |
| RPM | -debugsource |
debugsource |
| RPM | -llvmjit |
llvmjit |
| DEB | -dbgsym |
dbgsym |
| DEB | -dbg |
dbg |
| 任意 | 以上都不匹配 | main |
pattern 区分大小写,只有两种形态:精确字符串,或使用 *、?、[...] 的 shell glob。
不支持正则、版本比较、否定,也没有表达式语法。空规则、空或带首尾空白的 pattern、
同一字段内重复的 pattern、非法 glob,都是配置错误:
策略顺序固定:先 exclude,后 limit。被排除的包逐条报告,不算失败:
签名
签名配置挂在仓库级(不是 Dist 级),覆盖两条互相独立的信任链:软件包本身, 以及客户端在信任其他一切之前先验证的仓库元数据。
树形是固定的:signing.rpm 下有 packages 与 metadata;signing.deb 下 只有
metadata —— DEB 包体永远不会被重签,因为 APT 通过 Release 验证整个仓库,
而不是逐包签名。
rpm.packages
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
mode |
string | 无 key 时 never,有 key 时 fill |
never、fill 或 always。 |
key |
key 引用 | 无 | 当前签名身份;公钥用于验证,匹配私钥必须存在于 rpm 使用的 GPG 环境。mode 不是 never 时必填。 |
trusted_keys |
key 引用列表 | 空 | fill 额外认可的公钥。 |
三种模式:
never—— 原样保存输入字节。包进来时带什么签名(或没有签名),客户端拿到的就是什么。fill—— 对没有签名、或签名无法被key及trusted_keys验证的包补签; 已经能验证通过的包保持字节不变。always—— 最终每个包都必须由key有效签名。已经由该 key 签好的保持字节, 其余一律重签。
有 key 时默认是 fill,因为它是唯一保留上游签名的模式。设成 fill 或 always 却不给
key 是错误:
trusted_keys 列出哪些公钥的签名被 fill 视为"已经合格"。key 的公钥部分自动受信,
不需要重复列出。同一个引用写两次是错误:
RPM 包签名是唯一会调用外部程序的操作:SOW 对 私有 stage 副本 调用环境里的
rpm --addsign / rpm --resign,永远不碰你的输入文件。私钥必须已经存在于 rpm 使用的
GPG 环境中。
rpm.metadata 与 deb.metadata
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
key |
key 引用 | 无 | 给仓库元数据签名的私钥。 |
passphrase |
passphrase 引用 | 无 | 私钥有口令时使用。 |
配置 rpm.metadata.key,每个 RPM 架构视图会额外发布分离签名
repodata/repomd.xml.asc;配置 deb.metadata.key,每个 DEB Dist 会额外发布
clearsign 的 InRelease 与分离的 Release.gpg。没配 key 就不生成这些文件 ——
repomd.xml 与 Release 则始终会写。
file:// 与 env:// 引用由 SOW 在 进程内 完成签名,不需要 gpg 可执行文件。
只有 agent:// 需要环境里有 gpg。
改变 key 引用或它背后的 fingerprint 会让相关 Dist 变 dirty ——
签名身份是每个 Dist 已构建配置摘要的一部分。下一次 sow build 重新签名并推进新的代。
key 引用文法
key 引用是下列四种写法之一:
| 形态 | 例子 | 说明 |
|---|---|---|
| 路径 | keys/repo-signing.asc |
ASCII-armored key 文件。相对路径相对 工作区根目录 解析,不是当前目录。 |
file://<path> |
file:///secure/repo-signing.asc |
与上一行等价,只是显式写出。绝对路径因此是三个斜杠。 |
env://<VAR> |
env://SOW_METADATA_KEY |
环境变量里存的是 armored key 内容本身,不是路径。变量名须匹配 [A-Za-z_][A-Za-z0-9_]*。 |
agent://<fingerprint> |
agent://7F721C4AD40F...CF3B |
委托给环境里的 gpg-agent。fingerprint 为 16、40 或 64 位十六进制,不区分大小写。 |
其他 scheme 一律拒绝:
引用分两阶段校验。文法 在解析时检查,失败退出码 2;引用 能否解析出真实密钥
由 sow config check 和每条写命令检查,失败退出码 6:
秘密内容永远不会离开引用本身。sow config show --all 只显示引用与解析出的 fingerprint:
私钥与口令永远不会写进 sow.yml、SQLite、操作日志、JSON 输出或错误文本。
passphrase 引用
passphrase 接受与 key 引用相同的路径、file://、env:// 三种写法,
但 不接受 agent:// —— 口令是一个值,不是密钥句柄。
两条规则:
-
有 passphrase 没有 key 是错误,因为它没有可解锁的对象:
-
passphrase 与
agent://key 同时出现是错误。私钥由 agent 持有并自行处理口令交互, 第二条口令通道只会被忽略:
发布目标
每个目标把一个已配置 Repository 绑定到一个存储命名空间。目标名使用与仓库相同的小写文法。
| 字段 | 必填 | 含义 |
|---|---|---|
repository |
是 | 本目标拥有的现有 Repository。 |
provider |
是 | filesystem 或 r2。 |
endpoint |
是 | 无尾斜杠的规范 file:///absolute/path,或 R2 的规范 https://host。 |
region |
R2 | 必须是 auto;filesystem 禁用。 |
bucket |
R2 | 小写规范 bucket 名;filesystem 禁用。 |
prefix |
是 | 相对公共树前缀;空串表示存储命名空间根。 |
credential |
R2 | env://NAME 或 file:///absolute/path;禁止内联秘密。 |
public_endpoint |
是 | 以 / 结尾、用于公共内容/缺失校验的规范 URL。Filesystem 接受 https://、http:// 或 file://;R2 必须为 HTTP(S)。 |
max_cache_ttl |
是 | 有界、规范的非负 Go Duration,包括显式 0s;溢出会被拒绝。 |
authoritative_workspace |
是 | 必须为 true。 |
single_writer |
是 | 必须为 true。 |
exclusive_write_authority |
是 | 必须为 true。 |
三个 authority 布尔值是显式安全确认,不是默认值。同一存储上的目标前缀不能重叠; filesystem 目标也不能解析到重叠的有效路径。这些规则防止并发写者破坏条件式发布与 GC。
对 provider: filesystem,配置校验只检查 URL 形态与重叠关系。真正发布时,endpoint 目录
必须已经存在、不能是 symlink,并且必须解析为唯一规范真实目录。SOW 会在 endpoint 下创建
配置的 prefix,但不会创建 endpoint 本身。
首次 publish 会持久绑定 Repository、Provider Storage Identity 与 Prefix。之后修改 Target Name、
public_endpoint 或 max_cache_ttl,必须通过
sow publish TARGET --rebind 得到操作者显式确认。
Provider、Storage Endpoint、Region、Bucket、Prefix 与 Repository Identity 均不能 rebind;应配置
新目标。每次接受的 rebind 都会追加不可变 Binding Revision。Pending Maintenance 会阻止 TTL
变化;Filesystem Conditional-delete Maintenance 还会阻止公共端点变化。
R2 凭据是私有引用。环境变量值或被引用文件必须包含一份严格 JSON 文档,不能写路径或 Shell 赋值:
临时凭据可以增加可选的 "session_token":"..."。未知字段、尾随内容、缺少 Access/Secret,
以及超过 64 KiB 的文档都会被拒绝。config show、JSON 输出与公共树不会包含凭据材料。
完整示例
一个工作区,两个仓库:一个受保护的生产仓库(两条元数据签名链 + RPM 补签), 一个不签名、不过滤的临时仓库。
用之前先验证:
sow.yml 里没有什么
有些你可能以为能配的东西,是 有意 不做成配置项的:
- 仓库路径。 仓库永远位于
<workspace>/<name>,没有path:字段。 见仓库布局。 - APT component。 固定为
main;YUM 没有 component 概念。 - 架构视图。 由
architectures与包头共同推导,不能逐包声明。 - 内联秘密。 目标只接受 credential 引用;key 与 passphrase 材料同样留在引用背后。
- 自动保留数量。 保留是显式
sow retain add/rm操作,不是配置中的滚动计数。
延伸阅读
2 - 包引用
sow rm、sow show、sow where 都接受一个 PACKAGE 参数。本页定义你能在那里写什么。
三条命令共用同一套文法,只有对 歧义名称 的处理不同。
这里的内容与 sow add 无关 —— add 接受的是文件系统路径,不是包引用。
五种形态
| 形态 | 例子 | 匹配 |
|---|---|---|
| 内容摘要 | sha256:d06d7f23b9cf...b98b1229 |
恰好一个包对象 |
| RPM 坐标 | rpm:pev2-0:1.23.0-1.noarch |
恰好一个 RPM |
| DEB 坐标 | deb:libpq5=18.3-1.pgdg12+1:amd64 |
恰好一个 DEB |
| 完整文件名 | pev2-1.23.0-1.noarch.rpm |
以该文件名存储的包 |
| 裸包名 | pev2 |
该名称的全部版本与架构 |
前三种是 精确 引用:指名道姓,要么命中要么失败。后两种是便利写法,可能匹配多个对象。
你不需要手工拼这些字符串。sow ls 会直接打印每个包的摘要与坐标,可以原样粘回命令行:
内容摘要
已存储包体完整字节的 SHA-256。这是 SOW 里最强的引用形式:它就是对象身份,不可能有歧义。
摘要必须完整且小写。不支持 前缀匹配,也不做大小写折叠 —— 位数不足或大写都属于用法拒绝, 不是"没找到":
注意这个摘要覆盖的是 已存储 的字节。如果仓库对 RPM 包体做了重签,
对象摘要与你交给 sow add 的那个文件的摘要就不一样了。
RPM 坐标
完整 NEVRA,加 rpm: 前缀。每一段都必填,包括 epoch —— 包本身没有 epoch 时写 0。
在 shell 里请加引号:NEVRA 含冒号,否则可能被历史展开或路径补全改写。
前缀与 epoch 都是必需的。少任何一个,这串字符就会被当成裸名解析,从而什么都找不到:
架构那一段取自 RPM 包头:x86_64、aarch64 或 noarch。它 不是 规范族名 ——
noarch 包这里就写 noarch,尽管 SOW 内部把它归类为 neutral(中性)。
DEB 坐标
Debian 身份三元组,加 deb: 前缀。版本是含 epoch 与 revision 的完整 Debian 版本号;
架构是生态名(amd64、arm64、all),不是规范族名。
三段都必填。deb:libpq5=18.3-1.pgdg12+1 不带架构,匹配不到任何东西。
完整文件名
包存储时的完整文件名,含扩展名:
看着目录列表操作时,这是最好敲的写法。但它 不是身份 —— SOW 不用文件名区分包, 理论上两个不同对象可以叫同一个名字。脚本里请优先用坐标或摘要。
裸包名
只写二进制包名:
它的含义取决于命令:
-
sow rm把它理解为所选 Dist 中该名称的 全部 版本与原生架构。这是有意设计的 —— 下架一个包通常意味着全部下架。先用-c预览: -
sow show与sow where要求它唯一命中。这两条命令描述的是单个包, 名称匹配多个时会连同候选列表一起拒绝:每个候选都同时给出坐标与摘要,所以修正方式就是把其中一条粘回命令行。
哪些写法不成立
不带 rpm: 前缀的 NEVRA 看起来像坐标,实际会被当作裸名解析,
而裸名里不含 epoch 和架构:
另外,这里没有 glob、没有正则、没有版本区间,也没有 --all 参数。
如果你想按模式 筛选 一批包,那是 sow.yml 里的成员策略,
不是命令行选择器。命令行永远只用来指代 已经存在 的包。
作用域
引用总是在某个作用域内解析,而作用域由常规的选择参数决定,与引用写法无关:
| 命令 | 默认作用域 | 收窄方式 |
|---|---|---|
sow rm |
所选仓库的所选 Dist | -r、-d(存在多个时必填) |
sow show |
所选仓库 | -r、-d |
sow where |
工作区内全部仓库 | -r、-d |
sow where 是那条"广搜"命令 —— 当你知道某个包在某处、但不知道在哪个仓库时用它。
sow show 则是在一个仓库内把一个对象的细节全部展开。
两条命令没找到时的措辞也不同,可以据此判断自己跑的是哪一条:
坐标与身份
上面的坐标形态是包的 逻辑身份。SOW 强制约束:一个仓库内,一个坐标最多对应一个内容对象。
用已存在的坐标加入一个 不同 的文件是硬冲突 —— SOW 不会悄悄挑一个赢家,也没有 --replace。
因此,两个只有签名不同的包仍然会冲突,因为它们坐标相同。如果你真的要重签发布,
请提高 release 号;如果只是把同一个输入再加一次,SOW 会识别出来并报告 reused。
延伸阅读
3 - 仓库布局
SOW 的 Managed 布局只有一种:软件包体在 pool/ 下只存一份,dists/
只保存客户端视图元数据。对外服务、复制或发布时,单位始终是完整仓库目录。
Plain 模式
sow create 在现有软件包旁写入索引,不修改无关文件:
平面 RPM 元数据引用裸文件名,平面 DEB 元数据使用 ./<filename>。构建期间
.sow-plain-stage-* 保存私有生成输出。Plain 没有持久 journal 或 recovery 状态;下次
create 会丢弃保留命名空间里的陈旧临时路径并重建。不得服务或复制这些临时路径。
Managed 工作区
去重不跨仓库边界。.sow/ 与 pending 目录权限为 0700。Pending 包体文件直接使用最终
公开权限 0644,因此提升只需修改命名空间。私有状态
可能包含尚未发布的包体、从凭据派生的状态与恢复数据。
<repo>.db 与可重建的软件包事实缓存都属于私有状态,不改变公共仓库布局或
sow/v3 配置标识。
SOW 0.4 使用内部数据库 Schema v12。Append-only Publication-target Binding Revision、Package
Facts、Signer Projection 与 Recovery Evidence 均只存在于 <repo>.db。v0.3 数据库必须先备份,
再通过 sow repo migrate 升级;绝不要手工修改 PRAGMA user_version,也不要脱离匹配的公共/私有
Repository 状态单独复制数据库。
规范包池
每个包体只有一条规范路径:
源码名取自 RPM SOURCERPM 或 DEB Source;缺失时回落到二进制包名。
前缀是首个小写字符,以 lib 开头时取前四个字符:
| Source | 示例 |
|---|---|
postgresql-18 |
pool/p/postgresql-18/libpq5_18.3-1.pgdg12+1_amd64.deb |
blackbox_exporter |
pool/b/blackbox_exporter/blackbox_exporter-0.28.0-1.x86_64.rpm |
libfoo |
pool/libf/libfoo/libfoo1_1.0-1_amd64.deb |
Pool 对象不可变。从 Dist 删除成员关系不会立即删除字节;sow gc 只有在检查当前、保留、
恢复、发布以及活动维护操作等全部安全根后,才会处理不可达包体。
RPM 纯元数据视图
这里没有 dists/<dist>/<arch>/pool/。原生包只出现在匹配架构的元数据中,noarch
出现在每个架构视图中。rpm-md 回指规范包池:
该布局要求客户端在完整 Repository Root 内正确处理 rpm-md 相对路径。默认 dnf reposync
会拒绝父级跳转 href,因为下载目标逃出
View Root。下游工具需要自包含 Leaf 时,请显式导出:
导出目录有自己的 pool/ 和改写后的 href;它是兼容性产物,不是规范 Managed 仓库。
DEB 视图
Packages 从 archive 根引用同一规范包池:
Release 使用 SHA256 清单并声明 Acquire-By-Hash: yes。校验和命名的 rpm-md 文件与
APT by-hash 条目让上一组元数据在可变指针最后替换时仍然可达。
发布目标
filesystem 与 r2 目标都会在配置前缀下得到同一棵逻辑公共树:
发布单位始终是完整仓库命名空间。不要只发布某一个 RPM 架构目录,它的 href 会有意回指 根包池。
名称与服务边界
仓库名与 Dist 名必须匹配 [a-z0-9][a-z0-9._-]*。.、..、.sow、pool、
dists、sow.yml、workspace.lock、workspace-ops 与 repo-locks 在相应位置为
保留名。SOW 会拒绝大小写不敏感的池路径冲突,使产物能在 Linux 与默认 macOS 文件系统间迁移。
Web 服务器应指向 <workspace>/<repo>/,而不是工作区根目录。公共仓库需要同时包含
pool/ 与 dists/;私有 .sow/ 必须隐藏。
延伸阅读
4 - 退出码
每条 sow 命令都以七个退出码之一结束。它们是稳定的、对所有命令一致,
并且就是设计来给脚本做分支判断的 —— 区分"这件事失败了"和"这件事被正确地拒绝了",
正是设置多个非零码的全部意义。
| 码 | 含义 |
|---|---|
0 |
完整成功,或幂等 no-op |
1 |
运行时 I/O、解析器、渲染器或未知内部错误 |
2 |
用法、工作区发现或配置错误 |
3 |
部分成功:至少一项已提交,至少一项失败 |
4 |
写锁不可用 —— 被占用且指定了 --no-wait,或等待超时 |
5 |
完整性/恢复错误,或 check 判定当前结果不可交付 |
6 |
预期拒绝:冲突、protected、无匹配、架构不兼容 |
人类可读的结果写 stdout,警告与诊断写 stderr。每个码在 stderr 上有稳定的消息前缀,
在 JSON 输出中有对应的 class:
| 码 | stderr 前缀 | JSON class |
|---|---|---|
1 |
随子系统而异 | runtime |
2 |
usage error: / workspace discovery error: / configuration error: |
usage |
3 |
... batch partially succeeded |
partial |
4 |
lock unavailable: |
lock |
5 |
integrity or recovery error: |
integrity |
6 |
operation rejected: |
rejected |
sow create 是前缀那一列的例外:它不在 Managed 层内,stderr 上打印的是原始领域错误
(plain: scan …、plain: marker gate …),没有 operation rejected: 前缀。该前缀仍然出现在
它的 JSON errors[].message 里。
0 —— 成功或无操作
命令完成了你要求的事,或者发现无事可做。两者都算成功:对未变化的目录重跑
sow create,或对 clean 仓库执行 sow build,都返回 0 并如实说明。
"noop":true 才是区分"没干活"与"干了活"的依据,退出码不区分这两者。
sow status 是有意为之的特例:只要状态数据库可读,它在 clean、dirty、
recovering、error 四种状态下都返回 0 —— 让脚本去读结构化状态,
而不是从退出码反推。需要"闸门"时请用 sow check。
1 —— 运行时错误
I/O、解析或渲染层面出了问题:目录不可写、磁盘满、包读不出来。 这类是 环境问题,不是用法问题。
stage 目录之所以一开始就创建,正是为了让这类失败发生在 任何东西被发布之前。 本来就有合法索引的仓库,索引依然完好。
2 —— 用法、发现或配置错误
你要求的事情 CLI 无法执行:未知参数、目标不明确、找不到工作区,或 sow.yml 解析不通过。
什么都没有尝试执行。
未知参数:
互斥参数:
目标不明确 —— 仓库有两个 Dist,而命令需要确定其一:
当前目录向上找不到任何工作区 —— 错误会说明搜索位置与修复方式:
起始目录本身不是真实目录(比如符号链接,macOS 上的 /tmp 就是)时,搜索根本不会开始:
配置文件格式有问题 —— 注意错误会指出具体行号:
sow.yml 的所有文法与 schema 错误都归到这一码。
3 —— 部分成功
一个批次里有的项已提交、有的项失败。这个码存在的意义是:你永远不必猜测一次失败的
sow add 是否让仓库毫发无损 —— 返回 3 就意味着合法的包 已经进去了,
失败的那些会被逐条点名。
失败的输入文件原地不动。加上 --json 时,已提交的项仍然完整列出 ——
非零退出 从不 截断 result:
sow init 在已提交了部分声明的仓库或 Dist、随后在后面某项上失败时,也用这个码。
4 —— 锁不可用
另一个进程持有写锁。SOW 在设计上就是单写者(single-writer), 所以这是 正常且预期 的结果 —— 重试,或者多等一会儿。
带 --no-wait 时立即失败:
带超时时,恰好等待这么久后失败:
-T 0(默认)一直等待。只读命令不取写锁,永远不会返回 4;
sow status 甚至把这种争用作为一个字段报告出来:
5 —— 完整性、恢复,或不可交付
两种不同的情况共用这个码,它们的含义都是"先别把这棵树发出去"。
常见的那种:仓库的期望状态领先于已构建的内容 —— sow add --skip 之后,
或者改了策略/签名之后,对它执行 sow check。每一层校验都通过,
仓库只是 尚未收敛:
解决办法是 sow build。这正是部署脚本应该拿来做闸门的码 ——
它区分的是"磁盘上的树完整且最新"与"磁盘上的树完整但过期"。
少见的那种是真正的完整性失败:状态数据库、journal 与文件树互相矛盾,
且 SOW 无法安全地自行裁决。此时它拒绝覆盖任何东西,你应该从备份恢复,而不是强行修复。
这里 有意 没有 --force。
6 —— 预期拒绝
命令写法正确、环境也没问题,是 SOW 主动判定"不行"。 这些是策略与安全决策,不是故障。
受保护的仓库:
匹配不到任何东西的引用:
有歧义的裸名 —— 候选会一并列出,方便你挑一个:
工作区不允许的架构。注意逐项错误会点名检测到的值,并告诉你去哪里改:
目录里没有任何可索引的包:
--pigsty 完成标记挡住了对既有构建的覆盖:
签名 key 引用文法正确但解析不出密钥 —— 文法错误是 2,解析失败是 6:
在脚本里使用
这些码的设计目标就是让部署流水线 不必解析文本 即可分支:
这里的 mirror 是 pigsty 已配置的 Publication Target。
两个值得养成的习惯:把 4 当作 可重试 而不是致命错误;
永远不要把 6 当作崩溃 —— 它通常意味着需要改的是你的输入,而不是 SOW。
延伸阅读
5 - JSON 输出
所有产出数据的命令都接受 --json。输出是 stdout 上的 一行 版本化信封 ——
不管是哪条命令产生的,都能直接管道给 jq。
(此处为便于阅读做了折行,实际输出是一行。)
信封结构
| 字段 | 类型 | 含义 |
|---|---|---|
schema |
string | 恒为 sow.cli/v1。解析其他字段前先检查它。 |
command |
string | 实际调用的命令,含子命令:add、repo ls、config show。 |
ok |
bool | errors 为空时为 true,等价于退出码 0。 |
repository |
string 或 null | 选定的仓库;工作区级与 Plain 模式命令为 null。 |
operation |
string 或 null | 写命令的 Operation ID,只读命令为 null。 |
result |
object 或 null | 命令专属载荷,详见下文。 |
errors |
array | 零个或多个 {code, class, message} 对象。 |
七个字段 永远存在。命令在有意义工作之前失败时(例如未知参数、发现失败,或尚未选中
Repository 就遇到无效配置),result 为 null。已提交的部分结果,以及 check、
rm --check 的诊断结果,即使命令非零退出也会保留。
errors
| 字段 | 含义 |
|---|---|
code |
进程退出码 —— 1 到 6。 |
class |
runtime、usage、discovery、config、partial、lock、integrity、rejected 之一。discovery 与 config 是退出码 2 的稳定细分类。 |
message |
与写到 stderr 的文本相同。 |
请对 class 做分支判断,不要匹配 message 文本。message 里含路径和包名,会变;class 不会。
批次部分成功时,ok 是 false,同时 result 会完整列出已提交的内容。
不要因为退出码非零就丢掉载荷 —— 对 add 来说,那正是你了解"哪些包落地了"的唯一途径。
Operation ID 是字符串
Operation ID 是 64 位值,序列化为十进制 字符串,因为它经常超出 IEEE 754 双精度
能精确表示的范围。在 JavaScript 里,JSON.parse 处理裸数字会静默损坏它们。
请保持字符串形态;jq 原样处理即可。
Generation ID 是固定宽度字符串
Generation ID 覆盖完整的无符号 64 位范围,并固定序列化为 20 位、左侧补零的十进制字符串。
generation、built_generation、base_generation 以及表示 Generation 的 base 字段都应
按字符串处理;固定宽度也能保持普通字节序比较与数值顺序一致。
stdout 与 stderr
结果和 JSON 信封写 stdout;警告与错误诊断写 stderr,同时 也出现在 errors 数组里。
所以这样写是可行的:
各命令的 result 形态
create
| 字段 | 含义 |
|---|---|
dir |
被索引的绝对目录 |
rpm、deb |
各格式的包数量 |
kept |
进入索引的文件名,已排序 |
removed |
被 --pigsty 清理删除的包;否则为空 |
marker |
是否写入了 repo_complete |
marker_sha256 |
marker 文件的摘要;仅 --pigsty 时出现 |
noop |
索引本来就正确、什么都没改时为 true |
recovered |
为兼容稳定 schema 保留;Plain create 没有 journal 恢复,始终为 false |
signed |
被重签的文件名;仅 --sign-with 时出现 |
init
对已存在的工作区重跑时,计数为 0,existing 说明找到了什么:
config check 与 config show
config show 返回有效配置本身,形态与规范化之后的
sow.yml 一致:
带 --all 时,签名条目会额外携带 key_fingerprint。私钥内容与口令永不出现。
repo ls / repo new / repo show
repo ls 返回数组;repo new 与 repo show 返回同一形态的单个对象。
packages 统计包池中不同的包对象数;memberships 统计 Dist 成员关系数 ——
同一个包出现在两个 Dist 里,前者计一次,后者计两次。
repo rm 只返回结果:
dist ls / dist new / dist show
每个架构条目同时给出两个名字:family 是配置里用的规范名,
ecosystem_arch 是发布树里出现的名字 —— RPM 两者相同,DEB 分别是 amd64/arm64。
desired_members 大于 built_members,或 dirty: true,都表示还欠一次 sow build。
effective_config_sha256 是所有喂给渲染器的输入的摘要;它一变,该 Dist 就变 dirty。
dist rm 与 repo rm 一致:{"name":"el9","noop":false,"removed":true}。
add
每个输入路径对应一条 items,顺序稳定。status 是该项的总体结果,
dists 给出 逐 Dist 的裁决:
status |
含义 |
|---|---|
accepted |
新包对象,已建立成员关系 |
reused |
完全相同的对象已存在;可能仍为其他 Dist 新增成员关系 |
excluded |
被策略挡下 —— 看 dists 区分是 excluded 还是 limited |
failed |
未被接纳;error 给出原因 |
逐 Dist 的取值是 accepted、excluded、limited。同一条命令里,
一个包可以被某个 Dist 接受、被另一个 Dist 限流:
失败项携带 error,不带包字段:
memberships_added 与 memberships_removed 双向计数,因为 limit 可能在接纳新版本的
同一个操作里淘汰旧版本。
rm
带 -c/--check 运行时 check 为 true,此时 什么都没写,changes 是一份预测。
注意 removed 只列出成员关系的移除 —— rm 永远不删除包池字节。
build
noop: true 表示期望状态已经与已构建的树一致,没有产生新的代。
dists 列的是被纳入考量的 Dist,不一定是真正重建了的那些。
status
status 取值为 clean、dirty、recovering、error。部署脚本该读的字段是
ready_to_copy —— 但记住 status 在任何状态下都返回 0,所以要判断 字段,不是退出码:
pending 统计 add --skip 之后私有保存、尚未发布的包体。
repository_locked 报告当前是否有其他进程持有写锁。
check
稳态 Check 按固定顺序返回九层,每层给出检查项数与问题。未完成布局迁移则只返回 config、
state、public-modes 与 layout-transition,随后停止并返回不可交付。dirty 仓库可以让全部稳态层
ok: true,但仍以退出码 5 失败 —— 因为层校验的是 自洽性,
而 ready_to_copy 报告的是 时效性:
changes
| 字段 | 取值 |
|---|---|
op |
add、update、delete |
phase |
payload、metadata、pointer、delete |
path |
永远相对仓库根,永远用 / 分隔 |
size、sha256 |
add 与 update 有;delete 没有 |
按这个 phase 顺序施加变更,客户端永远不会取到悬空引用:先包体,
再校验和命名的元数据,然后是协议指针(repomd.xml、Release),最后才删除被取代的文件。
sow changes 0 把当前整棵树作为一个 add 集合给出 —— 也就是一份完整交付清单。
ls / show / where
ls 返回包对象数组;show 在 package 下返回恰好一个。
值得关注的字段:
| 字段 | 含义 |
|---|---|
architecture |
包头里的原始写法:x86_64、noarch、amd64、all |
canonical_arch |
SOW 用于分组的族:x86_64、aarch64 或 neutral |
payload_sha256 |
仅 RPM —— 签名无关摘要,用于识别同一包的重签副本 |
signature_key |
包内嵌签名的 key ID(包带签名时) |
storage |
已发布为 pool,--skip 加入的为 pending |
dists / built_dists |
期望成员集 与 上一次构建实际发布的集合 |
dists 比 built_dists 长,是判断"还欠一次 build"的另一种方式。
where 搜索整个工作区,返回位置而不是完整对象:
publish、retain、gc、export
Managed 生命周期命令使用同一 envelope,数字 Generation 仍序列化为 JSON 字符串:
| 命令 | 重要 result 字段 |
|---|---|
publish |
repository、target、provider、generation、attempt、checkpoint、phase、objects、noop |
publish --abort |
repository、target、provider、attempt、phase、objects |
publish --rebind |
与 publish 相同;Binding Revision 属于持久私有审计状态,不新增 Wire Field |
retain add / retain rm |
repository、record、record_identity、path |
retain ls |
repository、generations[],元素使用同一 retained record 形态 |
本地 gc |
operation、repository、base_generation、generation、objects、bytes、noop |
目标 gc |
repository、target、provider、phase、reports、candidates、deleted_objects、deleted_bytes、retained_objects、pending_grace、completed_attempts、noop |
export rpm-leaf |
repository、repository_id、generation、dist、arch、directory、method、signed、signer_identity、packages、files、manifest_sha256 |
attempt、checkpoint 或本地 GC operation 等可选 identity 没有值时直接省略。
R2 目标 GC 会把候选计入 retained,SOW 从不报告自己执行了远端删除。
log
sow log 返回操作账本,由新到旧:
payload_json 与 result_json 是 内含 JSON 的字符串,不是对象。
它们原样保存以保证审计记录字节稳定;需要二次解析:
传入 Operation ID 会返回完整细节 —— 状态迁移、结构化 build_progress 事件、包、成员关系与每一个文件动作:
sow log prune 返回它清理了什么:
注意 before 会回显裸日期在本地时区解析出的绝对时间戳。
log export 不是信封
sow log export 输出 JSON Lines —— 每行一条完整的 Operation 记录,
没有信封,也没有 --json 参数。它是给归档用的,不是给单条命令脚本用的:
它拒绝覆盖已存在的文件,也拒绝父目录是符号链接的目标。
一个完整例子
仓库既自洽又最新时才允许部署,然后列出该复制哪些文件:
延伸阅读
6 - 平台与集成
本页说明 SOW 提供哪些构建目标、工作区依赖什么存储语义,以及自动化集成具体覆盖哪些行为。 仓库生成在 SOW 二进制内部完成;部署后的最终门禁仍是实际软件包管理器。
Release 目标
| 操作系统 | amd64 |
arm64 |
制品 |
|---|---|---|---|
| Linux | 是 | 是 | 归档、RPM、DEB |
| macOS | 是 | 是 | 归档 |
| Windows | 否 | 否 | 不支持 |
Release 二进制使用 CGO_ENABLED=0,不需要语言运行时。0.4.0 制品使用 Go 1.27.0 构建;
源码 Module 要求 Go 1.27.0 或更新版本。归档包含 README.md、CHANGELOG.md 与 Apache-2.0
LICENSE,Linux 软件包会随二进制安装同一份
协议文件。使用 sow version 查看产品版本、目标 OS/架构与构建工具链。
工作区文件系统
Managed 工作区应放在本地 POSIX 文件系统上。正确性依赖建议锁、fsync、基于描述符的路径
校验与同文件系统原子 rename;NFS 等网络文件系统不属于受支持的工作区位置。
公共 <workspace>/<repo>/ 树是另一条边界:它是闭合的 pool/ + dists/ 命名空间,可整根
复制或发布,不依赖 SQLite、私有 journal 或 view-local hardlink identity。必须保持完整
Repository,不得暴露 .sow/。
SOW 会拒绝符号链接控制路径、不安全普通文件、重叠 filesystem target,以及大小写折叠后冲突 的 Pool 路径,使同一个 Repository 可以在大小写敏感的 Linux 与默认大小写不敏感的 macOS 文件系统间移动。
自动化集成矩阵
| 表面 | 环境 | 已验证行为 |
|---|---|---|
| 生产 CLI 干净环境 | Linux CI | 构建交付二进制;生成混合 Plain RPM/DEB 元数据;初始化 sow/v3;创建 RPM/DEB Dist;加入 Fixture;执行查询、build、check、changes、config 与 log 命令 |
| Plain APT 客户端 | Ubuntu 22.04 容器 | 通过 HTTP 服务 sow create 输出;在显式信任未签名源时执行 apt-get update、包发现、精确版本选择、下载与安装 |
| RPM 分离签名切换 | AlmaLinux 8、9、10 容器 | 使用真实 DNF 客户端遍历 repomd.xml / repomd.xml.asc 串行切换状态,并固定各组合的成功/失败行为 |
| S3 兼容传输 | 固定 MinIO 容器 | 验证 Bucket 列表、HEAD、GET、仅创建/CAS 写入、重放、条件式 Multipart Upload、对象元数据、重试与 Prefix 约束 |
| Release 打包 | Linux CI | 构建四个归档、两个 RPM、两个 DEB 与 SHA256SUMS;检查包内路径、Apache-2.0 元数据与协议文件字节 |
DNF 签名切换是协议测试,不是完整 Managed RPM 安装;APT 作业覆盖未签名 Plain 仓库,不覆盖 Managed 元数据签名。正式上线前,应使用部署中的确切 dnf/APT 版本、仓库 URL、访问策略与 签名策略完成验收。
仓库客户端契约
Plain RPM 仓库在包文件旁提供 repodata/;Plain DEB 仓库在包文件旁提供 Packages 与
Packages.gz。配置好客户端信任策略后,可通过 file:// 或 HTTP 使用。
Managed 客户端必须消费完整 Repository Root:
- APT 索引位于
dists/<dist>/main/binary-<arch>/,并引用根级pool/。Release声明 SHA-256 by-hash 索引;配置签名后增加InRelease与Release.gpg。 - RPM 元数据位于
dists/<dist>/<arch>/repodata/,通过相对位置回指根级pool/。必须服务 整个 Repository,不能只发布一个架构目录。
默认 dnf reposync 会拒绝规范 Managed RPM 的父级相对包路径。该工作流应使用
sow export rpm-leaf 生成自包含副本。导出使用本地包路径并带有
完成清单,但不是第二个规范 Repository。
发布 Provider
| Provider | 契约 |
|---|---|
filesystem |
发布到预先存在且安全的 file:// endpoint 下。Target GC 只有在缓存 grace 与存储/公共缺失证据成立后,才执行精确条件删除。 |
r2 |
通过 S3 兼容存储传输发布。Target GC 只写入精确候选报告,绝不删除远端对象。 |
两种 Provider 都在配置 Prefix 下发布同一棵完整 pool/ + dists/ 树。public_endpoint 属于
Target 校验的一部分;SOW 不创建 HTTP 服务、DNS 记录、Bucket Policy、CDN 或凭据。启用生产
发布前,应先在非生产 Prefix 验证这些由部署方负责的表面。
Filesystem 与 R2 的 HTTP(S) 端点共用 canonical-GET 内容 verifier;Filesystem 还可使用基于
描述符的 file:// 校验,R2 公共端点则必须为 HTTP(S)。Target Name、public_endpoint 与
max_cache_ttl 只能通过显式 publish --rebind
修改;Storage Identity 与 Prefix 不可变。
部署门禁
交付前必须通过深度校验,并检查物理变更计划:
发布后,再访问实际 repomd.xml 或 Release URL,并运行目标软件包管理器。本地构建、Provider
写入、HTTP 可达与客户端安装是四个独立检查。