跳转到主要内容

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

返回本页常规视图.

SOW 博客

发布注记与项目动态

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

1 - 设计归档

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

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

“设计历史”系列把最初的 v0.1 产品计划、现存 44 份 ADR、112 份日期化证据报告,以及 v0.2 重置过程,提炼为少量可持续维护的叙事。原始规划 Prompt、评审与执行记录仍可通过不可变源码 Tag 和 Commit 查阅;它们是第一手史料,不是与本站并行的第二套文档目录。

权威与证据

本专栏是持续维护的设计理由与决策历史权威来源。当前命令、配置与运维行为仍以 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-run,sow audit 也不是有效命令。

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

最终决策

SOW 0.4.0 保留原生 filesystem 与 r2 发布 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_endpoint 或 max_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.0、 v0.2.0、 v0.3.0 与 v0.4.0 源码 Tag 中。

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

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

这些工作留下了可复用的安全思想,但组合后的产品表面过大。太多概念都可能拥有或重新解释同一份 包体与公开路径;在小型本地仓库模型稳定之前,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_endpoint 与 max_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.xml、Release 或 InRelease 这样的 协议指针才是提交边界。只有新指针已经持久化、旧读者与缓存窗口已经关闭后,旧对象才可删除。

恢复成本服从状态成本

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。

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

1.7 - v0.2 重置:用 Plain 与 Managed 取代万能控制面

SOW 如何用两条小型执行路径、明确所有权、SQLite 状态与仓库核心,替换 v0.1 的 Git/CAS/Edge 平台。
开发设计线不等于发布版本

归档中的“v0.2”设计线最初使用 C2 View-local RPM Hardlink。这是一条真实实现过的开发线, 但在最终 v0.2.0 Tag 之前已经被单包体设计替换。正式发布的 v0.2.0 已经使用唯一正典 Pool 与纯元数据 View。

归档中仍有数份 v0.2/v0.3 文件声称“v0.2.0”使用 C2 Hardlink;它们写于 Tag 产生之前, 当时的“v0.2.0”指计划中的 Build。实际交付边界由最终 0.2.0 Changelog、Release Note 与源码 Tag 决定,三者都记录唯一正典 Pool 与纯元数据 View。

2026-07-31,v0.1 源码基线刚刚封存,SOW 就开始了一次产品重置。目标不是挽救每个 V1 子系统, 而是找出能够替代 Pigsty 日常软件包构建,同时保留既有安全教训的最小 Repository 产品。

最终形成的边界沿用至今:Plain 负责可重建目录,Managed 负责长期 Repository 所有权。

为什么不在 V1 上逐步删功能

v0.1 把五类不同任务耦合在一起:

  1. 构建有效 APT/YUM/Asset Repository;
  2. 维护 Package History 与 Snapshot;
  3. 迁移庞大的 Pigsty 旧拓扑;
  4. 发布到多套 Cloud/CDN Target;
  5. 执行商业 Edge Access Control。

逐项删除功能仍会保留 Git/CAS/Route 所有权模型。重置则直接追问:Repository Engine 真正需要 拥有哪些事实?

最初 Phase One 的边界与功能列表同样重要:

  • 不使用 V1 Git Database、Route/Manifest/Quarantine 模型或全局 Content-addressed Product Layer;
  • 不提供 Upstream Sync、Pull/Push/Copy/Move/Promote、Edge Entitlement、CDN Bootstrap 或商业拓扑;
  • 第一阶段不提供 Remote Publication/State、Object Storage、GC、Snapshot/Freeze、 Managed Export 或 Serving;
  • 不提供 Modulemd、Source-package Build、Web UI、跨 Repository 去重或 Multi-writer。

即使 V1 已经为 Upstream Sync 与 Channel Promotion 留下真实 Streaming、Fuzz 与确定性 Proof 证据,重置仍有意把它们移出产品。Package Acquisition 是输入边界,不应成为 Repository Engine 中的第二个 Owner。运维者可以用专门 Mirror/Download 工具准备包集合,再由 SOW 负责 Admission、 Membership、Build 与 Publication。部分 Phase-one 排除项——Publication、Retention、GC 与 External Export——在最终 v0.2.0 Tag 前,以更小的 Repository 所有权模型重新进入。

两条隔离的执行路径

Plain:重建调用者拥有的目录

Plain 模式处理调用者拥有的 RPM/DEB 目录:

sow create /srv/repo

它扫描 Package、渲染协议 Metadata、验证结果并最后安装 Pointer。Plain 只拥有生成的 Metadata, 不拥有 Workspace、Repository Database、Generation History 或长期 Membership。

因为 Desired State 就是“当前目录里的 Package”,正确恢复方法是重新运行构建。后续版本进一步 贯彻这一原则,删除 Plain Journal 与重复包体读取。

Managed:拥有生命周期与历史

Managed 模式引入包含多个独立 Repository 的 Workspace:

Workspace
├── sow.yml
├── .sow/                    私有配置、锁、SQLite、Journal
└── <repository>/
    ├── pool/                正典包体
    └── dists/               客户端协议视图

每个 Repository 拥有自己的 Pool、Package Object、Membership、Desired State、Built Generation、Changeset、Lock、Database 与 Recovery State。不同 Repository 不会暗中去重 或共享计数器。

Dist 是具名 RPM/DEB Membership Set。Architecture View 是 Membership 的投影,不是新的 Owner。 Neutral Package(noarch / all)会进入每个适用架构,但不会复制逻辑 Membership。

哪些决定替换了 V1 概念

