这是本节的多页打印视图。 .
SOW 博客
-
1: 设计归档
- 1.1: 协同发布决策记录
- 1.2: SOW 设计演进:从路由图到仓库引擎
- 1.3: 发布与恢复
- 1.4: 系统模型
- 1.5: 设计原则
- 1.6: 为什么 SOW 每个仓库只保留一棵包体树
- 1.7: v0.2 重置:用 Plain 与 Managed 取代万能控制面
- 1.8: SOW v0.1 到底证明了什么
- 1.9: SOW v0.1 决策账本:哪些被保留,哪些已退役
- 1.10: SOW v0.1 原始计划:软件仓库的 Git
-
2: 发布注记
- 2.1: SOW v0.7.1
- 2.2: SOW v0.7.0
- 2.3: SOW v0.6.1
- 2.4: SOW v0.6.0
- 2.5: SOW v0.5.0
- 2.6: SOW v0.4.0
- 2.7: SOW v0.3.0
- 2.8: SOW v0.2.0
SOW 的发布说明、设计决策与项目动态。SOW 是 Pigsty 出品的 APT 与 YUM 软件仓库管理器。
1 - 设计归档
设计归档解释 SOW 在仓库布局、状态模型和发布流程上的选择。 文章日期记录决策成文时间,修改日期记录后续修订。
了解当前设计,可以从以下文章开始:
历史系列介绍 v0.1 产品计划、设计决策与测试,以及 v0.2 重构和后续版本,并链接到对应源码快照。 当前命令见 SOW 文档,各版本变化见发布说明。
1.1 - 协同发布决策记录
SOW 0.4.0 保留了原生 filesystem 与 r2 发布后端。早期提案曾计划使用 rclone,并增加远端审计和预览命令。
最终选择原生后端,是因为 SOW 需要逐项记录条件写入及其结果。
当前命令语法见 sow publish。
为什么保留原生后端
发布计划为每个文件记录预期 SHA-256 和操作。后端需要创建不可变对象,避免覆盖已有内容,并按条件更新可变入口文件。 SOW 记录操作结果,用于中断后继续发布。
批量复制工具需要额外的身份与检查点模型才能保留这些规则。0.4 选择改进已有后端:
- 支持条件式分段上传;
- 分别设置响应头和响应体空闲超时;
- 对可重复执行的操作重试;
- 通过公共 URL 校验变化的文件。
这保留了已有发布状态,也省去了传输工具的迁移。
发布流程
原生后端先上传软件包与不可变元数据,再更新仓库索引。 随后检查公开文件,并为目标记录发布检查点。下次发布以该检查点为基准,计算需要更新的文件。
修改目标设置
目标名称、公共 URL 或缓存时间的变化记录在绑定修订中,原发布历史继续保留。 使用不同存储位置时,需要配置新目标。
继续中断的发布
SOW 为每次发布保存计划与进度。作出提交决定前,确认索引未变化后即可放弃该次发布。 作出提交决定后,恢复流程完成已保存的计划。 上传、校验和恢复因此可以使用同一份发布历史。
单独配置访问服务
运维人员配置 HTTP 访问、缓存与客户端信任。SOW 通过配置的公共端点检查发布结果。
完整执行顺序见发布与恢复,配置与命令见
sow publish。
1.2 - SOW 设计演进:从路由图到仓库引擎
SOW 在 v0.1 中探索了以 Git 为核心的软件分发控制面,在 v0.2 中转为仓库引擎。 本文记录这次变化,以及后续到 v0.7.0 的改进。
下文版本均对应正式 Git 标签,包括 v0.7.0。部分归档计划曾用“v0.2”“v0.3”指代开发中的设计; 其中在 RPM 视图内创建硬链接的 C2 布局,已在 v0.2.0 发布前被替换。
早期工作的细节见四篇配套文章:
- v0.1 原始产品计划说明最初的范围,以及范围为何过大。
- v0.1 决策账本记录现存的 44 份 ADR 及其后续变化。
- v0.1 到底证明了什么汇总 112 份按日期记录的测试与评审报告。
- v0.2 重置说明 Plain/Managed 模式拆分,以及从 C2 改为单包体布局的过程。
简要历史
| 版本 | 主要变化 | 公共布局 |
|---|---|---|
| v0.1 | Git/CAS 状态、Route、边缘鉴权、云端工作流与 Pigsty 迁移 | 已退役 |
| v0.2 | Plain/Managed 模式、每仓库一个包池、纯元数据视图、保留 Generation 与目标发布 | 引入沿用至今的 pool/ + dists/ 布局 |
| v0.3 | 移除 V1、缓存包属性、简化 Plain 构建、批量提交包体 | 不变 |
| v0.4 | 单遍校验、独立 RPM 信任环、目标重新绑定、迁移与恢复修复 | 不变 |
| v0.5 | 包路径保留、移除与恢复修复、签名检查、可复现的 Plain 时间戳 | 不变 |
| v0.6 | 保留上一版 Plain RPM 索引、记录 Managed 发布时间、强化文件检查、一致的状态读取、减少重复 I/O | 不变 |
| v0.7 | Plain RPM 裁剪、口令文件签名、tag 267 ECDSA/EdDSA 验签、caret 版本排序,以及迁移/发布/恢复修复 | 不变 |
早期版本的实现保存在源码标签中: v0.1.0、 v0.2.0、 v0.3.0、 v0.4.0、 v0.5.0、 v0.6.0、 v0.6.1 和 v0.7.0。
v0.1:问题很有价值,产品概念太多
第一版计划覆盖 Git 状态、内容寻址存储、Route 与 View、快照、远端存储服务、边缘鉴权和旧系统迁移。 现存 44 份 ADR(编号至 0045)记录了这些决策及其失败情形。
组合后的范围使所有权难以解释。多个对象都可能描述同一份包体与公开路径。 本地仓库生成也依赖云端、边缘服务和迁移概念。
v0.2 重置移除了 Git 权威数据库、独立 CAS 层、Route/View/Snapshot 命令模型和边缘访问控制, 把本地仓库构建作为核心任务。
v0.2:先建立小而明确的所有权模型
v0.2 将产品拆成两条执行路径:
- Plain 扫描调用者拥有的目录,就地重建 RPM 或 DEB 元数据。
- Managed 管理一个包含独立仓库的工作区。每个仓库都有自己的包池、Dist、SQLite 状态、锁和恢复记录。
这次重置引入了沿用至今的术语:包对象、成员关系、期望状态(Desired)、已构建状态(Built)、 Generation(构建快照)和变更集。稳定锁文件协调访问,协议指针在其他生成文件就绪后替换, Managed 操作日志记录中断后的恢复方式。
最初的 C2 实现仍会在每个 RPM 视图中创建包体硬链接。这一布局在发布前发生了变化。
v0.2.0:把 Repository Root 变成交付单元
单包体决策起草于 2026-08-05,并在 8 月 8 日随 v0.2.0 发布。
它移除了 RPM 视图中的包体别名。一个仓库只有一套 pool/ 与纯元数据 dists/,
完整仓库根目录是复制、托管、鉴权与发布的单位。
v0.2.0 也明确区分了本地与远端状态:
- 包对象、Desired/Built 状态、Generation、变更集与本地保留记录属于仓库。
- 发布尝试、已应用检查点、远端清单、缓存宽限期与删除记录属于一个仓库和一个发布目标前缀。
仓库之间不对包体去重。一个目标前缀只有一个所有者。 兼容性导出在原仓库之外创建独立副本。
v0.3:在同一模型上做减法与优化
v0.3 保持 v0.2.0 公共布局不变。它删除了 V1 运行时与迁移测试工具,引入包属性缓存, 并将 Plain 生成简化为可重建的单遍操作。成员关系改为批量展开,包体改为分组提交,减少文件系统操作。
v0.4:强化校验与恢复
v0.4 将 Managed 深度校验改为有界单遍读取。RPM 验证必须在一个信任环内完整通过, 不能拼接不同信任环的局部结果。显式重新绑定操作可以更新发布目标的可变设置。 迁移、发布恢复与公共交付检查也更严格。
早期协同发布提案曾考虑用 rclone 批量传输。最终设计保留原生存储适配器, 让 SOW 在同一状态机中检查对象身份、记录回执、控制提交顺序并恢复发布。
v0.5:保留包 URL 与已提交状态
v0.5.0 于 2026-09-29 发布,公共布局不变,托管状态升级为 schema 13。 新增的包池路径索引阻止不同字节占用已有 URL,也覆盖历史发布记录中的 URL。
该版本还修复了移除 Dist 后重新添加包的恢复问题,保留中断前已提交的更改,
并在修改期望状态前检查冲突的签名策略。Plain create --metadata-timestamp 为可复现 RPM 元数据
提供显式时间戳,默认值为零。完整变化见
v0.5.0 更新日志。
v0.6:校验后复用,保留已提交结果
v0.6 保持 schema 13 和公共布局不变。每次新元数据构建都记录一个发布时间,恢复时复用原值。 文件检查把已校验字节绑定到实际打开的文件,后续步骤可以在同一次操作内安全复用校验结果。
SQLite 读取使用一致快照。提交后的维护步骤失败时,命令仍保留已提交结果。 Plain RPM 更新保留当前与紧邻上一版元数据,让客户端能够继续获取旧索引引用的文件。
这些变化改进现有模型,不增加全仓库扫描或另一套恢复状态机。 行为与升级细节见 v0.6.0 发布注记。
v0.7:平面 RPM 裁剪、口令文件与恢复加固
v0.7.0 保持 schema 13 与公共布局不变。新增的 sow prune 为无状态平面 RPM 目录提供最高版本保留:
SOW 按 (name, arch) 对 .rpm 文件分组,只保留每组中 epoch:version-release 最高的文件,
先更新 repodata/ 再删除被取代的旧 RPM 文件,并在修改文件前拒绝存在最高 EVR 并列冲突的目录。
该版本还为加密签名密钥加入了口令文件支持(--gpg-passphrase-file、SOW_GPG_PASSPHRASE_FILE 与 sow.yml 中的 password_file),
支持 tag 267 中的 RPM 仅头部 ECDSA 与 EdDSA 签名,实现了 RPM caret(^)与 tilde(~)版本段排序,
并加固了 schema 迁移、沿用视图检查以及中断后的 gc / retain rm 计划恢复。完整细节见 v0.7.0 发布注记。
每次重构都保留下来的原则
v0.1 的部分规则继续用于更小的模型:
- 每个持久事实只有一个所有者。
- 包体与可重建元数据分开保存。
- 不可变包体与元数据就绪后,再替换协议指针。
- 提交意图持久化后,通过前向恢复完成操作。
- 删除前检查引用关系与精确身份;远端删除还要检查缓存宽限期与存储服务能力。
- 兼容性结果记录产生它的源码版本、客户端、存储服务和环境。
1.3 - 发布与恢复
构建生成本地快照(Generation)。发布把快照中的文件上传到目标,并记录进度,以便中断后继续。 每个目标分别保存检查点,记录上次完成发布的内容。
状态归属
| Repository 记录 | 目标记录 |
|---|---|
| 软件包、Desired 状态与 Built 状态 | 发布尝试与检查点 |
| Generation 与变更集 | 远端清单 |
| 保留的包和元数据引用 | 宽限期与删除记录 |
发布顺序
上传包体时,SOW 创建新对象或复用相同字节,不会用其他内容覆盖不可变路径。 修改第一个可变 APT 索引别名或协议入口文件前,SOW 会先记录提交意图。
每个视图都先上传引用内容,再更新入口文件:
更新期间,各视图可能暂时使用不同构建的索引。分离签名与索引也分两次写入。 因此,客户端可能需要重试索引刷新。公共校验完成后,SOW 才记录检查点。
公共访问校验
SOW 通过 public_endpoint 下载变化的文件和受影响的索引入口,并核对内容。
未变化的软件包无需重复下载。
公共端点可能仍返回缓存中的旧文件。SOW 会在 max_cache_ttl 内等待并重试。
校验失败时,发布不会记录新的完成检查点。
继续或放弃发布
重新运行原来的 sow publish TARGET 命令即可继续。恢复沿用保存的计划,不会改用更新的本地构建。
| 已保存的状态 | 下一步 |
|---|---|
| 尚未记录提交意图 | 重试发布,或用 --abort 核对状态并放弃该次发布 |
| 已记录提交意图 | 重试发布,完成保存的更新 |
| 已记录检查点 | 完成剩余维护;目标已完成更新时无需再次上传 |
| 文件身份冲突 | 检查报错中的文件和目标配置,再重试 |
--abort 清理符合条件的私有文件系统暂存目录,保留远端对象,并记录已放弃的上传。
后续发布可复用字节相同的对象。
目标身份
存储身份由后端、端点、区域和桶组成;目标身份还包括前缀。首次发布将这些值绑定到 Repository,并按预期目标路径(endpoint + prefix)检查文件系统目标是否彼此重叠或与工作区路径重叠;
后续发布根据该绑定查找对应历史。
修改目标名称、public_endpoint 或 max_cache_ttl 后,运行
sow publish TARGET --rebind。
SOW 会追加绑定修订记录。Repository、存储身份或前缀变化时,需要配置新目标。
修改 max_cache_ttl 前,先完成该目标尚未结束的维护。
文件系统目标正在执行条件删除时,需等删除流程完成后才能修改 public_endpoint。
缩短 TTL 不会提前结束已记录的缓存宽限期。
移除已发布的视图
目标仍在配置中且已有发布检查点时,SOW 会阻止撤销其现有 Dist、架构或签名入口。
SOW 会检查完整的已发布 Generation(包括沿用未变更的视图),并要求在发布目标撤销前先运行全仓库构建 sow build -r REPO。
需要停用该目标时,从 sow.yml 的 targets 中删除对应条目,再运行 sow config check。
这会保留旧远端文件与发布历史,不会删除远端仓库。
随后可以修改本地 Dist 配置并重建。要发布删减后的视图集合,配置一个使用不同前缀的新目标,并让客户端使用新 URL。
--rebind 不能修改目标前缀。配置规则见 sow publish。
保留与回收软件包
保留的 Generation 保存元数据、清单和指向共享 Pool 的包引用。本地垃圾回收会检查以下引用:
- 当前 Desired 与 Built 成员关系;
- 保留的 Generation;
- 进行中的操作与恢复日志;
- 发布恢复和宽限期。
只有引用全部消失,且文件仍与记录的身份一致时,SOW 才删除包。
因此,保留 Generation 可以保留其软件包,无需再复制一棵包目录。
如果 sow gc 或 sow retain rm 中断,SOW 会保留已记录的计划,重新运行命令即可安全完成或丢弃中断计划。
文件系统目标删除要求清单完整、目标由当前工作区独占且宽限期已过。 SOW 按记录的文件身份执行条件删除,再检查公共端点是否已返回文件不存在,最后记录结果。 R2 目标 GC 只报告候选并保留对象。
1.4 - 系统模型
SOW 用配置文件保存仓库设置,用 SQLite 记录托管状态,用公共目录向客户端提供文件。 模型分别记录选中的软件包、已完成的构建,以及已发布的内容。
对象
工作区
工作区包含 sow.yml、私有 .sow/ 目录和稳定的锁文件。
它集中保存配置,让命令可以找到所选仓库。
仓库
一个仓库拥有一份 pool/、一棵 dists/ 目录、一个状态数据库和一条 Generation 序列。
包去重只在该仓库内进行。两个仓库即使输入相同,也各自保存一份包副本。
包对象
SOW 使用包签名完成后的 SHA-256 标识包对象。名称、版本、release 和架构描述软件包,摘要标识实际字节。 重复加入同一对象会复用已有文件。同一包池路径对应不同内容时,SOW 报告冲突。
Dist 与成员关系
一个 Dist 为一种格式(RPM 或 DEB)选择软件包和设置。
同一个包可以属于多个 Dist,但只保存一个 Pool 文件。
架构中性包(noarch 或 all)出现在每个适用架构的视图中,无需复制包体。
Desired、Built 与 Generation
Desired 状态记录所选软件包和配置,Built 状态记录上一次完成的构建。 Generation 是该次构建的不可变清单,变更集则比较两份清单。
add --skip 会修改 Desired 状态而不重建索引。构建中断也可能留下未完成的操作。
用 sow status 查看差异,用 sow build 应用待构建变更或完成恢复。
发布目标
目标将一个仓库连接到存储后端和前缀。每个目标分别记录发布尝试、检查点、清单和清理状态。
目标身份与配置变更规则见发布与恢复。
状态流
Managed 操作用事务和日志记录进度。SOW 按待处理状态而非墙钟时间戳选择进行中的仓库变更,恢复时根据记录完成或回滚中断的步骤。
公共文件与私有文件
| 作为完整仓库提供访问 | 留在私有工作区中 |
|---|---|
<repo>/pool/ |
sow.yml |
<repo>/dists/ |
.sow/ 数据库与日志 |
| 生成的索引与签名 | 锁、暂存和恢复文件 |
公共目录副本可以提供软件下载。要继续管理仓库,还需保留匹配的私有工作区。 只有公共副本时,无法恢复成员关系、保留记录或发布历史。
锁与写入者
工作区和仓库各有自己的锁。多数读操作使用共享锁,写操作使用独占锁,避免多个进程同时修改同一份状态。
sow status 直接查询锁状态,便于查看正在进行的写入。
每个目标前缀必须由一个工作区负责写入。这些锁协调本地进程,不协调不同主机上的独立工作区。
1.5 - 设计原则
SOW 分别管理包存储、仓库构建和发布。以下原则说明各类状态由谁保存,以及操作如何使用这些状态。
每类记录由一个对象管理
| 记录 | 管理范围 |
|---|---|
| 包体与包身份 | Repository |
| Desired 成员关系与 Built 状态 | Repository |
| Generation 与变更集 | Repository |
| 目标绑定与修订记录 | Repository 与发布目标 |
| 发布尝试与检查点 | Repository 与目标前缀 |
| 远端清单与删除记录 | Repository 与目标前缀 |
Plain RPM 上一版索引标记(repodata/.sow-plain-rpm-history.json) |
Plain RPM 目录 |
每个 Repository 分别保存自己的包副本,每个发布目标也分别保存一份。 因此,各仓库和目标可以独立管理包的保留与删除。
保存包体,重建索引
Managed 模式将包体保存在 pool/ 中;Plain 模式使用输入目录中的包体。
索引和架构视图描述这些包,可以重新生成。
Managed 垃圾回收只删除已无当前引用、保留引用和恢复引用的包。Plain 模式直接根据当前目录重建索引;
sow prune 则先为保留的最高 EVR RPM 更新 repodata/,再删除被取代的旧 .rpm 文件。
提供完整公共目录
Managed Repository 根目录包含 pool/ 与 dists/。复制或提供整个根目录,客户端才能根据索引解析包路径。
如果消费者需要独立的 RPM 架构目录,使用 sow export rpm-leaf。
工作区的 sow.yml 和 .sow/ 放在 HTTP 根目录之外,其中保存配置、数据库、锁、日志和恢复文件。
先上传包,再更新索引入口
发布按以下顺序执行:
协议入口文件包括 repomd.xml、Release 和 InRelease。
SOW 先上传它们引用的文件,再更新入口文件。旧文件保留到宽限期结束,让缓存了旧索引的客户端仍可下载。
按已记录的状态恢复
Plain 操作中断后,重新运行 sow create 或 sow prune,即可扫描包目录、重建索引并完成剩余清理。
Managed 发布会记录计划和进度。记录提交意图前,SOW 可以核对状态并放弃该次发布。
记录提交意图后,重新运行 sow publish TARGET 完成已记录的更新。
文件身份冲突时,恢复会停止并指出需要检查的对象。
用实际消费者检查布局
包管理器需要解析包 URL,镜像工具还可能限制输出目录结构。 完整 Repository 与导出的 RPM 叶目录应分别测试,并记录客户端版本与目录布局。 兼容性参考列出自动化覆盖范围。
在本地协调写入
每个目标前缀由一个工作区负责写入。SOW 使用锁协调本地进程,不协调多个独立工作区。
R2 发布支持增量上传。R2 垃圾回收只报告候选并保留远端文件,因为该后端未实现所需的条件删除操作。
1.6 - 为什么 SOW 每个仓库只保留一棵包体树
这项决策起草于 2026-08-05,随 2026-08-08 的 SOW v0.2.0 发布。SOW 0.7.0 仍使用这一布局: 每个仓库保存一棵包体树,各发行版视图引用同一棵树。
决策
每个 Repository 的公共目录如下:
上图展示 RPM 视图。APT 索引位于 dists/<dist>/main/binary-<architecture>/,
Release 文件位于 dists/<dist>/。
一个包对象在一个仓库内只有一个包体路径。将同一个包加入多个 Dist 或架构视图,只增加成员关系和元数据引用。
复制、静态托管、鉴权和发布都以完整的 pool/ + dists/ 根目录为单位。
单个架构目录是客户端视图,不是完整的软件仓库。
从 C2 开发线到 v0.2.0
早期 C2 开发线在根包池中保存一份包体,再在每个 RPM 视图内创建包体硬链接。
这让部分 reposync 客户端可以直接使用独立的架构目录,同时节省本地空间。
硬链接在其他环节带来几个问题:
- 对象存储把每条路径存为独立对象,重新产生包副本。
- 归档和跨文件系统复制可能丢失硬链接关系。
- 视图内的包路径看起来像独立文件,实际仍由仓库管理。
- 保留与回收容易依赖链接计数,而不是实际包引用。
- 仓库布局需要迁就特定镜像工具的输出目录要求。
v0.2.0 发布前,最终布局删除了 RPM 视图中的包体别名。归档中的 C2 文档描述的是该开发线。
元数据如何引用包池
APT 的 Filename 是相对仓库根目录的路径,因此可以直接引用包体:
RPM 的 <location href> 使用从视图目录到包池的相对路径:
渲染器和检查器使用同一个路径函数。函数计算相对路径、转义文件名,再检查解析结果是否仍指向仓库内的原包路径。
只要 pool/ 与 dists/ 的相对位置不变,就可以把整个公共目录复制到其他目录、HTTP 站点、桶或前缀,
无需改写元数据。
其他方案
| 方案 | 未采用的原因 |
|---|---|
| 每个 RPM 视图放包体硬链接或副本 | 产生路径别名和重复对象存储键 |
| 包体符号链接 | 对象存储不支持,静态服务与归档的处理也不一致 |
使用 /pool/... 绝对路径 |
客户端可能从站点根目录解析,破坏带前缀的仓库 URL |
绝对 URL 或 xml:base |
将元数据绑定到一个站点,搬迁时需要修改 |
| CDN 改写或重定向对象 | 仓库依赖额外的路由配置 |
| 工作区或桶级共享包池 | 需要跨仓库协调锁、保留与删除 |
为镜像工具导出独立目录
包管理器需要解析元数据引用并下载包体。镜像工具还可能要求所有软件包都位于指定视图目录之下。
需要独立 RPM 架构目录时,运行 sow export rpm-leaf。命令在仓库外生成包文件和指向本地文件的元数据;即使所选 RPM Dist 在当前已构建 Generation 中沿用了未变更视图,同样可以直接导出。
默认复制包体;同一文件系统可使用 --hardlink,但导出包必须保持只读,因为写入硬链接也会修改包池文件。
导出目录不属于 Generation,不是发布源,也不参与成员关系或垃圾回收。
客户端与镜像工具测试见兼容性参考。
用 sow check 校验本地路径、摘要和文件引用,用对应客户端测试安装与镜像下载。
结果
- 包身份、成员关系、Generation、保留与本地 GC 都在仓库内管理。
- 发布尝试、检查点、远端清单、宽限期与删除记录按目标前缀管理。
- 每个包文件对应一个静态对象存储键。
- 删除包体前检查引用和文件身份,不依赖硬链接计数。
- 复制完整仓库根目录,或通过导出命令生成独立的 RPM 架构目录。
1.7 - v0.2 重置:用 Plain 与 Managed 取代万能控制面
2026-07-31,v0.1 源码基线封存后,SOW 开始了一次产品重置。 目标是构建一个小型仓库引擎,替代 Pigsty 日常软件包构建工作流。 重置保留了已有的完整性与恢复经验,同时移除了大部分控制面。
最终形成的分工沿用至今:Plain 重建调用者拥有的目录,Managed 管理仓库及其历史。
开发设计最初在 RPM 视图内使用 C2 硬链接,随后在 8 月 8 日发布 v0.2.0 前被替换。 部分归档计划曾把 C2 构建称为“v0.2.0”,但正式标签已经采用单一包池与纯元数据视图。
为什么不在 V1 上逐步删功能
v0.1 把五类任务耦合在一起:
- 构建有效的 APT/YUM/静态制品仓库。
- 维护包历史与快照。
- 迁移庞大的 Pigsty 旧拓扑。
- 发布到多个云存储与 CDN 目标。
- 执行商业边缘访问控制。
逐项删除功能仍会保留 Git/CAS/Route 所有权模型。 重置直接重新确定仓库引擎需要管理哪些事实。
第一阶段排除了:
- V1 Git 数据库、Route/Manifest/Quarantine 模型与全局内容寻址存储层;
- 上游同步、拉取、推送、复制、移动、晋级、边缘权限、CDN 初始化与商业拓扑;
- 远端发布与状态、对象存储、垃圾回收、快照与冻结、Managed 导出和托管服务;
- modulemd、源码包构建、Web 界面、跨仓库去重与多写入者操作。
V1 已经为上游同步和渠道晋级积累了流式处理、模糊测试与确定性验证结果,重置仍将它们移出产品。 包获取改为外部任务。运维者可以用专门的镜像或下载工具准备包集合, 再由 SOW 负责接纳、成员关系、构建与发布。 发布、保留、垃圾回收和外部导出后来在 v0.2.0 正式标签前回归,并遵循更小的仓库所有权模型。
两条隔离的执行路径
Plain:重建调用者拥有的目录
Plain 模式处理调用者拥有的 RPM 或 DEB 目录:
它扫描包、生成协议元数据、验证结果,最后替换协议指针。 Plain 负责生成的元数据,不维护工作区、仓库数据库、Generation(构建快照)历史或长期成员关系。
期望状态就是当前目录中的包,因此中断后重新运行构建即可恢复。
后续版本进一步简化了这条路径,移除了 Plain 日志与重复包体读取;v0.7.0 又在同一目录所有权与重跑恢复模型下加入了无状态最高 EVR 保留命令 sow prune。
Managed:拥有生命周期与历史
Managed 模式引入包含多个独立仓库的工作区:
每个仓库管理自己的包池、包对象、成员关系、期望状态、已构建 Generation、变更集、锁、数据库与恢复状态。 仓库之间不自动去重,也不共享计数器。
Dist 是具名的 RPM 或 DEB 包成员集合。架构视图是成员集合的投影。
架构无关包(noarch / all)会进入每个适用架构,而逻辑成员关系只有一份。
哪些决定替换了 V1 概念
| V1 概念 | v0.2 替代项 |
|---|---|
| 作为权威状态的 Git 提交与引用 | 每仓库 SQLite,加显式文件系统记录与操作日志 |
| 全局 CAS 与 Route 物化 | 仓库自己的包池与协议视图 |
| View/Snapshot/Stable 引用图 | Dist 成员、期望状态版本、已构建 Generation 与保留状态 |
| 一套通用控制命令 | 分开的 Plain 与 Managed 命令接口 |
| 隐式旧拓扑 | 严格的 sow.yml、显式名称与选择器 |
| 跨产品发布流程 | 仓库构建与目标发布分离 |
| 直接复用 V1 模块 | 只移植解析、生成、版本比较和签名等独立组件 |
简化后,不借助 Git 或边缘路由器,也能解释私有控制状态与公共仓库内容。
Desired 与 Built
Managed 模式明确记录中断状态:
- 期望状态(Desired):SQLite 中当前的包与成员关系。
- 已构建 Generation(Built):最近一次完整生成并验证成功的公共树。
- 变更集(Changeset):两次已构建状态的精确差异。
新的 Built Generation 完成前,Desired 可以先变化。 操作日志记录未完成的工作,状态查询显示 Desired 与 Built 的差异。 恢复过程根据记录的操作阶段决定还需要完成哪些步骤。
P0 到 P3
实现按依赖顺序推进:
| 阶段 | 内容 |
|---|---|
| P0 | Plain create、真实 RPM/DEB 元数据、签名、Pigsty 过滤、最后替换协议指针 |
| P1 | 工作区、仓库与 Dist 生命周期,严格配置,包身份、成员关系与查询 |
| P2 | 添加、移除、策略与构建操作,日志、崩溃恢复与安全修改 |
| P3 | 检查、变更集、交付、规模、兼容性与发布验证 |
每个阶段都有 API 契约、实现规格、对抗评审与验收记录。
C2 Hardlink 绕路
最初的 Managed RPM 布局在根包池保存包文件原件,并在每个架构视图下创建硬链接。
元数据使用本地 pool/... 路径。
这套 C2 布局通过了固定版本的 AlmaLinux 9 reposync 测试。
别名共享 inode,因此不占用重复的本地包体空间。
部署时,这种布局产生了额外成本:
- 对象存储会将每条路径保存为独立的完整对象。
- 归档与跨文件系统复制可能丢失硬链接关系。
- 每个 Dist 与架构下的别名都表现为独立包文件。
- 保留与垃圾回收容易依赖 inode 和链接计数。
- 完整仓库无法一对一映射为静态对象键。
2026-08-05 的快照记录了这套 C2 实现与本地测试结果。
单包体契约起草于 8 月 5 日,8 月 7 日以“next”设计提交,8 月 8 日正式标签前完成实现。 最终发布的布局是:
默认 EL reposync 因而成为已知兼容性限制,独立 RPM 叶目录改为显式外部导出。
完整理由见单包体仓库。
重置复用了什么
重置保留了不依赖 V1 所有权模型的独立组件:
- RPM/DEB 解析与原生版本比较;
- 元数据生成、压缩、签名与验证;
- 安全文件系统操作;
- 部分客户端测试数据与故障测试;
- 精确包身份与来源检查。
Git 状态、Route 回执、边缘组件、旧迁移机制与云控制面只作为参考或待移除代码, 不再是新核心的依赖。
从验收到发布
开发线在 8 月 1 日记录 P0/P1 验收,8 月 2 日记录 P1–P3 验收,8 月 5 日运行本地发布检查。 单包体实现、打包和 CI 修正进入后,最终 v0.2.0 源码于 8 月 8 日打标签。
正式版本还交付了文件系统与 R2 发布、Generation 保留、外部 RPM 叶目录导出和本地垃圾回收。 R2 远端删除从一开始就被禁用,针对 R2 的 GC 只报告候选对象。
哪些东西仍在定义 SOW
- Plain 与 Managed 是隔离模式,恢复成本不同。
- 工作区负责配置与发现,仓库负责包与历史。
- 包对象身份不可变,成员关系是逻辑多对多关系。
- Desired 与 Built 状态分开记录。
- 写入使用稳定锁文件,最后替换协议指针。Managed 日志记录中断操作,Plain 通过重新构建恢复。
- 命令语法、JSON 输出格式与退出码提供明确的自动化接口。
- 协议、客户端、镜像工具和存储服务分别验证兼容性。
第一手设计快照
- v0.2 开发设计快照: 产品需求、API、C2 架构、评审与本地验收。
- 单包体“next”设计: 正式标签前采纳的设计。
- v0.2.0 发布注记与 v0.2.0 源码标签:正式发布内容。
后续 v0.3 到 v0.7 的改进见设计演进。
1.8 - SOW v0.1 到底证明了什么
v0.1 归档包含 112 份报告,日期从 2026-07-11 到 7 月 31 日。 它们记录了客户端测试、规模测量、故障恢复、迁移和云存储实验。 本文汇总主要结果,并链接到代表性报告。
每项结果对应报告中记录的源码版本、测试数据、客户端与环境。 当前支持情况见兼容性参考。
证据时间线
| 时间 | 报告数 | 主要工作 |
|---|---|---|
| 2026-07-11 | 3 | 真实 APT/DNF 基线、72,310 个包的清单扫描、5 万个上游包的流式处理 |
| 2026-07-12 | 35 | APT/YUM 协议、CAS/GC、发布顺序、恢复、MinIO、性能、迁移、构建与供应链检查、云测试工具 |
| 2026-07-13 | 2 | RPM 信任验证与选定包集合的物化恢复 |
| 2026-07-14–16 | 14 | 旧拓扑、仓库族迁移、物理所有权、完整副本纳管与包签名清单 |
| 2026-07-17–20 | 39 | Cloudflare/R2/COS/边缘服务安全、服务身份验证、切换回执、配置上限与检查点约束下的删除 |
| 2026-07-22 | 10 | 暂存、应用、清理、回滚与残留删除中的文件描述符和路径身份 |
| 2026-07-26–29 | 7 | 递归持久化、恶意并发写入、替换结果、保留核查副本与存储服务修正 |
| 2026-07-30–31 | 2 | 从空环境验证 APT/YUM/静态制品最小可用产品,核对最终旧版源码基线 |
哪些结论有较强证据
真实客户端与大仓库
最早的测试使用真实 APT 与 DNF 客户端,并测量处理大仓库的成本:
- 真实 APT/DNF 客户端兼容;
- 72,310 个包的清单扫描;
- 5 万个上游包的流式处理;
- 独立的 5 万包 APT、YUM、物化、发布计划与旧包纳管测试。
这些测量支持了大仓库采用流式处理与有界并发的要求。
协议完整性与异常情形
失败的实验找出了协议设计的限制:
- APT 固定别名反例 表明旧客户端行为限制了别名切换的原子性。
- YUM 原始别名与签名桥接反例 区分旧 BaseURL 兼容与固定到 Generation(构建快照)的元数据。
- YUM Generation 原子性 记录哪些协议指针操作可以原子化,哪些不能。
这些实验说明,协议格式与客户端事务需要分别测试。
故障与恢复
归档测试了同步、发布、物化、快照、归档和远端恢复被中断的情况。 后期报告进一步检查文件描述符与精确路径身份,并覆盖回滚、隔离和保留核查记录。
代表性记录包括:
这些测试促成了不同恢复路径:放弃未提交的工作,完成提交意图之后的工作,或在记录冲突时停止。
旧系统迁移
迁移报告枚举了旧 Make 目标、物理布局、仓库族、客户端配置、包签名清单和回滚方式。 修改前先使用只读副本,并核对精确回执。
这项工作验证了一条具体的 Pigsty 切换路径,也建立了一种方法: 把迁移假设记录为数据,在每一步前检查,切换结束后移除迁移代码。
云存储与边缘服务实验
归档包含真实和模拟的 R2、MinIO、Cloudflare、COS 与边缘服务测试。 代表性报告包括:
- 真实 R2 存储协议;
- R2 发布存储事务;
- 完整存储服务的检查点约束删除;
- Cloudflare 初始化、私有域名、服务身份验证和回滚记录。
部分测试使用指定的非生产资源,另一些离线运行或使用模拟服务,报告中分别注明。 在 R2、MinIO 或 COS 上测得的存储行为,对应具体服务与所测 API。
哪些做法进入了设计方法
这项工作形成了六条实践:
- 说明测了什么。 区分源码检查、模拟故障、客户端测试与真实服务测试。
- 保留失败结果。 记录设计在什么条件下无法工作。
- 随结果记录输入。 保存源码版本、容器与客户端版本、存储服务、路由和测试数据。
- 测量规模。 记录大仓库的内存、调用次数与并发行为。
- 重新测试新布局。 历史结果与产生它的模型一起保存。
- 检查每次发布构建。 测试支持的平台,扫描依赖并运行静态分析。检查能否重现构建结果。
第一手资料
v0.1.0 验证报告目录保留全部报告, 包括命令、结果和测试条件。
继续阅读 v0.2 重置,了解这些经验如何用于更小的产品模型。
1.9 - SOW v0.1 决策账本:哪些被保留,哪些已退役
v0.1.0 标签中有 44 份架构决策记录(ADR),编号为 0001 至 0045,缺少 0006。 每份 ADR 记录一个问题、当时的决策及实现必须满足的条件。 下表说明仓库模型变化后,各项决策的后续情况。
| 状态 | 含义 |
|---|---|
| 保留 | 原则以基本相同的范围继续用于当前 SOW。 |
| 演化 | 问题仍在,但所有权或实现已经改变。 |
| 迁移历史 | 决策用于 Pigsty/V1 切换。 |
| 退役 | 决策属于已移除的 Git/CAS/Route/Edge 模型。 |
决策日期从 2026-07-11 到 7 月 28 日,ADR-0035 另有 7 月 29 日与 31 日的修订。 多数早期 ADR 在实现开始后,于 7 月 19 日集中进入 Git。 ADR-0032 包含已废弃的基础设施细节,因此这里只提供摘要。
核心状态、发布与信任
| ADR | 原始决策 | 后续命运 |
|---|---|---|
| 0001 | Git 是权威状态,SHA-256 CAS 保存包体,引用表达视图与历史,发布按各目标的事务流程执行。 | 演化。 Git/CAS/Route 所有权已退役,单一所有者、协议指针最后提交、独立目标状态与执行前检查得以保留。 |
| 0002 | 分别验证公共路由、不可变 Generation(构建快照)和客户端、存储服务兼容性。 | 演化。 Route 机制已退役,协议、客户端和存储服务仍分别验证。 |
| 0003 | 快照需要经过验证的 Generation 副本,并按清单保留。 | 演化。 通过 CopyObject 复制快照树的方式已退役;保留的 Generation 只保存元数据与引用集合,不复制包体。 |
| 0004 | 边缘令牌验证与部署采用同一套跨服务商、带版本的接口。 | 退役。 边缘访问权限不再由 SOW 仓库引擎管理。 |
| 0005 | 逐项决定旧 Make 目标是由 SOW 接替、退役,还是因策略限制而拒绝。 | 迁移历史。 完成切换后退出产品。 |
| 0007 | 公共包与受访问控制的包共用一个仓库。 | 退役。 商业边缘服务拓扑退出产品,单一所有者原则保留。 |
| 0008 | 垃圾回收计算完整引用关系,删除前要求显式确认。 | 演化。 当前 GC 检查引用关系与精确身份,旧 --apply --confirm 接口已退役;远端删除还检查缓存宽限期与存储服务能力。 |
| 0009 | 远端 Generation 可见后,通过前向恢复完成发布。 | 保留。 提交意图仍是放弃或核对未提交操作与前向恢复之间的边界。 |
| 0010 | 元数据签名绑定精确公私钥身份,密钥轮换作为整个仓库的状态变化处理。 | 演化。 当前签名者记录与信任环仍检查精确身份,不再使用 V1 引用拓扑。 |
| 0011 | 远端删除需要目标所有权、清单、检查点、宽限期与条件删除能力。 | 保留。 R2 不满足所需的原子删除能力,因此只报告候选对象。 |
| 0012 | 明确规定私有源站、缓存拓扑与缓存清理映射。 | 退役。 CDN 与私有源站部署不属于当前 SOW。 |
| 0013 | 持久化计划是恢复输入,使用前要重新验证。 | 保留。 Managed 恢复仍会重新验证计划、文件、目标身份和公共交付结果。 |
| 0014 | RPM 来源记录保留精确签名包数据,并区分接纳策略与历史证明。 | 演化。 当前包认证与独立信任环替代了 V1 回执编码。 |
| 0015 | 配置负责命名目标,已发布引用与检查点属于持久状态。 | 保留。 Generation 与目标无关;发布尝试和检查点按目标前缀保存。 |
| 0016 | 云适配器使用具体、有界的 SDK/API 接口。 | 演化。 具体存储服务接口保留,V1 Edge/COS 细节退役。 |
| 0017 | 在稳定的仓库密钥环中验证 RPM 签名,并绑定已打开包体的身份。 | 演化。 v0.4 的独立多信任环验证强化了这一规则。 |
| 0018 | 选定包集合在一个有界本地事务中完成暂存、验证、提交与恢复。 | 演化。 Managed 操作使用恢复日志;Plain 暂存元数据并最后替换指针,中断后重新构建。 |
旧拓扑、兼容性与迁移
| ADR | 原始决策 | 后续命运 |
|---|---|---|
| 0019 | 为旧客户端固定 EL7 元数据格式与压缩行为。 | 迁移历史。 当前支持平台见兼容性文档。 |
| 0020 | 为每条旧物理路径与选择器分组指定所有者。 | 迁移历史。 用于消除切换歧义,未进入当前仓库模型。 |
| 0021 | 根据固定验证结果复现跨 EL 的 yum/infra/{arch} 投影。 |
迁移历史。 这项特殊投影已退出产品。 |
| 0022 | 物理所有权变化时保留包仓库历史。 | 演化。 Generation、保留引用与迁移日志继续保存历史,不再依赖 V1 Route。 |
| 0023 | 接纳或使用保存仓库状态的 Git 对象前,重新核对其内容。 | 演化。 Git 权威状态已退役,文件描述符、路径与摘要检查保留。 |
| 0024 | 物化路由回执只授予限定范围的读取与退役权限。 | 演化。 路由回执已退役,检查和删除仍限定对象与操作范围。 |
| 0025 | 锁绑定具体进程实例与稳定 inode,不因等待超时而夺锁。 | 演化。 稳定锁 inode 与不按时间夺锁的规则保留,V1 进程实例租约已退役。 |
| 0026 | 创建离线归档前持久化意图,并严格检查输入。 | 退役。 离线归档投影已退出产品范围。 |
| 0027 | 旧包体通过路径、包内容与来源检查后,才纳入仓库管理。 | 迁移历史。 旧包纳管计划已结束,包接纳检查以更小的范围保留。 |
| 0028 | 检查 DEB 时只打开元数据所需的 control 归档,拒绝有歧义的容器。 | 保留。 DEB 解析仍限定读取范围与输入大小。 |
| 0029 | 由所有者明确规定最低客户端版本与 EL8 固定版本。 | 迁移历史。 当前平台策略见兼容性参考与发布注记。 |
| 0030 | 仅依据已评审的问题集合摘要,修复旧 YUM 索引中缺失的包体引用。 | 迁移历史。 这项来源例外没有变成通用忽略选项。 |
| 0031 | 启用受访问控制的旧内容前,修复精确校验和并记录启用结果。 | 退役。 商业版本启用流程退出产品,无法验证时停止修复的原则保留。 |
云端与边缘控制面
| ADR | 原始决策 | 后续命运 |
|---|---|---|
| 0032 | 只允许一组由所有者指定的 Cloudflare 测试资源例外通过。 | 退役。 这是针对特定测试的历史例外。 |
| 0033 | 服务就绪检查使用独立注册表与所有权记录。 | 退役。 服务初始化注册表已退出 SOW。 |
| 0034 | Worker 初始化使用租约、两阶段恢复、精确资源身份与可逆状态。 | 退役。 Cloudflare 部署不再由仓库引擎负责。 |
| 0035 | 使用前验证服务身份、运行时绑定、日志接收端与租约所有权。 | 退役。 这套边缘控制面已退出产品。 |
| 0036 | R2 缺少条件 DeleteObject;能力探测与重复身份检查通过后,可显式启用 checkpoint-fenced 无条件删除。 |
演化;删除降级未继承。 能力探测和条件不足时停止的规则保留。当前 SOW 禁用 R2 远端删除,只报告 GC 候选对象。 |
| 0037 | 上传机密字节前,先证明边缘服务会拒绝未授权访问。 | 退役。 边缘访问控制现由外部部署负责。 |
| 0038 | YUM 切换需要有有效期的回执,绑定精确端点、Generation 与信任数据。 | 迁移历史。 用于核对客户端切换,随后退役。 |
| 0039 | 脱字符在 Go、边缘服务、日志与源站路由中采用唯一 URL 编码。 | 演化。 旧路由已退役,路径只编码一次的规范保留。 |
有界配置与派生状态恢复
| ADR | 原始决策 | 后续命运 |
|---|---|---|
| 0040 | 分配内存或执行前,限制配置条目数与展开后的拓扑大小。 | 保留。 当前解析器与状态格式仍有明确大小和数量上限。 |
| 0041 | 保留未知的最终投影暂存目录,以便核查。 | 演化。 当前恢复仍保留冲突记录,并在无法核实时停止。 |
| 0042 | 派生状态替换明确区分成功、回滚、保留与阻塞结果。 | 演化。 Managed 日志记录操作阶段;记录状态与文件不一致时停止恢复。 |
| 0043 | 文件修改检查描述符身份,并规定同一操作系统用户恶意并发写入的保护范围。 | 保留。 路径检查拒绝身份变化,操作系统权限仍负责隔离其他写入者。 |
| 0044 | 只有匹配精确操作权限与确认令牌,才可删除保留的核查副本。 | 演化。 V1 命令已退役,删除前限定对象与操作权限的规则保留。 |
| 0045 | 对无日志残留进行有界分类,不能静默接纳或删除。 | 保留。 当前恢复仍区分已知状态、安全残留与冲突记录。 |
账本背后的共同模式
延续下来的决策有几条共同规则:
- 每个事实只有一个所有者。
- 修改前检查精确身份。
- 不可变内容就绪后,再提交协议指针。
- 提交意图持久化后,通过前向恢复完成操作。
- 删除前检查引用关系与删除条件。
- 限制输入大小,明确产品范围。
- 记录实际验证过的客户端与存储服务。
不可变的 v0.1.0 ADR 目录仍是第一手 资料。下一篇文章继续汇总独立的 v0.1 证据记录。
1.10 - SOW v0.1 原始计划:软件仓库的 Git
v0.1 计划始于 2026-07-11,并于 7 月 31 日封存为 v0.1.0。 它计划用一个 Go 控制面替换 Pigsty 的 Makefile 与 rclone 工作流,统一管理 APT、YUM 和静态制品。 范围包括本地状态、包历史、上游同步、渠道、视图、快照、双云发布、CDN 行为、商业访问控制和迁移。
当时的核心说法是:“制品仓库的 Git”。 软件分发确实需要不可变内容、可变名称、历史、晋级、 发布与回滚。这个类比帮助描述了需求,也扩大了产品范围。 Git/CAS/Route/Edge 模型后来退役,当前模型见系统模型。
第一版设计要解决什么
旧系统已经发布了几十条稳定 URL,供 APT、DNF、脚本和安装入口使用。 替代系统既要保留这些 URL,又要补足 Makefile 工作流难以可靠实现的能力:
- 每个已接纳包体只有一个身份;
- 期望状态与各云实际发布的状态分开记录;
- 快照与历史记录明确指出哪些文件仍在使用;
- 增量上传与缓存清理成本随变更量增长;
- 元数据准备完成、协议指针尚未发布时发生崩溃,仍可恢复;
- 上游索引、包签名与迁移例外都有来源记录;
- 商业私有对象不会通过公共索引或缓存键泄漏;
- 使用真实 APT/DNF 客户端检验兼容性。
原始计划将这些需求纳入同一套所有权模型。
v0.1 选择的模型
Git 保存权威状态
.sow/state 是由内嵌 Go Git 库操作的普通工作树。
清单、引用、来源记录、配置摘要、目标检查点和安全标签都保存在 Git 中。
SQLite 只提供查询数据,可以从 Git 重建。
每次状态迁移都有持久历史,一次引用更新就是本地提交边界。 这也要求包管理、发布、迁移和操作意图都采用同一种 Git 状态模型。
CAS 拥有字节
包体进入以 SHA-256 命名的不可变包池,公开树通过硬链接指向包池。 能否回收包体取决于引用可达性。这样可以低成本地去重和保留历史, 但包池与公开树必须位于同一文件系统,路径、回执和 inode 身份也成为产品契约的一部分。
Ref 表达产品语义
Repository、View、Snapshot、Stable、History 和各目标的远端引用,描述内容的用途与保留范围。 移动可变 View 不会删除 Stable 或历史引用。 删除 View 只改变引用,垃圾回收仍是独立操作,执行前要检查完整引用关系。
每个目标分别记录发布历史
Cloudflare R2 与腾讯 COS 是独立目标。 每个目标分别准备不可变对象、持久化提交意图、移动协议指针、清理 CDN 缓存、验证公共可见性, 并推进自己的检查点。一朵云成功后,不会仅因另一朵云失败而回滚。
发布顺序是:
当前发布仍沿用这一顺序。缓存宽限期与存储服务的删除能力检查用于远端垃圾回收。
Route 与 Edge 属于仓库正确性
公共树保留旧 URL,Route 根据 Generation(构建快照)选择不可变元数据。 私有对象位于 Cloudflare Worker 与 EdgeOne 共享的边缘服务接口之后。 鉴权必须先于源站访问,令牌不能进入源站 URL、缓存键或日志。
这使 CDN 路由、令牌验证、服务部署、日志接收端和缓存拓扑都成为仓库模型的前置条件。
这套设计为什么有吸引力
模型为各部分规定了明确规则:
- 每个持久事实都有所有者;
- 不可变字节与可变名称分离;
- 本地与各远端目标的发布状态可以独立观察;
- 最后发布协议指针,使未完成的工作可以恢复;
- 垃圾回收计算完整引用关系;
- 使用前重新验证计划与回执;
- 分别验证客户端、存储服务和本地实现;
- 将旧系统迁移范围记录为数据。
后续版本以更少的机制保留了这些规则。
为什么后来必须重置
到七月底,SOW v0.1 已经包含:
- 充当内部数据库协议的 Git 提交与引用;
- 全局内容寻址存储与硬链接物化层;
- Route、View、Snapshot、Generation、Projection、Receipt 和 Lease 等概念;
- 内置 APT/YUM/静态制品同步与旧系统迁移;
- 云 SDK、边缘服务初始化、服务身份验证、缓存清理、令牌和私有源站逻辑;
- 44 份现存 ADR 和 112 份按日期记录的验证报告。
每一项都针对具体失败情形,但组合后产生了几个问题。
第一,同一个包与公共路径被过多对象描述。它同时是 CAS 对象、清单条目、View 成员、 物化 Route、Snapshot 引用和远端目标对象。
第二,本地仓库生成与云端、边缘服务生命周期耦合。只想构建 YUM 或 APT 仓库的用户, 也要理解商业访问控制和分布式发布。
第三,迁移计划变成了永久子系统。旧拓扑、Make 目标、CDN 行为和服务例外在切换时有用, 却不应长期留在仓库模型中。
最后,为了在恶意并发写入和崩溃后恢复每条派生路径,系统引入了越来越多的身份检查、日志、 隔离与退役机制,复杂度超过了仓库核心任务的需要。
上游同步与渠道晋级也被移出产品。V1 已经验证过包获取的流式处理、异常输入、URL 和校验顺序, 但下载流程会引入独立的策略、重试、来源与远端所有权模型。 重置将包获取交给外部工具,让 SOW 专注于包接纳、仓库管理和发布。
重置后保留下来的东西
| v0.1 教训 | 当前形态 |
|---|---|
| 每个持久事实只有一个所有者 | 系统模型中的仓库与目标前缀所有权 |
| 包原件与生成索引分开管理 | 一个 pool/ 加纯元数据 dists/ |
| 包体先准备,协议指针最后提交 | 发布与恢复 |
| 提交后只能前向恢复 | Managed 操作日志与发布日志 |
| 使用前验证计划与路径 | 精确身份、路径范围和重新绑定检查 |
| 删除前检查完整证据 | 引用关系与对象身份;远端删除还检查缓存宽限期与存储服务能力 |
| 生成的服务端配置是派生状态 | 完整仓库根目录是托管单位,服务配置由运维者管理 |
| 兼容性需要逐项验证 | 平台与集成 |
| 历史结果与版本绑定 | 按日期记录的设计与发布文章 |
被淘汰的东西
- Git 产品状态与“SQLite 只是可丢弃缓存”的模型;
- 跨产品 CAS 与硬链接物化层;
- Route/View/Snapshot 公共命令模型;
- 生成的 Nginx 配置片段、Route 回执与 V1 服务控制状态;
- 内置上游同步、晋级与旧 Make 目标迁移;
- 作为仓库前置条件的边缘令牌权限与私有源站拓扑;
- 多云发布控制面、服务初始化与身份验证系统;
- V1 专用投影回执、租约、隔离命令和恢复接口。
部分实现思想后来在更小的范围内回归。SOW 管理仓库及其发布,包获取与服务托管仍由外部工具负责。
时间线
| 日期 | 里程碑 |
|---|---|
| 2026-07-11 | 目标、构思、技术调研、客户端与规模基线 |
| 2026-07-12 | Git/CAS/发布核心契约与首批协议、存储服务验证 |
| 2026-07-13–16 | 包信任、事务、旧拓扑与迁移验证 |
| 2026-07-17–20 | 服务初始化、边缘机密性、删除能力与输入上限 |
| 2026-07-22–29 | 路径身份、恶意并发写入、隔离与残留恢复 |
| 2026-07-30–31 | 从空环境验证最小可用产品,封存 v0.1.0 源码 |
| 2026-08-01 起 | Plain/Managed 重置,随后移除 V1 运行时 |
第一手资料与后续记录
v0.1.0 docs/ 树保留了原始
架构契约、
需求追踪、
迁移材料、ADR 和历史验证报告。
2 - 发布注记
查看各版 SOW 的变化和升级步骤,版本按从新到旧排列。
SOW 0.7.1 升级到 Go 1.27.4,更新依赖,并修复恢复、输入校验和 S3/R2 处理。0.5.0 至 0.7.0 的工作区无需迁移数据库,详见 0.7.1 升级说明。
SOW 0.7.0 新增平面 RPM 仓库裁剪与口令文件签名,修复迁移和恢复缺陷并更新依赖。0.5.0 至 0.6.1 的工作区无需迁移数据库,详见 0.7.0 升级说明。
SOW 0.6.1 升级到 Go 1.27.3,更新依赖,并修复无效输入的错误信息。 已使用 0.5.0 或 0.6.0 的工作区无需迁移数据库,详见升级说明。
2.1 - SOW v0.7.1
v0.7.1 ·
SOW 0.7.1 更新 Go 工具链与依赖,并修复恢复、软件包校验、RPM 导出和 S3/R2 处理中的问题。
本说明覆盖 0.7.0 以来的变化。数据库 schema v13、配置 schema sow/v3、CLI 与公开仓库布局保持兼容。0.5.0 至 0.7.0 的工作区无需迁移数据库。
恢复与仓库状态
- 修复未启用有效签名时,旧版
dist.new和dist.init操作中断后的恢复。 - 将无法证明可安全清理的 C2 迁移暂存文件完整保留到
.sow/<repo>/abandoned/,并报告路径。空暂存目录会被移除,重复恢复安全,本地 GC 可继续执行。删除前请检查报告路径中的文件。 - 在删除批次之间重新打开目录游标,避免 macOS APFS 与 BSD 文件系统上的清理遗漏。
- 将多语句 SQLite 读取放入同一个只读事务。大型 Dist 成员查询分批执行,避免超过 SQLite 参数上限。
- 规范化旧仓库中带小数秒的时间记录,并拒绝超出
1..9999的年份。
软件包校验与 RPM 导出
- Plain
create、prune的全部路径及 Managed 入库一致拒绝 RPM v6。 - 限制 DEB control 归档与 APT/RPM 元数据流的解压大小,包括过大的 PAX 头部。
- 识别 Linux
renameat2不受支持时的错误,并保留跨平台回退实现。 - 无关的 filesystem 发布目标未挂载时,仍可执行
export rpm-leaf;实际物理路径重叠仍会被拒绝。 - 导出元数据签名使用所选 Dist 已记录的构建发布时间,后续其他 Dist 的构建不再改变该时间。导出
repomd.xml的 data 时间戳仍为0。 sow add同时接收显式文件和其父目录时,允许重复输入同一软件包。
S3 与 R2
新版 AWS SDK 会重试不带响应体的 HTTP 429。重试耗尽后,SOW 保留 HTTP 错误分类。
SOW 还能从组合错误中提取 HTTP 状态码。中止 multipart 上传时返回 HTTP 404,表示上传已被移除。列表与 multipart 操作的错误 XML 响应也增加了大小限制。
工具链与依赖
源码构建要求 Go 1.27.4 或更新版本。发布准备期间,已逐一核对全部 35 个显式依赖的最新可用版本。
| 组件 | 0.7.0 | 0.7.1 |
|---|---|---|
| Go | 1.27.3 | 1.27.4 |
| AWS SDK core / S3 | 1.47.8 / 1.114.9 | 1.48.0 / 1.114.10 |
| Smithy | 1.28.8 | 1.28.9 |
| klauspost/compress | 1.20.3 | 1.20.4 |
| modernc/libc | 1.80.1 | 1.81.0 |
| golang.org/x/sys | 0.49.0 | 0.50.0 |
| GoReleaser | 2.20.0 | 2.20.1 |
同时更新 AWS 内部模块、pprof、x/mod、x/sync 和 modernc 编译器依赖,保留本地 RPM 解析器安全补丁。第三方许可声明已与新版本构建同步。
升级
停止工作区写入,以保留硬链接的方式备份完整工作区。安装 0.7.1,再对每个配置中的仓库执行构建与检查:
没有变化时,构建为 no-op。0.3 或 0.4 工作区仍需先执行 sow repo migrate REPOSITORY。Plain 仓库无需迁移数据库。
SOW 0.6.1 及更早版本无法读取包含 passfile 的配置,降级前须移除这些字段。RPM v6 仍不受支持。
本版本提供四个 Linux/macOS 归档、两个 RPM、两个 DEB,以及 SHA256SUMS。详见升级指南和完整变更。
.rpm
| 文件 | 校验和 |
|---|---|
| sow-0.7.1-1PGSTY.aarch64.rpm Linuxarm64SHA-256 | 633dec35eede026280143f272783fddf3453361a71df3b38f134567b9f921b0e |
| sow-0.7.1-1PGSTY.x86_64.rpm Linuxamd64SHA-256 | d4ccae2f3a184d28a92da695cdb46848a46496d137399063ca1d40250fc112c4 |
.deb
| 文件 | 校验和 |
|---|---|
| sow_0.7.1-1PGSTY_amd64.deb Linuxamd64SHA-256 | 127559a2fec7d7532ad1a8d41dce0ba2d61ada3454fa7ef7fc82756d3ab99bd1 |
| sow_0.7.1-1PGSTY_arm64.deb Linuxarm64SHA-256 | 8d69f623a1741aff4dc4c001059f3fc4279d5b0430d25c4d72704a72ff77dc4e |
.tar.gz
| 文件 | 校验和 |
|---|---|
| sow_0.7.1_darwin_amd64.tar.gz macOSamd64SHA-256 | cc1d68911bc80c473d73ace6cbfd2ab586394ea0fb44709fb597d3b4fd44731c |
| sow_0.7.1_darwin_arm64.tar.gz macOSarm64SHA-256 | 8120188108b98c79beab3bc0c081f0a4bed15c45533ff43bf69f4be8672551a4 |
| sow_0.7.1_linux_amd64.tar.gz Linuxamd64SHA-256 | d1fd60997a86a2048bfe38a5b23a5bcab07e1d99b171133e64ff2b75e4694179 |
| sow_0.7.1_linux_arm64.tar.gz Linuxarm64SHA-256 | 2adbe314ec4033825bc54effd870bfdbdd366858a35145e7831d298f72438f5c |
2.2 - SOW v0.7.0
v0.7.0 ·
SOW 0.7.0 新增 平面 RPM 仓库裁剪 与统一的 口令文件签名接口,并修复发布审核中发现的迁移、恢复、发布、RPM 验签与版本排序缺陷。
发布状态: SOW 0.7.0 已正式发布,可从 GitHub 或下载页获取。
本说明覆盖 0.6.1 以来的变化。数据库 schema v13、配置 schema sow/v3 与公开仓库布局保持不变,0.5.0 至 0.6.1 的工作区无需迁移数据库。
裁剪旧版 RPM
sow prune DIR 为每组包名与架构保留最高的 Epoch/Version/Release,先发布过滤后的索引,再删除旧版 RPM。
--dry-run只校验和预览,不写入文件。可重复的--protect NAME保留精确包名的所有版本,每个名字都必须匹配到包。--metadata-timestamp SECONDS设置索引发布时间,默认值与create一样为0。- 替换包必须通过内嵌摘要校验。SOW 将已校验、已索引的字节绑定到包含变更时间的稳定文件身份,在每次删除之前和完成之前检查。即使原地改写后恢复大小与 mtime,也会停止后续删除。
- 清理中断后可以重跑。已有的
repo_complete标记只在成功完成后恢复;错误结果会报告已经完成的修改,已删除的软件包不会自动恢复。
Prune 面向本地发布源中顶层的普通二进制 RPM 文件。它拒绝托管工作区、DEB 包或索引、包文件符号链接、源码 RPM、同一包名与架构中相同的 EVR、RPM v6、带签名的 repomd.xml,以及 SOW 无法生成的独立元数据。Prune 不执行签名。缓存旧索引的客户端可能仍需要旧 RPM,发布裁剪结果前需安排相应的保留策略。
详见 prune 命令参考。
用口令文件签名
通过 --passfile PATH、SOW_GPG_PASSFILE 或 sow.yml 中每个密钥的 passfile,为受保护的 RPM 包签名密钥及 RPM/APT 元数据密钥提供口令,包括 GPG agent 密钥。
优先级依次为命令行、密钥配置(包括旧的 passphrase 引用)、环境变量。文件为非空 UTF-8 单行,最多 64 KiB;只去掉末尾一个 LF 或 CRLF,不裁剪空格和制表符。参数使用普通路径,不接受 file://。命令行和环境变量中的相对路径以当前目录为基准,配置中的路径以工作区根目录为基准,不搜索默认文件。
同一接口也用于签名前检查、初始化、迁移、叶子仓库导出和中断恢复。口令不会进入进程参数、仓库状态或公开文件。口令文件必须是普通文件,路径中不能有符号链接;应放在仓库外并限制读取权限。旧版元数据 passphrase 引用继续受支持。
各命令的支持范围及外部 GPG 要求见口令文件契约。
RPM 兼容性
- ECDSA 与 EdDSA 签名: 接受现代 RPM 写入 tag 267(
RPMSIGTAG_DSA)的这两种头部签名,真实 Ed25519 与 ECDSA 签名包现在可通过验证;错误放入该标签的 RSA 签名仍会被拒绝。 - Caret 排序: 修正 Version 与 Release 比较,使
2.0^20250611这样的快照排在2.0.1之前,托管仓库的版本上限策略也会保留较新的正式版本。 - 外来元数据:
create保持既有行为,替换repomd.xml时不保留 SOW 无法生成的 group、updateinfo、modules 或未知记录。这保持了对旧版 Pigsty 仓库的兼容,包括 Pigsty v4.4.0 在 EL8/9 上生成的 modules。现在文本模式会警告丢弃的数据类型,JSON 模式在非空时增加dropped_metadata字段。外部元数据文件和签名文件本身不由 SOW 维护。 - Prune 元数据保护: 修改前拒绝上述独立记录,发布前再次检查;包索引的 SQLite 和 zchunk 编码仍可接受。Prune 还会拒绝超过 16 MiB 检查上限的旧索引。Create 仍可替换超大或格式错误的旧索引,但不能报告无法读取的数据类型。
如果客户端需要额外元数据,请继续用相应的生成工具维护。重建后应重新生成外部 repomd.xml.asc 签名。
托管仓库与恢复
- 迁移后的仓库保持可写。 已完成的 C2 迁移固定引用迁移时创建的 Generation,后续构建不再使
check、build、add或gc拒绝该迁移锚点;repo rm也不再把已完成迁移误判为活动转换。受影响的仓库升级后即可恢复,无需手工修复数据库。 - 保护全部已发布指针。 撤下保护检查现在覆盖已发布 Generation 的全部指针,包括最近一次发布未改变的视图。只要配置中的目标仍在发布某个 Dist、架构或签名伴生文件,就会拒绝移除它。应先停用该目标绑定,或使用新的目标名称和前缀发布。旧的公开文件会保留,需单独清理,普通发布不会自动撤下它们。
- 无关离线目标不再阻塞操作。 发布、撤销和目标 GC 只将所选目标与其他目标的预期物理路径比较,真实的物理路径重叠仍会被拒绝。
- 恢复写到一半的计划。 GC 在 planned 阶段发现空计划、残缺计划或哈希不符时,可在尚未删除任何文件的前提下丢弃计划;保留移除在原目标仍存在时可重建计划。已经发生破坏性操作后的矛盾状态仍会报错。
- 导出未重建的 Dist。 后续 Generation 未重建、但仍为 clean 的 Dist 可执行
export rpm-leaf,签名和不签名两种情况均可。 - 容忍时钟回拨。 中断恢复通过唯一活动操作的 ID 识别任务,不再按墙上时间排序选择操作。
依赖与发布检查
| 组件 | 0.6.1 | 0.7.0 |
|---|---|---|
| AWS SDK core / S3 | 1.47.3 / 1.114.4 | 1.47.8 / 1.114.9 |
| Smithy | 1.28.6 | 1.28.8 |
| SQLite / libc | 1.61.0 / 1.79.0 | 1.62.0 / 1.80.1 |
| Debian 包解析器 | 0.21.1 | 0.21.3 |
| YAML | go.yaml.in/yaml/v3 3.0.5 |
go.yaml.in/yaml/v4 4.0.0 |
| GoReleaser | 2.18.3 | 2.20.0 |
构建基线仍为 Go 1.27.3。YAML 保留严格的单文档校验与规范化配置输出;受限的本地 RPM 解析器补丁及第三方许可声明随依赖变更更新。
Integration 新增 EL8、EL9、EL10 受保护密钥签名的必跑测试。EL8 检测 rpmsign --rpmv3 能力,只跳过工具不支持的重复签名场景。发布门禁覆盖单元和竞态测试、APT/DNF 客户端、S3 兼容发布、静态分析、漏洞可达性、可复现的干净源码构建,以及归档、RPM、DEB 内容检查。本次审核未运行可选的托管 R2 live 测试;macOS amd64 已交叉编译,但未在 arm64 审核主机上执行。
升级
停止写入任务,以保留硬链接的方式备份完整工作区,安装 0.7.0,再对每个配置中的仓库执行:
没有变化的构建为空操作。0.3 或 0.4 工作区仍需先执行 sow repo migrate REPOSITORY。Plain 仓库没有数据库迁移步骤。
含有 passfile 的配置不能由 0.6.1 或更早版本读取,降级前应删除这些字段。口令文件不允许符号链接的限制保持不变,RPM v6 仍不受支持。
本版本生成 Linux 与 macOS 的 amd64、arm64 归档、两个 RPM、两个 DEB,以及 SHA256SUMS。详见安装与升级说明和 0.6.1 以来的完整差异。
.rpm
| 文件 | 校验和 |
|---|---|
| sow-0.7.0-1PGSTY.aarch64.rpm Linuxarm64SHA-256 | ea62ad17c62c39c606c2c942d6f0b0acbfb1d0788df71d872746b653753fefdf |
| sow-0.7.0-1PGSTY.x86_64.rpm Linuxamd64SHA-256 | 3111afe9857fc44d22a63072ab520d47cd96df4074b632f255c4087030833055 |
.deb
| 文件 | 校验和 |
|---|---|
| sow_0.7.0-1PGSTY_amd64.deb Linuxamd64SHA-256 | 9e579c560b0564bd45481f5d205cfa76edadcb1eedd6e787bd845ed5aba5425f |
| sow_0.7.0-1PGSTY_arm64.deb Linuxarm64SHA-256 | 4384d343890e84dec8f917c5f8d6a8612bc98af28b55aec96000f1ea9d2bb59b |
.tar.gz
| 文件 | 校验和 |
|---|---|
| sow_0.7.0_darwin_amd64.tar.gz macOSamd64SHA-256 | 21f276d940b44264be67ac1b020e50004ec9e77292f12061b880093175f01af1 |
| sow_0.7.0_darwin_arm64.tar.gz macOSarm64SHA-256 | bd4232eec5d329a51ed2db8f58d43341c30875e8693278b4c5d10c946d814433 |
| sow_0.7.0_linux_amd64.tar.gz Linuxamd64SHA-256 | 4ad5c1faa853f3e295abab3b1c070b1e9d2c337af3afcbca7384808c3c3df291 |
| sow_0.7.0_linux_arm64.tar.gz Linuxarm64SHA-256 | 9453c84501406a0a31044120c9b26ef0eb4865bf45c0d204efd043cf650b323c |
2.3 - SOW v0.6.1
v0.6.1 ·
SOW 0.6.1 是一次维护更新,升级依赖并改善错误信息。 命令接口、数据库 schema v13、配置 sow/v3 与公开仓库布局保持不变。 已使用 0.5.0 或 0.6.0 的工作区无需迁移数据库。
软件包与归档见下载页,校验和来自 v0.6.1 发布页。
工具链与依赖
源码构建现在要求 Go 1.27.3 或更新版本。所有发布二进制均使用 Go 1.27.3 构建。 本版更新了加密、S3 访问、压缩、SQLite 与 Debian 包解析等依赖。
| 依赖 | 版本 |
|---|---|
| ProtonMail go-crypto | 1.5.2 |
| AWS SDK for Go / S3 | 1.47.3 / 1.114.4 |
| klauspost/compress | 1.20.3 |
| modernc SQLite / libc | 1.61.0 / 1.79.0 |
| pault.ag/go/debian | 0.21.1 |
| go.yaml.in/yaml/v3 | 3.0.5 |
| golang.org/x/crypto / x/sys | 0.58.0 / 0.49.0 |
YAML 解析切换到 YAML 官方团队维护的模块,现有 sow.yml 配置格式不变。
RPM 解析器继续保留已有的本地安全补丁。
第三方许可清单已更新,随每个归档与 Linux 软件包分发。
维护修复
- 修复无效路径与 OpenPGP armor 类型的错误信息,不再出现
%!w(<nil>)。 涉及日志导出、RPM 元数据生成、签名校验与工作区检查。 原来被拒绝的无效输入仍然会被拒绝。 - CI 快照包直接从 Makefile 读取版本,避免遗漏硬编码版本号。 同步更新发布工具与 GitHub Actions,包括 GoReleaser 2.18.3、deadcode 0.52.0 和 govulncheck 1.9.0。
升级
更换可执行文件前,停止工作区写入任务并备份完整工作区。 安装 0.6.1 后,对每个仓库执行构建与检查:
把 REPOSITORY 换成 sow.yml 中的仓库名。没有变化的仓库会报告空操作,保留原构建快照编号与时间戳。
安装命令以及从 0.3、0.4 迁移的步骤,见安装指南。
发布验证覆盖包测试、竞态检查、APT/DNF 客户端、S3 兼容传输,以及 50,000 包的 RPM 元数据生成。 升级与回退检查使用已发布的 0.6.0 二进制创建 RPM/DEB 工作区,由 0.6.1 操作后,再用 0.6.0 重新打开。
漏洞扫描结果
源码分析与保留符号的对照构建均未发现受影响的符号或导入包。
GO-2026-5932 涉及未使用的旧 golang.org/x/crypto/openpgp 包。
SOW 使用维护中的 ProtonMail 分支;四个发布目标的导入图均不包含该旧包。
发布二进制移除了符号表。对这类二进制,govulncheck -mode=binary 会退回模块级判断并报告此提示,
因此其二进制扫描结果与源码扫描不同。
发布附件包含 Linux 与 macOS 的 amd64、arm64 归档,两个 RPM、两个 DEB,以及 SHA256SUMS。
源码变化见完整差异。
2.4 - SOW v0.6.0
v0.6.0 ·
SOW 0.6.0 改进仓库更新、文件校验和中断恢复,保持现有命令、配置格式与公开仓库布局不变。 已使用 0.5.0 的 Managed 工作区无需迁移数据库。
本文记录自 0.5.0 以来的变化。升级步骤见安装说明, 制品发布状态见下载页。
仓库维护者需要了解的变化
| 范围 | 0.6.0 的变化 | 对操作的影响 |
|---|---|---|
| Plain RPM 更新 | 保留当前与紧邻上一轮以校验和命名的元数据 | 客户端可以继续下载上一版索引引用的元数据 |
| Managed 构建 | 元数据与签名共用一个记录的发布时间 | 恢复保留原时间;没有变化的构建保留已有输出 |
| 文件校验 | 在认证读取期间持续核对文件身份 | 检测文件替换与内容改写,包括恢复原修改时间的改写 |
| 签名密钥 | 读取完整 OpenPGP 密钥集合 | 检查全部受信密钥,拒绝格式错误的输入 |
| 状态与恢复 | 保持锁、读取一致性和已提交结果 | 维护错误不会使已完成的变更看起来已经回滚 |
| 大型仓库 | 复用已验证数据,限制并发工作量 | 减少构建、导入、检查和发布中的重复工作 |
Plain RPM 更新保留上一轮索引
客户端先下载 repomd.xml,再请求其中列出的元数据文件。
如果这两步之间发生仓库更新,旧元数据可能在客户端下载前就被清理。
Plain sow create 现在保留当前与紧邻上一轮以校验和命名的 RPM 元数据。
没有变化的运行不会推进该窗口。下一次索引变化后,更早的元数据可以被清理。
小文件 .sow-plain-rpm-history.json 记录保留引用。
复制目录后还要继续构建时,一并保留该文件。它不是操作日志;Plain 构建中断后仍通过重新执行 create 恢复。
此窗口不保留已删除的包文件,也不覆盖所有客户端缓存期限。 缓存跨越多轮更新时,在发布层保留所需的软件包与元数据。 YUM 迁移指南说明发布顺序、签名与旧客户端缓存的处理方法。
Plain 时间戳仍由调用者指定,--metadata-timestamp 默认值仍为 0。
替换其他生成工具或恢复旧软件包内容时,保留已发布的最大时间戳。
Managed 构建保留一个发布时间
新的 Managed 构建在发布受影响 Dist 的索引前,记录一个精确到秒的 UTC 时间。
APT Release 日期、RPM repomd.xml 的 data 时间戳和元数据签名共用该值。
RPM revision 仍使用构建快照编号。
所选时间遵守已有构建时间与所选签名密钥启用时间的下限。 密钥不可用或尚未生效时,命令返回错误。请保持主机时钟正确。
恢复操作复用已经记录的时间。如果时间发生漂移,SOW 会在发布后续 Dist 入口前拒绝继续。 空操作构建不创建新的构建快照,也不刷新时间戳或签名。
此规则适用于新的元数据构建操作,包括 add、rm 自动触发的构建和新建 Dist。
布局迁移与旧日志保留此前的时间戳规则。
独立的 export rpm-leaf 输出仍使用 RPM data 时间戳 0。
rm --check 预览仍为只读操作,预测成员、路径、动作,以及结果的修订号和构建快照编号。
实际执行会选择自己的发布时间,因此含时间信息的字节、哈希与大小可能不同于预览。
校验实际读取的文件
元数据与软件包校验现在将摘要绑定到打开的文件身份,并在读取前后核对。 快速校验和复用校验结果也采用相同检查。 这能发现文件替换、等长改写,以及改写后恢复原修改时间的情况。
并发创建硬链接可能改变文件属性,但不改变软件包内容。 SOW 正确处理这种情况,并保留成功校验时的文件身份。 只读硬链接导出因此可以与仓库检查并行使用。
路径遍历还会核对实际打开的条目与此前检查的条目是否相同。 访问已知子路径时,父目录只需具备遍历权限,不再额外要求目录列表读取权限。 软件包仍需可读,受控路径仍拒绝不安全的符号链接和文件类型。
读取完整的签名密钥集合
一个受信密钥文件可以包含多个公钥或连续的 ASCII 装甲密钥块。 SOW 现在读取完整输入,不再遇到第一个块后就停止。 格式错误、被截断或不受支持的密钥内容会被拒绝,不会被静默跳过。
签名 key 引用仍须对应一个主密钥。
将密钥集合放入 trusted_keys,每项元数据或软件包签名设置使用一个签名身份。
具体规则见签名参考。
保持状态与恢复结果一致
SQLite 身份检查现在保留现有连接持有的锁。 组合仓库查询从同一个数据库快照读取相关状态。 符合条件的 WAL 数据库在每次打开时验证一次,避免同一连接内反复执行完整验证。
数据库提交可能已经成功,后续检查点或清理才发生错误。
SOW 现在保留这种情况下已经提交的命令结果。
重试前查看结果、sow status 和 sow log;非零退出码不代表事务已回滚。
Dist 状态也会显示未完成的仓库维护,包括本地 GC 和日志裁剪。 即使期望成员与已构建成员数量相同,也不会隐藏这些待处理操作。 移除操作日志同时保留策略造成的结果与显式选中的移除结果。
对于已经启动的开发期 C2 布局迁移,repo migrate --abort 会保留可信迁移标记。
即使首份日志或基准清单尚未生成,也能完成中止。
日志已经绑定迁移计划时,SOW 会按记录的基准清单核对计划,并拒绝矛盾的控制记录。
非空迁移暂存目录会被完整移入 .sow/<repo>/abandoned/。
命令通过 stderr 和 JSON warnings 报告保留路径或清理问题。
暂存清理告警不会阻止中止操作,公开目录保持不变。
仓库选择成功后,即使迁移失败,响应中也会保留仓库名称。
尚未启动迁移的 C2 仓库仍不支持直接迁移。
如果旧版中止操作已经移除了可信标记,需要恢复匹配备份或创建新工作区。
支持的恢复路径见 repo migrate。
减少重复工作
本版减少 I/O,没有增加额外的全仓扫描或恢复状态机:
- 在同一次操作中,复用已认证的 DEB 字节完成解析与检查。
- 文件身份仍匹配时,复用当前软件包与 Dist 的校验结果。
- 复用已验证的 RPM 导出结果,避免再次读取同一份副本。
- 避免导入暂存阶段的重复写入,移除没有使用的初始输出目录树哈希。
- 只查询所选软件包的成员关系,并复用本次操作整理出的软件包数据。
- 保持软件包暂存任务相互独立,并按指定并发数限制工作量。
sow check 仍会在每次运行时读取并哈希每个独立的物理包文件,不会退化为仅检查文件指纹。
R2 清单检查现在每页最多并发执行八个 HEAD 请求。 排队任务与已完成结果的数量都有上限,取消与失败时也一样。 上传重试不再仅因重新定位源文件就重启空闲超时,必须有实际传输进展。 R2 垃圾回收仍只报告候选对象,不删除远端对象。
构建、打包与验证
源码构建要求 Go 1.27.2 或更新版本,质量检查使用 staticcheck 0.8.2。 即使 Go 自动选择了下载的工具链,修补后的 RPM 解析模块也能完成上游来源验证。
发布目标仍为 Linux 与 macOS 的 amd64、arm64。
打包产物包括四个归档、两个 RPM、两个 DEB 和 SHA256SUMS。
归档与安装包继续包含许可证文件和第三方许可声明。
可复现源码交付会将本地代理说明排除在产品源码之外。
回归覆盖增加了文件替换、恢复原修改时间、并发硬链接、完整密钥集合、提交取消、提交后失败、 元数据时间恢复、迁移中止、Plain 索引保留和 R2 有界请求。 本版还移除未使用的代码,以及依赖 Go 运行时内部实现的脆弱 I/O 插桩。
本地发布前核验覆盖 macOS/Linux 测试、race 检查、APT/DNF 集成、S3 兼容传输、 5 万包 RPM 元数据生成、打包,以及从已发布 0.5.0 二进制升级。 部署验收仍使用实际的包管理客户端和发布目标。
从 0.5.0 升级
数据库 schema v13、配置 sow/v3 与公开 pool/ + dists/ 布局均保持不变。
- 停止工作区写入。
- 备份完整工作区,并保留硬链接。
- 安装 SOW 0.6.0。
- 对每个仓库执行:
将 REPOSITORY 与 /srv/sow 换成实际值。
已处于最新状态的 0.5.0 仓库无需迁移数据库或强制重建,build 会报告空操作。
需要把新的构建快照交付到配置目标时,再执行发布。
从 0.3 或 0.4 工作区升级时,先执行 sow repo migrate REPOSITORY,再构建。
详见完整升级步骤。
2.5 - SOW v0.5.0
v0.5.0 ·
SOW 0.5.0 为已有 YUM 仓库增加发布时间参数,并修复 Managed 仓库的恢复、签名和包路径处理。 构建过程也减少了对未变化软件包的重复校验。
安装方式见下载 SOW。使用 0.3 或 0.4 Managed 工作区时,先迁移各个 Repository,再进行写操作。
升级 Managed 工作区
- 停止向工作区写入的命令。
- 备份完整工作区,包括
sow.yml、.sow/和所有 Repository 根目录。 - 安装 SOW 0.5.0。
- 逐个迁移、构建并检查 Repository。将
pgsql和/srv/sow换成实际名称与路径。
迁移将数据库更新到 schema v13。配置仍使用 sow/v3,公共目录仍由 pool/ 和 dists/ 组成。
首次构建刷新 RPM 认证与 APT 元数据记录;后续构建会复用匹配的 RPM 校验记录。
迁移后的数据库使用 SOW 0.5.0 或更新版本。备份与安装步骤见 安装与升级。
保持 YUM 发布时间递增
如果新 repomd.xml 中最大的 data 时间戳早于缓存值,EL7 YUM 可能拒绝新索引。
修改校验和、revision 或文件修改时间都不能替代这项比较。
替换已有元数据生成器时,使用新增参数:
| 设置 | 结果 |
|---|---|
| 默认行为 | RPM data 时间戳仍为 0 |
--metadata-timestamp SECONDS |
设置三条 RPM data 记录的时间戳 |
| 取值范围 | 0 到 253402300799 的整数 |
Plain repomd.xml revision |
仍为 0 |
| 软件包字节与 DEB 索引 | 不受此参数影响 |
| 压缩 XML 与 gzip 头 | 仅修改时间戳时保持不变 |
该参数只适用于 create,Managed 构建、发布和 RPM 叶目录导出均不接受它。
包字节与选项相同时,重复构建会生成相同元数据;强制重签软件包可能改变输入字节。
在维护脚本中保存已发布的最大时间戳。内容变化时递增时间戳,恢复旧包集合也按一次新发布处理。 如果之前的转换已将时间戳重置为零,用转换前的发布历史恢复该值。
如果客户端校验仓库签名,每次修改后都要签署最终的 repomd.xml。
--sign-with 签署 RPM 包,不会签署索引。
YUM 迁移指南介绍时间戳选择、索引签名和已有客户端缓存的测试方法。
仓库与发布修复
- 删除 Dist 时,不再误删后来加入其他 Dist 的软件包。
- 提交 Desired 变更前,检查签名策略和包池路径冲突。
- 恢复操作保留已提交的软件包选择,并完成中断的清理。
- 已发布的包路径始终绑定原内容,本地垃圾回收也不会解除绑定。发布恢复不会用其他字节覆盖该路径。
- 仅大小写不同的文件名不能创建第二个 Pool URL,即使字节相同也不例外。 大小写不敏感的工作区和文件系统目标还会拒绝目录别名。
- 并发执行
export rpm-leaf --hardlink与check时,不再将未变化的包误报为损坏。 - 本地
gc跳过包只存在于远端的发布记录。 - 输入路径可以经过符号链接父目录;软件包文件本身仍须通过文件检查。
- SQLite 检查点被读连接延迟时,不再将已完成的写入报告为失败。macOS 写连接使用
fullfsync。
签名与命令输出
APT 验签兼容 GnuPG 的末尾换行约定。新元数据签名使用 SHA-256,新的构建会检查签名密钥当前是否可用。 Agent 签名固定签名时间,并选择可用的签名子钥。新 APT 发布拒绝弱签名摘要;历史验签沿用记录中的规则。
sow repo migrate 报告迁移前后的 schema 版本。未迁移的 Repository 会给出迁移命令。
命令收到 SIGINT 或 SIGTERM 后返回退出码 130。全部被拒绝的 add 批次会显示各文件的错误。
file:// R2 凭据必须指向普通文件。
RPM 依赖
新增回归用例覆盖事务前、事务后脚本依赖,包括 Cloudberry、Percona 和 Grafana 软件包使用的组合。 依赖渲染继续遵循现代 createrepo_c 的规则。上游变更见 createrepo_c PR #427。
软件包签名流程使用 RPM v4 格式。SOW 0.5.0 不支持 RPM v6 格式签名。 生成的仓库元数据使用 XML,不包含 SQLite rpm-md 或模块流。 格式与客户端覆盖见兼容性。
构建与安装包
源码构建需要 Go 1.27.1 或更新版本。本版使用 x/crypto v0.56.0 与 GoReleaser v2.18.2。
二进制归档和原生安装包均包含 THIRD_PARTY_NOTICES。
发布文件包括四个 Linux/macOS 归档、两个 RPM、两个 DEB 和 SHA256SUMS。
指定版本文件见 v0.5.0 发布页。
兼容性测试位于 qa/compat/,性能测试位于 qa/perf/。S3 集成测试使用按摘要固定的 pgsty/silo 镜像。
2.6 - SOW v0.4.0
v0.4.0 ·
SOW 0.4.0 改进了 Managed 仓库的校验与恢复。check 只对每个物理软件包计算一次内容哈希;RPM 验签分别判断各组密钥的信任结果;发布目标可通过有记录的重绑定操作修改公共 URL 或缓存时间。
Plain 行为与公共 pool/、dists/ 布局保持不变。本文记录 0.4.0;升级到当前版本请按安装指南操作。
从 0.3 升级到 0.4
停止工作区写入,备份完整工作区并保留硬链接。安装 0.4.0 后,先迁移每个已配置的 Repository,再继续维护:
本例的仓库名为 pigsty 与 pgsql,请换成 sow.yml 中的实际名称。保留备份用于回退:SOW 0.3 无法打开迁移后的数据库。
Schema v11 修复了 0.3 的状态错误:一个 Dist 仍为 dirty 时,Repository 可能错误显示 clean。现在,Dist 变更与仓库状态在同一事务中更新。迁移还修复发布与签名者记录;从未记录的历史签名身份仍标记为未验证。
Schema v12 为发布目标增加只追加的绑定历史。初次绑定、迁移回填与后续重绑定都有独立修订记录。
每个物理软件包只计算一次内容哈希
每次 sow check 对每个唯一物理软件包执行一次完整内容哈希,包括缓存指纹仍匹配的文件。同一文件的硬链接共用这个结果。校验结果绑定实际读取的文件,SOW 会检测检查期间的替换或修改。
检查保留的构建快照、最终清单与变更时,复用同一结果。增加 Dist 或保留的构建快照不会增加包哈希次数。DEB 与未签名 RPM 需要一次完整读取;已签名 RPM 最多再从主包头读到文件末尾,验证签名。
包缓存只查询本次选中的摘要,并按限定批次读取 SQLite。64 MiB 测试包的重复构建不读取包内容;指纹变化与缓存缺失共用一次校验。
分别验证 RPM 密钥集合
RPM 签名包在每组候选密钥内独立检查。每个可识别签名包都必须在该组中通过验证,且至少一个通过的签名必须认证软件包载荷。
这保留了对历史 CentOS 签名与双签软件包的支持,也避免把不同密钥组中的部分成功结果拼成错误的单密钥验证结论。
当前策略密钥、保留的构建快照密钥与组合受信任密钥共用同一次签名读取。增加密钥只改变信任判断,不增加包读取次数。
修改发布目标设置
使用 sow publish TARGET --rebind 修改目标的可变配置,同时保留存储身份与恢复状态:
可通过 --rebind 修改 |
需要配置新目标 |
|---|---|
| 目标名称 | Repository 身份 |
public_endpoint |
Provider 或存储 endpoint |
max_cache_ttl |
Region、bucket 或 prefix |
普通发布会报告配置变化,并提示使用 --rebind。重绑定取得发布锁、记录修订,也可继续执行已作出提交决定但被中断的发布。清理未完成时,TTL 与 filesystem 公共 URL 的变更受到限制。此前记录的缓存截止时间继续生效。
检查公共文件与恢复上传
Filesystem 与 R2 的 HTTP(S) 检查分别限制等待响应头和响应体停止传输的时间。只要持续接收数据,大文件传输可以超过两分钟。
SOW 检查普通客户端 URL。过期内容与 404 最多等待 max_cache_ttl;临时 408、425、429 与 5xx 响应使用较短重试时间。响应过大时校验失败。Filesystem 清理在删除后检查公共端点是否返回 404 或 410;R2 清理仍只报告候选对象。
其他发布改进包括:
- 创建目标绑定前检查 filesystem 路径别名,包括大小写不敏感卷上的别名。
- 中断的指针更新包含记录中的旧内容或新内容时,可继续执行;遇到其他内容则报告问题。
- 每次发布只检查发生变化的公共对象,不再下载完整构建快照。
- R2 使用可重试读写、空闲超时与限制并行数的条件式分段上传,完成上传前检查整对象 SHA-256。
CLI 与维护修复
- 重复执行
sow add时,即使包已在包池中,也会构建此前因--skip或配置变化而处于 dirty 状态的所选 Dist。 sow rm --check使用与删除相同的配置检查,并返回只读预览。- 所有 Managed 命令提供可读文本输出。
sow.cli/v1JSON 在命令失败时保留部分结果或诊断结果。 - 局部构建、Dist 变更、迁移、保留与垃圾回收过程保持签名者记录一致。
- 移除已退役的 APT 构建器、外部排序、空 Dist 与 Git 跟踪的 by-hash 实现。
构建与发布检查
本版使用 Go 1.27.0,0.3 使用 Go 1.26.5。AWS SDK、SQLite、压缩与密码学依赖同步更新。构建检查固定使用 staticcheck v0.8.1、deadcode v0.49.0 与 govulncheck v1.7.0。
发布检查覆盖 Go 测试、静态分析、漏洞与 RPM 分支源码检查、竞态测试、Linux amd64/arm64 构建,以及上述包读取次数限制。
本版包含四个 Linux/macOS 归档、两个 RPM、两个 DEB 与 SHA256SUMS。归档包含 sow、README.md、CHANGELOG.md 与 LICENSE。Linux 软件包随可执行文件安装协议文件。
获取 0.4.0
从 v0.4.0 GitHub Release 获取该版本制品与校验清单。安装后执行 sow version;0.3 工作区按上文迁移。
现在首次安装请使用当前下载页。当前行为见 Managed 工作区、签名模型与发布与恢复。
2.7 - SOW v0.3.0
v0.3.0 ·
SOW 0.3.0 减少了重复包读取与数据库查询。Plain 生成只遍历一次包内容;Managed 构建缓存已解析的包数据,并批量提交新包池文件。本版还移除了已退役的运行时与发布代码。
Plain:每个软件包只读一次
默认不签名的 sow create 流程对每个 RPM 或 DEB 只做一次哈希与解析,--jobs 控制并行任务数。SOW 根据解析结果生成元数据,写入索引前再次检查软件包集合与文件属性是否变化。
Plain 构建中断后,从软件包目录重新运行即可。0.3 移除了 Plain 的操作日志、回滚副本与重复包哈希;Managed 恢复机制不变。
Pigsty 预处理现在保留包头架构为 i386、i486、i586 或 i686 的 RPM。它们继续进入仓库元数据与 repo_complete 校验清单。DEB i386 与精确匹配 Patroni 3.0.4 的过滤规则保持不变。
Managed:减少查询与包读取
成员表增加了按软件包 SHA-256 的反向索引。SOW 通过一次有序查询读取期望成员与已构建成员,替代逐对象查询。项目基准中,列出含 5,000 个对象的 Dist 从约 4.1 秒降至 33 毫秒;50,000 个对象可在一秒内完成。
Schema v10 新增可重建的包数据缓存,以软件包 SHA-256 为键。导入时校验并解析新包,保存生成元数据所需的信息。构建时批量读取缓存;条目缺失或损坏时,从已校验的包文件重建。
包池不变时,重复构建通过设备号、inode、大小、修改时间与状态变更时间识别未变化文件。指纹变化时,执行完整 SHA-256 检查。sow check 仍会读取并校验完整包内容。
RPM 元数据与 DEB 架构索引使用受 --jobs 限制的并行任务。构建快照清单与变更记录改为批量写入。
批量提交与构建进度
Managed 构建将软件包分批移入公共包池,每批最多 512 个对象或 1 GiB。SOW 先持久保存目标目录,再移除 pending 名称。批次中断时,无论包位于 pending、包池还是两处,恢复流程都能处理。
Pending 文件在私有 0700 目录中使用最终的 0644 权限。构建不再全程为每个 pending 包持有文件描述符,减少了大型仓库的资源占用。
长时间构建会为渲染、包文件提交、Dist 发布、规范化与完成阶段记录 build_progress 事件。使用 sow log 查看进度。
统一仓库实现
本版移除了已退役的 V1 CLI 与运行时、云与 CDN 发布代码、Edge Worker、已完成的迁移程序及其旧测试。当前 CLI 使用统一的仓库实现。
R2 发布保留 SOW 所需的存储操作:list、HEAD、GET 与条件 PUT。远端垃圾回收只报告候选对象,不执行删除。
其他变更
- 本地垃圾回收可处理大小写不敏感文件系统中记录的唯一包池路径别名,删除前检查包路径、大小与摘要。
- 归档包含
LICENSE。RPM 与 DEB 分别把 Apache-2.0 协议安装到/usr/share/licenses/sow/LICENSE与/usr/share/doc/sow/copyright。 - CI 覆盖格式化、模块一致性、静态分析、死代码检查、测试、竞态检测、源码交付与软件包构建。
- 集成测试覆盖混合 RPM/DEB 流程、Ubuntu 22.04 上未签名 Plain APT 安装、AlmaLinux 8/9 与
pgsty/el10的 DNF 签名切换,以及固定 MinIO 版本上的 S3 条件操作。
获取 0.3.0
在 v0.3.0 GitHub Release 下载该版本的 Linux/macOS 归档、Linux RPM/DEB 软件包与 SHA256SUMS。安装后执行 sow version。该源码标签要求 Go 1.26.5 或更新版本。
现在首次安装请使用当前下载页。当前行为见 Plain 模式、Managed 工作区与平台与集成。
2.8 - SOW v0.2.0
v0.2.0 ·
SOW 0.2.0 引入了面向 RPM 与 DEB 仓库的 Plain 和 Managed 工作流。它以单个 Go 可执行文件,在 Linux 或 macOS 上构建静态软件包仓库。
本文记录 0.2.0 发布时的功能。当前安装与操作方法见现行文档。
构建平面仓库
Plain 模式索引目录顶层的 RPM 与 DEB 文件:
RPM 生成 repodata/,DEB 生成 Packages 与 Packages.gz,混合目录同时生成两种索引。Plain 模式以目录内容作为状态,不需要工作区或数据库。
本版还加入 --sign-with,为未签名 RPM 软件包签名。使用 --overwrite 可替换已有 RPM 签名。Plain 模式不生成或签署 APT Release 文件。
持续管理仓库
Managed 模式记录软件包成员,并构建具名 Dist:
每个 Repository 在 pool/ 下保存一份软件包文件。Dist 的 RPM 或 APT 元数据位于 dists/。每次构建记录一个快照,供 SOW 比较变更并保留选定的仓库状态。
0.2.0 加入了以下功能:
sow/v3配置与软件包成员策略;- RPM、APT 元数据签名,以及 RPM 软件包签名;
status、check、changes与操作日志;- 可恢复的 filesystem 与 R2 发布;
- 构建快照保留与本地垃圾回收;
- 为需要独立 RPM 目录的工具提供
rpm-leaf导出。
服务或发布完整 Repository 目录树,让元数据能访问共享包池。对已配置目标,sow publish 管理上传顺序与中断恢复。R2 清理只报告候选对象,不删除远端对象。
发布测试与源码
本版加入 APT/DNF 与 S3 兼容集成测试,并通过标签触发 GoReleaser,构建 Linux/macOS 归档与 Linux RPM/DEB 软件包。历史代码与测试保留在 v0.2.0 源码标签。