补齐 AGC 已发布渠道清单只读核对与线上差异取证
Project CI / AI game creator shell Rust crates (push) Successful in 1m28s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m55s
Project CI / Frontend tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Successful in 1m28s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m55s
Project CI / Frontend tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
- 新增 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 更新产物命名待核对
This commit is contained in:
@@ -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 身份,避免同一台机器上并存/覆盖行为与预期不符。
|
||||
|
||||
@@ -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`):只读拉取 `<OSS>/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 构建取证。
|
||||
|
||||
@@ -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 更新产物的渠道身份。
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user