跳转到主要内容

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

返回本页常规视图.

SOW 博客

发布注记与项目动态

SOW 的发布注记、设计札记与项目动态 —— Pigsty 出品的自包含 APT / YUM 软件仓库管理器。

1 - 设计归档

按日期归档 SOW 在所有权、仓库布局、发布、恢复与兼容性边界上的架构决策。

设计归档解释 SOW 为什么采纳某项契约、否决了哪些替代方案,以及结论实际达到了哪一层证据。 文章按 date 排列,记录设计理由首次形成可维护正文的日期;lastmod 记录后续编辑或与 实现对齐的日期。功能实际在哪个版本交付,仍以发布注记与源码 Tag 为准。

权威与证据

本专栏是持续维护的设计理由与决策历史权威来源。当前命令、配置与运维行为仍以 SOW 文档为准;历史版本的精确行为则绑定对应源码 Tag 与发布注记

每项运维结论都应与它真正达到的证据层一致:

设计 -> 实现 -> 聚焦测试 -> 真实客户端/Provider 实跑 -> 发布物

前一层通过,不代表后一层自动通过。平台与集成 记录自动化客户端、Provider 与文件系统覆盖;Release 制品属于独立交付门禁。

1.1 - 协同发布决策记录

SOW v0.4.0 保留原生发布 Provider 的最终决策,以及未交付的早期 rclone 提案边界。
SOW 0.4.0 已完成最终决策

本记录的早期版本曾提议采用 rclone Executor、publish --dry-run 与独立 sow audit 命令。这些表面均未进入 0.4.0:SOW 不依赖 rclone,publish 没有 --dry-runsow audit 也不是有效命令。

本页保留决策边界,避免历史提案被误解为当前产品。规范行为以 sow publish发布与恢复为准。

最终决策

SOW 0.4.0 保留原生 filesystemr2 发布 Provider。SOW 同时拥有 Repository 语义,以及 保持这些语义所需的传输操作:

  • 冻结 Generation 与精确 Change Plan;
  • 只创建的 Immutable Object 与 Conditional Mutable Write;
  • 持久 Commit Intent 与确定性 Pointer 顺序;
  • Attempt、Checkpoint、Inventory、Grace 与 Recovery Evidence;
  • Provider 校验与 Canonical Public Visibility Check。

公开承诺保持克制:发布顺序确定、可重启,并最终收敛到一个冻结 Generation;但它不是覆盖所有 Object Key 的原子事务,也不创建 DNS、Bucket Policy、CDN 配置或客户端信任策略。

为什么没有采用 Executor 提案

通用 Bulk-copy Executor 会增加另一个需要版本化的依赖,同时削弱 Plan 中单个 Object 与其 Conditional-write Receipt 的一一对应关系。对于无法保留 SOW 逐对象 SHA-256 Metadata 的批量工具, 还需要引入第二套 Checkpoint Identity 模型。

0.4 因而选择强化既有、更小的传输边界:有界条件式 Multipart Upload、分阶段 Header 与 Idle Progress Deadline、可重放安全操作的重试、精确 Changed-closure Verification,以及共用的 Public HTTP Verifier。这样保留了现有 Checkpoint 模型,也避免把传输迁移伪装成普通升级。

0.4.0 实际交付

原生增量发布

publish 计算精确 Generation Delta,在 Pointer 之前写入 Payload 与校验和寻址 Metadata,持久化 Commit Intent,按确定顺序推进 View,验证 Provider/Public Evidence,并记录 Applied Checkpoint。 Target 已经是当前代时返回幂等 No-op。

操作者确认的 Rebind

publish TARGET --rebind 可修改 Target Name、public_endpointmax_cache_ttl,同时保留稳定 Storage/Target Identity。Storage Endpoint、Provider、Region、Bucket、Repository 与 Prefix 变化 都必须配置新 Target。每次接受的 Rebind 都追加不可变 Audit Revision,并保留 Active Commit-intent Attempt 的前滚恢复。

强化公共可见性

Filesystem 与 R2 HTTP(S) 目标共用 Canonical-GET Verification、陈旧内容的 Cache-TTL 处理、短有界 Transient Retry、独立 Header/Body-idle Deadline 与 Oversize Guard。Filesystem 条件删除还会等待 Canonical Public 404/410 Evidence。R2 删除仍保持禁用、只报告候选。

操作者流程

sow build -r pgsql
sow check -r pgsql
sow changes -r pgsql
sow publish prod

changes 是只读的本地 Generation Delta,不是远端 Dry Run。publish 只接受已配置 Target,并由 自身完成远端 Preflight。机器可读结果使用 --json

报告配置漂移时,应先核对字段。只有允许的可变修正才使用 --rebind;Storage 或 Prefix 变化 必须创建新 Target。

恢复决策

是否已经记录持久 Commit Intent?
├── 否 -> 重跑 publish,或对账后 publish --abort
└── 是 -> 重跑 publish;前滚是唯一合法方向

重复原命令就是 Resume 操作,不存在 --resume。Attempt、Checkpoint、Provider 或 Public Evidence 互相矛盾时失败关闭,不会选择一段看起来方便的历史。

明确边界

  • 没有全前缀远端 sow audit 命令。sow check 证明本地 Repository;普通发布证明精确 Changed Closure 与受影响的 Public Pointer。
  • R2 Target GC 记录精确、只报告的候选,绝不删除远端对象。
  • 多独立写者、分布式锁、自动 Cache Purge、DNS 管理与任意 Executor Plugin 不属于当前契约。
  • Provider 写入成功不代表包管理器验收;部署中的 dnf/APT Client 与签名策略是独立 Release Gate。

参考资料

