按使用者要求定的版本规则(自动升的是**开发标识**,发布版本与 tag 仍由人批准): - **开发号自动递增**:display_version(development) = `<正式版本的主.次>.<自 v<正式版本> 以来的提交数>` (正式 0.1.0 之后第 1 个提交 0.1.1、第 12 个 0.1.12;恰在发布提交上 = 正式号)。 它**不写进 CHANGELOG**(发布记录只在发版时动),由 git 算出来 ⇒ 每个提交都有自己的号、数字只增。 - **浅克隆不猜号**:`--depth` 会把条数算少 ⇒ 浅仓库里拒绝算号,退回 `<版本>-dev+g<短SHA>`; 检出步骤在仓库有 tag 时 `--unshallow` 补全历史。 - 接线三处齐全:`version.py --identity` → 门禁 `-X …CommitsSinceRelease=<n>`;Makefile 的 `COMMITS_SINCE_RELEASE`;Dockerfile 同名 build-arg;Go 侧新增 `CommitsSinceRelease` 与 DisplayVersion() 新规则;契约测试**交叉核对**"注入的提交数 == git 算出的"(防注入错号)。 - **首个正式版本 0.1.0**:CHANGELOG 的 `[Unreleased]` 提成 `## [0.1.0] - 2026-10-13`; 头部补"版本怎么走"两行;RELEASE_RUNBOOK 补递增规则与"下一次发布把次版本加一"。 - 发版预检新增:打印**当前开发号**,并挡住"候选正式版本 <= 当前开发号"(会把开发号当正式号;建议次版本加一)。 验证:version.py --self-test(含 tag 后第 2 个提交 = 0.2.2、恰在 tag 上 = 0.2.0、无 tag 不猜号); release_preflight --self-test(21 分支,含开发号两条);go test 本地三态 + 临时仓库 tag+2 提交实测 (Python 与 Go 都得出 0.2.2);反向夹具 85/0;make verify 15/15 success。
6.0 KiB
发布说明(正式版本)
这份文档是破坏性变化与数据迁移步骤的落脚点:
CHANGELOG.md里以**破坏性**开头的条目 必须链接到本文(或本文的某个小节)。发布记录本身仍在CHANGELOG.md—— 本文不维护第二份变更列表。本仓 CI 只做 CI:不打 tag、不发 Release、不部署。发布与部署都是独立的人工审批动作。
一、正式发布(由人批准后执行)
-
确认基线:
git rev-parse HEAD拿到完整 40 位 SHA,工作区干净(git status --porcelain为空)。 -
写记录:把这次要发的东西从
[Unreleased]提到一个正式版本条目## [x.y.z] - YYYY-MM-DD(四类小节,无空栏目;破坏性变化写在### Changed里并链接本文)。 版本号只增不减(回退走部署层,不改这个文件);每个项目只有本文件这一个版本来源。 -
跑预检(只读):
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。 -
由人执行(预检不做这件事):创建注释 tag
vX.Y.Z(指向验收过的那个提交,完整 40 位 SHA) 并建关联 Release,正文取自CHANGELOG.md。⚠️release_preflight.py --execute已被禁用 (见第 3 步),不要试图用它自动发版。 -
读回:
git ls-remote --tags origin里该 tag 的 SHA == 本地git rev-parse vX.Y.Z^{commit}; Release 的tag_name正确。已发布的 tag 不移动、不覆盖。
⚠️ 三个状态分开说:CI 成功 ≠ 正式发版 ≠ 已部署。
二、构建身份怎么看
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 + 指向该提交的注释 tagv<版本号>+ 该提交里 CHANGELOG 顶部版本一致; 缺任何一条 ⇒ 拒绝标为正式版(--identity --release以 rc=2 退出、身份退回development并列出缺什么)。普通mainCI 永远是development,不得因为"CHANGELOG 顶部有版本号"就自称正式版。- 二进制里的注入字段(
internal/version)与上表一一对应;契约测试断言: 注入 == CHANGELOG 顶部(发布构建)、开发构建必须是dev形状(不许声称正式版),且跳过不算通过。
三、破坏性变化 / 数据迁移怎么写
在 CHANGELOG.md 的 ### Changed 里:
- **破坏性**:接口 `/api/old` 下线,升级步骤见 [迁移步骤](docs/RELEASE_RUNBOOK.md#三破坏性变化--数据迁移怎么写)
要求:必须以 **破坏性** 开头(便于发版前扫出来)、必须带本文链接;仓内相对链接会被
scripts/check_changelog.py 对账(文件不存在 ⇒ 红)。迁移步骤要写清「先做什么、怎么验证、怎么回退」。
四、回滚
回滚不改 CHANGELOG.md、不动 tag:走部署层(恢复上一个已验收的镜像/版本 + 读回证据)。
本仓部署流程在 docs/OPERATIONS_RUNBOOK.md 与各环境的部署脚本里;部署是独立审批,
并要记录"实际部署的版本 + 完整 SHA"。