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

10 KiB
Raw Permalink Blame History

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 里、以 **破坏性** 开头,并链接发布文档:

    - **破坏性**:接口 `/api/old` 下线,升级步骤见 [迁移步骤](docs/RELEASE_RUNBOOK.md)
    

    检查器会对账仓内链接(docs/….md 不存在 ⇒ 红);

  4. 清干净工作区(预检会因为"有未提交改动"直接拦住);

  5. 跑一遍检查器与构建身份:

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,只此一份