修复 macOS 渠道发布挑中残留产物,改为构建期核对包内版本与渠道身份
Project CI / AI game creator shell Rust crates (push) Successful in 1m45s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m4s
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / Backend tests (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 / Native shell tests (push) Has been cancelled

- build-macos-ci:构建前按后缀清空 macos/ 下的 *.app.tar.gz / *.sig / *.dmg / *.dmg.sha256,构建后读 Info.plist 核对版本与渠道身份,生成清单后再断言选中本轮更新包
- 新增 macos-release-identity.mjs 与单测:回归用例直接用线上 dev-mac 里那份 0.1.139 release 身份包
- prepare-macos-codex.test:残留清理断言改成按后缀全覆盖,并校验清理在构建前、身份核对在验签前
- Jenkinsfile.ai-game-creator-shell-macos-build:mac Job 增加身份守卫用例
- check:agc-update-channel-manifests:下载后解出 mac 包内 Info.plist 核对版本与身份,并按 2026-09-21 决策只要求 darwin-aarch64
- 文档:两份里程碑按现行决策修正口径并记录线上缺陷,decision-log 记决策与边界,pitfalls 记录「按目录扫描挑产物会选中残留」
This commit is contained in:
kdletters
2026-09-28 22:09:57 +08:00
parent 3702e0f8e0
commit e23b0e871b
10 changed files with 410 additions and 43 deletions
+80 -14
View File
@@ -1,8 +1,9 @@
// 只读核对 OSS 上**已发布**的 AGC 更新渠道清单(不写任何远端对象)。
//
// 对应里程碑「AGC 更新发布管线渠道化」第 3 条与「AGC macOS 渠道更新落地」第 1 条:
// 清单内地址指向已存在的对象、签名对象与清单一致、universal macOS 的两个平台键指向
// 同一对象同一签名,并在允许下载时用产物里烘焙的 updater 公钥验证「签名 ↔ 安装包」。
// 清单内地址指向已存在的对象、签名对象与清单一致;macOS 渠道按 2026-09-21 决策只登记
// `darwin-aarch64`(不再登记 `darwin-x86_64`);允许下载时用产物里烘焙的 updater 公钥
// 验证「签名 ↔ 安装包」,并解出 mac 包内 `Info.plist` 核对版本与渠道身份。
//
// 用法:
// npm run check:agc-update-channel-manifests
@@ -12,6 +13,8 @@
// AGC_UPDATE_DOWNLOAD_LIMIT_MB=600 # 下载上限,超过则该平台标记为未验签
//
// 默认模式不下载安装包,只核对清单结构、对象存在性与签名对象一致性;渠道发布后先用它做快速回归。
// 打开下载后,macOS 渠道会额外核对「清单版本 == 包内版本」「包内 identifier/产品名 == 本渠道身份」——
// 2026-09-28 线上 `dev-mac/0.1.142` 就是「清单写 0.1.142、包里是 0.1.139 的 release 身份包」。
import { createHash } from 'node:crypto';
import { createWriteStream } from 'node:fs';
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
@@ -20,6 +23,10 @@ import path from 'node:path';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import * as tar from 'tar';
import { resolveChannelInstallIdentity } from '../apps/ai-game-creator-shell/scripts/channel-identity.mjs';
import { readMacosAppInfoIdentity } from '../apps/ai-game-creator-shell/scripts/macos-release-identity.mjs';
import {
readUpdaterPubkey,
verifyUpdaterSignature,
@@ -41,7 +48,10 @@ const DOWNLOAD_LIMIT_BYTES =
1024 *
1024;
const MACOS_PLATFORM_KEYS = ['darwin-aarch64', 'darwin-x86_64'];
// 2026-09-21 决策:macOS 只出 arm64 单架构,清单只登记 `darwin-aarch64`。
// 登记 `darwin-x86_64` 会把 arm64 产物发给 Intel 客户端(曾发生在 dev-mac 0.1.86)。
const MACOS_ARM64_PLATFORM_KEY = 'darwin-aarch64';
const MACOS_INTEL_PLATFORM_KEY = 'darwin-x86_64';
const WINDOWS_PLATFORM_KEY = 'windows-x86_64';
let failures = 0;
@@ -95,6 +105,59 @@ async function fileSize(filePath) {
return (await readFile(filePath)).length;
}
/** 从 tar.gz 里读出某个条目(macOS 更新包是 `<产品名>.app.tar.gz`)。 */
async function readTarEntry(filePath, predicate) {
const entries = [];
await tar.t({
file: filePath,
onentry: (entry) => {
entries.push(entry.path);
},
});
const target = entries.find(predicate);
if (!target) throw new Error('压缩包内没有目标文件');
const chunks = [];
await tar.t({
file: filePath,
filter: (entryPath) => entryPath === target,
onentry: (entry) => {
entry.on('data', (chunk) => chunks.push(chunk));
},
});
return Buffer.concat(chunks).toString('utf8');
}
/**
* macOS 更新包必须既是本轮版本、也是本渠道身份。
* 只核对「地址存在 + 签名匹配」会漏掉「签名对但装的是别的渠道、别的版本」。
*/
async function verifyMacosBundleIdentity(channel, key, manifest, artifactPath) {
const channelName = channel.replace(/-mac$/u, '');
const expected = resolveChannelInstallIdentity(channelName);
let identity;
try {
identity = readMacosAppInfoIdentity(
await readTarEntry(artifactPath, (entryPath) =>
entryPath.endsWith('/Contents/Info.plist'),
),
);
} catch (error) {
check(`${channel}/${key} 更新包可读出 Info.plist`, false, String(error));
return;
}
check(
`${channel}/${key} 更新包版本与清单一致`,
identity.version === String(manifest.version),
`bundle=${identity.version ?? ''} manifest=${manifest.version ?? ''}`,
);
check(
`${channel}/${key} 更新包身份属于本渠道`,
identity.identifier === expected.identifier &&
identity.name === expected.productName,
`bundle=${identity.identifier ?? ''}/${identity.name ?? ''} expected=${expected.identifier}/${expected.productName}`,
);
}
async function verifyChannel(channel, tempDir) {
console.log(`\n--- 渠道 ${channel} ---`);
const manifestUrl = `${OSS_BASE_URL}/${channel}/latest.json`;
@@ -178,20 +241,20 @@ async function verifyChannel(channel, tempDir) {
}
if (channel.endsWith('-mac')) {
const present = MACOS_PLATFORM_KEYS.every((key) => platforms[key]);
check(
`${channel} 同时提供两个 macOS 平台键(universal)`,
present,
`${channel} 只登记 darwin-aarch64(2026-09-21 单架构决策)`,
Boolean(platforms[MACOS_ARM64_PLATFORM_KEY]) &&
!platforms[MACOS_INTEL_PLATFORM_KEY],
`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}`,
);
}
const extraMacKeys = platformKeys.filter(
(key) => key.startsWith('darwin-') && key !== MACOS_ARM64_PLATFORM_KEY,
);
check(
`${channel} 没有多余的 macOS 平台键`,
extraMacKeys.length === 0,
`extra=${extraMacKeys.join(',')}`,
);
} else {
check(
`${channel} 提供 ${WINDOWS_PLATFORM_KEY} 平台键`,
@@ -281,6 +344,9 @@ async function verifyChannel(channel, tempDir) {
String(error),
);
}
if (key.startsWith('darwin-') && artifactPath.endsWith('.app.tar.gz')) {
await verifyMacosBundleIdentity(channel, key, manifest, artifactPath);
}
await rm(artifactPath, { force: true });
await rm(signaturePath, { force: true });
}