V1 概念 v0.2 替代项
Git Commit/Ref 正典状态 Per-Repository SQLite 加显式 Filesystem/Journal Evidence
全局 CAS 与 Route Materialization Repository-owned Pool 与 Protocol View
View/Snapshot/Stable Ref Graph Dist Membership、Desired Revision、Built Generation、Retained State
一套万能命令面 封闭的 Plain 与 Managed CLI 契约
隐式旧拓扑 严格 sow.yml、显式 Name 与 Selector
跨产品 Publication Saga Repository Build 与 Target Publication 分离
直接复用 V1 Package 只复用窄 Parser/Renderer/Version/Signing Port

最关键的简化是:不借助 Git 或 Edge Router,也能完整解释私有控制状态与公共 Repository Byte。

Desired 与 Built

Managed 模式把中断状态变成显式事实:

  • Desired State 是 SQLite 中当前 Package 与 Membership 意图;
  • Built Generation 是最近一次完整渲染并验证成功的公共树;
  • Changeset 是两份 Built State 的精确差异。

Desired 可以前进,而 Built 继续保持有效。因此 Build 失败时,不需要假装公共树已经变化,也 不需要假装请求从未发生。Operation Journal 与 Status 会显示两者差异。

这个区别在后续所有版本中都保留。

P0 到 P3

实现按依赖顺序推进:

阶段 契约
P0 Plain create、真实 RPM/DEB Metadata、签名、Pigsty Filter、Pointer-last Replacement
P1 Workspace/Repository/Dist Lifecycle、严格 Config、Package Identity、Membership 与 Query
P2 Add/Remove/Policy/Build、Journal、Crash Recovery 与 Safe Mutation
P3 Check、Changeset、Handoff、规模、兼容性与 Clean-delivery Evidence

每个阶段都有 API Contract、Implementation Spec、Adversarial Review 与 Traceability/ Acceptance Record。这些文档在构建 Release 时有用;其中长期有效的决定由本文提炼,不再分别 成为公共权威。

最初的 Managed RPM 布局在 Root Pool 保存正典文件,并在每个 Architecture View 下创建 Hardlink。Metadata 使用安全的本地 pool/... href。这套 C2 布局让固定的 AlmaLinux 9 reposync 流程通过,而且 Alias 共享 inode,因此本地字节没有重复。

隐藏代价出现在交付边界:

  • Object Storage 会把每条 Path 展开成独立完整对象;
  • Archive 与跨文件系统 Copy 可能丢失 Hardlink 身份;
  • 每个 Dist/Architecture Alias 都看起来像另一份正典 Payload;
  • Retention 与 GC 容易依赖 inode/link-count;
  • 完整 Repository 无法一对一映射到 Static Object Key。

2026-08-05 的发布前设计快照准确记录了 C2 实现与本地验收,但它不是最终 v0.2.0 Release Contract。

单包体契约起草于 8 月 5 日,8 月 7 日以“next”设计提交,并在 8 月 8 日 Tag 前完成实现。 因此正式 Release 已经交付:

<repository>/
  pool/       每个 Package Object 一份正典包体
  dists/      纯元数据 APT/RPM View

默认 EL reposync 成为已接受兼容性限制;独立 RPM Leaf 改为显式外部 Export。完整理由见 单包体仓库。

重置复用了什么

团队没有从空代码开始,而只保留不携带 V1 所有权假设的窄组件:

  • RPM/DEB Parsing 与原生 Version Comparison;
  • Metadata Rendering、Compression、Signing 与 Validation;
  • Safe Filesystem Primitive;
  • 选定 Client Fixture 与 Fault Pattern;
  • 精确 Package Identity 与 Provenance Check。

Git State、Route Receipt、Edge Component、旧迁移机制与 Cloud Control Plane 只是参考材料或 删除候选,不是新 Core 的依赖。

Release 证据边界

开发线在 8 月 1 日记录 P0/P1 Acceptance,8 月 2 日记录 P1–P3 Acceptance,8 月 5 日完成 一次本地 Release-gate Run。最终 v0.2.0 源码在单包体实现、Packaging 与 CI 修正进入后,于 8 月 8 日打 Tag。

最终 Release 还交付 Filesystem/R2 Publication、Retained Generation、外部 RPM Leaf Export 与本地 GC;R2 Remote Delete 从一开始就被禁用,Target GC 只保留并报告 Candidate。

这个区别非常重要:

  • 8 月 5 日 C2 PASS 只证明产生它的开发快照;
  • 8 月 8 日 v0.2.0 Tag 与 Release Note 定义实际交付;
  • 后续 v0.3/v0.4 测试不会改写任何一项历史结果。

哪些东西仍在定义 SOW

  • Plain 与 Managed 是隔离模式,恢复成本不同;
  • Workspace 是 Config/Discovery Scope,Repository 是 Package/History Scope;
  • Package Object Identity 不可变,Membership 是逻辑多对多关系;
  • Desired 与 Built State 分离;
  • 写入使用稳定 Lock、有界 Journal、Pointer-last Install 与 Fail-closed Recovery;
  • CLI Syntax、JSON Envelope 与 Exit Class 构成封闭 Automation Contract;
  • Protocol、Client、Mirror Tool 与 Provider 兼容性分别报告。

当前形态见上手文档、系统模型与 设计原则。

第一手设计快照

后续 v0.3 与 v0.4 的强化过程见设计演进。

1.8 - SOW v0.1 到底证明了什么

整理 v0.1 的 112 份日期化证据报告:真实客户端、规模、故障、迁移与 Provider 结论证明了什么,又从未证明什么。
版本绑定的证据

本页每项结果都只属于当时记录的 v0.1 源码、布局、Fixture 与环境,不能证明当前 SOW 的兼容性或发布就绪。