1.2 - SOW 设计演进:从 Route Graph 收敛到 Repository Core

v0.1 如何退役、v0.2 如何交付单包体 Repository Core,以及 v0.3-v0.4 如何继续强化。

本文对应 2026-08-10 围绕 Repository Core 完成的 SOW 收敛决策。它从早期规划、ADR、评审、 迁移与验证目录中提炼值得维护的结论,但不会把每份过程制品都变成另一套产品文档。

部分封存目录使用“v0.2”“v0.3”表示连续的发布前设计线;下文版本号只指正式 Git Tag。 C2 硬链接开发线在 v0.2.0 打 Tag 之前已经被替换。

简要历史

版本线 设计中心 历史状态
v0.1 Git/CAS 所有权、Route-aware Materialization、Edge 鉴权、Provider 工作流与 Pigsty 迁移 已发布 v0.1.0;后续被取代
v0.2 本地 Plain/Managed 引擎、Repository Scope 单包体树、纯元数据视图、Retained Generation 与 Target Scope 发布 已发布 v0.2.0;公共布局延续至今
v0.3 移除 V1、Package Facts 缓存、Plain 单遍构建与包体有界组提交 已发布 v0.3.0;公共布局不变
v0.4 单遍校验、独立 RPM Trust Ring、显式 Target Rebind,以及迁移/恢复强化 本文更新时的当前发布基线

每条已发布版本线的精确代码与陈述,仍保存在 v0.1.0v0.2.0v0.3.0v0.4.0 源码 Tag 中。

v0.1:问题很有价值,产品概念太多

第一版计划探索了一套宽广的软件分发控制面:Git-backed Canonical State、CAS、Route 与 View、 Materialized Snapshot、远端 Provider、Edge 鉴权、旧系统迁移与证据门禁删除。四十五份 ADR 和庞大的验证树,把许多单点失败模式都写得很清楚。

这些工作留下了可复用的安全思想,但组合后的产品表面过大。太多概念都可能拥有或重新解释同一份 包体与公开路径;在小型本地仓库模型稳定之前,Edge、Provider、Migration 与 Repository 已经 彼此耦合。

因此 v0.2 重置时淘汰了 Git 正典产品状态、独立 CAS 产品层、Route/View/Snapshot 抽象、 把 Edge Entitlement 作为仓库前置条件的设计,以及“用一个模型同时拥有本地生成、远端分发、 旧系统迁移与 CDN 策略”的目标。

v0.2:先建立小而明确的所有权模型

v0.2 有意把产品拆成两条运行路径:

  • Plain 扫描调用者拥有的目录,就地重建 RPM 或 DEB 元数据。
  • Managed 拥有一个包含独立 Repository 的 Workspace;每个 Repository 都有自己的 Pool、 Dist、SQLite 状态、锁、Operation 与恢复证据。

这次重置建立了沿用至今的持久对象词汇:Package Object、Membership、Desired/Built State、 Generation、Changeset 与显式 Operation Journal。它还确定了稳定锁文件、Pointer-last 文件替换、 Fail-closed 恢复,以及“测试或规格不能自动升级成真实客户端证据”的原则。

开发过程中,C2 布局仍会在每个 RPM View 中创建包体硬链接。封存档案准确记录了这条发布前 设计线,但不能把它重新标注成正式 v0.2.0 Tag 已交付的行为。

v0.2.0:把 Repository Root 变成交付单元

2026-08-05 的单包体决策从 RPM View 中移除了正典包体别名,并在三天后的 v0.2.0 正式交付。 一个 Repository 只有一个正典 pool/ 与纯元数据 dists/;完整根目录才是复制、托管、 鉴权与发布单元。

v0.2.0 同时明确了状态归属:

  • Package Object、Desired/Built、Generation、Changeset 与本地 Retention 属于 Repository。
  • Publication Attempt、Applied Checkpoint、远端 Inventory、Grace 与删除证据属于一个 Repository 加一个 Target Prefix。

设计明确否决跨 Repository 去重与分布式 Prefix 仲裁。这些不是“还没做的优化”,而是会改变 所有权与失败模型的另一类产品。兼容性导出始终位于正典状态之外。

v0.3:在同一模型上做减法与优化

v0.3 保持 v0.2.0 公共布局不变。它删除退役的 V1 CLI/Runtime 与迁移 Harness,引入 Package Facts 缓存,把 Plain 生成简化为可重建的单遍投影,批量展开 Membership,并用有界 组提交替代逐对象 Promotion。这些变化减少了工作量与代码表面,但没有改变 Repository pool/ + dists/ 的所有权。

v0.4:强化证据,不再增加一套模型

v0.4 保持 v0.2.0 引入、v0.3 优化的公共布局不变,把重点放在完整性与恢复。Managed 深度校验 收敛成有界单遍契约; RPM Trust 必须来自一条独立验证的 Trust Ring,不能拼接无关密钥的证据;可变 Target 设置只能 通过显式、可审计的 Rebind 修正;Migration、Publication Recovery 与公共交付检查也得到强化。

早期协同发布提案曾考虑把批量传输交给 rclone。最终决策继续使用原生 Provider,因为发布正确性 取决于 Provider Receipt、精确对象身份、提交顺序、Checkpoint State、公共可见性与恢复, 而不只是字节传输。

每次重构都保留下来的原则

v0.1 的产品模型虽然被淘汰,其中真正有用的教训仍然保留:

  • 每个持久事实都需要一个明确 Owner;
  • 包体与可重建投影不能共享 Authority;
  • 不可变包体与元数据先准备,可变 Pointer 最后提交;
  • Commit 之后只能前滚恢复,证据矛盾必须 Fail Closed;
  • 删除需要同时满足可达性、Grace、所有权与 Provider Capability;
  • Path、Client、Mirror、Provider、Release 与 Deployment 属于独立证据门禁;
  • 历史 PASS 始终绑定当时的版本、源码与环境。

