- **抬哪一档成为硬门禁**:破坏性条数取**目标提交里候选那一段** ⇒ `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。
10 KiB
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. 日常改动(平时就做,不用发版)
- 改完代码,把使用者可见的变化写进
CHANGELOG.md顶部## [Unreleased]的四类小节 (Added/Changed/Fixed/Security;分类可省略,但别留空栏目); - 提交前跑本仓门禁:
make verify(唯一权威入口,本地与 CI 同一条命令); - 推送。不需要每次提交都升版本、也不需要打 tag。
过程记录不进变更记录:审查轮次、命令输出、逐条修正明细放 docs/archive/;
未合并事项放 docs/BACKLOG.md。日常改动不要求每个提交都写,有使用者可见的变化才记。
3. 准备发版
3.1 先把记录补齐
-
把
## [Unreleased]改成## [x.y.z] - YYYY-MM-DD(三段十进制、不许前导零、日期合法;版本号只增不减)。 ⚠️ 本仓还没有正式发布过时:没有可 bump 的基线,版本号由你指定 (预检会把它列成阻塞项并给一个建议值,不替你编号); -
该版本块里按四类小节写(分类可省略,但不要空栏目):
### Added/### Changed/### Fixed/### Security; -
破坏性变化 / 数据迁移写在
### Changed里、以**破坏性**开头,并链接发布文档:- **破坏性**:接口 `/api/old` 下线,升级步骤见 [迁移步骤](docs/RELEASE_RUNBOOK.md)检查器会对账仓内链接(
docs/….md不存在 ⇒ 红); -
清干净工作区(预检会因为"有未提交改动"直接拦住);
-
跑一遍检查器与构建身份:
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 跑发布预检(只读,不产生任何副作用)
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. 批准后执行(只有这一步会动远端)
# 三个参数缺一不可;--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,只此一份 |