v0.1 归档包含 112 份日期化 Evidence Report,覆盖 2026-07-11 至 2026-07-31。把它们全部 发布成博客只会降低公共站点的可用性:其中很多是窄故障闭包、重复的 Current-source Audit, 或某个具体实现 Seam 的执行记录。

但直接丢弃也会损失同样重要的信息。这些报告说明:哪些设计决定来自真实客户端与反例,哪些只 达到 Local/Mock 层,以及团队在哪些地方拒绝把不完整证据包装成 PASS。

本文保留的正是这条边界。

证据时间线

时间 报告数 主要工作
2026-07-11 3 真实 APT/DNF 基线、72,310 包 Manifest 扫描、50k Upstream Streaming
2026-07-12 35 APT/YUM 协议、CAS/GC、发布顺序、恢复、MinIO、性能、迁移、构建/供应链门禁与云验收夹具
2026-07-13 2 RPM Trust Closure 与 Selected-set Materialization Recovery
2026-07-14–16 14 旧拓扑、Family Migration、物理所有权、完整副本纳管与包签名 Inventory
2026-07-17–20 39 Cloudflare/R2/COS/Edge Safety、Provider Attestation、Cutover Receipt、配置上限与 Checkpoint-fenced Deletion
2026-07-22 10 Stage、Apply、Prune、Rollback 与 Residue Cleanup 的 Descriptor/Path Identity
2026-07-26–29 7 递归持久性、Hostile Writer、Replacement Outcome、Preserved Audit 与 Provider 修正
2026-07-30–31 2 Clean-room APT/YUM/Asset MVP 与最终 Legacy-source Baseline

哪些结论有较强证据

真实客户端与大仓库

最早的证据没有只验证生成的 XML/Text,而是使用真实 APT 与 DNF,并显式测量规模:

这些结果支撑了 Streaming 与 Bounded Concurrency 要求,但不证明后续每种布局都自动继承同样 的客户端行为与性能。

协议闭包与负反例

有些设计修正来自负面证明,而不是乐观实现:

延续下来的教训不是 V1 URL Scheme,而是:看似合规的仓库与某个客户端事务,是两个不同结论。

故障与恢复

归档覆盖被中断的 Sync、Publish、Materialize、Snapshot、Archive 与 Remote Restore。后期报告 进一步把文件操作绑定到 Descriptor 与精确 Path Identity,并覆盖 Rollback、Quarantine 与 Preserved Evidence。

代表性记录包括:

这也是当前 SOW 区分 Pre-commit Abandon、Post-intent Forward Recovery 与 Contradictory Evidence 的原因,而不是只提供一个笼统的“重试”。

旧系统迁移

迁移证据枚举旧 Make Target、物理布局、Repository Family、Consumer Config、Package-signature Inventory 与 Rollback,并在 Mutation 前使用 Read-only Copy 与精确 Receipt。

这证明了一条特定 Pigsty Cutover Path,却不支持把旧拓扑永久带进产品。最有价值的结果是一套 纪律:把迁移假设变成显式数据与可执行 Gate,切换结束后再让它退役。

Provider 与 Edge 实验

归档包含真实和模拟的 R2、MinIO、Cloudflare、COS 与 Edge 工作。较强报告都明确声明范围:

其中一些使用 Owner 指定的 Nonproduction Resource,另一些是 Offline 或 Mock Validation。 一项真实 R2 结果从未证明 COS 条件替换、EdgeOne Cache 行为或当前 SOW Target State Machine。

归档没有证明什么

即使证据很多,原文仍保留明确开放边界:

  • 单个 PoC 不能证明所有 APT、DNF、Proxy、Mirror Tool 或 Provider;
  • MinIO S3 兼容不等于 Cloudflare R2 或腾讯 COS 语义;
  • Design/Adversarial Review 不是 Implementation Evidence;
  • Local/Mock Provider 结果不是生产部署;
  • PASS 始终绑定 Source Revision、Fixture、URL Layout 与环境;
  • v0.1 证据不能验证后来的 Plain/Managed 或 Single-payload 设计。

这些限制是归档的优点,不应在整理历史时被“润色”掉。

哪些做法进入了设计方法

证据计划留下六条长期实践:

  1. 标明证据层。 Design、Source、Unit/Fault Test、Real Client、Provider、Artifact 与 Production 彼此独立。
  2. 保留负结果。 一个反例往往比另一个 Happy Path 更能定义安全边界。
  3. 把 PASS 绑定到不可变输入。 Source Revision、Container/Client Version、Provider、 Route 与 Fixture 都属于结果。
  4. 把规模作为契约。 大仓库必须测量 Memory、Call Count 与 Concurrency。
  5. 证据随模型退役。 历史证明可以解释过去,不能静默升级新布局。
  6. 让构建与供应链检查长期存在。 Toolchain Portability、Dependency Vulnerability、 Static Analysis、Dead-code Closure 与 Reproducible Delivery 是持续 Release Gate,不是一次性清理。

这些实践如今写入设计原则与当前 兼容性参考。

第一手资料

不可变的 v0.1.0 Evidence 目录 保留全部日期化报告。本文是维护中的解释,原文件则是审计轨迹。

继续阅读 v0.2 重置,可以看到这些教训如何在产品模型大幅缩小 后继续存在。

1.9 - SOW v0.1 决策账本:哪些被保留,哪些已退役

逐项整理 v0.1 现存的 44 份 ADR:当时解决什么问题,后来是延续、演化、仅服务迁移,还是随 V1 一同退役。
历史决策账本

