Files
t 950c1b429b feat(release): 抬档门禁(破坏性 ⇒ 0.x 抬次 / ≥1.0.0 抬主)+ 1.0.0 前置(稳定契约承诺)
原来预检只"打印破坏性条数",抬哪一档靠人自觉 ⇒ 改成硬门禁:

- **抬档门禁**:破坏性条数取自**目标提交里候选版本那一段**(发版事实看提交,不看工作区)。
  `0.x` 阶段有破坏性 ⇒ 必须抬**次版本**(或主版本),只抬修订 ⇒ 阻断并报出该抬到哪一档;
  `≥1.0.0` 之后有破坏性 ⇒ 必须抬**主版本**,抬次/修订都阻断。
  另加"候选号必须大于上一个正式版本"(版本只增不减)。
- **1.0.0 前置**:上一版本是 `0.x` 时,候选 `1.0.0` = 对外承诺契约稳定 ⇒ 必须先有写实的
  《稳定契约承诺》(覆盖 接口 / 数据模型 / 配置 / CLI 输出与退出码,不留 `待填`/`TODO`/`TBD`),
  **且候选发布记录里链接它**,缺任一条 ⇒ 阻断;`1.x → 2.0.0` 不重复要求。
- 预检新增"版本走向"事实行(上一版本 → 候选 / 抬哪一档 / 破坏性 N 条 ⇒ 要求哪一档 ✅⛔);
  `docs/RELEASE_RUNBOOK.md` 新增"抬哪一档"对照表与《稳定契约承诺》段(故意留占位)。

验证:release_preflight --self-test 32 分支全过(含新增 9 条);反向夹具新增 §24 六条真实临时仓库用例
(0.x 只抬修订 ⇒ 阻断、0.x 抬次 ⇒ 放行、≥1.0.0 抬次 ⇒ 阻断、1.0.0 留占位 ⇒ 阻断、写实+链接 ⇒ 放行、
写实但没链接 ⇒ 仍阻断)⇒ 夹具 91/0;make verify 15/15。
于是 0.1.0 之后的第一个提交(本提交)开发号 = 0.1.1。
2026-10-13 17:13:44 +08:00

8.0 KiB
Raw Permalink Blame History

发布说明(正式版本)

这份文档是破坏性变化与数据迁移步骤的落脚点:CHANGELOG.md 里以 **破坏性** 开头的条目 必须链接到本文(或本文的某个小节)。发布记录本身仍在 CHANGELOG.md —— 本文不维护第二份变更列表。

本仓 CI 只做 CI:不打 tag、不发 Release、不部署。发布与部署都是独立的人工审批动作。

一、正式发布(由人批准后执行)

  1. 确认基线:git rev-parse HEAD 拿到完整 40 位 SHA,工作区干净(git status --porcelain 为空)。

  2. 写记录:把这次要发的东西从 [Unreleased] 提到一个正式版本条目 ## [x.y.z] - YYYY-MM-DD(四类小节,无空栏目;破坏性变化写在 ### Changed 里并链接本文)。 版本号只增不减(回退走部署层,不改这个文件);每个项目只有本文件这一个版本来源。

  3. 跑预检(只读):python3 scripts/release_preflight.py --version X.Y.Z —— 输出两段:准备建议(还差什么)与 发布就绪判定(是否就绪)。 ⚠️ 预检不执行、也不推荐自动发布路径:自动发布路径已禁用 —— --execute / --approve-* 只保留参数兼容(不报 unknown argument),传了会被明确拒绝(rc=2,拒绝发生在收集事实之前 ⇒ 零写操作、零 API 调用)。正式发版动作由人按本文执行。 就绪 = 证据齐(缺一条都不算):

    判据 说明
    候选版本已是目标提交里的顶部版本 查 git show <sha>:CHANGELOG.md;只在工作区写了、没提交 ⇒ 不就绪;只在历史里出现过(顶部是别的版本)⇒ 也不就绪;预检会把该提交的顶部版本打出来
    完整 40 位 SHA 指向准确提交 目标提交 == HEAD,或 --sha 逐字符一致
    记录检查通过 结构与 tag 一致性(真查远端)
    必需 context 集合全部 success 集合来自 .gitea/required-contexts.txt;缺失/为空 ⇒ 没有策略 ⇒ 阻断;失败 / 缺项 / pending / 查询异常一律阻断
    远端 tag 已核对且未被占用 查不到远端 ⇒ 阻断
    Release 不存在 只有 HTTP 404 才算「不存在」;403/5xx/超时 ⇒ 查询异常 ⇒ 阻断
    首发版本号 还没有已发布版本 ⇒ 必须显式指定;不把「记录里有这个版本」当作已发布

    自查(不依赖网络、不打真 tag):python3 scripts/release_preflight.py --self-test、 python3 scripts/version.py --self-test。

  4. 由人执行(预检不做这件事):创建注释 tag vX.Y.Z(指向验收过的那个提交,完整 40 位 SHA) 并建关联 Release,正文取自 CHANGELOG.md。⚠️ release_preflight.py --execute 已被禁用 (见第 3 步),不要试图用它自动发版。

  5. 读回:git ls-remote --tags origin 里该 tag 的 SHA == 本地 git rev-parse vX.Y.Z^{commit}; Release 的 tag_name 正确。已发布的 tag 不移动、不覆盖。