这些原则如今落在一套更小的对象模型中,详见设计原则系统模型单包体仓库发布与恢复

文档权威边界

现在的维护边界很简单:

  • SOW 文档定义当前用户可见的命令、配置、格式与运维行为。
  • 本设计专栏定义维护中的设计理由与按日期记录的决策历史。
  • 发布注记定义版本级交付与升级声明。
  • 源码 Tag 保存精确的历史实现、测试与已被取代的原始文字。

原始规划 Prompt、中间评审、生成的证据摘要与废弃规格仍是历史材料。它们可以解释一个结论如何 形成,但不再是与本站并行的文档权威。

1.3 - 发布与恢复

发布、恢复、保留与安全删除仓库对象的 target-scoped 状态机。

构建与发布是两个独立状态迁移。构建产生与 target 无关的 Generation;发布把该 Generation 应用到一个供应商前缀,并记录足以在不猜测的前提下恢复的证据。

所有权拆分

Repository 作用域 Target prefix 作用域
Package Object Publication Attempt
Desired 与 Built 状态 Applied Checkpoint
Generation 与 Changeset 远端 inventory
retained 包体/元数据引用 grace 与删除证据

这项拆分避免把 filesystem 发布成功当成 R2 的证据,也避免一个 target 的半完成尝试污染另一个。

持久 Target Identity 与 Rebind

Target 有两层嵌套身份:Storage Identity 覆盖 Provider、Endpoint、Region 与 Bucket;Target Identity 再加入公共树 Prefix。首次绑定后,Repository Identity、两层 Target Identity 与三个 Authority Acknowledgement 都不可变。这样配置文件变化时,Attempt、Checkpoint、Grace 与 Maintenance 才不会被错误解释成另一命名空间的证据。

Target Name、public_endpointmax_cache_ttl 只能通过显式 publish --rebind 修改。首次绑定、迁移回填与 每次接受的 Rebind 都追加不可变 Binding Revision。Rebind 与 Publish 使用同一组独占锁,并在 事务内重查不可变字段;Storage 或 Prefix 变化必须使用新 Target。

Target Maintenance 未完成时,TTL 无论增加还是减少都被冻结;Filesystem Conditional-delete Workflow 还会冻结 public_endpoint。改变当前 TTL 不会重写历史 Grace:删除资格时间取已存 Deadline 与 Applied Checkpoint 加当前 Grace Minimum 中更晚的一个。

发布阶段

plan
  -> 只增不删的包体
  -> 校验和命名的元数据
  -> 持久 commit intent
  -> 按视图逐一写协议指针
  -> Provider 与 Canonical Public Verification
  -> applied checkpoint
  -> grace
  -> 证据门禁删除

提交意图之前,只允许写 add-only object。publish --abort 可以 reconcile 并移除私有 filesystem stage,但不会删除远端对象。SOW 保留精确的 abandoned-object evidence,让后续尝试可以安全识别 并复用相同字节。

首个可变 APT stable alias 或协议指针写入前,commit intent 必须已经持久化。此后唯一合法恢复 方向是前向。对象存储没有多 key 原子提交,因此 SOW 允许一个有界 mixed-generation 窗口, 并按确定顺序逐个把 view 前滚。

指针顺序

每个 view 都先安装不可变内容;签名伴随文件先于对应的可变指针:

RPM: 校验和命名 repodata -> repomd.xml.asc -> repomd.xml
APT: by-hash/direct indexes -> Release.gpg -> InRelease/Release

因此客户端一旦看到新指针,就一定能取到它声明的全部对象与签名。

公共验证

日常发布验证精确 Changed Closure 与所有受影响的最终 Pointer,不会从公网重新下载每个未变化 Package。Provider Receipt 与 Public Visibility 是两种独立证明。Filesystem 与 R2 HTTP(S) 目标共用 Canonical-GET Verifier:响应 Header 与 Body Idle 分别计时;Size+1 防止超长响应; 408/425/429/5xx 使用短有界重试;陈旧内容或缺失对象按 Cache TTL 重试。

No-cache 请求只是 Revalidation Hint;只有之后的普通 Canonical GET 看到期望字节,发布才能写入 Checkpoint。Filesystem 条件删除以 HTTP 404/410 证明公共缺失;陈旧 200 会重试至 TTL。 file:// 则使用精确、基于描述符的缺失证明。R2 Public Absence 与远端删除仍保持禁用。

Filesystem 别名会在持久绑定前按 Prospective Canonical Path 检查,包括大小写不敏感卷上的大小写 别名。Backend/Path Preflight 失败不会创建 Binding Row,也不会创建 Target Prefix。

已发布指针栅栏

一个 configured target 已经拥有 Applied Checkpoint 后,本地配置不能静默撤销该 target 仍拥有的 公开 Dist、architecture 或签名指针。操作者必须先退役/解绑 target,或者用新名称和新 prefix 发布替代品。

这样,危险的“配置漏了一项”会变成显式生命周期决策。

不复制包体的保留机制

Retained Generation 保存元数据、manifest 与引用集合,不保存另一棵包体树。Repository-local 可达性包括:

  • 当前 Desired/Built Membership;
  • retained Generation 引用;
  • active operation 与 recovery journal;
  • publication grace 与 recovery root。

只有正典 Pool 对象位于完整 closure 之外,且当前文件身份仍与候选记录精确一致时,本地 GC 才允许删除它。

