e23b0e871b
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 记录「按目录扫描挑产物会选中残留」
108 lines
4.0 KiB
JavaScript
108 lines
4.0 KiB
JavaScript
/**
|
||
* macOS 发布产物的身份与版本守卫。
|
||
*
|
||
* 为什么单独抽出来:2026-09-28 线上核对发现 `dev-mac/0.1.142` 清单指向的更新包
|
||
* 其实是 **0.1.139 的 release 身份包**(`world.genarrative.ai-game-creator.release`)。
|
||
* 根因是 mac 构建目录里会留下上一轮(甚至上一个渠道身份)的 `*.app.tar.gz`,而
|
||
* `generateUpdateManifest()` 是按优先级扫描目录挑产物的——于是「清单写 0.1.142、
|
||
* 包里是 0.1.139 且是另一个渠道身份」。这类错误在客户端上表现为「更新后版本没变
|
||
* 或渠道身份被换掉」,只能靠构建期失败关闭拦住。
|
||
*
|
||
* 约束:只做纯函数与显式断言,方便单测复现线上那份坏产物;不在这里读文件系统。
|
||
*/
|
||
|
||
/** 会被构建期残留影响的 mac 产物后缀:更新包、签名、首装 DMG 与其摘要。 */
|
||
const STALE_MACOS_ARTIFACT_SUFFIXES = [
|
||
'.app.tar.gz',
|
||
'.app.tar.gz.sig',
|
||
'.dmg',
|
||
'.dmg.sha256',
|
||
];
|
||
|
||
function extractPlistString(plistText, key) {
|
||
const pattern = new RegExp(
|
||
`<key>${key}</key>\\s*<string>([^<]*)</string>`,
|
||
'u',
|
||
);
|
||
const match = plistText.match(pattern);
|
||
return match ? match[1] : null;
|
||
}
|
||
|
||
/** 从 `Info.plist` 文本里读出发布相关的身份字段;缺字段返回 null,由断言决定是否致命。 */
|
||
export function readMacosAppInfoIdentity(plistText) {
|
||
if (typeof plistText !== 'string' || plistText.trim().length === 0) {
|
||
throw new Error('Info.plist 内容为空,无法核对产物身份');
|
||
}
|
||
return {
|
||
version: extractPlistString(plistText, 'CFBundleShortVersionString'),
|
||
identifier: extractPlistString(plistText, 'CFBundleIdentifier'),
|
||
name: extractPlistString(plistText, 'CFBundleName'),
|
||
displayName: extractPlistString(plistText, 'CFBundleDisplayName'),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 断言本轮构建出来的 `.app` 就是本渠道本轮该发的产物。
|
||
*
|
||
* 三个字段都要对上:版本必须等于即将写进清单的版本;identifier 与产品名必须来自
|
||
* 渠道安装身份(`channel-identity.mjs`),否则同一台机器上的 dev / release 会互相顶掉。
|
||
*/
|
||
export function assertMacosAppMatchesChannelIdentity({
|
||
identity,
|
||
expectedVersion,
|
||
expectedProductName,
|
||
expectedIdentifier,
|
||
}) {
|
||
const problems = [];
|
||
if (identity.version !== expectedVersion) {
|
||
problems.push(
|
||
`版本不一致:产物 ${identity.version ?? '(缺失)'},本轮清单 ${expectedVersion}`,
|
||
);
|
||
}
|
||
if (identity.identifier !== expectedIdentifier) {
|
||
problems.push(
|
||
`bundle identifier 不一致:产物 ${identity.identifier ?? '(缺失)'},本渠道 ${expectedIdentifier}`,
|
||
);
|
||
}
|
||
const names = [identity.name, identity.displayName].filter(Boolean);
|
||
if (
|
||
names.length === 0 ||
|
||
names.some((value) => value !== expectedProductName)
|
||
) {
|
||
problems.push(
|
||
`产品名不一致:产物 ${names.join(' / ') || '(缺失)'},本渠道 ${expectedProductName}`,
|
||
);
|
||
}
|
||
if (problems.length > 0) {
|
||
throw new Error(
|
||
`macOS 产物身份核对失败:${problems.join(';')}。` +
|
||
'这通常意味着构建目录里残留了上一轮/其它渠道的产物,或渠道身份没有注入 Tauri 构建。',
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 列出构建前必须清掉的残留产物:构建目录里的更新包/签名/首装包只属于本轮,
|
||
* 留着就会让按目录扫描的清单生成挑到旧文件。
|
||
*/
|
||
export function listStaleMacosArtifacts(filePaths) {
|
||
return filePaths.filter((filePath) =>
|
||
STALE_MACOS_ARTIFACT_SUFFIXES.some((suffix) => filePath.endsWith(suffix)),
|
||
);
|
||
}
|
||
|
||
/** 断言清单最终选中的更新包就是本轮写出的那一个,避免「签名对但选错包」。 */
|
||
export function assertManifestArtifactMatchesExpected({
|
||
artifactPath,
|
||
expectedPath,
|
||
}) {
|
||
if (!artifactPath || !expectedPath) {
|
||
throw new Error('清单产物路径缺失,无法核对本轮更新包');
|
||
}
|
||
if (artifactPath !== expectedPath) {
|
||
throw new Error(
|
||
`清单选中的更新包不是本轮产物:选中 ${artifactPath},本轮应为 ${expectedPath}`,
|
||
);
|
||
}
|
||
}
|