950c1b429b
原来预检只"打印破坏性条数",抬哪一档靠人自觉 ⇒ 改成硬门禁: - **抬档门禁**:破坏性条数取自**目标提交里候选版本那一段**(发版事实看提交,不看工作区)。 `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。
102 lines
8.0 KiB
Markdown
102 lines
8.0 KiB
Markdown
# 发布说明(正式版本)
|
||
|
||
> 这份文档是**破坏性变化与数据迁移步骤的落脚点**:`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 成功 ≠ 正式发版 ≠ 已部署**。
|
||
|
||
## 二、构建身份怎么看
|
||
|
||
```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"。 |