远端删除是一项能力

远端物理删除还要求权威 inventory、target ownership、grace 到期、必要的缓存 absence evidence, 以及原子条件删除 primitive。无法满足这些条件的供应商仍可用于发布,但 SOW 只能报告不可达 候选,不能发出不安全的无条件删除。

r2 Provider 采用这种处理:发布路径已实现,但明确禁用远端物理删除,target GC 只报告候选。

恢复结果

持久边界 合法结果
没有 commit intent reconcile 后放弃或重试
已有 commit intent 只能前滚
已有 Applied Checkpoint 收敛并进入 grace
证据互相矛盾 失败关闭,不虚构状态

1.4 - 系统模型

连接软件包、Dist、Generation 与发布目标的对象模型和状态迁移。

SOW 刻意把模型分层:配置表达意图,数据库记录有主状态,公共树是确定性投影。 任何一层都不能悄悄替代另一层。

对象层级

Workspace
├── Repository
│   ├── Package Object
│   ├── Dist
│   │   └── Membership -> Package Object
│   ├── Desired 状态
│   ├── Built Generation
│   └── Retained Generation 引用
└── Publication Target
    └── Repository + provider + prefix

Workspace

Workspace 提供发现、配置与协调,拥有 sow.yml、私有 .sow/ 目录和稳定锁路径; 它不是包体去重域。

Repository

Repository 是最小的自包含公共归档,也是软件包身份的作用域。它拥有一份 pool/、一组 dists/、一个状态数据库和一条 Generation 序列。即使输入相同,两个 Repository 也不共享 包体与计数器。

Package Object

Package Object 由完成全部包签名后的最终 SHA-256 标识。名称、版本、release 与架构等逻辑坐标 只是元数据,digest 才是字节身份。重复加入同一 digest 是幂等操作;同一正典 Pool 路径出现 不同字节则是硬冲突。

Dist 与 Membership

Dist 是一套 APT 或 RPM 发布策略及其成员集合。Membership 是多对多关系:一个 Package Object 可以属于多个 Dist,但不会因此多出一份正典包体。中性包(all / noarch)会投影到每个 适用架构视图中,逻辑上仍只有一条成员关系。

Desired、Built 与 Generation

Desired 是配置和包操作想要的状态;Built 是最后一次完整渲染并校验通过的公共树。 Generation 是该 Built 状态的不可变清单,Changeset 则是两份清单之间的精确差异。

Desired 与 Built 分离后,中断状态可以被诚实描述:意图可能已经改变,但上一次提交的公共树 仍然有效。

Publication Target

target 把一个 Repository 绑定到供应商 endpoint 与 prefix。发布尝试、已应用 checkpoint、 远端 inventory、grace 与删除证据都属于 target。构建 Repository 与目标无关,发布则不是。

Storage Identity(Provider/Endpoint/Region/Bucket)、Target Identity(Storage 加 Prefix)与 Repository Identity 都是持久身份。只有 Target Name、Public Endpoint 与 Cache TTL 可通过操作者 确认的 Rebind 修改;每次接受的变化都会追加不可变 Binding Revision。

状态流

软件包输入
    -> 检查/签名/哈希
    -> Package Object + Membership
    -> Desired 状态
    -> 渲染并校验
    -> Built Generation + Changeset
    -> target 发布尝试
    -> applied checkpoint

每一条箭头都受 journal 或事务保护;后一阶段消费前一阶段的不可变身份,不重新解释可变路径。

公共状态与私有状态

公共:必须整体复制 私有:绝不能对外服务
<repo>/pool/ sow.yml
<repo>/dists/ .sow/ 数据库与 journal
协议签名与索引 锁、stage、恢复前镜像
Generation 描述的常规文件 凭据与供应商回执

只复制公共树可以得到一个有效的静态仓库,但它不是权威写入者:缺少匹配的私有状态时, 它无法安全续接发布历史、保留策略或垃圾回收。

锁边界

Workspace 生命周期操作取得 Workspace 锁;Repository 变更取得稳定 Repository 锁。 两者都需要时,固定先 Workspace、后 Repository,释放顺序相反。稳定锁路径避免 rename 或恢复 操作在新 inode 上意外形成第二个写入者。

模型假设每个配置 target prefix 只有一个权威 Workspace 且写权限独占;多个独立 Workspace 之间的分布式仲裁不在目标范围内。

1.5 - 设计原则

SOW 用来约束所有权、派生状态、发布与证据的一组不变式。

SOW 首先不是一个“元数据生成器”,而是一个所有权与状态迁移系统,只是它的输出恰好是 APT 与 RPM 仓库。下面这些原则让整套系统保持可推理。

每个持久事实只有一个主人

每类持久事实都只有一个作用域与权威:

事实 所有者
包体与软件包身份 Repository
Desired 成员关系与 Built 状态 Repository
Generation 与 Changeset Repository
发布目标绑定与 Revision Repository + Target Storage/Prefix
发布尝试与已应用 Checkpoint Repository + target prefix
远端 inventory、grace 与删除证据 Repository + target prefix

状态不会在 Repository 或发布前缀之间暗中共享。因此同一个包可以在不同 Repository 或 target 中各存一份;这是有意为之:本地去重不能制造分布式所有权。

正典数据与可重建投影

Managed 模式下 pool/ 中的包体是正典数据;Plain 模式下,顶层包文件本身就是正典。 协议索引、架构视图、报告与兼容导出都是投影,永远不会成为包体的第二个主人。

耐久规则服从权威来源。Managed 通过有记录的操作移除投影,且只有计算完所有 live / retained owner 的可达性才删除正典数据;Plain 则直接按当前包目录重新生成自有索引路径。