v0.1.0 Tag 中现存 44 份 ADR,编号从 0001 到 0045,其中缺少 0006。 本文保留它们的意义,但不会把每份历史实现契约重新发布成当前指南。

即使实现已经退役,ADR 仍有价值:它记录团队拒绝忽略的失败模式、当时选择的边界,以及危险 操作进入执行前必须取得的证据。

下面把每项决策分为四种命运:

命运 含义
保留 原则以基本相同的边界继续存在于当前 SOW。
演化 问题与安全规则仍在,但所有权或实现已经改变。
迁移历史 决策只服务一次性的 Pigsty/V1 切换,不再是产品契约。
退役 决策属于已经退出当前 SOW 的 Git/CAS/Route/Edge 产品模型。

主要决策日期覆盖 2026-07-11 至 2026-07-28;ADR-0035 另有 7 月 29 日与 31 日的后续修订。 多数早期 ADR 在已经指导实现与证据工作的情况下,于 7 月 19 日集中进入 Git。v0.1.0 Tag Tree 已经核对,包含下表所代表的同一组 44 条 ADR Path。其中一条原文包含已废弃基础设施细节,因此 本文有意不提供直接 Deep Link。

核心状态、发布与信任

ADR 原始决策 后续命运
0001 Git 是正典状态,SHA-256 CAS 拥有包体,Ref 表达 View/History,发布按 Target Saga 执行。 演化。 Git/CAS/Route 所有权退役;单一 Owner、Pointer-last、独立 Target 状态与证据门禁保留。
0002 Public Route、不可变 Generation 与 Client/Provider 兼容使用独立验收门。 演化。 Route 机制退役;协议、客户端与 Provider 证据仍然分开。
0003 Snapshot 需要经过验证的 Generation Copy 与 Inventory-bound Retention。 演化。 CopyObject Snapshot Tree 退役;Retained Generation 只保存 Metadata 与 Refset,不复制包体。
0004 Edge Token 验证与部署共享一套跨 Provider 版本化契约。 退役。 Edge Entitlement 不再属于 SOW Repository Engine。
0005 每个旧 Make Target 明确映射到 SOW、Retire 或 Policy Reject。 迁移历史。 映射完成切换使命后退出活动产品。
0007 公共包与 Gated 包共享一个正典仓库,而不是独立 Pro Bucket。 退役。 商业 Edge 拓扑退出产品;更一般的单 Owner 原则保留。
0008 GC 服从完整 Reachability,破坏性操作需要显式确认。 保留。 当前本地/Target GC 仍要求闭包、精确身份、Grace 与能力。
0009 Remote Generation 可见后只能前滚恢复,不能虚构回滚。 保留。 Commit Intent 仍是 Abandon/Reconcile 与 Forward-only 的边界。
0010 Metadata Signing 绑定精确公私钥身份,轮换是 Repository-wide Transition。 演化。 当前 Signer Evidence 与 Trust Ring 保留精确身份,不再使用 V1 Ref 拓扑。
0011 Remote Delete 必须具备 Target Ownership、Inventory、Checkpoint、Grace 与条件删除证据。 保留。 R2 无法满足原子条件删除,因此继续 Report-only。
0012 Private Origin、Cache Topology 与精确 Purge Mapping 都是显式部署契约。 退役。 CDN/Private-origin 部署不属于当前 SOW。
0013 持久化 Plan 只是需要重新验证的恢复输入,不能盲目重放。 保留。 Managed Recovery 仍会重新绑定 Plan、File、Target Identity 与公共证据。
0014 RPM Provenance 记录精确 Packet Evidence,并区分 Ingestion Policy 与历史证明。 演化。 当前 Package Authentication 与独立 Trust Ring 替代 V1 Receipt 编码。
0015 配置只命名 Target,Published Ref 与 Checkpoint 属于正典状态。 保留。 Target-neutral Generation 与 Target-prefix Attempt/Checkpoint 仍是不同 Owner。
0016 Cloud Adapter 使用具体、有界 SDK/API 契约,不建立通用 Provider 抽象。 演化。 具体 Provider 边界保留,V1 Edge/COS 细节退役。
0017 已接纳 RPM 签名必须在稳定 Repository Keyring 与已打开包体身份上闭合。 演化。 v0.4 独立 Multi-ring Verification 强化了同一信任边界。
0018 选定包集合按一个有界本地事务完成 Stage、Validate、Commit 与 Recovery。 演化。 V1 Materialization 退役;当前 Plain/Managed Pointer-last Stage 保留事务原则。

旧拓扑、兼容性与迁移

