Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 47 additions & 10 deletions docs/release-governance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 处
Expand All @@ -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<X.Y.Z>" —— 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 <release 分支检出>`,
结论也会以 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。挂在它下面的
Expand All @@ -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 ...` 真构建 + 单独点头
Expand Down
5 changes: 5 additions & 0 deletions tests/test_release_readiness_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
# 判据里有两条要读它(extra-files 的类型白名单与标注一致性),漏搬就会以 FileNotFoundError
# 崩在"判据未能执行"上 —— 那正是脚本设计上要区分开的那一档(崩 != 漂移,但同样不能放行)。
"release-please-config.json",
"docs/release-governance.md",
)


Expand Down Expand Up @@ -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(
{
Expand Down
7 changes: 7 additions & 0 deletions tests/test_version_consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 明写的"装配中间物"
Expand Down
Loading