公共树是交付单元

Repository 根由 pool/ + dists/ 组成;整棵树才是静态托管、复制、鉴权与发布单元。 某个 RPM 架构 leaf 是客户端视图,但它不是一个独立拥有状态的 Repository。

sow.yml.sow/、锁、journal、凭据与恢复文件都是私有状态,绝不能随公共树对外服务。

包体做准备,指针做提交

发布顺序固定为:

包体 -> 不可变/校验和命名的元数据 -> 可变指针 -> 宽限期 -> 删除

包体与不可变元数据可以提前写入而暂不可见;repomd.xmlReleaseInRelease 这样的 协议指针才是提交边界。只有新指针已经持久化、旧读者与缓存窗口已经关闭后,旧对象才可删除。

恢复成本服从状态成本

Plain 没有需要保存的期望状态历史。它成本最低的正确恢复方式就是重新做一遍内容扫描并覆盖重建, 因此不保存事务 journal,也不花与包体大小成正比的 I/O 去证明上一次尝试。

Managed 状态不同。提交意图之前,如果精确 reconcile 能证明公共指针从未改变,操作可以放弃; 提交意图之后只能前向恢复。Managed 不猜测一次半完成发布“应该已经成功”,而是比较 journal、 manifest、checkpoint、供应商身份与公共树。

证据互相矛盾时操作必须停止。一次可见的拒绝,比仓库历史悄悄分叉更安全。

兼容性是一张矩阵

协议合规、普通客户端、镜像工具、对象存储布局、代理规范化与文件系统语义是不同问题。 SOW 分别记录它们,并用与结论对应的真实客户端或供应商验证。

因此规范 Repository 与导出的 RPM 镜像 leaf 是两种独立产物;其中一种产物的证据不能证明 另一种产物兼容。

证据不会自动升级

规格不是实现,单测不是真实客户端结果,本地 Hugo 构建也不是公网发布。日期化结果始终绑定 当时的源码 revision、环境与版本;布局变化后,必须重跑对应门禁,不能把旧 PASS 直接继承。

非目标让模型保持诚实

当前契约不承诺跨 Repository 去重、重叠多写者、bucket 全局协调、任意第三方镜像工具兼容, 也不在供应商缺少原子条件删除时承诺安全远端删除。这些排除项属于安全契约,不是“尚未补完”。

1.6 - 为什么 SOW 每个仓库只保留一棵包体树

用唯一正典 Pool 与纯元数据视图,替代 RPM 视图内包体硬链接的设计决策。

这项决策起草于 2026-08-05 的 v0.2 发布前设计收敛阶段,并在 2026-08-08 随 SOW v0.2.0 正式交付,随后延续为 v0.3 与 v0.4 的布局契约。它记录的是架构选择,不是笼统的兼容性 PASS:协议闭包、普通软件包客户端、镜像工具、静态托管与对象存储始终属于彼此独立的证据层。

最终决策

每个 SOW Repository 只拥有一棵公共树:

<repository>/
  pool/                         # 正典包体
  dists/
    <dist>/
      <architecture>/           # 只有协议元数据

一个 Package Object 在一个 Repository 内只有一个正典包体路径。把同一对象加入多个 Dist 或架构视图,只会增加 Membership 与元数据引用,不会再创建一份正典包文件。

完整的 pool/ + dists/ 根目录才是受支持的复制、静态托管、鉴权与发布单元。单个架构 Leaf 只是客户端视图,不是独立拥有状态的仓库。

从 C2 开发线到 v0.2.0

早期发布前 C2 开发线已经把每个包在根 Pool 中存一份,但会在每个 RPM View 内再创建包体 硬链接。这套布局可以让选定的 reposync 客户端得到自包含 Leaf,同时避免在本地复制字节。

问题在于,这种 inode 优化无法穿过完整交付边界:

  • 对象存储会把每条路径展开成独立 Key,别名重新变成重复包体;
  • 归档与跨文件系统复制可能展开或丢失硬链接身份;
  • View 内包路径看起来像正典数据,但所有权其实仍属于 Repository;
  • Retention 与 GC 容易误把 link count 当作产品状态;
  • 为迁就单个镜像工具,会迫使正典树服从它的本地输出假设。

在 v0.2.0 打 Tag 之前,最终单包体设计已经从正典 RPM View 中删除包体别名。归档中的 C2 文档仍准确描述那条开发线,但不能用来描述正式 v0.2.0 Release Tag。

元数据如何引用 Pool

APT 的 Filename 本来就是相对 Archive Root 的路径,因此软件包条目直接写正典路径:

Filename: pool/p/pkg/pkg_1.0_amd64.deb

RPM 则根据实际 View 目录与正典 Pool 路径计算每个 <location href>

view:    dists/el9/x86_64/
payload: pool/p/pkg/pkg-1.0-1.x86_64.rpm
href:    ../../../pool/p/pkg/pkg-1.0-1.x86_64.rpm

Renderer 与 Checker 复用同一个路径函数:先规范化 Repository-relative Path,再计算字面父级 导航,对数据字节恰好转义一次,基于目录 URI 解析结果,并要求 Round Trip 精确回到原 Pool 路径、且不能离开 Repository。

这样公共树仍然可搬迁:只要 pool/dists/ 的相对关系不变,同一份字节无需改写 元数据,就可以复制到另一目录、HTTP Origin、Bucket 或 Prefix。

被否决的方案