⚠️ 三个状态分开说:CI 成功 ≠ 正式发版 ≠ 已部署。

二、构建身份怎么看

python3 scripts/version.py --identity     # JSON:release_version / build_kind / commit_sha / display_version / built_at
情形 build_kind release_version display_version
普通开发构建(有正式基线,tag 也在) development 顶部已发布版本 <主.次>.<自该 tag 的提交数>,如 0.1.12
开发构建恰好在发布提交上(提交数 = 0) development 同左 就是那个正式版本号
普通开发构建(还没有正式基线 / 取不到 tag) development null dev+g<短SHA>
发布构建(显式发布上下文 + 证据齐) release 顶部已发布版本 X.Y.Z(另带完整 SHA)

开发版本自动递增规则:<正式版本的主.次>.<自 v<正式版本> 以来的提交数> —— 第 1 个提交 0.1.1、第 12 个 0.1.12;数字只增、每次提交必不同,且不写进 CHANGELOG (发布记录只在发版时动)。打新 tag 后从新 tag 重新起算 ⇒ 所以下一次正式发布请把"次版本"加一 (0.1.x 之后发 0.2.0),这样正式号永远严格大于此前出现过的所有开发号,不会撞号。 证据字段:commits_since_release(自 tag 以来的提交数,无 tag 为 null)。

  • build_kind=release 只能来自显式的发布上下文(BUILD_KIND=release),并且要核证据: 完整 40 位 SHA + 指向该提交的注释 tag v<版本号> + 该提交里 CHANGELOG 顶部版本一致; 缺任何一条 ⇒ 拒绝标为正式版(--identity --release 以 rc=2 退出、身份退回 development 并列出缺什么)。普通 main CI 永远是 development,不得因为"CHANGELOG 顶部有版本号"就自称正式版。
  • 二进制里的注入字段(internal/version)与上表一一对应;契约测试断言: 注入 == CHANGELOG 顶部(发布构建)、开发构建必须是 dev 形状(不许声称正式版),且跳过不算通过。

二·补、抬哪一档(硬门禁,预检会拦)

抬哪一位 什么情况 预检判据(不满足 ⇒ 阻断,发不出去)
修订 0.1.N → 0.1.N+1 只修 bug、内部改动:对外行为不变、无破坏性、无成规模新能力 候选号必须 > 当前开发号(开发号到 0.1.7 时只能发 0.1.8 及以上);候选 ≤ 上一个正式版本 ⇒ 阻断(号只增不减)
次版本 0.1.x → 0.2.0 有破坏性变化(0.x 阶段一律走次版本),或成规模的对外新能力/行为变化 候选发布记录里破坏性条数 ≥1 且当前是 0.x ⇒ 必须抬次版本;只抬修订 ⇒ 阻断
主版本 0.x → 1.0.0、1.x → 2.0.0 首次声明"对外契约稳定"(1.0.0);之后任何破坏性变化(2.0.0) ≥1.0.0 阶段破坏性 ⇒ 必须抬主版本(抬次/修订都阻断);首发 1.0.0 还要求本文《稳定契约承诺》写实并在发布记录里链接它

判据里用的"破坏性条数"取自目标提交里候选版本那一段(发版事实看提交里的记录,不看工作区)。

二·补·2、《稳定契约承诺》(发 1.0.0 的前置条件)

⛔ 现在故意留着占位:只要还有 (待填) / TODO / TBD,预检在候选为 1.0.0(且上一个正式版本是 0.x)时就会阻断。 逐项写实、并在 CHANGELOG.md 的 ## [1.0.0] 记录里链接本文后,门禁才放行。四项一个都不能少。

  • 接口(待填):对外 HTTP / CLI / SDK 接口的稳定范围与版本策略(哪些字段冻结、变更怎么通知)。
  • 数据模型(待填):库表 / 文件 / 消息结构的兼容窗口与迁移责任。
  • 配置(待填):配置项与默认值的冻结范围、弃用流程、回退方式。
  • CLI 输出与退出码(待填):脚本可依赖的输出形态与退出码约定。

三、破坏性变化 / 数据迁移怎么写

在 CHANGELOG.md 的 ### Changed 里:

- **破坏性**:接口 `/api/old` 下线,升级步骤见 [迁移步骤](docs/RELEASE_RUNBOOK.md#三破坏性变化--数据迁移怎么写)

要求:必须以 **破坏性** 开头(便于发版前扫出来)、必须带本文链接;仓内相对链接会被 scripts/check_changelog.py 对账(文件不存在 ⇒ 红)。迁移步骤要写清「先做什么、怎么验证、怎么回退」。

四、回滚

回滚不改 CHANGELOG.md、不动 tag:走部署层(恢复上一个已验收的镜像/版本 + 读回证据)。 本仓部署流程在 docs/OPERATIONS_RUNBOOK.md 与各环境的部署脚本里;部署是独立审批, 并要记录"实际部署的版本 + 完整 SHA"。