Files
eng-standards-main/07-发版操作说明(tag与Release).md
t 3b26457165 docs: 抬档门禁 + 1.0.0 前置(稳定契约承诺)
- **抬哪一档成为硬门禁**:破坏性条数取**目标提交里候选那一段** ⇒ `0.x` 有破坏性必须抬**次版本**(或主版本),
  `≥1.0.0` 有破坏性必须抬**主版本**;只抬修订/抬次不够都阻断。无破坏性时抬哪档不限,
  但候选号必须严格大于上一正式版与当前开发号(号只增不减)。
- **`1.0.0` 首见的前置**:《稳定契约承诺》必须写实(接口 / 数据模型 / 配置 / CLI 输出与退出码,
  不留 `待填`/`TODO`/`TBD`)并在 `## [1.0.0]` 记录里链接它;`1.x → 2.0.0` 不重复要求。
- 同步:正本 §6 五次修正(⑭⑮)、07 发版判据表、"抬哪一档"对照表、templates/release-preflight.py、
  templates/README.md、04 §M(V-14~V-17)。

验证:templates/release-preflight.py --self-test 32 分支全过;试点仓夹具 91/0、make verify 15/15。
2026-10-13 17:13:44 +08:00

171 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 07 · 发版操作说明(注释 tag 与 Gitea Release)
> 正本条款见 `01-规范-Git与CI-CD(Gitea版).md` **§6**。本文件只讲**怎么操作**。
> 适用:单主干 `main` 的所有仓库(本机 Gitea)。
> ⚠️ 一句话前提:**CI 不会替你发版**。CI 只做检查;打 tag、建 Release 是**人在明确批准后**的动作。
---
## 0. 三个状态,先说清楚(别混成一个"绿")
| 状态 | 什么时候算成立 | 证据 |
|---|---|---|
| CI 成功 | 提交推上去,必需检查全过 | 提交上的 status context + run 链接 + **完整 SHA** |
| 正式发版 | 批准后打了注释 tag、建了 Release | 远端 tag SHA == 验收过的提交;Release 的 `tag_name` 读回一致 |
| 部署 | 某个环境真的在跑这个版本 | `/version` 的 `commit_sha` + 部署读回证据 |
"CI 绿"不代表"发过版";"发过版"不代表"部署了"。汇报时必须说是哪一个。
---
## 1. 版本号的来源与构建身份
**构建身份**(`python3 scripts/version.py --identity`):
| 情形 | `build_kind` | `release_version` | `display_version` |
|---|---|---|---|
| 开发构建(有正式基线 + tag) | `development` | 顶部已发布版本 | **`<主>.<次>.<自该 tag 的提交数>`**(如 `0.1.12`) |
| 开发构建恰在发布提交上(提交数 0) | `development` | 同左 | 就是那个正式版本号 |
| 开发构建(无基线 / 取不到 tag / 浅克隆) | `development` | 空 | `dev+g<短SHA>` |
| 发布构建(**显式发布上下文** + 证据齐) | `release` | 顶部已发布版本 | `X.Y.Z`(另带完整 SHA) |
**开发号自动递增**:第 1 个提交 `0.1.1`、第 12 个 `0.1.12` —— **不写进 CHANGELOG**、数字只增;
因此**下一次正式发布请把次版本加一**(`0.1.x` → `0.2.0`),保证正式号严格大于此前所有开发号。
⚠️ 开发构建**不许**只显示上一正式版本号,也不许自称正式版;`build_kind=release`
只能来自显式上下文(`BUILD_KIND=release`),普通 `main` 构建永远是 `development`。
- **本环境默认**:仓库根 `CHANGELOG.md` **顶部那条已发布版本**就是版本号的唯一来源。
- 读它:`python3 scripts/version.py`(解析不到 ⇒ 非 0 退出,构建据此失败)。
- 构建注入:`-ldflags "-X <mod>/internal/version.Number=<该值>"`;CI 断言"注入 == 来源",且注入的断言
**被 SKIP 也算不通过**(`contracts` 检查项)。
- ⚠️ 不要再放 `VERSION` 文件、不要把版本写进源码常量、不要在 CI 里用 `git describe`
(检出是 `--depth 50`,取不到 tag ⇒ 静默给错值)。
- 某个项目要用别的权威来源,**必须在它自己的 `AGENTS.md` 里写明**,并让构建与 CI 都读那一个来源。
---
## 2. 日常改动(平时就做,不用发版)
1. 改完代码,把**使用者可见**的变化写进 `CHANGELOG.md` 顶部 `## [Unreleased]` 的四类小节
(`Added` / `Changed` / `Fixed` / `Security`;分类可省略,但别留空栏目);
2. 提交前跑本仓门禁:`make verify`(唯一权威入口,本地与 CI 同一条命令);
3. 推送。**不需要**每次提交都升版本、也不需要打 tag。
**过程记录不进变更记录**:审查轮次、命令输出、逐条修正明细放 `docs/archive/`;
未合并事项放 `docs/BACKLOG.md`。日常改动**不要求每个提交都写**,有使用者可见的变化才记。
---
## 3. 准备发版
### 3.1 先把记录补齐
1. 把 `## [Unreleased]` 改成 `## [x.y.z] - YYYY-MM-DD`
(三段十进制、**不许前导零**、日期合法;版本号只增不减)。
⚠️ **本仓还没有正式发布过时**:没有可 bump 的基线,版本号**由你指定**
(预检会把它列成阻塞项并给一个建议值,不替你编号);
2. 该版本块里按**四类小节**写(分类可省略,但**不要空栏目**):
`### Added` / `### Changed` / `### Fixed` / `### Security`;
3. **破坏性变化 / 数据迁移**写在 `### Changed` 里、以 `**破坏性**` 开头,并**链接发布文档**:
```markdown
- **破坏性**:接口 `/api/old` 下线,升级步骤见 [迁移步骤](docs/RELEASE_RUNBOOK.md)
```
检查器会**对账**仓内链接(`docs/….md` 不存在 ⇒ 红);
4. 清干净工作区(预检会因为"有未提交改动"直接拦住);
5. 跑一遍检查器与构建身份:
```bash
python3 scripts/check_changelog.py --tags --require-remote # 结构 + 正式 tag 与提交内版本一致(真查远端)
python3 scripts/version.py --identity # 构建身份(开发/发布、显示版本、完整 SHA)
```
### 3.2 就绪判据(预检说的「发布就绪」是什么意思)
**就绪 = 证据齐**,缺一条都不算:
| 判据 | 说明 |
|---|---|
| 候选版本**已是目标提交里的顶部版本** | 查 `git show <sha>:CHANGELOG.md`;只在工作区写了、没提交 ⇒ 不就绪;只在**历史**里出现(顶部是别的版本)⇒ **也不就绪** |
| 完整 40 位 SHA 指向准确提交 | 目标提交 == HEAD(或显式 `--sha` 逐字符一致) |
| 记录检查通过 | `check_changelog.py --tags --require-remote`(四类小节、破坏性变化带文档链接、tag 一致性) |
| **必需 context 集合全部 success** | 集合来自 `.gitea/required-contexts.txt`(缺失/空 ⇒ 没有策略 ⇒ 阻断);**失败 / 缺项 / pending / 查询异常一律阻断** |
| 远端 tag 已核对且未占用 | 查不到远端 ⇒ 阻断(未核验不等于可以发) |
| Release 不存在 | **只有 HTTP 404 才算「不存在」**;403/5xx/超时 ⇒ 查询异常 ⇒ 阻断 |
| 首发版本号 | 还没有已发布版本 ⇒ 必须**显式指定**(`--version X.Y.Z`);**不把「记录里有这个版本」当作已发布** |
| 版本号不与开发号撞车 | 候选正式版本必须**严格大于当前开发号**(预检会挡住 `<=` 的候选;下一次发布用次版本加一) |
| **抬档够不够(硬门禁)** | 候选发布记录里有破坏性变化 ⇒ `0.x` 阶段必须抬**次版本**(或主版本)、`≥1.0.0` 必须抬**主版本**;只抬修订 ⇒ 阻断。判据取**目标提交里候选那一段**,不看工作区 |
| **`1.0.0` 的前置** | 必须先把《稳定契约承诺》(接口 / 数据模型 / 配置 / CLI 输出与退出码)写实——**不许留占位**——并在 `## [1.0.0]` 记录里链接它;否则阻断。`1.x → 2.0.0` 不重复要求 |
预检输出分两段:**`准备建议`**(还差什么)与 **`发布就绪判定`**(是否就绪)。
⚠️ 预检是**只读**的:**自动发布路径已禁用**(`--execute` / `--approve-*` 仅保留参数兼容,传了会被
**明确拒绝**并 rc=2,且零写操作、零 API 调用),输出里也不推荐自动路径 —— 正式发版动作由人按本文执行。
### 3.3 跑发布预检(只读,不产生任何副作用)
```bash
python3 scripts/release_preflight.py # 默认建议 patch
python3 scripts/release_preflight.py --version 0.2.0 # 首发必须显式指定版本号
python3 scripts/release_preflight.py --no-api # 显式不查远端 ⇒ 必然"不就绪"(只列事实)
```
自查(不依赖网络):`python3 scripts/release_preflight.py --self-test`
(就绪判定逐条阻断 + `git show <sha>:CHANGELOG.md` 的临时仓库验证)与
`python3 scripts/version.py --self-test`(正式构建证据的正/负例,全部在临时仓库里造 tag)。
它会打印:待发布版本 / **完整提交 SHA** / 该提交上的 CI 成功证据 / `[Unreleased]` 原文 /
同名**远端 tag 与 Release 是否已存在** / 遗留风险与未验证项,并在最后给出
**"可以发"或"不能发 + 阻塞项"**。
预检的只读性是可验证的:`python3 scripts/release_preflight.py --self-test`
(断言写类 git 命令与 `POST/PUT/DELETE/PATCH` 在预检路径上**被拒**)。
### 3.3 请主人批准
批准要包含三样:**版本号**、**准确提交的完整 SHA**、**那次 CI 的成功证据**(run 链接)。
不要拿"本地跑过了"当 CI 证据,也不要用短 sha 当"准确提交"。
---
## 4. 批准后执行(只有这一步会动远端)
```bash
# 三个参数缺一不可;--approve-version / --approve-sha 必须**逐字符**等于预检结果
python3 scripts/release_preflight.py --execute \
--version 0.2.0 --approve-version 0.2.0 --approve-sha <完整40位SHA>
```
内部顺序固定:**打注释 tag (`git tag -a v0.2.0`) → 推送该 tag → 建 Gitea Release → 读回**。
读回判据(缺一即不算完成):
- 远端 tag 的 SHA == 本地 tag 指向的提交(完整比对,不是前缀);
- Release 的 `tag_name` 读回一致;
- Release 说明**来自 CHANGELOG**,不另写正文。
⚠️ tag 推送成功、建 Release 失败 ⇒ **不要重复推 tag**(tag 已经存在会被拒),先把 Release 补上。
---
## 5. 发版之后
- **部署是另一件事**(§5 / 各项目 CD runbook):正式发版 ≠ 已部署。
- 已发布的 tag **不移动、不覆盖**。要"重来一次"就发一个新版本号。
- 回滚走部署层(恢复旧产物),**不改版本历史**。
- 汇报时给:tag、完整 SHA、Release 链接、CI 证据、(若已部署)`/version` 读回。
---
## 6. 常见错误(都踩过)
| 症状 | 原因 | 处理 |
|---|---|---|
| 日常 CI 红了,说我"没有 tag" | 把 tag 一致性检查写成了"必须有 tag" | **没有 tag 不算失败**:开发版本不打 tag 是常态,检查只能在**存在**正式 tag 时判 |
| tag 打上了,但变更记录里没有这个版本 | tag 指向的提交里 `CHANGELOG.md` 顶部还是旧的版本号 | 检查会红:`tag 版本 != 该提交里 CHANGELOG 顶部的版本`;先补 CHANGELOG 再打 tag |
| 打了轻量 tag | `git tag v1.2.3`(没有 `-a`) | 正式 tag 必须是**注释 tag**;已发布的轻量 tag 不要强推覆盖,发新版本号 |
| 预检通过了却发不出去 | 预检是**只读**的,发版要显式 `--execute` 加两个批准参数 | 按 §4 执行,别指望预检替你发 |
| CI 里 `git describe` 拿不到版本 | 检出是 `--depth 50`,没有 tag | 不要用 `git describe` 取版本;读 `CHANGELOG` 顶部 |
| Release 说明和 CHANGELOG 不一致 | 又写了一份正文 | Release 说明**来自 CHANGELOG**,只此一份 |