ADR 原始决策 后续命运
0019 为旧客户端冻结 EL7 Metadata 格式与压缩行为。 迁移历史。 当前平台支持以兼容性文档而非这份 Freeze 为准。
0020 每条旧物理 Path 与 Selector Group 都有明确 Owner。 迁移历史。 它消除了切换歧义,但不是当前仓库模型。
0021 以精确冻结证据复现跨 EL yum/infra/{arch} Projection。 迁移历史。 这条特殊 Projection 已退出产品。
0022 物理所有权变化不能破坏 Package Repository 历史连续性。 演化。 Generation、Retained Ref 与 Migration Journal 在没有 V1 Route 的情况下延续连续性。
0023 正典 Git Object 在接纳和使用前必须重新绑定精确字节。 演化。 Git Authority 退役;Descriptor/Path/Digest 身份检查保留。
0024 Materialized Route Receipt 是窄化的 Read/Retirement Capability。 演化。 Route Receipt 退役;Capability-bound 检查与删除保留。
0025 Lock 绑定精确 Process Instance 与稳定 inode,不按时间自动偷锁。 演化。 稳定 Lock inode 与拒绝超时偷锁的原则保留,V1 Process-instance Lease 机制未保留。
0026 Offline Archive 使用 Durable Intent 与严格 Archive Admission。 退役。 Offline Archive Projection 已离开当前范围。
0027 旧包体只有通过 Path、Package 与 Provenance Admission 才进入正典状态。 迁移历史。 Adoption 计划结束;严格 Package Admission 以更窄形式保留。
0028 DEB 只打开 Metadata 所需的 Control Archive,并拒绝歧义 Container。 保留。 窄解析与有界输入仍属于 APT Package Boundary。
0029 Client Floor 与 EL8 Freeze 是显式 Owner Policy,不从代码猜测。 迁移历史。 当前平台策略由兼容性参考与 Release Note 决定。
0030 旧 YUM Missing Body 只能依据已评审 Blocker-set Digest 修复。 迁移历史。 这项窄 Negative Provenance 例外没有变成通用 Ignore。
0031 Gated 旧内容需要精确 Checksum Repair 与 Activation Evidence。 退役。 Pro Activation 离开范围;Fail-closed Repair 成为通用教训。

Provider 与 Edge 控制面

ADR 原始决策 后续命运
0032 只有一个 Owner 明确指定的 Cloudflare 测试 Tuple 可例外进入。 退役。 它只是窄化的历史测试例外,从未成为通用部署规则。
0033 Read-only Provider Readiness 拥有独立 Registry 与所有权证据。 退役。 Provider Bootstrap Registry 已离开当前 SOW。
0034 Worker Bootstrap 使用 Lease、两阶段恢复、精确资源与可逆状态。 退役。 Cloudflare Deployment 不再由 Repository Engine 负责。
0035 使用前 Attest Provider Identity、Runtime Binding、Log Sink 与 Lease Ownership。 退役。 这套 Edge 控制面退出产品。
0036 R2 缺少 Conditional DeleteObject,因此允许在确定性 Capability Probe 与多层 Identity/Fence Proof 后,显式启用 checkpoint-fenced 无条件删除降级。 演化;降级未继承。 Capability Probe 与 Fail-closed Default 保留,但当前 SOW 禁用 R2 Remote Delete,Target GC 只报告 Candidate。
0037 Gated 发布必须在上传机密字节前证明 Edge Denial。 演化。 Edge 产品表面退役,暴露前证明鉴权的原则保留。
0038 YUM 切换需要绑定 Endpoint、Generation 与 Trust Bytes 的短期 Receipt。 迁移历史。 它让危险切换可审计,随后退役。
0039 Caret 在 Go、Edge、Log 与 Origin Route 中只有一种规范 URL 拼写。 演化。 旧 Route 退役;Encode-once Path Canonicalization 保留。

有界配置与派生状态恢复

ADR 原始决策 后续命运
0040 在分配或执行前限制配置基数与展开后的拓扑。 保留。 当前 Parser 与 State Wire 仍有显式 Size/Cardinality Limit。
0041 未知 Final Projection Stage 应保留审计,不得猜测删除。 演化。 当前 Recovery 仍保留矛盾证据并 Fail Closed。
0042 Derived-state Replacement 有明确 Success、Rollback、Preserved 与 Blocked Outcome。 保留。 当前 Operation 报告精确 Recovery Outcome,不折叠成简单成败。
0043 文件 Mutation 绑定 Descriptor Identity,并诚实声明 Same-UID Hostile Writer 边界。 保留。 Path Safety Fail Closed,但不宣称保护超出 OS Ownership Boundary 的攻击面。
0044 Preserved Audit Copy 只能通过精确 Capability 与 Confirmation Token 退役。 演化。 V1 命令退役;Capability-bound Deletion 原则保留。
0045 Unjournaled Residue 必须有有界分类器,不能静默接纳或删除。 保留。 当前恢复仍区分 Known State、Safe Residue 与 Contradictory Evidence。

账本背后的共同模式

真正留下来的不是最复杂的 V1 机制,而是机制下面的小型不变式:

  • 每个事实只有一个 Owner;
  • Mutation 前绑定精确 Identity;
  • 不可变内容先准备,Pointer 最后提交;
  • Commit Intent 之后只能前向恢复;
  • 删除前必须取得完整证据;
  • 输入有明确上限,Non-goal 必须诚实;
  • 兼容性结论绑定实际测试过的 Client/Provider。

Git Database、Route Graph、Edge Bootstrap 与迁移命令都可以替换,这些不变式不可以。它们如今 维护在设计原则、系统模型与 发布与恢复中。

不可变的 v0.1.0 ADR 目录仍是第一手 资料。下一篇文章继续汇总独立的 v0.1 证据记录。

1.10 - SOW v0.1 原始计划:软件仓库的 Git

SOW 为什么最初选择 Git/CAS/Route/Edge 控制面,这套模型解决了什么,又为何后来围绕更小的 Repository Core 重置。
历史设计记录

本文描述 2026-07-11 启动、并在 2026-07-31 封存为 v0.1.0 源码基线的原始计划。 Git/CAS/Route/Edge 产品模型已经退役;当前行为以 SOW 文档和维护中的 系统模型为准。

SOW 最初并不是一个小型仓库索引器。原始目标是用一个 Go 控制面,替换 Pigsty 中庞大的 Makefile + rclone 工作流,统一管理 APT、YUM 与静态制品,覆盖本地状态、包生命周期、上游 同步、Channel、View、Snapshot、双云发布、CDN 行为、商业访问控制、迁移、恢复与验证。

