修复 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
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:
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user