方案 为什么不进入正典布局
每个 RPM View 放包体硬链接或副本 制造路径别名与重复对象 Key,把文件系统优化伪装成持久所有权
包体 Symlink 无法移植到对象存储,在静态服务、归档与信任边界上也不安全
使用 /pool/... 绝对 href 不同客户端会把它解释成 Host Root 或 Repository-relative,也会破坏非根 Prefix
绝对 URL 或 xml:base 把元数据绑定到单一 Origin,无法原样搬迁
依赖 CDN Rewrite 或 Redirect Object 把外部 Router 变成仓库正确性的一部分
Workspace 或 Bucket 全局 CAS 引入 SOW 并不承诺的跨 Repository 所有权、锁、Retention 与删除协调

镜像工具兼容属于导出问题

普通软件包客户端只需解析元数据引用并取得包体;镜像工具还可能强制要求每个包都位于它收到的 View 目录之下。这是两种不同契约。

SOW 不会为了覆盖所有镜像工具而扭曲正典 Repository。需要独立 RPM Leaf 时, sow export rpm-leaf 会在仓库外生成带本地元数据的兼容制品。默认使用 Copy; --hardlink 只是受控只读边界内、同文件系统上的显式优化。导出目录不是 Generation、 发布源、Membership Owner 或 GC Root。

客户端与镜像工具结果统一记录在兼容性矩阵。 结构化 sow check 成功只能证明路径、摘要与闭包不变式, 不能把尚未运行的客户端或 Provider 单元格自动升级为 PASS。

结果

  • Package Identity、Membership、Generation、Retention 与本地 GC 都保持 Repository Scope。
  • Publication Attempt、Checkpoint、远端 Inventory、Grace 与删除证据保持 Target-prefix Scope。
  • 每个正典文件一对一映射到静态对象 Key。
  • 包体删除服从引用闭包与精确文件身份,不依赖 inode link count。
  • 不支持只复制某个 RPM 架构 Leaf;应复制 Repository Root 或显式创建 Leaf Export。

当前实现机制见包池与元数据视图;周边所有权与生命周期契约见 系统模型发布与恢复

2 - 发布注记

SOW 发布注记,涵盖功能、性能、正确性、打包与验证。

SOW 发布注记涵盖功能、性能、正确性、打包与验证,并按发布时间从新到旧排列。

2.1 - SOW v0.4.0

SOW v0.4.0 让 Managed 校验严格收敛为单遍认证,引入独立 RPM 信任环校验与安全发布目标重绑定,并强化迁移、恢复与公共交付验证。

SOW 0.4.0 是一次面向 Managed 仓库的完整性与恢复版本:它明确了深度校验的 I/O 契约, 禁止跨无关密钥拼装 RPM 信任,提供可审计的发布目标可变配置修正路径,并补齐 v0.3 迁移与 发布中断恢复的剩余缺口。

Plain 仓库行为与公共 pool/ + dists/ 布局均未改变。

从 0.3 升级

必须逐个显式迁移 v0.3 Repository

先停止全部 Workspace 写入并完成备份,再安装 0.4.0;在执行普通读写命令之前,对每个 Repository 运行一次 sow repo migrate REPOSITORY。迁移是显式、单向的;完成后不要再用 SOW 0.3 打开数据库。

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
sow check -r pigsty -C /srv/sow
sow check -r pgsql -C /srv/sow

Schema v11 修复了 v0.3 的 Dist 生命周期缺陷:过去一个 Dist 变化时,另一个仍 dirty 的 Dist 可能被遗漏,导致 Repository 错误显示为 clean;现在 Repository 状态始终在修改 Dist 的同一事务 中派生。迁移也会修复发布与签名者证据,但不会猜测 v0.3 未记录的历史签名身份:这类身份保持 显式未验证,不能变成 retained trust assertion。

Schema v12 回填只追加的发布目标绑定账本。首次绑定、迁移回填与之后每次操作者确认的 rebind 都会形成彼此独立、不可变的 revision。

每个物理包体只做一次真实性认证

每次 sow check 都会对每个唯一物理包体执行恰好一次权威内容哈希,即使缓存指纹仍然匹配也 不会跳过。证据绑定设备号、inode、size、mtime、ctime,以及真正被读取的文件描述符。同一物理 对象的硬链接可以共享证明;路径替换或并发身份变化会让证据失效并失败关闭。

验证 retained Generation、遍历最终 Manifest 与生成 sow changes 时复用同一份已认证证据。 因此增加 retained Generation 或 Dist 不会放大包体哈希次数。DEB 与无签名 RPM 只需一遍完整 包体流;带签名 RPM 最多再增加一遍从主 Header 到 EOF 的签名流,且与 Dist 或信任环数量无关。

软件包事实也只按本次选中的摘要集合,以有界、确定性的 SQLite 批次读取。64 MiB 包体的暖构建 不读取包体;指纹漂移与 facts 缺失共享同一次权威包体读取,不再分别扫描。

独立 RPM 信任环

RPM 嵌入签名会在每个候选 trust ring 内独立验证:所有可识别签名 packet 必须在同一个 ring 中 通过,且至少一条通过的路径必须认证 payload。这样既保留历史 CentOS v3/v4 签名与有意双签包的 支持,又禁止把不同 ring 的 packet×key 成功项拼成虚假的单 key retained 信任结论。

当前策略 ring、每个 retained 单 key ring 与组合 trusted ring 共用同一遍签名流。增加密钥只会 改变授权结论,不会增加读取包体的次数。

安全重绑定目标与公共验证

sow publish TARGET --rebind 是显式、由操作者确认的修正路径:它允许修正目标的可变配置, 同时保留原存储身份与恢复状态。

可通过 --rebind 修改 必须配置新目标
目标名称 Repository 身份
public_endpoint Provider 或存储 endpoint
max_cache_ttl region、bucket 或 prefix

