diff --git a/docs/release-governance.md b/docs/release-governance.md index 5d4eb7d..9ed7934 100644 --- a/docs/release-governance.md +++ b/docs/release-governance.md @@ -8,7 +8,7 @@ ## 1. 版本号规范 -- 遵循 SemVer `MAJOR.MINOR.PATCH`。**已发布最新 = v2.2.4(2026-09-22)**。 +- 遵循 SemVer `MAJOR.MINOR.PATCH`。**已发布最新 = v2.2.5(2026-09-22)**。 版本号出现在 11 处(`version.json`/`pyproject.toml`/`config.yaml`/`desktop/*`/`scripts/installer/setup.nsi`), 但只有 tag + GitHub Release 同时存在才算发出去;核对:`gh release view v<版本>`。 > 其中 `desktop/package-lock.json` 在 `.gitignore` 里(不是仓库内的版本位);仓库内跟踪的 9 处 @@ -28,26 +28,58 @@ > `scripts/installer/setup.nsi` 的 `OutFile`/`APP_VERSION`/`VIProductVersion` > (NSIS 注释符是 `;`,用不了 `generic` 要求的 `# x-release-please-version` 行内标记)、 > `deploy/kubernetes/deployment.yaml` 的镜像 tag(要跟 ghcr 上真存在的标签走)。 + > **第 10 处是散文**:本文开头那句"已发布最新 = v" —— RP 不碰它,而它真会漂 + > (v2.2.5 发出去之后这里还停在 v2.2.4),所以已被 `test_all_version_sites_agree` + > 当版本位钉住,补齐时顺手改一行。 > `version.json` 里 RP 只抬 `$.version`,**`changelog` 与 `release_date` 也是手工位** > (壳的 `updater.rs` 会把 changelog 显示给用户,只抬版本号会发出"自称 2.2.4、说明写着 2.2.3"的包)。 - > 所以下一条 release PR 上,红在这几处是**预期行为**,补一个 commit 即可 —— + > 补齐这几处的那条提交要加在 release 分支上、**并且紧接着就合**:任何一次 main push 都会让 + > RP 重写那条分支,把你补的提交冲掉(2026-09-22 真被冲掉过一次 `1c2597a`)。 > 失败信息里会逐条标注哪处是『RP 自动』、哪处是『手工同步』, > 并由 `test_extra_files_entries_are_all_actionable` 钉住"每条 extra-files 今天确实能命中"。 - 版本位:`pyproject.toml` + `config.yaml`(release-please 驱动前端缓存参数需人工补齐,见本地 AGENTS.md #9(AGENTS.md 为本地维护、不随仓库分发))+ `CHANGELOG.md`。 - **发版有两条路,别同时走**: - 1. 自动:合入 release PR(RP 在 main push 后自动开/刷新,如 #120)并等它自己打 tag 发 Release; - 2. 手工:`git tag -a` + `gh release create`(v2.2.2/v2.2.3 走的就是这条)。 - 手工发版之后 RP 会在下一条 release PR 里把版本号再抬一格(它按 manifest 算), - 所以手工发完要把 `.release-please-manifest.json` 一起抬到刚发的版本,否则两边在版本号上互踩。 + 1. 自动:合入 release PR(RP 在 main push 后自动开/刷新)并等它自己打 tag 发 Release。 + **已于 2026-09-22 走通一次**:v2.2.5 的 tag `9125a3e`、Release 与 4 个资产 + (`SHA256SUMS`、`SHA256SUMS.scripts`、wheel、sdist)都是 RP 自己产出/挂上的。 + 走这条路必须知道四件事: + - **它只抬 5+1 处自动位**,剩下 5 类手工位(`config.yaml`、`Cargo.lock`、 + `setup.nsi` 三处、k8s 镜像 tag、`version.json` 的 `changelog`/`release_date`) + 要你在 release 分支上**另加一条提交**补齐;而**任何一次 main push 都会让 RP 重写那条分支**, + 把你补的提交冲掉(今天冲掉过一次:`1c2597a`)。所以顺序是"补齐 → 立刻合",中间别合别的。 + - **DCO 曾经让它结构上不可合**:RP 的提交作者是 `github-actions[bot]`,永远签不出 + `Signed-off-by`,而分支保护要求 DCO —— 已按"提交作者"豁免(#135),并留下 + `tests/test_dco_bot_exemption.py` 钉住"豁免不是后门"。 + - **release PR 拿不到 CI**(见下面那段),所以合并前的判据是本地跑 + `python scripts/check_release_readiness.py --root `, + 结论也会以 commit status `release-gate` 打在 PR head 上(把它加成必需检查是 owner 的一键决定)。 + - **它不会替你发出镜像**:"bot 的 `GITHUB_TOKEN` 不级联"这条规则不止吃 release PR 的 CI, + 也吃**它自己打的 tag 和它自己建的 Release** 这两个下游事件。实测对照(2026-09-22): + v2.2.2 / v2.2.3 / v2.2.4 三次手工 `git tag` 各留下 `docker-publish.yml` 一条 + `ev=push br=vX.Y.Z` 和 `gpg-signed-release.yml` 一条 `ev=release`; + v2.2.5 这两个工作流上**都是 0 条**(`gh run list --workflow docker-publish.yml` 复算)。 + 于是合完 release PR 后要立刻补一手 `gh workflow run docker-publish.yml --ref vX.Y.Z` —— + `metadata-action` 的 `type=semver` 在 tag ref 上就能出 `:2.2.5`,**不用改工作流**。 + 不补的后果是具体的:`deploy/kubernetes/deployment.yaml` 指着 ghcr 上不存在的标签, + 而且这不能靠"把镜像 tag 退回上一版"消红 —— 那会让 `test_all_version_sites_agree` + 把版本位一致性一起判红(2026-09-22 的 v2.2.5 就卡在这一点上,见 §2 第 5 步)。 + GPG 签名那一格现在不痛(`GPG_PRIVATE_KEY` 未配,作业进 skip 分支只出 notice), + 但 owner 配上密钥后,RP 发的每个版本都要把 `gpg-signed-release.yml` 一起手工补跑。 + 2. 手工:`git tag -a` + `gh release create`(v2.2.2 / v2.2.3 / v2.2.4 走的就是这条)。 + 手工发版之后 RP 会在下一条 release PR 里把版本号再抬一格(它按 manifest 算), + 所以手工发完要把 `.release-please-manifest.json` 一起抬到刚发的版本,否则两边在版本号上互踩。 + —— 反过来也成立:走自动路径时别顺手再手工打同版本的 tag。 > **走第 1 条时,release PR 上看不到任何 CI 检查**(实测 #120:`gh pr checks 120` 是空的)。 > 原因是 GitHub 的固定行为:由 `GITHUB_TOKEN` 产生的提交不再级联触发 workflow, > 而 release PR 的提交正是 bot 用 `GITHUB_TOKEN` 推的。 > 后果很具体:§1 上面列的那些**手工同步位**在 release PR 上不会变红 —— > `test_all_version_sites_agree` 要到合并进 main 之后才红,那时 Release 已经发出去了。 - > 所以合 release PR 之前**必须本地跑**:`.venv/Scripts/python.exe -m pytest tests/test_version_consistency.py` - > (外加 §2 第 1–2 步的手工位补齐)。要把它变成机器闸,就得给 release PR 配一个 - > 由 `workflow_dispatch`/`push` 触发、能对 release 分支的 head SHA 报 commit status 的作业 —— - > 那是权限决策,不在本文档的"现状"里。 + > 所以合 release PR 之前**必须本地跑**:`python scripts/check_release_readiness.py --root <检出>` + > (外加 §2 第 1–2 步的手工位补齐)。机器闸已经补上了一半(#134):`release-please.yml` 会在 + > 开/刷新 PR 之后跑同一个脚本,并把结论以 commit status `release-gate` 报在 release 分支的 + > head SHA 上 —— 它是**可见信号不是拦截**,把它加成必需检查仍是 owner 的一键决定。 + > 而且要注意它只覆盖"版本位/可发布性"这一类判据:pytest 全量、真机验收这些在 release PR 上 + > 永远不会跑,那是本地路径的活。 历史上 `release-please.yml` 曾是**结构性空转**(`skip-github-pull-request: true` + 缺 config/manifest + 5 个 v4 不认的入参 → 每次 main push 输出 `found 0 possible releases` 后绿, v2.2.2 因此三次绿 run 都没 Release);已修,现在它真的会开 PR。挂在它下面的 @@ -67,6 +99,11 @@ 4. 手工发完把 `.release-please-manifest.json` 的 `"."` 抬到刚发的版本并推 main, 否则 RP 会在下一条 release PR 里把版本号再抬一格(两边互踩) 5. CI 盯到终态;容器镜像**发布走 `docker-publish.yml`**,上线时按 digest 钉,禁止 `:latest` + - **v2.2.5 起这条要自己踩**:走自动发版路径时 RP 用 `GITHUB_TOKEN` 打的 tag 不级联, + `docker-publish.yml` 不会因此运行,`:2.2.5` 这个标签在 ghcr 上就不存在(见 §1 第 4 条)。 + 补法:`gh workflow run docker-publish.yml --ref v<版本>`,跑完用 + `gh run list --workflow docker-publish.yml --branch v<版本>` 确认有了一条 completed/success, + 再回头核 `deployment.yaml` 指的那个 tag 真的存在 —— 顺序反了就是"清单自称能上线、实际拉不到镜像"。 > 便携分卷(core/torch/model,~26 GB)与桌面增量包**不在**第 3 步的默认资产里: > 需要 `scripts/release_gate.ps1 -ModelDir ... -RuntimeDir ... -TorchWheelDir ...` 真构建 + 单独点头 diff --git a/tests/test_release_readiness_gate.py b/tests/test_release_readiness_gate.py index 57e448f..b73bda1 100644 --- a/tests/test_release_readiness_gate.py +++ b/tests/test_release_readiness_gate.py @@ -40,6 +40,7 @@ # 判据里有两条要读它(extra-files 的类型白名单与标注一致性),漏搬就会以 FileNotFoundError # 崩在"判据未能执行"上 —— 那正是脚本设计上要区分开的那一档(崩 != 漂移,但同样不能放行)。 "release-please-config.json", + "docs/release-governance.md", ) @@ -116,6 +117,10 @@ def _complete_hand_sites(root: Path) -> None: f'OutFile "TTSMultiModel-Setup-v{NEW}.exe"\n!define APP_VERSION "{NEW}"\nVIProductVersion "{NEW}.0"\n', encoding="utf-8", ) + gov = root / "docs" / "release-governance.md" + gov.write_text( + gov.read_text(encoding="utf-8").replace("已发布最新 = v" + CUR, "已发布最新 = v" + NEW), encoding="utf-8" + ) (root / "version.json").write_text( json.dumps( { diff --git a/tests/test_version_consistency.py b/tests/test_version_consistency.py index e5a2902..ff0bacd 100644 --- a/tests/test_version_consistency.py +++ b/tests/test_version_consistency.py @@ -146,6 +146,13 @@ def grab_json(rel: str, label: str) -> str: r"image:\s*ghcr\.io/\S+/tts-multimodel:([0-9][^\s\"]*)", "k8s 镜像 tag", ), + # 第 10 处:文档里那句"已发布最新"。它以前只是散文,于是会这样漂 —— + # v2.2.5 由 release-please 发出去之后,这行还停在 v2.2.4(RP 不碰散文,手工补齐时也没带上它)。 + "docs/release-governance.md": grab( + "docs/release-governance.md", + r"\*\*已发布最新 = v(\d+\.\d+\.\d+)", + "治理文档声称的最新已发布版本", + ), } # 安装器**内嵌**的那份版本:`setup.nsi` 的 `File "version.json"` 取的是 # `scripts/installer/version.json`,而它是 .gitignore:436 明写的"装配中间物"