Files
eng-standards-main/07-发版操作说明(tag与Release).md
T
姚章浩 79c154f9af docs: 版本条目改固定四类 + 破坏性变化固定写法 + 构建身份(口径变更)
对着试点仓落地时暴露的问题做的口径与模板修正(本轮不推、不打 tag、不部署):

- 条目形状:四个自定义必填节 → **固定四类** Added/Changed/Fixed/Security
  (分类可省略、不许空栏目);过程记录进 docs/archive/,未合并事项进 docs/BACKLOG.md。
- 破坏性变化/迁移:固定写在 Changed 里、以 `**破坏性**` 开头并**必须链接发布文档**,
  仓内 `docs/….md` 链接**对账**(不存在即红)。
- 构建身份:release_version / build_kind / commit_sha / display_version / built_at;
  开发构建显示 X.Y.Z-dev+g<短SHA>(无基线 dev+g<短SHA>),**不许只显示上一正式版本号、
  不许自称正式版**;release 只认显式发布上下文。
- **还没有正式版本一律放行**:不判红;首个正式版本号**由人指定**(预检把它列成阻塞项)。
- **没核对远端就不许说校验成功**:CI/预检用 `--require-remote`(本地模式明确写「未核对远端」)。
- 修正"CI 取不到 tag 所以不用 tag"的过时口径:正式版本**就是**用注释 tag,
  只是**构建时不要用 git describe 取版本号**;**测试 tag 只在临时测试仓库里创建**。
- 模板同步:templates/check-changelog.py、templates/release-preflight.py、
  新增 templates/version.py;ci-gate.py 的 version 项在 CI 下强制核对远端;
  new-project.sh 落 scripts/version.py 并按四类重写 CHANGELOG 骨架;
  README/07/模板 README 同步;04 增加 §I(V-1~V-4 四条,含负例在哪)。

验证:templates 两个检查器 --self-test 全过;new-project.sh bash -n 通过;
生成的新工程里 check_changelog --tags 绿、--identity 给出 dev+g<短SHA>、
CI=true 下 version 项因"未核对远端"判红(预期);试点仓反向夹具 80/0。
2026-10-13 14:59:48 +08:00

7.3 KiB
Raw 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
开发构建(有正式基线) development 顶部已发布版本 X.Y.Z-dev+g<短SHA>
开发构建(还没有正式版本) development 空 dev+g<短SHA>
发布构建(显式发布上下文) release 顶部已发布版本 X.Y.Z(另带完整 SHA)

⚠️ 开发构建不许只显示上一正式版本号,也不许自称正式版;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 跑发布预检(只读,不产生任何副作用)

python3 scripts/release_preflight.py                # 默认建议 patch
python3 scripts/release_preflight.py --bump minor   # 或 --version 0.2.0

它会打印:待发布版本 / 完整提交 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,只此一份