普通 publish 遇到不一致时会明确提示 --rebind,绝不会静默接受新值。Rebind 与 publish 使用同一 组独占锁,追加一条审计 revision,并可继续前滚处于 commit-intent 的 active attempt。目标维护 未完成时禁止改变 TTL;filesystem 正在进行 conditional-delete 维护时禁止改变公共端点。历史 grace deadline 永远不会被缩短。

Filesystem 与 R2 的 HTTP(S) 公共端点现在共用同一套强化 verifier。响应 Header 与 Body 空闲进度 分别设定 Deadline,因此大对象只要持续前进,即使总传输超过两分钟也可完成。普通 canonical GET 始终是最终权威;no-cache 请求只用于促进 revalidation。陈旧内容与 404 按 max_cache_ttl 等待, 408/425/429/5xx 使用较短的有界重试窗口,超长响应失败关闭。Filesystem 公共缺失必须最终由 canonical 404/410 证明。R2 远端删除仍明确禁用,Target GC 只报告候选。

Filesystem 目标还会在持久绑定之前按 prospective physical path 检查别名,包括大小写不敏感 卷上的大小写别名。预检失败不会留下 binding row,也不会创建目标 prefix。

恢复、CLI 与实现收敛

  • 增量发布恢复只接受每个指针的精确旧 Checkpoint 或目标 Generation 字节;未知或第三种身份 仍然失败关闭。
  • 日常发布只验证精确 Changed Object Set,不再下载完整 Public Generation。No-op 与 Applied-checkpoint Recovery 复用完整私有 Inventory Evidence;Target GC 只扫描所需协议 Pointer。
  • R2 操作采用分阶段 Header/Idle Deadline、有界条件式 Multipart Upload、可重试读写,并在 Complete Multipart 前核对整对象 SHA-256。
  • Generation signer row 成为覆盖局部构建、Dist 增删、迁移、Retention 与本地 GC 的精确 Manifest side table。
  • 默认重复执行 sow add 时,即使 Package Object 已复用,也会收敛先前 --skip 或配置变更 留下的选中 dirty Dist。
  • sow rm --check 与写操作共享配置门禁,返回精确、完全不写入的预览;结果不再依赖 scratch 文件系统是否同设备。
  • 所有 Managed 命令现在都有面向人的输出;--json 继续提供稳定 sow.cli/v1 Envelope,并在 失败时保留已提交的部分结果及诊断性的 check/preview 结果。
  • 退役的 APT v1 Builder、External Sort、Empty-Dist 实现与 Git-tracked by-hash ledger 已移除; 当前 Plain/Managed APT 解析与渲染路径仍有覆盖。

验证与制品

Release 门禁覆盖完整 Go 测试、vet、staticcheck、dead-code reachability、漏洞与 RPM Fork Provenance 检查、Race Test、Linux amd64/arm64 构建,以及上述确定性包体/facts I/O 契约。 Release 二进制使用 Go 1.27.0 从 v0.4.0 源码 Tag 构建。从源码构建现在要求 Go 1.27.0 或更新 版本(0.3 为 1.26.5);AWS SDK、SQLite、压缩与加密依赖同步刷新,质量门禁固定 staticcheck v0.8.1、deadcode v0.49.0 与 govulncheck v1.7.0。

本次制品包含四个 Linux/macOS 归档、两个 RPM、两个 DEB 与 SHA256SUMS。每个归档包含 sowREADME.mdCHANGELOG.mdLICENSE;Linux 原生包随二进制安装 Apache-2.0 许可证。

获取发布版本

通过下载页面获取平台对应命令与已验证摘要,或查看 GitHub Release。安装后先运行 sow version, 逐个迁移 v0.3 Repository,再以 sow check 作为发布前门禁。

长期维护的契约见 Managed 工作区签名模型发布与恢复

2.2 - SOW v0.3.0

SOW v0.3.0 减少 Plain 与 Managed 仓库的重复包体工作,引入软件包事实缓存与有界提交,强化持久性,并收敛发布质量门禁。

SOW 0.3.0 围绕性能、持久性与产品边界完成了一次集中改进:Plain 仓库生成只需一遍包体处理; Managed 仓库消除了逐对象成员查询,复用经过认证的软件包事实,并用有界组提交提升载荷。正式 交付的二进制与发布流水线也统一收敛到 SOW 实际支持的仓库工作流。

Plain:一遍完成包体处理

默认无签名 sow create 路径通过 --jobs 控制并行度,每个 RPM 或 DEB 只做一次哈希与解析, 随后直接利用保留的检查结果渲染元数据。发布前只做最后一次软件包集合与文件 stat 快照检查, 无需再次读取和哈希所有包体,也能拒绝并发输入变更。

Plain 元数据是可以重建的派生状态。实现不会创建操作日志、恢复垃圾区、回滚前镜像,也不会 重复计算包体哈希。进程中断后重新运行 sow create,即可从软件包目录重建元数据。

Pigsty 预处理会保留 RPM Header 中架构为 i386i486i586i686 的软件包, 让它们继续进入仓库元数据与 repo_complete 校验清单;DEB i386 与精确匹配 Patroni 3.0.4 的过滤规则仍按预期生效。

Managed:扩展规模,不放松完整性

成员关系表增加了按包体 SHA-256 的反向索引,Desired 与 Built Membership 通过一次有序批量 投影展开,不再为每个对象单独查询。项目基准中,包含 5,000 个对象的 Dist 列表从约 4.1 秒 缩短至 33 毫秒;50,000 个对象从超过十分钟仍未完成,缩短到约 300 毫秒。