当时采用的说法是:“制品仓库的 Git”。 这确实回应了一个真实问题:软件分发同时具有 不可变内容、可变名称、历史、晋级、发布与回滚。问题不在这个类比完全错误,而在它诱导 SOW 承担了过大的产品表面。

第一版设计要解决什么

旧系统已经发布了几十条被 APT、DNF、脚本与安装入口依赖的稳定 URL。替代系统既要保持这些 URL,又要提供 Makefile 工作流难以安全表达的能力:

  • 每个已接纳包体只有一个身份;
  • Desired State 与每朵云实际发布的状态分离;
  • Snapshot 与历史保留不能靠猜测哪些文件仍然存活;
  • 增量上传与 Purge 成本应与变更集成正比;
  • Metadata 准备完成、Pointer 尚未发布时崩溃,系统仍可恢复;
  • 上游索引、包签名与迁移例外都要有 Provenance;
  • 商业私有对象不能通过公共索引或 Cache Key 泄漏;
  • 兼容性必须来自真实 APT/DNF 客户端,而不只是元数据语法正确。

原始计划有意把这些问题视为同一套所有权系统。

v0.1 选择的模型

Git 是正典状态

.sow/state 是由内嵌 Go Git 库操作的普通 Worktree。Manifest、Ref、Provenance、配置摘要、 Target Checkpoint 与安全标签都是 Git 内容;SQLite 只是可以重建的查询投影。

这样,每次状态迁移都有持久历史,一次 Ref 更新可以成为本地 Commit Boundary。代价是: 包管理、发布、迁移与操作意图,都必须进入同一种 Git 形状的权威模型。

CAS 拥有字节

包体进入以 SHA-256 命名的不可变 Pool;公开树通过 Hardlink 指向 Pool,因此能否回收由 Reachability 决定,而不是由文件名决定。这让本地去重与历史保留成本很低,但也要求 Pool 与 物化树位于同一文件系统,并把 Path、Receipt 与 inode 身份变成产品契约的一部分。

Ref 表达产品语义

Repository、View、Snapshot、Stable、History 与每个 Target 的 Remote Ref 描述内容的意义和 保留边界。移动一个可变 View 不会删除 Stable 或历史可达性;删除 View 只改变引用,GC 始终是 另一项受证据约束的操作。

发布是一条 Saga

Cloudflare R2 与腾讯 COS 是相互独立的 Target。每个 Target 分别准备不可变对象、持久化 Commit Intent、移动协议 Pointer、Purge CDN、验证公共可见性,并推进自己的 Checkpoint。 一朵云成功后,不会仅因另一朵云失败而回滚。

当时已经形成了延续至今的顺序:

不可变包体
  -> 不可变元数据
  -> 持久 Commit Intent
  -> 可变协议 Pointer
  -> 公共验证
  -> Checkpoint
  -> Grace
  -> 证据门禁删除

这套顺序延续至当前 SOW。

Route 与 Edge 属于仓库正确性

公共树保持旧 URL,Generation-aware Route 选择不可变 Metadata;私有对象则位于 Cloudflare Worker 与 EdgeOne 共享的 Edge 契约之后。鉴权必须先于 Origin 访问,Token 不得进入 Origin URL、Cache Key 或日志。

这解决了机密性闭包,却也让 CDN Route、Token Verifier、Provider 部署、日志 Sink 与缓存拓扑 都变成仓库模型的前置条件。

这套设计为什么有吸引力

v0.1 具备不少强性质:

  • 每个持久事实都有明确 Owner;
  • 不可变字节与可变名称分离;
  • 本地与各 Remote Target 的发布状态独立可观测;
  • Pointer-last 发布让半完成工作可以恢复;
  • GC 必须计算完整 Reachability Closure;
  • Plan 与 Receipt 会重新验证,而不是盲目信任;
  • Client、Provider 与实现证据彼此独立;
  • 旧系统迁移面被当作数据,而不是口口相传的运维知识。

后续产品没有抛弃这些思想,而是在删掉大量承载机制后继续保留它们。

为什么后来必须重置

到七月底,SOW v0.1 已经包含:

  • 作为内部数据库协议的 Git Commit 与 Ref;
  • 全局 CAS 与 Hardlink Materialization;
  • Route、View、Snapshot、Generation、Projection、Receipt 与 Lease;
  • APT/YUM/Asset Sync 与旧系统迁移;
  • Cloud SDK、Edge Bootstrap、Provider Attestation、Purge、Token 与私有 Origin;
  • 44 份现存 ADR 文件和 112 份日期化 Evidence 报告。

每一项都在解决真实失败模式,但组合起来产生了几个问题。

第一,同一个包与公共路径被太多对象“拥有”:它同时是 CAS Object、Manifest Entry、View Member、Materialized Route、Snapshot Reference 与 Remote Target Object。

第二,本地软件仓库生成与 Cloud/Edge 生命周期强耦合。只想构建一个 YUM 或 APT 仓库的用户, 也要承担商业访问控制和分布式发布的概念成本。

第三,迁移计划变成了永久产品子系统。旧拓扑、Make Target、CDN 行为和 Provider 例外在迁移 时很重要,却不应永久进入软件仓库抽象。

最后,在 Hostile Writer 与 Crash 条件下恢复每条派生 Path,需要的 Identity、Journal、 Quarantine 与 Retirement 机制,已经超过核心任务值得承担的复杂度。

Upstream Sync 与 Channel Promotion 也是一次有意的范围裁剪。V1 已为 Package Fetch 留下真实 Streaming、Fuzz、URL 与 Proof-order 证据,但 Acquisition 会引入自己的 Policy、Retry、 Provenance 与 Remote Ownership。重置把包获取改为外部输入问题,让 SOW 可以拥有 Repository Admission 与 Publication,而不必同时成为万能 Mirror Orchestrator。

