From 7da6de0b4e62f279cddae6935d35e825cd1cf44d Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:18:37 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E8=B7=A8=E6=B8=A0=E9=81=93?= =?UTF-8?q?=E4=BA=A7=E7=89=A9=E9=9A=94=E7=A6=BB=E6=A0=B8=E5=AF=B9=E5=B9=B6?= =?UTF-8?q?=E6=8A=8A=20dev-mac=20=E7=BC=BA=E9=99=B7=E9=92=89=E5=88=B0?= =?UTF-8?q?=E5=AD=97=E8=8A=82=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - check:agc-update-channel-manifests:新增「不同渠道更新包不得字节相同」断言(需 AGC_UPDATE_VERIFY_DOWNLOAD=1),并核对 Windows 安装包文件名带本渠道产品名 - 只读核对 dev-win/dev-mac/release-win/release-mac:dev-win 0.1.154、release-win 0.1.150、release-mac 0.1.139 身份与版本一致;dev-mac 0.1.142 与 release-mac 0.1.139 的更新包 sha256 相同,字节级证明 dev 分区放了 release 渠道的产物 - 渠道安装身份隔离里程碑补本轮核对(发布产物身份 + 跨渠道唯一性),真机并存仍待验收 - macOS 里程碑与 pitfalls、decision-log 记录字节级取证与新断言 --- ...里程碑】AGC macOS渠道更新落地-2026-09-17.md | 2 + ...€�里程碑】AGC渠道安装身份隔离-2026-09-21.md | 11 ++++ .../shared-memory/decision-log.md | 1 + docs/project-memory/shared-memory/pitfalls.md | 2 +- .../check-agc-update-channel-manifests.mjs | 65 +++++++++++++++++++ 5 files changed, 80 insertions(+), 1 deletion(-) diff --git a/docs/project-memory/plans/【里程碑】AGC macOS渠道更新落地-2026-09-17.md b/docs/project-memory/plans/【里程碑】AGC macOS渠道更新落地-2026-09-17.md index 25bdf802c..be6feca6d 100644 --- a/docs/project-memory/plans/【里程碑】AGC macOS渠道更新落地-2026-09-17.md +++ b/docs/project-memory/plans/【里程碑】AGC macOS渠道更新落地-2026-09-17.md @@ -63,6 +63,8 @@ - 清单自洽的部分(都 PASS):`version=0.1.142`、`pub_date=2026-09-24T11:35:31Z`、`commit=c07c10c0c`;`platforms` 只有 `darwin-aarch64`(符合 2026-09-21 单架构决策);更新包对象 315,510,533 字节与 `.sig` 420 字节都存在,清单签名与 `.sig` 文本一致;下载后字节数一致、用 `tauri.conf.json` 里烘焙的 updater 公钥验签通过(`alg=ED`、`keyId=cb883447e3e87c4e`);首装 DMG `陶泥儿开发版_0.1.142_aarch64.dmg` 存在。 - **两处真 FAIL**:把更新包解开看 `Contents/Info.plist`,里面是 **`CFBundleShortVersionString=0.1.139`**(清单写的是 0.1.142)和 **`CFBundleIdentifier=world.genarrative.ai-game-creator.release` + `CFBundleName=陶泥儿 Release`**(本渠道应为 `world.genarrative.ai-game-creator` / `陶泥儿开发版`),产物文件名也叫「陶泥儿 Release.app.tar.gz」。 - 结论:`dev-mac` 渠道当前给 arm64 客户端提供的更新包是**旧版本 + 另一个渠道身份**的包。这既让「升级后版本没变」成立,也可能把 release 身份的应用装到 dev 渠道用户机器上,违反渠道安装身份隔离约定。第 1 条保持未勾选。 + - **字节级佐证(2026-09-28 补)**:`dev-mac/0.1.142/陶泥儿 Release.app.tar.gz` 与 `release-mac/0.1.139/陶泥儿 Release.app.tar.gz` 的 sha256 完全相同(`1dfc9deb79f7fa5fd66d8a8a47b80e3a6ff125b8762cabfd32d3e16dec946e4e`)。也就是说 dev 分区里放的就是 release 渠道那一次构建的产物,不是「版本号抄错」。 + - 对照:`release-mac` 自己是对的(`0.1.139` + `world.genarrative.ai-game-creator.release` / `陶泥儿 Release`),`release-win`(0.1.150)与 `dev-win`(0.1.154)的产物名与签名核对也都通过。核对脚本现在还会断言「不同渠道的更新包不得字节相同」,修复前这条会精确报出 `dev-mac = release-mac`。 - 根因与修复(本轮已落地,见 decision-log 2026-09-28 条目): - 根因:`build-macos-ci.mjs` 之前只删「本轮要写的文件名」,构建目录里上一轮遗留的 `陶泥儿 Release.app.tar.gz` 不会被清掉;而 `generateUpdateManifest()` 是**扫描目录按优先级挑产物**,于是挑走了那个残留文件。 - 修复:构建前按后缀清空 `macos/` 下的 `*.app.tar.gz`、`*.app.tar.gz.sig`、`*.dmg`、`*.dmg.sha256`;构建后读产物 `Contents/Info.plist` 断言版本与渠道身份;生成清单后再断言清单选中的就是本轮更新包。守卫逻辑在 `apps/ai-game-creator-shell/scripts/macos-release-identity.mjs`,单测 `macos-release-identity.test.mjs` 用线上那份 0.1.139 release 身份包做回归(`node --test` 59 passed,含新增 6 条)。 diff --git a/docs/project-memory/plans/【里程碑】AGC渠道安装身份隔离-2026-09-21.md b/docs/project-memory/plans/【里程碑】AGC渠道安装身份隔离-2026-09-21.md index 167dadebb..cc76ae282 100644 --- a/docs/project-memory/plans/【里程碑】AGC渠道安装身份隔离-2026-09-21.md +++ b/docs/project-memory/plans/【里程碑】AGC渠道安装身份隔离-2026-09-21.md @@ -47,3 +47,14 @@ - `node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs apps/ai-game-creator-shell/scripts/release-oss.test.mjs apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs apps/ai-game-creator-shell/scripts/cargo-features.test.mjs`:**60 passed / 0 failed**,含「release / beta 的 Windows 与 macOS 构建只写自己渠道分区、不能写 dev 桥」四条分区隔离用例。 - 仍未执行:真实渠道打包(需要签名私钥与发号/上传授权)、双渠道真机并存安装、macOS 构建节点实跑。 + +## 本轮核对(2026-09-28,已发布产物的渠道身份只读核对) + +发布产物已经在 OSS 上,可以用只读脚本核对「渠道身份是否真的落进产物」,不用等到真机安装。`npm run check:agc-update-channel-manifests`(`AGC_UPDATE_CHANNELS=dev-win,dev-mac,release-win,release-mac AGC_UPDATE_VERIFY_DOWNLOAD=1`)结果: + +- `dev-win`(0.1.154,commit `76cdb96c5`):文件名 `陶泥儿开发版_0.1.154_x64-setup.exe`、对象存在、`.sig` 与清单一致、公钥验签通过 → 默认渠道身份不变。 +- `release-win`(0.1.150,commit `5c648ab9c7`):文件名 `陶泥儿 Release_0.1.150_x64-setup.exe`、签名与摘要核对通过 → 非默认渠道用独立产品名。 +- `release-mac`(0.1.139,commit `87e52860a7`):清单只登记 `darwin-aarch64`(符合 2026-09-21 决策);解出更新包内 `Info.plist` 是 `world.genarrative.ai-game-creator.release` / `陶泥儿 Release` / `0.1.139`,与清单版本、渠道身份一致。 +- **`dev-mac` 有真实缺陷**:清单 `0.1.142` 指向的更新包解出来是 `world.genarrative.ai-game-creator.release` / `陶泥儿 Release` / `0.1.139`,而且与 `release-mac/0.1.139` 的产物**字节完全相同**(`sha256=1dfc9deb79f7…`)。也就是说 dev 分区里放的其实是 release 渠道那份包,渠道隔离在这一次发布上被破坏(签名字段都是真的,只有身份和版本错了)。根因是 mac 构建目录残留产物被清单扫描挑中,已在 2026-09-28 修复(构建前清空同类产物 + 构建后断言包内版本与渠道身份),需要用修好的 mac 管线重发一次 `dev-mac`。 +- 新增的跨渠道断言:同一次核对里不同渠道的更新包不得字节相同(`1dfc9deb79f7: dev-mac/darwin-aarch64 = release-mac/darwin-aarch64` 就是修复前会命中的形态)。 +- 仍未勾选:真机上同时安装 `dev` 与 `release`、各自原地更新。上面这些只证明「产物身份正确」,不代替真机并存验收。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index d5ce04e7e..1ada97d5e 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -9706,6 +9706,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 决策(构建期失败关闭):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 双键)。 +- 决策(跨渠道产物不得同字节):同一次核对里若下载到多个渠道的更新包,脚本会断言它们的 sha256 两两不同——不同渠道的 `productName` / `identifier` 不同,产物就不可能相同。事故取证:`dev-mac/0.1.142` 的更新包与 `release-mac/0.1.139` 的更新包 sha256 都是 `1dfc9deb79f7fa5fd66d8a8a47b80e3a6ff125b8762cabfd32d3e16dec946e4e`,字节级证明 dev 分区放的是 release 渠道那次构建的产物。全量核对用 `AGC_UPDATE_CHANNELS=dev-win,dev-mac,release-win,release-mac AGC_UPDATE_VERIFY_DOWNLOAD=1`(约 950 MiB 下载)。 - 决策(口径回归):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 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。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 7dcbb4b2d..a33b01a9b 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -6103,6 +6103,6 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - **现象**:线上 `https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/dev-mac/latest.json`(`version=0.1.142`、`commit=c07c10c0c`)把更新包指向 `陶泥儿 Release.app.tar.gz`;下载解开看 `Contents/Info.plist`:`CFBundleShortVersionString=0.1.139`、`CFBundleIdentifier=world.genarrative.ai-game-creator.release`、`CFBundleName=陶泥儿 Release`。同一份清单的首装 DMG 却是 `陶泥儿开发版_0.1.142_aarch64.dmg`。也就是说签名是真的、对象也在,但**版本与渠道身份都是错的**。 - **根因**:`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,正是这个缺陷。 +- **判据**:只读核对要**打开产物看身份**,不能只看「地址存在 + 签名匹配」。`npm run check:agc-update-channel-manifests`(`AGC_UPDATE_VERIFY_DOWNLOAD=1`)现在会解出 mac 包的 `Info.plist`,断言「包内版本 == 清单版本」且「包内 identifier/产品名 == 本渠道身份」,并额外断言**不同渠道的更新包不得字节相同**;本轮对线上取样得到三条 FAIL,正是这个缺陷。字节级佐证:`dev-mac/0.1.142/陶泥儿 Release.app.tar.gz` 与 `release-mac/0.1.139/陶泥儿 Release.app.tar.gz` 的 sha256 完全相同(`1dfc9deb79f7…`),说明 dev 分区里放的就是 release 那一次构建的产物。 - **处理(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)。 - **教训**:凡是「按目录扫描挑产物」的发布步骤,都要么先清空同类产物、要么按本轮预期路径断言;只删「本轮要写的名字」等于把上一轮的坏包留在候选集里。现在共享入口 `generateUpdateManifest()` 也补了与平台无关的 `assertArtifactVersionMatches()`(文件名带 `_0.1.153_` 这类版本段时必须是本轮版本),所以即使将来某个流水线不再 `git clean -fdx`,旧安装包也会被拒绝而不是被发出去。还有一条更一般的:核对线上清单时,先看 decision-log 的现行口径(这里 macOS 已是 arm64 单架构),别拿过期里程碑文字当契约。 diff --git a/scripts/check-agc-update-channel-manifests.mjs b/scripts/check-agc-update-channel-manifests.mjs index 6cc0060d2..544e4b554 100644 --- a/scripts/check-agc-update-channel-manifests.mjs +++ b/scripts/check-agc-update-channel-manifests.mjs @@ -56,6 +56,8 @@ const WINDOWS_PLATFORM_KEY = 'windows-x86_64'; let failures = 0; let skipped = 0; +/** 下载过的更新包摘要:用于「不同渠道不能共用同一份产物字节」这条隔离断言。 */ +const downloadedArtifacts = []; function check(name, ok, detail = '') { if (!ok) failures += 1; console.log( @@ -261,6 +263,20 @@ async function verifyChannel(channel, tempDir) { Boolean(platforms[WINDOWS_PLATFORM_KEY]), `platforms=${platformKeys.join(',')}`, ); + // 渠道身份会写进产物文件名的产品名,这里先用清单里的地址核对(下载后再验签与摘要)。 + const expectedIdentity = resolveChannelInstallIdentity( + channel.replace(/-win$/u, ''), + ); + const artifactNames = platformKeys.map((key) => + decodeURIComponent(new URL(String(platforms[key].url ?? '')).pathname), + ); + check( + `${channel} 安装包文件名带本渠道产品名`, + artifactNames.every((name) => + name.includes(expectedIdentity.productName), + ), + `names=${artifactNames.map((name) => path.basename(name)).join(',')} expected=${expectedIdentity.productName}`, + ); } if (channel === 'dev-win') { @@ -315,6 +331,13 @@ async function verifyChannel(channel, tempDir) { `downloaded=${size} head=${artifactHead.length}`, ); const sha256 = await sha256Of(artifactPath); + downloadedArtifacts.push({ + channel, + platformKey: key, + sha256, + size, + url, + }); if (channel === 'dev-win' && key === WINDOWS_PLATFORM_KEY) { const bridgeJson = await ( await fetch(`${OSS_BASE_URL}/latest.json`) @@ -359,6 +382,48 @@ try { await rm(tempDir, { recursive: true, force: true }); } +// 渠道隔离:同一次核对里如果下载到多个渠道的更新包,它们不能是同一份字节—— +// 不同渠道的 productName / identifier 不同,产物就不会相同(2026-09-28 的 dev-mac +// 事故正是「dev 分区里放的其实是 release 渠道那份包」,字节级完全相同)。 +const channelNames = [ + ...new Set( + downloadedArtifacts.map((item) => item.channel.replace(/-(win|mac)$/u, '')), + ), +]; +if (!VERIFY_DOWNLOAD) { + skip( + '不同渠道的更新包互不相同(渠道隔离)', + '需要 AGC_UPDATE_VERIFY_DOWNLOAD=1 才能比对产物字节', + ); +} else if (channelNames.length < 2) { + skip( + '不同渠道的更新包互不相同(渠道隔离)', + `本次只核对到 ${channelNames.length} 个渠道(${channelNames.join(',')});用 AGC_UPDATE_CHANNELS=dev-win,dev-mac,release-win,release-mac 可覆盖全量`, + ); +} else { + const byHash = new Map(); + for (const item of downloadedArtifacts) { + const bucket = byHash.get(item.sha256) ?? []; + bucket.push(item); + byHash.set(item.sha256, bucket); + } + const collisions = [...byHash.entries()].filter( + ([, items]) => + new Set(items.map((item) => item.channel.replace(/-(win|mac)$/u, ''))) + .size > 1, + ); + check( + '不同渠道的更新包互不相同(渠道隔离)', + collisions.length === 0, + collisions + .map( + ([hash, items]) => + `${hash.slice(0, 12)}: ${items.map((item) => `${item.channel}/${item.platformKey}`).join(' = ')}`, + ) + .join(';'), + ); +} + console.log( failures === 0 ? `\n全部通过${skipped > 0 ? `(${skipped} 项跳过)` : ''}`