e53992fdc2
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
- build-macos-ci 固定 aarch64-apple-darwin:单架构构建、只跑 arm64 隔离 smoke、首装包命名 <产品名>_<版本>_aarch64.dmg,构建清单记录 smokes 与实际清单平台键 - stage-node-runtime 的 universal-apple-darwin 恢复失败关闭并说明原因:随包 Node 只有单架构官方发行版,按宿主架构放行会让 Intel 上这份侧车不可执行,且通用包自检按 process.arch 校验抓不到 - 守卫用例改为断言入口固定 arm64 目标与 _aarch64.dmg 后缀、universal 目标必须抛错 - Jenkinsfile 与三份技术/运维文档同步单架构口径:清单只登记 darwin-aarch64,Intel 非目标,恢复 universal 的前置条件写在文档里 - 共享记忆补 2026-09-21 决策记录,pitfalls 记下「只带宿主架构的通用包」陷阱,避免再次用过渡实现糊过去
202 lines
7.4 KiB
JavaScript
202 lines
7.4 KiB
JavaScript
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/*',
|
||
// 上一次发布的 commit 落在 master 上,取到它更新摘要才不会退化成「最近提交」。
|
||
'refs/heads/master:refs/remotes/origin/master',
|
||
]) {
|
||
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 失败,
|
||
// 或让上一轮遗留的 .sig 让验签门禁误通过。
|
||
for (const required of [
|
||
// 清理对象用派生的产品名算出来,而不是写死某个名字。
|
||
'${updaterArtifactName}.sig',
|
||
'${firstInstallName}.sha256',
|
||
'fs.rmSync(stale, { 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('readProductName'), '入口必须从 Tauri 配置读产品名');
|
||
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);
|
||
}
|
||
});
|