From a4d4d04d97b54fa0ac2afd1357e6272d9925e5d8 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:56:50 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=20AGC=20=E5=B7=B2=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E6=B8=A0=E9=81=93=E6=B8=85=E5=8D=95=E5=8F=AA=E8=AF=BB?= =?UTF-8?q?=E6=A0=B8=E5=AF=B9=E4=B8=8E=E7=BA=BF=E4=B8=8A=E5=B7=AE=E5=BC=82?= =?UTF-8?q?=E5=8F=96=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 scripts/check-agc-update-channel-manifests.mjs:只读核对 OSS 渠道清单结构、对象存在性、签名对象一致性,AGC_UPDATE_VERIFY_DOWNLOAD=1 时下载产物用内置公钥验签 - package.json 注册 npm run check:agc-update-channel-manifests - 渠道化里程碑条目 3 勾选:dev-win/dev-mac 产物下载验签通过,dev-win 的 sha256/size 与旧协议指针一致;条目 5 记录 withCredentials 与归档 glob 的静态核对结果 - macOS 里程碑记录线上 dev-mac 清单仍是单架构(0.1.142 / c07c10c0c),universal 需 mac 构建机重发 - pitfalls 记录「单测绿不等于线上清单符合契约」与 dev-mac 更新产物命名待核对 --- ...里程碑】AGC macOS渠道更新落地-2026-09-17.md | 8 + ...里程碑】AGC更新发布管线渠道化-2026-09-17.md | 14 +- docs/project-memory/shared-memory/pitfalls.md | 7 + package.json | 1 + .../check-agc-update-channel-manifests.mjs | 301 ++++++++++++++++++ 5 files changed, 329 insertions(+), 2 deletions(-) create mode 100644 scripts/check-agc-update-channel-manifests.mjs 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 6a7391ff9..c9da95bc9 100644 --- a/docs/project-memory/plans/【里程碑】AGC macOS渠道更新落地-2026-09-17.md +++ b/docs/project-memory/plans/【里程碑】AGC macOS渠道更新落地-2026-09-17.md @@ -54,3 +54,11 @@ - 仍未验证(需要 macOS 节点):真实 macOS 构建与 DMG 产出、Apple 签名与公证、安装后重启接管新版本、`dev-mac` 渠道真实发布。 因此状态从 `in-progress` 改为 `implemented-awaiting-runtime-acceptance`:代码与脚本判据成立,缺的是 macOS 环境证据。 + +## 本轮核对(2026-09-28,线上 dev-mac 清单只读核对) + +- 第 1 条(`dev-mac` 清单含两个 macOS 平台条目且指向同一 universal 安装包与签名)**仍未勾选,并且这次拿到了线上反例**: + - `npm run check:agc-update-channel-manifests`(新增的只读核对脚本)拉取 `https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/dev-mac/latest.json`:`version=0.1.142`、`pub_date=2026-09-24T11:35:31Z`、`commit=c07c10c0c`,`platforms` 只有 **`darwin-aarch64`**,因此「同时提供两个 macOS 平台键」判 FAIL。 + - 同一份清单的对象与签名本身是自洽的:产物 `agc/dev-mac/0.1.142/陶泥儿 Release.app.tar.gz` 315,510,533 字节(HEAD 200)、`.sig` 420 字节且与清单内签名文本一致,下载后字节数一致、用 `tauri.conf.json` 里烘焙的 updater 公钥验签通过(`alg=ED`,`keyId=cb883447e3e87c4e`)。也就是说缺的不是签名可信度,而是 **universal(双平台键)发布**。 + - 时间线解释:这次 `dev-mac` 发布(2026-09-24 `c07c10c0c`)早于 universal macOS 改造;同期 `dev-win` 已经发到 `0.1.154`(2026-09-28,commit `76cdb96c5`)。要让线上清单符合契约,需要用 mac 构建机重新发布一次 `dev-mac`。 + - 待复核观察:该 `dev-mac` 更新产物文件名是「陶泥儿 **Release**.app.tar.gz」,而同一渠道的首装 DMG 是「陶泥儿**开发版**_0.1.142_aarch64.dmg」。channel-identity 约定 `dev` 渠道展示名为「陶泥儿开发版」,重发时要确认 mac 更新产物用的是渠道身份而不是 release 身份,避免同一台机器上并存/覆盖行为与预期不符。 diff --git a/docs/project-memory/plans/【里程碑】AGC更新发布管线渠道化-2026-09-17.md b/docs/project-memory/plans/【里程碑】AGC更新发布管线渠道化-2026-09-17.md index 575ae0615..91c8790e6 100644 --- a/docs/project-memory/plans/【里程碑】AGC更新发布管线渠道化-2026-09-17.md +++ b/docs/project-memory/plans/【里程碑】AGC更新发布管线渠道化-2026-09-17.md @@ -35,9 +35,9 @@ - [x] 渠道清单版本来自统一总号;显式传入的号低于本渠道当前清单版本时构建失败关闭,另一个渠道清单不受影响(原「按渠道独立递增」口径已由 2026-09-20 总版本号方案取代)。 - [x] 渠道与目标平台不匹配、缺少签名私钥或私钥密码错误时发布失败关闭,不产生半成品清单。 -- [ ] 发布后 OSS 上安装包、签名与渠道清单三者一致:清单内地址指向已存在的对象,签名与安装包匹配。 +- [x] 发布后 OSS 上安装包、签名与渠道清单三者一致:清单内地址指向已存在的对象,签名与安装包匹配(2026-09-28 只读核对已发布的 `dev-win` / `dev-mac` 渠道,两个渠道的产物都下载后验签通过,见文末「本轮核对」)。 - [x] universal macOS 产物的两个平台键指向同一对象同一签名,不存在只挂单一架构键或指向不存在对象的情况。 -- [ ] Jenkins 归档与日志中不出现签名私钥内容,凭据只注入构建进程。 +- [ ] Jenkins 归档与日志中不出现签名私钥内容,凭据只注入构建进程。(静态核对:`jenkins/Jenkinsfile.ai-game-creator-shell-build` 用 `withCredentials` 注入 `TAURI_SIGNING_PRIVATE_KEY` / `..._PASSWORD`,归档 glob 只含 `bundle/**/*.exe|*.sig|latest.json|legacy-latest.json|release-notes.txt` 与 `.jenkins-source-commit`,没有私钥文件;真实 Jenkins 运行的日志脱敏仍需一次 CI 构建取证。) - [x] 未显式指定渠道时按目标平台取默认渠道,且 `--no-bundle` smoke 路径仍不读远端版本、不改版本、不生成清单。 ## 证据要求 @@ -62,3 +62,13 @@ - 条目 6:同一批的 `no-bundle smoke skips version writes and manifest generation`、`release context resolves explicit targets before environment/default and fails closed`。 - 条目 1:原验收口径「按渠道独立递增」已被 2026-09-20 的总版本号方案取代——`build-release.mjs` 的 `prepareReleaseVersion()` 用统一总号,`resolveRemoteHighWaterVersion(channel, target)` 只做本渠道高水位断言(`assertRequestedVersionNotBelowChannel`);`node --test scripts/agc-global-version.test.mjs` → 8 passed,含「传入低于本渠道清单的号时失败关闭」「写后回读不一致失败关闭」「nextVersion 只在 patch 位递增」。本文件的范围与条目已按当前口径改写。 - 仍未勾选:条目 3(发布后 OSS 三者一致需要真实发布);条目 5(Jenkins 日志/归档不含私钥需要真实流水线证据)。 + +## 本轮核对(2026-09-28,已发布渠道清单只读核对) + +- 已勾选(条目 3:发布后 OSS 上安装包、签名与渠道清单三者一致)——本轮以**已发布**的 OSS 对象核对,不做任何上传 + - 新增 `scripts/check-agc-update-channel-manifests.mjs`(`npm run check:agc-update-channel-manifests`):只读拉取 `/agc/<渠道>/latest.json`,核对清单结构、平台键、地址前缀、安装包与 `.sig` 对象存在性、清单内签名与 `.sig` 文本一致,并在 `AGC_UPDATE_VERIFY_DOWNLOAD=1` 时下载产物、用产物里烘焙的 updater 公钥验签。 + - **`dev-win`(v0.1.154,`pub_date=2026-09-28T13:24:53Z`,commit `76cdb96c5`)**:清单 `windows-x86_64` 指向 `agc/dev-win/0.1.154/陶泥儿开发版_0.1.154_x64-setup.exe`(165,984,391 字节,HEAD 200);`.sig` 对象 436 字节且与清单内签名文本一致;下载后字节数与 HEAD 一致,`sha256=7867734e3991…` 与旧协议指针 `agc/latest.json` 的 `sha256`/`size` 完全一致;用公钥验签通过(`alg=ED`,`keyId=cb883447e3e87c4e`)。 + - **`dev-mac`(v0.1.142,`pub_date=2026-09-24T11:35:31Z`,commit `c07c10c0c`)**:清单 `darwin-aarch64` 指向 `agc/dev-mac/0.1.142/陶泥儿 Release.app.tar.gz`(315,510,533 字节,HEAD 200);`.sig` 对象 420 字节且与清单签名一致;下载后字节数一致、用同一公钥验签通过。 + - 结论:两个渠道的「安装包 ↔ 签名 ↔ 渠道清单」三者一致成立,条目 3 关闭。 + - **顺带发现的真实状态差异(留给 macOS 里程碑)**:已发布的 `dev-mac` 清单只有 `darwin-aarch64` 一个平台键,而当前代码的 universal 约定要求两个 macOS 平台键指向同一对象同一签名;这次发布(0.1.142 / `c07c10c0c`,2026-09-24)早于 universal 改造,需要用 mac 构建机 re-publish 一次才能让线上清单符合契约。 +- 条目 5(Jenkins 归档与日志不含私钥)仍是未勾选:静态可证部分已记录在验收行上(`withCredentials` 注入 + 归档 glob 不含私钥文件),真实 Jenkins 运行日志需要一次 CI 构建取证。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index ef7d096ca..161bf4707 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -6098,3 +6098,10 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - **现象**:本机跑 `npm run check:production-health-patrol` 输出 `[check:production-health-patrol] FAILED`,理由是「nginx gateway mode 巡检应成功。预期退出码 0,实际 2」,巡检 JSON 里 6 条 `service:*` 全是 `服务状态异常: spawn systemctl ENOENT`。同一轮的 `api:/healthz`、`bgfilter:/readyz`、`spacetimedb:/v1/ping`、`public:/` 都是 200。 - **根因**:这个 harness 用桩 `systemctl` 驱动巡检脚本,Windows 上 `spawn systemctl` 直接 `ENOENT`,服务态检查不可能通过;它验证的是 Linux + systemd 的生产形态。 - **做法**:本机只跑 `npm run check:production-health-patrol-env`(本轮 OK)确认巡检变量口径;`check:production-health-patrol` 留给服务器/CI 复核,别据此判定巡检脚本本身坏了。 + +## 2026-09-28 渠道清单的单元测试绿不等于线上清单符合契约:`dev-mac` 线上仍是单架构 + +- **现象**:`build-release` 的单元测试里 `universal uses the Mac channel and the same signed artifact for both architectures` 一直是绿的,但线上 `https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/dev-mac/latest.json`(`version=0.1.142`、`pub_date=2026-09-24`、`commit=c07c10c0c`)的 `platforms` 只有 `darwin-aarch64`。测试证明的是「代码会这么发」,不能证明「线上已经这么发」。 +- **判据**:用只读脚本核对已发布对象,而不是看单测——`npm run check:agc-update-channel-manifests`(`AGC_UPDATE_VERIFY_DOWNLOAD=1` 时下载产物验签)。本轮实测:`dev-win` 与 `dev-mac` 的安装包都存在、`.sig` 与清单签名文本一致、两个产物都能用 `tauri.conf.json` 的公钥验签通过(`alg=ED`,`keyId=cb883447e3e87c4e`),但 `dev-mac` 的「两个 macOS 平台键」判 FAIL。 +- **做法**:任何「线上对象/清单契约」条款都用只读核对脚本取证;发现线上落后时,先确认这次发布对应的 commit 与时间(本轮 mac 发布早于 universal 改造),再用目标平台的构建机重发,不要在 Windows 上伪造 mac 清单。 +- **附注**:该 `dev-mac` 更新产物叫「陶泥儿 Release.app.tar.gz」,而渠道首装 DMG 叫「陶泥儿开发版_0.1.142_aarch64.dmg」;`dev` 渠道展示名约定是「陶泥儿开发版」,重发时一并核对 mac 更新产物的渠道身份。 diff --git a/package.json b/package.json index baa9534c2..09cab4938 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "check:game-distribution-upload-safety": "node scripts/check-game-distribution-upload-safety.mjs", "check:game-distribution-upload-resume": "node scripts/check-game-distribution-upload-resume.mjs", "check:agc-project-snapshot-admin-http": "node scripts/check-agc-project-snapshot-admin-http.mjs", + "check:agc-update-channel-manifests": "node scripts/check-agc-update-channel-manifests.mjs", "check:game-distribution-sandbox-e2e": "node scripts/check-game-distribution-sandbox-e2e.mjs", "check:external-v1-scene-generation-smoke": "node scripts/check-external-v1-scene-generation-smoke.mjs", "check:game-distribution-web-e2e": "node scripts/check-game-distribution-web-e2e.mjs", diff --git a/scripts/check-agc-update-channel-manifests.mjs b/scripts/check-agc-update-channel-manifests.mjs new file mode 100644 index 000000000..1dd1d4057 --- /dev/null +++ b/scripts/check-agc-update-channel-manifests.mjs @@ -0,0 +1,301 @@ +// 只读核对 OSS 上**已发布**的 AGC 更新渠道清单(不写任何远端对象)。 +// +// 对应里程碑「AGC 更新发布管线渠道化」第 3 条与「AGC macOS 渠道更新落地」第 1 条: +// 清单内地址指向已存在的对象、签名对象与清单一致、universal macOS 的两个平台键指向 +// 同一对象同一签名,并在允许下载时用产物里烘焙的 updater 公钥验证「签名 ↔ 安装包」。 +// +// 用法: +// npm run check:agc-update-channel-manifests +// AGC_UPDATE_CHANNELS=dev-win,dev-mac # 要核对的渠道分区,默认两个 dev 分区 +// AGC_UPDATE_OSS_BASE_URL=https://.../agc # 覆盖 OSS 基址 +// AGC_UPDATE_VERIFY_DOWNLOAD=1 # 额外下载产物验签(默认只 HEAD 与读 .sig) +// AGC_UPDATE_DOWNLOAD_LIMIT_MB=600 # 下载上限,超过则该平台标记为未验签 +// +// 默认模式不下载安装包,只核对清单结构、对象存在性与签名对象一致性;渠道发布后先用它做快速回归。 +import { createHash } from 'node:crypto'; +import { createWriteStream } from 'node:fs'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { Readable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; + +import { + readUpdaterPubkey, + verifyUpdaterSignature, +} from '../apps/ai-game-creator-shell/scripts/verify-updater-signature.mjs'; + +const OSS_BASE_URL = ( + process.env.AGC_UPDATE_OSS_BASE_URL?.trim() || + 'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc' +).replace(/\/+$/u, ''); +const CHANNELS = (process.env.AGC_UPDATE_CHANNELS?.trim() || 'dev-win,dev-mac') + .split(',') + .map((value) => value.trim()) + .filter(Boolean); +const VERIFY_DOWNLOAD = /^(1|true)$/iu.test( + process.env.AGC_UPDATE_VERIFY_DOWNLOAD?.trim() ?? '', +); +const DOWNLOAD_LIMIT_BYTES = + Number(process.env.AGC_UPDATE_DOWNLOAD_LIMIT_MB?.trim() || '600') * + 1024 * + 1024; + +const MACOS_PLATFORM_KEYS = ['darwin-aarch64', 'darwin-x86_64']; +const WINDOWS_PLATFORM_KEY = 'windows-x86_64'; + +let failures = 0; +let skipped = 0; +function check(name, ok, detail = '') { + if (!ok) failures += 1; + console.log( + `${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` :: ${detail}` : ''}`, + ); +} +function skip(name, detail) { + skipped += 1; + console.log(`SKIP ${name}${detail ? ` :: ${detail}` : ''}`); +} + +function decodeMinisignText(value, label) { + const trimmed = String(value ?? '').trim(); + if (trimmed.length === 0) throw new Error(`${label} 为空`); + if (trimmed.startsWith('untrusted comment:')) return trimmed; + const decoded = Buffer.from(trimmed, 'base64').toString('utf8').trim(); + if (!decoded.startsWith('untrusted comment:')) { + throw new Error(`${label} 不是 minisign 内容`); + } + return decoded; +} + +async function head(url) { + const response = await fetch(url, { method: 'HEAD' }); + const length = Number(response.headers.get('content-length') ?? '0'); + return { status: response.status, length }; +} + +async function download(url, destination) { + const response = await fetch(url); + if (!response.ok || !response.body) { + throw new Error(`下载失败 ${response.status}:${url}`); + } + await pipeline( + Readable.fromWeb(response.body), + createWriteStream(destination), + ); +} + +async function sha256Of(filePath) { + const hash = createHash('sha256'); + hash.update(await readFile(filePath)); + return hash.digest('hex'); +} + +async function fileSize(filePath) { + return (await readFile(filePath)).length; +} + +async function verifyChannel(channel, tempDir) { + console.log(`\n--- 渠道 ${channel} ---`); + const manifestUrl = `${OSS_BASE_URL}/${channel}/latest.json`; + const response = await fetch(manifestUrl); + check( + `${channel} 渠道清单可读`, + response.status === 200, + `status=${response.status} ${manifestUrl}`, + ); + if (response.status !== 200) return; + const raw = await response.text(); + let manifest; + try { + manifest = JSON.parse(raw); + } catch (error) { + check(`${channel} 渠道清单是合法 JSON`, false, String(error)); + return; + } + const version = String(manifest.version ?? ''); + check( + `${channel} 清单版本与发布元数据齐备`, + /^\d+\.\d+\.\d+$/u.test(version) && + !Number.isNaN(Date.parse(String(manifest.pub_date ?? ''))) && + typeof manifest.commit === 'string' && + manifest.commit.trim().length > 0, + `version=${version} pub_date=${manifest.pub_date ?? ''} commit=${String(manifest.commit ?? '').slice(0, 10)}`, + ); + + const platforms = manifest.platforms ?? {}; + const platformKeys = Object.keys(platforms); + check( + `${channel} 清单至少有一个平台条目`, + platformKeys.length > 0, + `platforms=${platformKeys.join(',')}`, + ); + + const expectedPrefix = `${OSS_BASE_URL}/${channel}/${version}/`; + for (const key of platformKeys) { + const entry = platforms[key] ?? {}; + const url = String(entry.url ?? ''); + check( + `${channel}/${key} 地址指向本渠道版本目录`, + url.startsWith(expectedPrefix), + `url=${url}`, + ); + const artifactHead = await head(url); + const sizeOk = artifactHead.status === 200 && artifactHead.length > 0; + check( + `${channel}/${key} 安装包对象存在`, + sizeOk, + `status=${artifactHead.status} bytes=${artifactHead.length}`, + ); + const signatureHead = await head(`${url}.sig`); + check( + `${channel}/${key} 签名对象存在`, + signatureHead.status === 200 && signatureHead.length > 0, + `status=${signatureHead.status} bytes=${signatureHead.length}`, + ); + if (signatureHead.status === 200) { + const signatureText = await (await fetch(`${url}.sig`)).text(); + const manifestSignature = decodeMinisignText( + entry.signature, + `${channel}/${key} 清单签名`, + ); + const objectSignature = decodeMinisignText( + signatureText, + `${channel}/${key} 签名对象`, + ); + check( + `${channel}/${key} 清单签名与签名对象一致`, + manifestSignature === objectSignature, + ); + } + const downloadUrl = String(manifest.downloads?.[key]?.url ?? ''); + check( + `${channel}/${key} 首装下载地址指向已存在对象`, + downloadUrl.startsWith(expectedPrefix) && + (await head(downloadUrl)).status === 200, + `url=${downloadUrl}`, + ); + } + + if (channel.endsWith('-mac')) { + const present = MACOS_PLATFORM_KEYS.every((key) => platforms[key]); + check( + `${channel} 同时提供两个 macOS 平台键(universal)`, + present, + `platforms=${platformKeys.join(',')}`, + ); + if (present) { + const [first, second] = MACOS_PLATFORM_KEYS.map((key) => platforms[key]); + check( + `${channel} 两个 macOS 平台键指向同一对象与同一签名`, + first.url === second.url && first.signature === second.signature, + `sameUrl=${first.url === second.url} sameSignature=${first.signature === second.signature}`, + ); + } + } else { + check( + `${channel} 提供 ${WINDOWS_PLATFORM_KEY} 平台键`, + Boolean(platforms[WINDOWS_PLATFORM_KEY]), + `platforms=${platformKeys.join(',')}`, + ); + } + + if (channel === 'dev-win') { + const bridge = await fetch(`${OSS_BASE_URL}/latest.json`); + const bridgeOk = bridge.status === 200; + check(`${channel} 旧协议迁移指针可读`, bridgeOk); + if (bridgeOk) { + const bridgeJson = await bridge.json(); + const windowsEntry = platforms[WINDOWS_PLATFORM_KEY] ?? {}; + check( + `${channel} 旧协议指针指向同一批已发布对象`, + String(bridgeJson.downloadUrl ?? '') === + String(windowsEntry.url ?? '') && + (await head(String(bridgeJson.downloadUrl ?? ''))).status === 200, + `sha256=${String(bridgeJson.sha256 ?? '').slice(0, 12)} size=${bridgeJson.size ?? ''}`, + ); + } + } + + // 可选:下载产物,核对 sha256/size 并用产物内公钥验签。 + for (const key of platformKeys) { + const entry = platforms[key] ?? {}; + const url = String(entry.url ?? ''); + const artifactHead = await head(url); + if (!VERIFY_DOWNLOAD) { + skip( + `${channel}/${key} 签名 ↔ 安装包匹配`, + '未设置 AGC_UPDATE_VERIFY_DOWNLOAD=1,只做了对象存在性核对', + ); + continue; + } + if (artifactHead.length > DOWNLOAD_LIMIT_BYTES) { + skip( + `${channel}/${key} 签名 ↔ 安装包匹配`, + `产物 ${artifactHead.length} 字节超过下载上限 ${DOWNLOAD_LIMIT_BYTES}`, + ); + continue; + } + const fileName = `${channel}-${key}-${path.basename(decodeURIComponent(url))}`; + const artifactPath = path.join(tempDir, fileName); + const signaturePath = `${artifactPath}.sig`; + await download(url, artifactPath); + await writeFile( + signaturePath, + decodeMinisignText(entry.signature, `${channel}/${key} 清单签名`), + 'utf8', + ); + const size = await fileSize(artifactPath); + check( + `${channel}/${key} 下载对象字节数与 HEAD 一致`, + size === artifactHead.length, + `downloaded=${size} head=${artifactHead.length}`, + ); + const sha256 = await sha256Of(artifactPath); + if (channel === 'dev-win' && key === WINDOWS_PLATFORM_KEY) { + const bridgeJson = await ( + await fetch(`${OSS_BASE_URL}/latest.json`) + ).json(); + check( + `${channel} 旧协议指针 sha256/size 与实际产物一致`, + String(bridgeJson.sha256 ?? '').toLowerCase() === sha256 && + Number(bridgeJson.size ?? 0) === size, + `sha256=${sha256.slice(0, 12)} size=${size}`, + ); + } + try { + const result = verifyUpdaterSignature({ + artifactPath, + signaturePath, + pubkey: readUpdaterPubkey(), + }); + check( + `${channel}/${key} 签名 ↔ 安装包匹配(产物内公钥验签)`, + true, + `alg=${result.algorithm} keyId=${result.keyId}`, + ); + } catch (error) { + check( + `${channel}/${key} 签名 ↔ 安装包匹配(产物内公钥验签)`, + false, + String(error), + ); + } + await rm(artifactPath, { force: true }); + await rm(signaturePath, { force: true }); + } +} + +const tempDir = await mkdtemp(path.join(os.tmpdir(), 'agc-channel-check-')); +try { + for (const channel of CHANNELS) await verifyChannel(channel, tempDir); +} finally { + await rm(tempDir, { recursive: true, force: true }); +} + +console.log( + failures === 0 + ? `\n全部通过${skipped > 0 ? `(${skipped} 项跳过)` : ''}` + : `\n${failures} 项失败${skipped > 0 ? `,${skipped} 项跳过` : ''}`, +); +process.exit(failures === 0 ? 0 : 1);