Files
Genarrative/apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs
T
kdletters 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
修复 macOS 渠道发布挑中残留产物,改为构建期核对包内版本与渠道身份
- 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 记录「按目录扫描挑产物会选中残留」
2026-09-28 22:09:57 +08:00

222 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import { test } from 'node:test';
import {
lockedMacPackage,
validateArchiveListing,
verifyPackageIntegrity,
} from './prepare-macos-codex.mjs';
const lock = JSON.parse(
fs.readFileSync(new URL('../../../package-lock.json', import.meta.url)),
);
const version = JSON.parse(
fs.readFileSync(new URL('../package.json', import.meta.url)),
).devDependencies['@openai/codex'];
test('both macOS dependencies resolve from the lockfile without floating versions', () => {
assert.equal(
lockedMacPackage(lock, 'arm64', version).target,
'aarch64-apple-darwin',
);
assert.equal(
lockedMacPackage(lock, 'x64', version).target,
'x86_64-apple-darwin',
);
assert.throws(() => lockedMacPackage(lock, 'other', version));
assert.throws(() => lockedMacPackage(lock, 'x64', '0.0.0'));
});
test('native package integrity rejects tampering', () => {
const bytes = Buffer.from('pinned package');
const integrity = `sha512-${createHash('sha512').update(bytes).digest('base64')}`;
verifyPackageIntegrity(bytes, integrity);
assert.throws(() =>
verifyPackageIntegrity(Buffer.from('modified'), integrity),
);
});
test('archive traversal and non-package entries fail closed', () => {
validateArchiveListing(
'package/package.json\npackage/vendor/target/bin/codex\n',
);
for (const listing of [
'',
'/tmp/payload',
'package/../private',
'other/file',
'package/..\\file',
]) {
assert.throws(() => validateArchiveListing(listing));
}
});
test('CI pipeline is manual, publishes the macOS partition and never reuses a developer workspace', () => {
const pipeline = fs.readFileSync(
new URL(
'../../../jenkins/Jenkinsfile.ai-game-creator-shell-macos-build',
import.meta.url,
),
'utf8',
);
for (const required of [
'genarrative-agc-macos',
'disableConcurrentBuilds()',
'$AGC_AGENT_ROOT',
'StrictHostKeyChecking=yes',
'git merge-base --is-ancestor',
'allowEmptyArchive: false',
"string(name: 'AGC_UPDATE_CHANNEL', defaultValue: 'dev'",
'AGC_UPDATE_CHANNEL=${params.AGC_UPDATE_CHANNEL}',
"string(credentialsId: 'AgcUpdaterSigningKey'",
"string(credentialsId: 'AgcUpdaterSigningKeyPassword'",
"string(credentialsId: 'AliyunAccessKeyId'",
"string(credentialsId: 'AliyunaccessKeySecret'",
'AGC_RELEASE_VERSION',
'OSSUTIL_BIN',
// 并行度必须可调:节点是共用机器,写死容易把整机压满或反过来浪费一半核心。
"string(name: 'CARGO_BUILD_JOBS', defaultValue: '8'",
'CARGO_BUILD_JOBS=${params.CARGO_BUILD_JOBS}',
// Agent 工作区按约定匹配,不写死节点名:节点改名(-local → -01)后守卫仍成立。
'"$HOME"/Library/Jenkins/agents/*/workspace/*',
]) {
assert.ok(pipeline.includes(required), required);
}
assert.ok(
!pipeline.includes('genarrative-agc-macos-local'),
'Jenkinsfile 不得写死具体节点名',
);
// 这条管线是正式发布入口(与 Windows 对称):默认真发布,演练需显式勾选。
assert.match(
pipeline,
/booleanParam\(name: 'AGC_RELEASE_DRY_RUN', defaultValue: false/u,
'Channel 发布默认必须是真发布,演练只能显式勾选',
);
// 节点是办公机:离线期间排队的旧构建必须自行让位,且跳过要覆盖后续全部阶段。
assert.match(
pipeline,
/booleanParam\(name: 'SKIP_IF_SUPERSEDED', defaultValue: false/u,
);
// 仓库文件不得出现节点用户名/个人 Home 路径:换机或改名后必须仍然可用。
assert.ok(
!pipeline.includes('/Users/'),
'Jenkinsfile 不得写死个人 Home 路径,工具链位置应按 $HOME 展开',
);
assert.ok(
pipeline.includes('export PATH="$HOME/'),
'PATH 必须在 shell 步骤里按 $HOME 展开',
);
// 超时必须高于实测最慢(78 分钟冷构建 + 共用机器),否则会被中断在链接阶段。
assert.ok(
pipeline.includes('timeout(time: 150'),
'构建超时上限必须留出冷构建余量',
);
for (const diagnostic of ['macOS 发布失败', '被中断']) {
assert.ok(pipeline.includes(diagnostic), diagnostic);
}
assert.ok(
pipeline.includes('.jenkins-superseded-by'),
'必须记录被推进的标记供后续阶段判定',
);
assert.equal(
(pipeline.match(/env\.AGC_BUILD_SUPERSEDED != 'true'/gu) ?? []).length,
3,
'Toolchain / Package / Archive 三个阶段都必须按跳过标记收口',
);
for (const forbidden of [
'triggers {',
'cron(',
'pollSCM(',
'git clean -fdx',
// release:upload 会重新触发一次完整构建,既翻倍耗时也绕过本 Job 的验签门禁。
'release:upload',
]) {
assert.ok(!pipeline.includes(forbidden), forbidden);
}
});
test('macOS release entry verifies the updater signature before uploading', () => {
const entry = fs.readFileSync(
new URL('./build-macos-ci.mjs', import.meta.url),
'utf8',
);
const verifyIndex = entry.indexOf('verifyUpdaterSignature({');
const uploadIndex = entry.indexOf('uploadReleaseArtifacts(release');
assert.ok(verifyIndex > 0, '必须调用更新包验签');
assert.ok(uploadIndex > 0, '必须调用 OSS 上传');
assert.ok(verifyIndex < uploadIndex, '必须先验签再上传,验不过不得写 OSS');
// 无签名私钥时禁止构建:未签名的更新包会被客户端一律拒绝。
assert.ok(entry.includes('TAURI_SIGNING_PRIVATE_KEY'));
// `--no-sign` 会连带跳过 updater 的 minisign 签名,产物将没有 .sig,入口不得传它。
assert.ok(
!entry.includes("'--no-sign'"),
'--no-sign 会同时跳过 updater 签名,产物缺少 .sig',
);
// workspace 会跨构建保留产物:必须在构建前清掉构建目录里所有更新包/签名/DMG——
// 只删「本轮要写的名字」会漏掉其它渠道身份的残留(2026-09-28 线上 dev-mac/0.1.142
// 就是被 0.1.139 的 release 身份 *.app.tar.gz 顶掉的),清理必须按后缀全覆盖。
const cleanupIndex = entry.indexOf(
'listStaleMacosArtifacts(existingBundleFiles)',
);
const buildIndex = entry.indexOf('runTauriBuild(args, context)');
const identityIndex = entry.indexOf('assertMacosAppMatchesChannelIdentity({');
const manifestArtifactIndex = entry.indexOf(
'assertManifestArtifactMatchesExpected({',
);
assert.ok(cleanupIndex > 0, '必须清理构建目录里的残留产物');
assert.ok(cleanupIndex < buildIndex, '清理必须发生在构建之前');
assert.ok(identityIndex > buildIndex, '构建之后必须核对产物身份');
assert.ok(identityIndex < verifyIndex, '身份核对必须在验签与上传之前');
assert.ok(
manifestArtifactIndex > 0 && manifestArtifactIndex < uploadIndex,
'必须在清单生成后核对它选中的就是本轮更新包',
);
for (const required of [
'fs.rmSync(stale, { recursive: true, force: true })',
"'-ov'",
]) {
assert.ok(entry.includes(required), required);
}
});
test('macOS release entry and smoke script derive product names from config and the bundle', () => {
const entry = fs.readFileSync(
new URL('./build-macos-ci.mjs', import.meta.url),
'utf8',
);
// 产品名决定 *.app、updater 归档与 DMG 卷名:它必须从渠道安装身份派生,
// 写死会在换渠道或改名后静默找错对象。
assert.ok(
entry.includes('resolveChannelInstallIdentity'),
'入口必须从渠道安装身份派生产品名',
);
assert.ok(
entry.includes('resolveProductName(context.channel)'),
'产品名必须按当前发布渠道解析',
);
assert.ok(!entry.includes('陶泥儿'), 'macOS 发布入口不得写死产品名');
assert.ok(
entry.includes("const macTarget = 'aarch64-apple-darwin'"),
'macOS 发布入口必须固定单架构目标',
);
assert.ok(
entry.includes('_${version}_aarch64.dmg'),
'首装包名必须保留清单侧单架构分支唯一匹配所需的后缀(Tauri 口径 aarch64)',
);
const smoke = fs.readFileSync(
new URL('./check-macos-bundle.mjs', import.meta.url),
'utf8',
);
assert.ok(!smoke.includes('陶泥儿'), '校验脚本不得写死产品名');
for (const required of [
'path.basename(source)',
'Print :CFBundleExecutable',
"'--clone'",
]) {
assert.ok(smoke.includes(required), required);
}
});