重置后保留下来的东西

v0.1 教训 当前维护形态
每个持久事实只有一个 Owner 系统模型中的 Repository 与 Target-prefix 所有权
正典数据与投影不同 一个 pool/ 加纯元数据 dists/
包体先准备,Pointer 最后提交 发布与恢复
Commit 后只能前向恢复 Managed Operation 与 Publication Journal
Plan 与 Path 都是不可信输入 精确身份、Containment 与 Rebind 检查
删除需要完整证据 引用闭包、Grace、Absence 与 Provider Capability
生成的 Server Config 是派生状态 完整 Repository Root 是 Hosting Unit,Serving 仍由运维者显式配置
兼容性是一张矩阵 平台与集成
历史证据不会自动升级 日期化 Design 与 Release 记录

被淘汰的东西

  • Git 产品状态与“SQLite 只是可丢弃缓存”的模型;
  • 跨产品 CAS 与 Hardlink 物化层;
  • Route/View/Snapshot 公共命令模型;
  • 生成的 Nginx Include、Route Receipt 与 V1 Serving 控制状态;
  • 内置 Upstream Sync、Promote 与旧 Make Target 迁移;
  • 把 Edge Token Entitlement 与私有 Origin 当作仓库前置条件;
  • 多云发布控制面与 Provider Bootstrap/Attestation;
  • V1 专属 Projection Receipt、Lease、Quarantine 命令与恢复表面。

一些实现思想后来以更小的形式回归,但所有权边界已经改变:当前 SOW 首先是 Repository Engine,而不是试图拥有周边所有问题的软件分发平台。

时间线

日期 里程碑
2026-07-11 Goal、Brainstorm、技术调研、客户端与规模基线
2026-07-12 Git/CAS/发布核心契约与首批协议/Provider 证据
2026-07-13–16 包信任、事务、旧拓扑与迁移闭包
2026-07-17–20 Provider Bootstrap、Edge 机密性、删除能力与输入上限
2026-07-22–29 Path Identity、Hostile Writer、Quarantine 与 Residue Recovery 强化
2026-07-30–31 Clean-room MVP 证据与 v0.1.0 源码封存
2026-08-01 起 Plain/Managed 重置,并最终移除 V1 Runtime

第一手资料与后续记录

不可变的 v0.1.0 docs/ 树保留了原始 架构契约、 需求追踪、 迁移材料、ADR 与 Evidence。 这些文件用于解释历史实现,不能覆盖本站维护中的历史叙事或当前产品文档。

接下来可阅读 v0.1 决策账本、 v0.1 证据账本与 v0.2 重置。

2 - 发布注记

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

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

v0.5.0 为已有 Plain YUM 仓库新增显式发布时间; 此前 Managed 校验与迁移方面的改进见 v0.4.0。

2.1 - SOW v0.5.0

SOW v0.5.0 为 Plain RPM 仓库增加显式发布时间,在保留确定性构建的同时,支持迁移已有 YUM 仓库维护流程。

SOW 0.5.0 主要解决已有平面 YUM 仓库接入 SOW 时的兼容问题。核心新增功能是 sow create --metadata-timestamp SECONDS:替换原有元数据生成器时,发布者可以保留合法的 发布时间,兼容已有 EL7 YUM 客户端的缓存。

安装方式见下载页面,已有工作区的升级步骤见 安装与升级。

已有 YUM 仓库的发布时间

EL7 YUM 会比较新 repomd.xml 中最大的元数据时间戳与本地缓存。如果新索引的时间更早, YUM 可能拒收它并继续使用旧索引。新的 checksum、不同的 revision 或更新的文件修改时间, 都不能替代这项比较。

此前 Plain create 始终写入零时间戳,便于复现输出,但直接接管已有正时间戳的在线仓库时会 遇到上述问题。0.5 增加了显式指定发布时间的入口:

# PUBLISH_TIME 由发布者根据已记录的发布历史选择。
sow create /srv/yum.candidate --metadata-timestamp "$PUBLISH_TIME"

SECONDS 接受 0 到 253402300799 的整数。负数、非整数、溢出或重复参数会作为 用法错误拒绝。该参数只属于 create,不是 Managed build、publish 或 RPM leaf export 的选项。

项目 0.5 的行为
默认 data 时间戳 仍为 0
显式时间戳 写入 RPM repomd.xml 的三条 data 记录
repomd.xml revision Plain 模式仍为 0
RPM 字节与包时间 此参数不改变它们
压缩 XML 与 gzip 头 只改此参数时保持原字节
DEB 索引 不受此参数影响
重复构建元数据 包字节与选项相同,输出相同,空跑不写文件;强制重签属于另外的包修改操作

SOW 不会自动读取当前时钟,也不会替调用者维护发布时间。维护脚本应在上传目录外保存历史最大 发布时间;内容不变且现有索引、签名有效时复用它们,真正更新时递增时间。恢复旧包集合也属于 一次新发布,不能把发布时间一起倒退。若此前转换已经把在线索引重置为零,重建历史下限时还要 纳入可信的转换前备份。

repomd.xml 一旦变化,原有分离签名就不再有效。发布者必须重新签名、验签后再对外提供索引。 --sign-with 签署的是 RPM 包,不会签署 repomd.xml。 迁移指南说明完整操作顺序,以及 SOW 命令与外围维护流程 各自负责的工作。

RPM 依赖兼容性

