From 09ea8e167887bf57771550ab30aa2840d90ca2ef Mon Sep 17 00:00:00 2001 From: ReSerendipity Date: Tue, 22 Sep 2026 12:43:03 +0800 Subject: [PATCH 1/3] =?UTF-8?q?test(version):=20=E8=A1=A5=E4=B8=8A?= =?UTF-8?q?=E6=BC=8F=E6=8E=89=E7=9A=84=E4=B8=80=E5=A4=84=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E4=BD=8D=EF=BC=8C=E5=B9=B6=E6=92=A4=E6=8E=89=E6=88=91=E8=87=AA?= =?UTF-8?q?=E5=B7=B1=E5=86=99=E9=94=99=E7=9A=84=E4=B8=80=E6=9D=A1=E6=96=AD?= =?UTF-8?q?=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 准备发 2.2.3 时发现 #117 那份守卫有两处毛病,先改测试再动版本号: 1. 漏核一处:`scripts/installer/version.json` 是**安装器内嵌**的那份 (`setup.nsi:91 File "version.json"` 取的就是本目录里的文件),而仓库里没有任何脚本 重新生成它 —— makensis 是手工跑的。它现在停在 2.2.2/09-16 不动,装出来的壳就会拿一份 旧版本号做本地识别(那份代码自己的注释写着"缺失会静默跳过更新")。加进核对清单, 站点数从 9 抬到 10(断言里的下限一起改,否则漏加会静默通过)。 2. 我自己写反的一条:`minimum_shell_version == 本次版本`。它的语义是**下界** (低于它的壳不接受了),强令相等等于每次发版顺手把上一版壳判死;而且目前代码里 没人真读它(`updater.rs` 只在 `AppVersion` 结构里解析,`shell-update.json` 扁平契约里 没这个字段),所以那条断言既没有依据、又会在未来逼出错误策略。改成唯一站得住的关系: 必须是 x.y.z 且**不高于**本次版本,比较走整数元组(字典序会把 2.9 判成比 2.10 大)。 15 passed(版本位 + 完整性打包两组)。 Signed-off-by: ReSerendipity --- tests/test_version_consistency.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/test_version_consistency.py b/tests/test_version_consistency.py index 9c92922..e0eae1a 100644 --- a/tests/test_version_consistency.py +++ b/tests/test_version_consistency.py @@ -84,6 +84,11 @@ def test_no_hardcoded_old_version(): _SEMVER = re.compile(r"\d+\.\d+\.\d+") +def _ver_tuple(s: str) -> tuple[int, ...]: + """x.y.z → 可比较的整数元组(避免字典序把 2.9 判成比 2.10 大)。""" + return tuple(int(p) for p in s.split(".")) + + def _site_versions() -> dict[str, str]: """从各版本位精确取值;取不到就抛,避免"少一个站点"被当成"一致"。""" @@ -103,6 +108,10 @@ def grab_json(rel: str, label: str) -> str: sites = { "pyproject.toml": grab("pyproject.toml", r'^version\s*=\s*"([^"]+)"', "Python 包版本"), "version.json": grab_json("version.json", "更新契约版本"), + # 安装器**内嵌**的那份:`setup.nsi` 的 `File "version.json"` 取的是本目录里的这个文件, + # 而仓库里没有任何脚本重新生成它(makensis 是手工跑的)—— 不跟着抬, + # 装出来的壳就会拿一份旧版本号做本地识别(那份注释自己写着"缺失会静默跳过更新")。 + "scripts/installer/version.json": grab_json("scripts/installer/version.json", "安装器内嵌版本(壳本地识别)"), "config.yaml": grab("config.yaml", r'^version:\s*"?([^"\n]+)"?', "前端缓存参数版本"), "desktop/package.json": grab_json("desktop/package.json", "桌面壳 npm 版本"), "desktop/src-tauri/tauri.conf.json": grab_json("desktop/src-tauri/tauri.conf.json", "Tauri 壳版本"), @@ -130,7 +139,7 @@ def grab_json(rel: str, label: str) -> str: def test_all_version_sites_agree() -> None: sites = _site_versions() - assert len(sites) >= 9, f"只核到 {len(sites)} 个版本位,本条已失去意义" + assert len(sites) >= 10, f"只核到 {len(sites)} 个版本位,本条已失去意义" bad = {k: v for k, v in sites.items() if not _SEMVER.fullmatch(v)} assert not bad, f"这些版本位不是 x.y.z 形态:{bad}" distinct = set(sites.values()) @@ -150,4 +159,10 @@ def test_installer_artifact_names_track_the_version_site() -> None: assert f"TTSMultiModel-Setup-v{ver}.exe" in text, f"OutFile 还没跟到 v{ver}" assert f'VIProductVersion "{ver}.0"' in text, f"VIProductVersion 还没跟到 {ver}.0" data = json.loads((PROJECT_ROOT / "version.json").read_text(encoding="utf-8")) - assert data.get("minimum_shell_version") == ver, "更新契约的最低壳版本与本批版本不一致" + minimum = str(data.get("minimum_shell_version", "")) + assert _SEMVER.fullmatch(minimum), f"minimum_shell_version 不是 x.y.z 形态:{minimum!r}" + # 它是**下界**,不是"必须等于当前版本":语义是"低于它的壳不接受了"。 + # 强令相等等于每次发版都把上一版壳判死(而目前代码里还没人真的读它: + # updater.rs 只在 AppVersion 结构里解析它,shell-update.json 契约里没有这个字段), + # 所以这里只钉住唯一站得住的关系:下界不得高于本次版本。 + assert _ver_tuple(minimum) <= _ver_tuple(ver), f"minimum_shell_version={minimum} 高于本次版本 {ver}" From 7d8b2736027b4460cfda5a9e9884d3e56944a779 Mon Sep 17 00:00:00 2001 From: ReSerendipity Date: Tue, 22 Sep 2026 12:45:23 +0800 Subject: [PATCH 2/3] =?UTF-8?q?chore(release):=20v2.2.3=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E6=8A=8A=20wheel=20=E5=AE=8C=E6=95=B4=E6=80=A7?= =?UTF-8?q?=E6=B8=85=E5=8D=95=E4=BF=AE=E5=A4=8D=E9=80=81=E8=BF=9B=E5=8F=AF?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E7=9A=84=E4=BA=A7=E7=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v2.2.2 的 tag 指向 e64d0e6,而 #117 的修复晚于它 —— 也就是说**发布出去的 wheel 仍然缺 完整性自检三件套**:pip 安装方拿到的包,P0 保护照旧一条都不跑。这个补丁版就是为了把 修好的包交到人手里(发布流程见 docs/release-governance.md §2 第 3 步)。 版本位 11 处一起抬到 2.2.3(`.release-please-manifest.json` 必须同步,否则 main push 会被 #114 新加的自证步骤判"manifest 落后于最新 tag"而红): pyproject / config.yaml / version.json / **scripts/installer/version.json**(安装器内嵌那份, 没有脚本会重新生成它)/ desktop 三处 + Cargo.lock / setup.nsi 三处 / k8s 镜像 tag。 `minimum_shell_version` 保持 2.2.2:它是下界,抬到 2.2.3 等于把刚发的一版壳判死。 CHANGELOG 新增 [2.2.3] 段:Security 一条(wheel 缺三件套 + enforce 静默跳过,含产物级验收数字 1062→1065)、Bug Fixes 两条(release-please 三处静默失效、版本位守卫补漏与撤掉一条写反的断言), 并保留同 v2.2.2 口径的"已知未覆盖"三条(桌面安装器/分卷无 workflow、GPU 冒烟恒 skipped、 锁集真装待验)。 本次不重打 26 GB 便携分卷与增量包(未授权;这批不含权重变化)。 验证:`tests/test_version_consistency.py` 5 passed(10 处版本位一致 + OutFile/VIProductVersion 同源 + 下界不高于本次版本)、依赖/完整性守卫 28 passed 合计,三个 JSON 均可解析。 Signed-off-by: ReSerendipity --- .release-please-manifest.json | 2 +- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++ config.yaml | 2 +- deploy/kubernetes/deployment.yaml | 4 ++-- desktop/package.json | 2 +- desktop/src-tauri/Cargo.lock | 2 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/tauri.conf.json | 2 +- pyproject.toml | 2 +- scripts/installer/setup.nsi | 6 +++--- version.json | 4 ++-- 11 files changed, 47 insertions(+), 14 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d15f5ed..9485046 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.2.2" + ".": "2.2.3" } diff --git a/CHANGELOG.md b/CHANGELOG.md index a668232..abae178 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ ## [Unreleased] +## [2.2.3] - 2026-09-22 + +补丁版,只为把一处**随包分发缺口**送进可安装的产物:它修在 main 上,但晚于 v2.2.2 的 tag, +所以 v2.2.2 的 wheel 里仍然带着这个缺陷。 + +### Security + +* **integrity:** wheel 此前**没把完整性自检三件套打进包**(`integrity_manifest.json`、其 + `Ed25519` 签名、验签公钥),而 `config.yaml` 默认 `security.integrity_selfcheck.enforce: true`, + 且"清单不存在"分支只 `logger.info("跳过自检")` 就返回 → **纯 `pip install` 的部署路径上 + P0 完整性保护一条都没执行,配置却声称它在强制运行**(Docker 与便携包另外拷了源码树, + 所以容器启动探测一直是绿的,把这个缺口遮住了)。现在三件随包走,且 enforce 开着却没清单 + → `RuntimeError` 拒绝启动并给三条出路;非强制模式保持原跳过语义。验收落在**产物**而非声明: + 包内条目 1062 → 1065,并把 wheel 解到临时目录真跑一遍正负两向。 + 守卫 `tests/test_integrity_selfcheck_packaging.py` + CI 的 `Build (sdist/wheel)` 新增产物核对步骤。 + +### Bug Fixes + +* **ci:** `release-please.yml` 的三处静默失效已修(`skip-github-pull-request: true` 而仓库从无 + release PR → 每次 main push 都 `found 0 possible releases` 后成功;5 个 v4 不认的入参被整段忽略; + job 从未声明 `outputs:` → 挂在它下面的 `build-release`(sdist/wheel + SHA256SUMS)永远不跑)。 + 修好后它当场自动开出了下一条 release PR,故补上 `release-please-config.json` 与 + `.release-please-manifest.json`,并加"什么都没发生就硬失败/写进 job 摘要"的自证。 +* **version:** 版本位一致性守卫(#113 引入)补两处:漏核了**安装器内嵌**的 + `scripts/installer/version.json`(`setup.nsi` 的 `File "version.json"` 取的就是它,且没有脚本 + 会重新生成它);另撤掉一条我自己写反的断言 —— `minimum_shell_version` 是**下界**, + 强令它等于当前版本等于每次发版把上一版壳判死,改为只要求"不高于本次版本"。 + +### 已知未覆盖(同 v2.2.2 口径,未变) + +桌面安装包链路与便携分卷仍无 workflow 覆盖;GPU 冒烟因无注册 runner 在 CI 上恒为 skipped; +锁集的全新 venv 真装复验待执行。 + ## [2.2.2] - 2026-09-22