以不可变包体 SHA-256 为键的软件包事实缓存可以随时重建。Ingest 对每个新 RPM 或 DEB 完整 认证并解析一次,保留渲染元数据所需、与具体视图无关的事实。构建时批量载入这些事实并在内存中 匹配;记录缺失或损坏时,再从经过认证的包体惰性重建。

对于未改变的 Pool,暖构建使用设备号、inode、size、mtime 与 ctime 指纹校验载荷,包体读取量 降为零。指纹漂移时执行一次权威 SHA-256 校验并修复缓存路径;sow check 仍负责显式的完整 密码学审计。RPM 元数据产物与 DEB 架构索引使用有界 --jobs 并发,Generation Manifest 与 Changeset 行批量写入,最终规范化复用已有描述符快照,不再重新扫描 Pool。

有界提交与可观察构建

Managed 载荷提升采用有界的单写入者组提交。每批最多处理 512 个对象或 1 GiB:先创建公开 Pool 链接并持久化各目标目录,再移除 pending 名称并持久化共享的 pending 目录。崩溃因此只会留下 可恢复的“仅 pending”“精确双链接”或“仅 Pool”状态;两个名称同时持久丢失仍是完整性错误。

Pending 载荷在私有 0700 目录中直接使用最终的 0644 权限,因此提升只涉及命名空间变更。 Pending 源保护记录对象身份,不会在整个构建期间为每个软件包持有一个描述符,描述符占用保持 有界。发布载荷时也会先持久化目标目录项,再解除源名称。

长时间构建会为渲染、载荷提升、Dist 发布、规范化与收尾阶段追加结构化 build_progress 事件。它们可在 sow log 中观察,但不会为每个阶段增加数据库 checkpoint, 因此进度记录不会让本可成功的构建失败。

收敛发布与运行时边界

R2 发布传输只保留 SOW 实际使用的存储原语:list、head、get 与 conditional put。远端 GC 保持只报告、不删除对象的边界。未使用的云控制、CDN、Edge Worker、迁移程序与替代运行时路径 已从活动代码树移除,正式 CLI 只依赖统一的仓库核心;默认 go test ./... 因而覆盖完整的活动 实现。

正确性与发布质量

  • 本地 GC 可以安全移除 Generation 中记录的唯一大小写折叠 Pool 别名,这对大小写不敏感的 文件系统尤其重要。路径、大小与摘要仍必须标识同一不可变对象;存在歧义或漂移时拒绝执行。
  • 每个归档都包含 LICENSE。RPM 与 DEB 声明 Apache-2.0,并分别把许可证安装到 /usr/share/licenses/sow/LICENSE/usr/share/doc/sow/copyright
  • CI 强制执行格式化、模块整洁、vet、静态分析、死代码检查、性能测试编译、完整测试、竞态 测试、干净交付检查与软件包快照检查。
  • 集成门禁覆盖正式二进制的干净环境混合 RPM/DEB 流程、Ubuntu 22.04 上无签名 Plain APT 的 精确安装、AlmaLinux 8/9/10 的 DNF 签名切换探针,以及固定 MinIO 环境中的 S3 条件写操作。
  • 正式发布包含 macOS 与 Linux 的 amd64/arm64 归档、两个 Linux 架构的 RPM 与 DEB,以及 SHA256SUMS

获取发布版本

通过下载页面获取对应平台的安装命令,或在 GitHub Release 查看全部交付物。安装后运行 sow version,确认使用的是选定的二进制。

完整操作契约见 Plain 模式Managed 模式平台与集成

2.3 - SOW v0.2.0

SOW v0.2.0 提供 Plain 与 Managed RPM/DEB 仓库、可验证 Generation、签名、发布、保留、GC 与 RPM Leaf 导出。

SOW 0.2.0 是 Pigsty 出品的自包含 RPM/DEB 仓库管理器,Release 资产以单个 Go 可执行文件覆盖 Linux 与 macOS 构建目标。

两种工作模式

Plain 模式 给目录顶层已有的 RPM 与 DEB 文件就地生成索引:

sow create /srv/repo

它写入 repodata/PackagesPackages.gz。Plain 模式没有 Workspace、状态数据库、 Generation,也不生成或签署 DEB Release

Managed 模式 负责包成员关系与完整生命周期:

mkdir -p /srv/sow && cd /srv/sow
sow init .
sow repo new pigsty
sow dist new el9 --format rpm -r pigsty
sow add /path/to/packages/*.rpm -r pigsty -d el9
sow check -r pigsty

每个接纳的包体只在 pool/ 下保存一次;RPM 与 APT 客户端视图位于 dists/,并以不可变 Generation 落成。

生命周期控制

Managed 仓库提供:

  • 严格的 sow/v3 配置与显式成员策略;
  • RPM 元数据、APT 元数据与可选 RPM 包体签名;
  • 低成本 status 与作为发布门禁的九层 check
  • 可恢复的 filesystem 与 R2 发布尝试;
  • 显式保留 Generation 与基于可达性的本地 GC;
  • 面向拒绝 rpm-md 父级相对路径的消费者的独立 rpm-leaf 导出。

规范 Managed 树必须作为完整仓库交付。已配置目标使用 sow publish;其他传输方式应先把 整根复制到离线 staging,再原子切换。不要逐文件更新在线仓库。

兼容性证据

当前测试套件证明:两种格式都能由现行 CLI 在干净环境构建;Ubuntu 22.04 能消费 Plain APT 仓库;AlmaLinux 8/9/10 探针覆盖 RPM 分离签名行为;另有独立 S3 兼容 Provider Fixture。 这些探针本身不能证明完整的现行 Managed DNF/APT 或 R2 CLI 端到端验收。

确切声明边界见兼容性,全新安装入口见 快速上手