From c6ea0f0e34ad1f200264f70a351e2a6fcf7ed1d1 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:13:41 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=99=E5=85=B1=E4=BA=AB=E4=BA=A7=E7=89=A9?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8=E8=A1=A5=E7=89=88=E6=9C=AC=E5=AE=88?= =?UTF-8?q?=E5=8D=AB=EF=BC=8C=E9=81=BF=E5=85=8D=E6=97=A7=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E5=8C=85=E8=A2=AB=E5=8F=91=E5=87=BA=E5=8E=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - build-release:新增导出的 assertArtifactVersionMatches,generateUpdateManifest 选中的产物若文件名带 _x.y.z_ 版本段就必须等于本轮版本,否则失败关闭(macOS 无版本段的 .app.tar.gz 交给身份断言) - build-release.test:新增残留旧安装包被拒绝、同版本通过、无版本段返回 null 三条用例 - decision-log 与 pitfalls 补记该通用守卫与完整测试批次(apps/ai-game-creator-shell/scripts/*.test.mjs 110 passed) --- .../scripts/build-release.mjs | 21 ++++++++++++++ .../scripts/build-release.test.mjs | 29 +++++++++++++++++++ .../shared-memory/decision-log.md | 3 +- docs/project-memory/shared-memory/pitfalls.md | 2 +- 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/apps/ai-game-creator-shell/scripts/build-release.mjs b/apps/ai-game-creator-shell/scripts/build-release.mjs index fe13fade7..d1d14803f 100644 --- a/apps/ai-game-creator-shell/scripts/build-release.mjs +++ b/apps/ai-game-creator-shell/scripts/build-release.mjs @@ -117,6 +117,26 @@ function ossBaseUrl() { ).replace(/\/+$/u, ''); } +/** + * 产物文件名里的版本必须等于本轮发布版本。 + * + * `selectReleaseArtifact()` 是按目录扫描 + 优先级挑产物,构建目录里残留的旧版本安装包 + * (例如 `..._0.1.153_x64-setup.exe`)会被挑中,于是「清单写新版本、对象是旧版本」。 + * 名字里没有版本号的产物(例如 macOS 的 `<产品名>.app.tar.gz`)返回 null,由各入口的 + * 身份断言负责;Windows 这类带版本号的安装包在这里失败关闭。 + */ +export function assertArtifactVersionMatches(artifactPath, version) { + const match = path.basename(artifactPath).match(/_(\d+\.\d+\.\d+)_/u); + if (!match) return null; + if (match[1] !== version) { + throw new Error( + `发布产物版本与本次发布不一致:产物 ${match[1]},本次 ${version}(${path.basename(artifactPath)});` + + '构建目录里可能残留了上一轮安装包,请清理后再发布', + ); + } + return match[1]; +} + function readPackageJson() { return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); } @@ -669,6 +689,7 @@ export async function generateUpdateManifest( if (!artifact) { throw new Error(`未找到可发布的 AGC 安装包:${bundleRoot}`); } + assertArtifactVersionMatches(artifact, readPackageJson().version); const downloadArtifact = selectFirstInstallArtifact(files, { target, version: readPackageJson().version, diff --git a/apps/ai-game-creator-shell/scripts/build-release.test.mjs b/apps/ai-game-creator-shell/scripts/build-release.test.mjs index fe43ec984..da832d50e 100644 --- a/apps/ai-game-creator-shell/scripts/build-release.test.mjs +++ b/apps/ai-game-creator-shell/scripts/build-release.test.mjs @@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url'; import { agcReleasePathPatterns, + assertArtifactVersionMatches, buildRelease, buildTauriBuildArguments, compareVersions, @@ -1133,3 +1134,31 @@ test('scheduler skips the full build only for non-deploy paths', () => { assert.ok(skipLine.includes(pattern), `Full Build 跳过模式缺少 ${pattern}`); } }); + +test('rejects a stale installer picked up from the build directory', () => { + // generateUpdateManifest() 是按目录扫描挑产物:残留的旧版本安装包会被挑中, + // 必须在这里失败关闭,而不是把「清单新版本 + 对象旧版本」发出去。 + assert.throws( + () => + assertArtifactVersionMatches( + '/bundle/nsis/陶泥儿开发版_0.1.153_x64-setup.exe', + '0.1.154', + ), + /发布产物版本与本次发布不一致:产物 0\.1\.153,本次 0\.1\.154/u, + ); + assert.equal( + assertArtifactVersionMatches( + '/bundle/nsis/陶泥儿开发版_0.1.154_x64-setup.exe', + '0.1.154', + ), + '0.1.154', + ); + // macOS 更新包名里没有版本号,交给各入口的身份断言处理。 + assert.equal( + assertArtifactVersionMatches( + '/bundle/macos/陶泥儿开发版.app.tar.gz', + '0.1.154', + ), + null, + ); +}); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index cc064dd78..d5ce04e7e 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -9704,8 +9704,9 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 背景:用只读核对脚本 `scripts/check-agc-update-channel-manifests.mjs` 检查**已发布**的 OSS 渠道清单时发现,线上 `dev-mac/latest.json`(`version=0.1.142`、`commit=c07c10c0c`)指向的更新包解开后是 `CFBundleShortVersionString=0.1.139`、`CFBundleIdentifier=world.genarrative.ai-game-creator.release`、`CFBundleName=陶泥儿 Release`。签名验签、对象存在、`.sig` 与清单文本一致这些都对——错的是**版本与渠道身份**:dev 渠道的 arm64 客户端会被指向一个旧版的 release 身份包。 - 根因:`build-macos-ci.mjs` 以前只清理「本轮要写的确切文件名」,mac 构建目录里上一轮/其它渠道身份留下的 `*.app.tar.gz` 不会被删;`generateUpdateManifest()` 是按目录扫描 + 优先级选产物,于是选中了残留文件。mac 构建机复用 workspace,这类残留会长期存在。 - 决策(构建期失败关闭):mac 发布入口在构建前按后缀清空 `macos/` 下的 `*.app.tar.gz`、`*.app.tar.gz.sig`、`*.dmg`、`*.dmg.sha256`;构建后读 `.app/Contents/Info.plist`,断言 `CFBundleShortVersionString` 等于本轮发布版本、`CFBundleIdentifier`/`CFBundleName` 等于该渠道安装身份;生成清单后再断言清单选中的更新包就是本轮那一个。任一不符直接中止,不写 OSS。 +- 决策(产物选择器本身也要挡旧版本):`generateUpdateManifest()` 是共享入口,Windows 侧靠 Jenkins 的 `git clean -fdx` 才没踩到同一个坑,所以再补一道与平台无关的守卫——`assertArtifactVersionMatches()` 要求文件名里出现形如 `_0.1.154_` 的版本段时必须等于本轮版本(残留的 `_0.1.153_` 安装包会被挑中并因此失败关闭);macOS 的 `<产品名>.app.tar.gz` 不含版本段,返回 `null`,由 mac 入口的身份断言负责。 - 决策(只读核对也要看包内身份):`check:agc-update-channel-manifests` 在 `AGC_UPDATE_VERIFY_DOWNLOAD=1` 时下载 mac 更新包、解出 `Info.plist` 做同样断言;同时按 2026-09-21 决策断言 mac 渠道只登记 `darwin-aarch64`(不再要求 universal 双键)。 - 决策(口径回归):macOS 现行契约是 arm64 单架构(2026-09-21 决策),里程碑里「两个 macOS 平台键指向 universal 产物」的旧文字按现行决策改写;不得据此重新切回 universal,除非按该决策给出的恢复路径补齐按架构的 Node 运行时。 - 影响范围:`apps/ai-game-creator-shell/scripts/build-macos-ci.mjs`、新增 `apps/ai-game-creator-shell/scripts/macos-release-identity.mjs` 与其 `.test.mjs`、`apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs`、`jenkins/Jenkinsfile.ai-game-creator-shell-macos-build`、`scripts/check-agc-update-channel-manifests.mjs`、两份里程碑与本文件、pitfalls。 -- 验证:`node --test macos-release-identity.test.mjs prepare-macos-codex.test.mjs verify-updater-signature.test.mjs build-release.test.mjs cargo-features.test.mjs` → 59 passed(新增 6 条,回归用例直接喂线上那份 0.1.139 release 身份 plist,必须抛错);`npm run check:production-ops`、`check:encoding`、`check:doc-index`、prettier、eslint、`git diff --check` 通过;只读核对对线上 `dev-win` 全 PASS(含 158 MiB 产物下载验签与旧协议 sha256 一致),对线上 `dev-mac` 精确报出上面两条 FAIL。 +- 验证:`node --test apps/ai-game-creator-shell/scripts/*.test.mjs` → **110 passed / 0 failed**(其中定向批次 `macos-release-identity / prepare-macos-codex / verify-updater-signature / build-release / cargo-features` 60 passed;新增的 mac 身份回归用例直接喂线上那份 0.1.139 release 身份 plist,必须抛错;新增的产物版本守卫用例喂 `_0.1.153_` 残留安装包,必须抛错);`npm run check:production-ops`、`check:encoding`、`check:doc-index`、prettier、eslint、`git diff --check` 通过;只读核对对线上 `dev-win` 全 PASS(含 158 MiB 产物下载验签与旧协议 sha256 一致),对线上 `dev-mac` 精确报出上面两条 FAIL。 - 边界(未完成):修复只保证「以后再发不会再错」,线上 `dev-mac/latest.json` 仍指向那份坏包;需要一次带 Jenkins 凭据与授权的 mac 重新发布,然后重跑只读核对才算了结。Apple 代码签名与公证仍是 `adhoc`。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 034d210b9..7dcbb4b2d 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -6105,4 +6105,4 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - **根因**:`build-macos-ci.mjs` 以前只 `rmSync` 「本轮要写的确切文件名」,构建目录里上一轮(或其它渠道身份)留下的 `*.app.tar.gz` 不会被清;而 `generateUpdateManifest()` 是**扫描构建目录、按优先级挑产物**(同名优先级再按字典序),于是挑走了残留的 release 身份包。mac 构建机是复用 workspace 的,这类残留会长期存在。 - **判据**:只读核对要**打开产物看身份**,不能只看「地址存在 + 签名匹配」。`npm run check:agc-update-channel-manifests`(`AGC_UPDATE_VERIFY_DOWNLOAD=1`)现在会解出 mac 包的 `Info.plist`,断言「包内版本 == 清单版本」且「包内 identifier/产品名 == 本渠道身份」;本轮对线上取样得到两条 FAIL,正是这个缺陷。 - **处理(2026-09-28 已修)**:构建前按后缀清空 `macos/` 下的 `*.app.tar.gz`、`*.app.tar.gz.sig`、`*.dmg`、`*.dmg.sha256`;构建后读 `.app/Contents/Info.plist` 断言版本/identifier/产品名;生成清单后再断言 `release.artifact` 就是本轮那一个。守卫在 `apps/ai-game-creator-shell/scripts/macos-release-identity.mjs`,回归用例直接用线上那份 0.1.139 release 身份包(`node --test` 59 passed)。 -- **教训**:凡是「按目录扫描挑产物」的发布步骤,都要么先清空同类产物、要么按本轮预期路径断言;只删「本轮要写的名字」等于把上一轮的坏包留在候选集里。还有一条更一般的:核对线上清单时,先看 decision-log 的现行口径(这里 macOS 已是 arm64 单架构),别拿过期里程碑文字当契约。 +- **教训**:凡是「按目录扫描挑产物」的发布步骤,都要么先清空同类产物、要么按本轮预期路径断言;只删「本轮要写的名字」等于把上一轮的坏包留在候选集里。现在共享入口 `generateUpdateManifest()` 也补了与平台无关的 `assertArtifactVersionMatches()`(文件名带 `_0.1.153_` 这类版本段时必须是本轮版本),所以即使将来某个流水线不再 `git clean -fdx`,旧安装包也会被拒绝而不是被发出去。还有一条更一般的:核对线上清单时,先看 decision-log 的现行口径(这里 macOS 已是 arm64 单架构),别拿过期里程碑文字当契约。