依赖投影继续采用现代 createrepo_c 对事务前、事务后脚本依赖的处理规则。本次增加了这些阶段 单独出现,以及 Cloudberry、Percona、Grafana 软件包中实际阶段组合的回归用例。

这次增加的是测试覆盖,既没有修改依赖解析,也没有退回 createrepo_c 0.20.1 的输出规则。 依赖条目数量或 pre="1" 属性不同,本身不代表依赖丢失。相关上游变更见 createrepo_c PR #427。

仓库与发布修复

本版修复了历史 Dist 删除清理误删重新加入其他 Dist 的包。签名策略与 pool 路径检查前移, 不兼容输入会在提交期望成员前被拒绝;旧程序留下的中断操作有明确的恢复出口,保留已提交的 数据,字节遭篡改时仍然拒绝恢复。

已经发布的 payload 路径不能复用为不同内容,本地 GC 也不解除该约束;发布恢复不会覆盖 不可变 payload。SQLite 的只读快照不再让已经持久化的写入因 checkpoint 受阻而报错。 外部输入允许经过符号链接父目录,输入末级仍保留原有校验。

APT 验签兼容 GnuPG 的末尾换行约定,不忽略其他内容差异。新生成的 GPG 元数据签名统一使用 SHA-256,不受本地摘要偏好影响;新的元数据构建要求签名 key 当前可用。二进制归档与安装包 同时补齐第三方许可声明。

Pool 路径按大小写折叠比较,字节完全相同也不例外,因此仅大小写不同的文件名不会成为第二个 URL。 在大小写不敏感的文件系统上,SOW 还会拒绝仅大小写不同的源目录:工作区内在加入时拒绝,发布到 filesystem target 时在任何上传之前拒绝。并发执行的 export rpm-leaf --hardlink 与 check 不再把未变化的 Pool 字节误报为完整性错误。本地 gc 会忽略只存在于远端的发布清单条目(例如 R2 仅报告模式下保留的 payload)。

sow repo migrate 会报告 Schema 变化,未迁移的 Repository 会给出应执行的命令。退出码 130 只用于命令自身被中断。file:// R2 凭据必须是普通文件;v0.4.0 中省略了非空且成员全被排除的 Dist 的 add 操作,现在可由下一次 build 收尾。

验收外围维护流程

迁移时应针对实际部署的 YUM/DNF 版本与架构,使用已有 metadata 缓存验收。保持 URL 和 repo ID 不变,从旧索引切换到 SOW 索引,再切换到下一次内容更新,检查索引刷新、实际包下载, 以及所配置的包与仓库签名校验。声明包数量或切换次数时,应同时保留命令、源码版本、输入清单 和对应结果,便于复核。

维护脚本保存时钟、复用签名、选择性重签的能力,不属于 sow create 新增的内置功能。 Plain 仍是平面目录的多文件重建流程:不生成旧式 SQLite 元数据或模块流,也不提供整个仓库的 原子切换。

升级与开发者说明

  • 0.3 或 0.4 Managed 工作区必须先备份,再显式执行 sow repo migrate。Schema v13 为候选包池路径 增加索引,sow/v3 配置与公共 pool/ + dists/ 布局保持不变。随后逐个 build、check, 一次性刷新受影响的 RPM 认证与 APT 元数据契约,之后未变化的 RPM 可复用匹配的 Built 证据。
  • 取消操作先记录失败终态,再清理未提交的包字节;Ctrl-C 中断返回 130。空 Dist 全部输入被 策略排除后仍能正常恢复。长读连接可以延后日志裁剪后的空间回收。
  • Agent 元数据签名固定时钟,通过每个身份/时间的一次小消息探签选择实际可用子钥。新 APT 发布拒绝弱签名摘要,历史验签与冻结操作恢复仍遵守原有语义。
  • 长期维护的文档集中在本站。兼容性与性能检查分别移至 qa/compat/、qa/perf/,共享 RPM 测试夹具移至 internal/testdata/;干净源码交付清单同步采用这些路径。
  • 源码工具链升级为 Go 1.27.1,依赖采用 x/crypto v0.56.0;发布工作流固定 GoReleaser v2.18.2 及其 action 提交。
  • S3 兼容集成测试改为使用按 digest 固定的 pgsty/silo 镜像,即 PGSTY 维护的 MinIO 兼容对象存储,因为上游 minio/minio 镜像已无法从 Docker Hub 获取。
  • 0.5.0 暂不支持 RPM v6 包格式的签名,签名工作流请使用 RPM v4 格式包。
  • 2026-09-29 的源码 govulncheck 未发现可达漏洞。所依赖模块仍包含 GO-2026-5932 通告,当前代码没有导入其受影响包;剥离后的二进制扫描仍可能报告模块命中。 这不是“模块无任何通告”的声明,最终发布提交仍需重跑扫描。
  • 打 tag 前,应针对最终发布版本完成全量 Go 测试、race、静态分析、漏洞与上游来源检查、 确定性源码交付、客户端集成及归档/安装包校验。本地结果不能替代同一提交的 CI 与 Integration 成功记录。

新建平面目录仍可沿用 sow create DIR。对于已有在线 YUM 仓库,请先阅读 迁移已有 YUM 仓库,并在切换维护命令前确认实际二进制的 create --help 中列出了 --metadata-timestamp。

2.2 - 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。每个归档包含 sow、README.md、CHANGELOG.md 与 LICENSE;Linux 原生包随二进制安装 Apache-2.0 许可证。

获取发布版本

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

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

2.3 - 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 中架构为 i386、i486、i586 或 i686 的软件包, 让它们继续进入仓库元数据与 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.4 - 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/、Packages 与 Packages.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 端到端验收。

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