# 发布说明(正式版本) > 这份文档是**破坏性变化与数据迁移步骤的落脚点**:`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 :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 成功 ≠ 正式发版 ≠ 已部署**。 ## 二、构建身份怎么看 ```bash 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` 里: ```markdown - **破坏性**:接口 `/api/old` 下线,升级步骤见 [迁移步骤](docs/RELEASE_RUNBOOK.md#三破坏性变化--数据迁移怎么写) ``` 要求:**必须以 `**破坏性**` 开头**(便于发版前扫出来)、**必须带本文链接**;仓内相对链接会被 `scripts/check_changelog.py` **对账**(文件不存在 ⇒ 红)。迁移步骤要写清「先做什么、怎么验证、怎么回退」。 ## 四、回滚 回滚**不改** `CHANGELOG.md`、**不动** tag:走部署层(恢复上一个已验收的镜像/版本 + 读回证据)。 本仓部署流程在 `docs/OPERATIONS_RUNBOOK.md` 与各环境的部署脚本里;部署是**独立审批**, 并要记录"实际部署的版本 + 完整 SHA"。