060a850f74
Project CI / AI game creator shell Rust smoke (push) Successful in 1m28s
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (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 / AI game creator shell Rust crates (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
自动摘要改为读取提交消息首行,仅展示标题并忽略后续说明 更新摘要不再附带短 SHA 补充多行提交消息与摘要格式回归测试 同步客户端更新检查与开发运维文档
1233 lines
39 KiB
JavaScript
1233 lines
39 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { execFileSync } from 'node:child_process';
|
|
import {
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { test } from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import {
|
|
agcReleasePathPatterns,
|
|
buildRelease,
|
|
buildTauriBuildArguments,
|
|
collectRecentReleaseCommits,
|
|
collectReleaseCommits,
|
|
compareVersions,
|
|
createChannelConfig,
|
|
createLegacyUpdateManifest,
|
|
createUpdateManifest,
|
|
formatRecentReleaseNotes,
|
|
formatReleaseNotes,
|
|
generateUpdateManifest,
|
|
nextPatchVersion,
|
|
resolveManifestPlatformKeys,
|
|
resolvePreviousReleaseCommit,
|
|
resolveReleaseChannel,
|
|
resolveReleaseContext,
|
|
resolveReleasePartition,
|
|
resolveRemoteHighWaterVersion,
|
|
runTauriBuild,
|
|
selectFirstInstallArtifact,
|
|
selectReleaseArtifact,
|
|
updateManifestUrl,
|
|
} from './build-release.mjs';
|
|
import {
|
|
AGC_APP_IDENTIFIER,
|
|
AGC_PRODUCT_NAME,
|
|
resolveChannelInstallIdentity,
|
|
} from './channel-identity.mjs';
|
|
|
|
const windowsTarget = 'x86_64-pc-windows-msvc';
|
|
const universalTarget = 'universal-apple-darwin';
|
|
const packageVersion = JSON.parse(
|
|
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
).version;
|
|
|
|
function createDmgFixture(root, target, version = packageVersion) {
|
|
const architecture = target.startsWith('aarch64')
|
|
? 'aarch64'
|
|
: target === universalTarget
|
|
? 'universal'
|
|
: 'x64';
|
|
const dmg = path.join(root, `陶泥儿_${version}_${architecture}.dmg`);
|
|
writeFileSync(dmg, 'first installation disk image');
|
|
return dmg;
|
|
}
|
|
|
|
test('native sidecar builds accept universal and each macOS architecture', () => {
|
|
for (const target of [
|
|
universalTarget,
|
|
'aarch64-apple-darwin',
|
|
'x86_64-apple-darwin',
|
|
]) {
|
|
assert.deepEqual(buildTauriBuildArguments([], target), [
|
|
'build',
|
|
'--target',
|
|
target,
|
|
]);
|
|
}
|
|
});
|
|
|
|
function withEnv(overrides, run) {
|
|
const previous = new Map();
|
|
for (const [key, value] of Object.entries(overrides)) {
|
|
previous.set(key, process.env[key]);
|
|
if (value === undefined) delete process.env[key];
|
|
else process.env[key] = value;
|
|
}
|
|
try {
|
|
return run();
|
|
} finally {
|
|
for (const [key, value] of previous) {
|
|
if (value === undefined) delete process.env[key];
|
|
else process.env[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
function withSignedArtifact(fileName, run) {
|
|
const directory = mkdtempSync(path.join(os.tmpdir(), 'agc-release-test-'));
|
|
try {
|
|
const artifact = path.join(directory, fileName);
|
|
writeFileSync(artifact, 'installation package');
|
|
writeFileSync(`${artifact}.sig`, 'signature-content\n');
|
|
return run(artifact);
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function jsonResponse(body, status = 200) {
|
|
return {
|
|
status,
|
|
ok: status >= 200 && status < 300,
|
|
json: async () => body,
|
|
};
|
|
}
|
|
|
|
function withStubbedFetch(handler, run) {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url) => handler(String(url));
|
|
return Promise.resolve(run()).finally(() => {
|
|
globalThis.fetch = originalFetch;
|
|
});
|
|
}
|
|
|
|
test('selects an explicit release artifact when configured', () => {
|
|
const artifactPath = fileURLToPath(
|
|
new URL('../package.json', import.meta.url),
|
|
);
|
|
withEnv({ AGC_UPDATE_ARTIFACT: artifactPath }, () => {
|
|
assert.equal(selectReleaseArtifact([]), artifactPath);
|
|
});
|
|
});
|
|
|
|
test('does not select unsupported files', () => {
|
|
assert.equal(
|
|
selectReleaseArtifact(['/tmp/latest.json', '/tmp/readme.txt']),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test('channels are independent of platform and accept release and custom names', () => {
|
|
assert.equal(resolveReleaseChannel({}), 'dev');
|
|
for (const channel of ['dev', 'release', 'beta-2', 'a'.repeat(32)]) {
|
|
assert.equal(
|
|
resolveReleaseChannel({ AGC_UPDATE_CHANNEL: channel }),
|
|
channel,
|
|
);
|
|
assert.equal(
|
|
resolveReleasePartition(channel, windowsTarget),
|
|
`${channel}-win`,
|
|
);
|
|
assert.equal(
|
|
resolveReleasePartition(channel, 'aarch64-apple-darwin'),
|
|
`${channel}-mac`,
|
|
);
|
|
}
|
|
assert.equal(
|
|
resolveReleaseChannel({ AGC_UPDATE_CHANNEL: ' release ' }),
|
|
'release',
|
|
);
|
|
for (const channel of [
|
|
'',
|
|
' ',
|
|
'win',
|
|
'mac',
|
|
'windows',
|
|
'macos',
|
|
'darwin',
|
|
'linux',
|
|
'dev-win',
|
|
'dev-mac',
|
|
'Release',
|
|
'../dev',
|
|
'a/b',
|
|
'a_b',
|
|
'-beta',
|
|
'beta-',
|
|
'1beta',
|
|
'a'.repeat(33),
|
|
]) {
|
|
assert.throws(
|
|
() => resolveReleaseChannel({ AGC_UPDATE_CHANNEL: channel }),
|
|
/发布渠道无效/u,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('channel manifest URL and build-time endpoint follow the channel', () => {
|
|
withEnv({ AGC_UPDATE_OSS_BASE_URL: undefined }, () => {
|
|
assert.equal(
|
|
updateManifestUrl('dev', windowsTarget),
|
|
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/dev-win/latest.json',
|
|
);
|
|
assert.deepEqual(createChannelConfig('dev', 'aarch64-apple-darwin'), {
|
|
productName: AGC_PRODUCT_NAME,
|
|
identifier: AGC_APP_IDENTIFIER,
|
|
plugins: {
|
|
updater: {
|
|
endpoints: [
|
|
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/dev-mac/latest.json',
|
|
],
|
|
},
|
|
},
|
|
});
|
|
assert.equal(
|
|
updateManifestUrl('release', windowsTarget),
|
|
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/release-win/latest.json',
|
|
);
|
|
assert.equal(
|
|
createChannelConfig('beta-2', 'x86_64-apple-darwin').plugins.updater
|
|
.endpoints[0],
|
|
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/beta-2-mac/latest.json',
|
|
);
|
|
});
|
|
});
|
|
|
|
test('channel install identity isolates co-installed builds and keeps the default channel stable', () => {
|
|
// 默认渠道必须保持已发布客户端身份:改身份等于换一个 App,升级链会断。
|
|
assert.deepEqual(resolveChannelInstallIdentity('dev'), {
|
|
productName: AGC_PRODUCT_NAME,
|
|
identifier: AGC_APP_IDENTIFIER,
|
|
});
|
|
assert.deepEqual(resolveChannelInstallIdentity('release'), {
|
|
productName: '陶泥儿 Release',
|
|
identifier: `${AGC_APP_IDENTIFIER}.release`,
|
|
});
|
|
assert.deepEqual(resolveChannelInstallIdentity('beta-2'), {
|
|
productName: '陶泥儿 Beta-2',
|
|
identifier: `${AGC_APP_IDENTIFIER}.beta-2`,
|
|
});
|
|
|
|
// 同一台设备上不同渠道的安装目录、卸载项与数据目录必须互不相同。
|
|
for (const channel of ['release', 'beta-2', 'a'.repeat(32)]) {
|
|
const identity = resolveChannelInstallIdentity(channel);
|
|
assert.notEqual(identity.productName, AGC_PRODUCT_NAME);
|
|
assert.notEqual(identity.identifier, AGC_APP_IDENTIFIER);
|
|
assert.ok(identity.identifier.startsWith(`${AGC_APP_IDENTIFIER}.`));
|
|
}
|
|
|
|
for (const channel of ['dev-win', 'Release', 'win', 'beta-']) {
|
|
assert.throws(
|
|
() => resolveChannelInstallIdentity(channel),
|
|
/发布渠道无效/u,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('channel install identity is baked into the same build-time config as the endpoint', () => {
|
|
withEnv({ AGC_UPDATE_OSS_BASE_URL: undefined }, () => {
|
|
const config = createChannelConfig('release', windowsTarget);
|
|
assert.equal(config.productName, '陶泥儿 Release');
|
|
assert.equal(config.identifier, `${AGC_APP_IDENTIFIER}.release`);
|
|
assert.match(
|
|
config.plugins.updater.endpoints[0],
|
|
/\/release-win\/latest\.json$/u,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('channel products keep first-install selection working under the channel product name', () => {
|
|
const root = mkdtempSync(path.join(os.tmpdir(), 'agc-channel-dmg-'));
|
|
try {
|
|
const { productName } = resolveChannelInstallIdentity('release');
|
|
const dmg = path.join(root, `${productName}_${packageVersion}_aarch64.dmg`);
|
|
writeFileSync(dmg, 'channel first installation disk image');
|
|
writeFileSync(path.join(root, 'windows.exe'), 'wrong platform');
|
|
assert.equal(
|
|
selectFirstInstallArtifact([dmg, path.join(root, 'windows.exe')], {
|
|
target: 'aarch64-apple-darwin',
|
|
version: packageVersion,
|
|
}),
|
|
dmg,
|
|
);
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('packaged renderer receives the same channel as the updater manifest', () => {
|
|
const context = resolveReleaseContext([], {
|
|
AGC_BUILD_TARGET: windowsTarget,
|
|
AGC_UPDATE_CHANNEL: 'release',
|
|
});
|
|
let spawnOptions;
|
|
runTauriBuild([], context, {
|
|
// 必须 stub:真实 staging 会用宿主平台(如 macOS 的 darwin/arm64)去对默认的
|
|
// Windows 目标做一致性校验,在非 Windows 主机上直接失败——本用例只关心渠道注入。
|
|
stageRuntime: () => {},
|
|
spawn: (_binary, _args, options) => {
|
|
spawnOptions = options;
|
|
return { status: 0 };
|
|
},
|
|
});
|
|
assert.equal(spawnOptions?.env?.VITE_AGC_PLATFORM_CHANNEL, 'release');
|
|
});
|
|
|
|
test('macOS manifests advertise exactly the architectures actually built', () => {
|
|
assert.deepEqual(resolveManifestPlatformKeys(universalTarget), [
|
|
'darwin-aarch64',
|
|
'darwin-x86_64',
|
|
]);
|
|
assert.deepEqual(resolveManifestPlatformKeys('aarch64-apple-darwin'), [
|
|
'darwin-aarch64',
|
|
]);
|
|
assert.deepEqual(resolveManifestPlatformKeys('x86_64-apple-darwin'), [
|
|
'darwin-x86_64',
|
|
]);
|
|
assert.deepEqual(resolveManifestPlatformKeys(windowsTarget), [
|
|
'windows-x86_64',
|
|
]);
|
|
});
|
|
|
|
test('release context resolves explicit targets before environment/default and fails closed', () => {
|
|
for (const args of [
|
|
['--target', 'aarch64-apple-darwin'],
|
|
['--target=aarch64-apple-darwin'],
|
|
['-t', 'aarch64-apple-darwin'],
|
|
]) {
|
|
for (const env of [{}, { AGC_BUILD_TARGET: windowsTarget }]) {
|
|
const context = resolveReleaseContext(args, env);
|
|
assert.equal(context.target, 'aarch64-apple-darwin');
|
|
assert.equal(context.channel, 'dev');
|
|
assert.match(
|
|
context.bundleRoot.replaceAll('\\', '/'),
|
|
/target\/aarch64-apple-darwin\/release\/bundle$/,
|
|
);
|
|
assert.ok(Object.isFrozen(context));
|
|
}
|
|
assert.throws(
|
|
() => resolveReleaseContext(args, { AGC_UPDATE_CHANNEL: 'dev-win' }),
|
|
/发布渠道无效/,
|
|
);
|
|
}
|
|
assert.equal(resolveReleaseContext([], {}).target, windowsTarget);
|
|
assert.equal(
|
|
resolveReleaseContext([], { AGC_BUILD_TARGET: 'x86_64-apple-darwin' })
|
|
.channel,
|
|
'dev',
|
|
);
|
|
for (const args of [
|
|
['--target'],
|
|
['--target='],
|
|
['--target', '--no-bundle'],
|
|
['--target', windowsTarget, '--target=aarch64-apple-darwin'],
|
|
['--target', 'unknown'],
|
|
])
|
|
assert.throws(() => resolveReleaseContext(args, {}));
|
|
});
|
|
|
|
test('explicit macOS target drives version lookup, Tauri endpoint, artifact and manifest together', async () => {
|
|
const calls = [];
|
|
const seenContexts = [];
|
|
await withStubbedFetch(
|
|
(url) => {
|
|
calls.push(url);
|
|
assert.match(url, /\/dev-mac\/latest\.json$/);
|
|
return jsonResponse({ version: '0.1.67' });
|
|
},
|
|
() =>
|
|
withEnv(
|
|
{ AGC_BUILD_TARGET: undefined, AGC_UPDATE_CHANNEL: undefined },
|
|
() =>
|
|
buildRelease(['--target', 'aarch64-apple-darwin'], {
|
|
prepareVersion: async (context) => {
|
|
seenContexts.push(context);
|
|
assert.equal(
|
|
await resolveRemoteHighWaterVersion(
|
|
context.channel,
|
|
context.target,
|
|
),
|
|
'0.1.67',
|
|
);
|
|
},
|
|
build: (args, context) => {
|
|
seenContexts.push(context);
|
|
runTauriBuild(args, context, {
|
|
stageRuntime: () => {},
|
|
spawn: (_binary, command) => {
|
|
const configIndex = command.lastIndexOf('--config');
|
|
const config = JSON.parse(
|
|
readFileSync(command[configIndex + 1], 'utf8'),
|
|
);
|
|
assert.match(
|
|
config.plugins.updater.endpoints[0],
|
|
/\/dev-mac\/latest\.json$/,
|
|
);
|
|
assert.ok(command.includes('aarch64-apple-darwin'));
|
|
assert.ok(
|
|
!command.includes('--features=cocos-editor-execute'),
|
|
);
|
|
return { status: 0 };
|
|
},
|
|
});
|
|
},
|
|
generateManifest: (context) => {
|
|
seenContexts.push(context);
|
|
withSignedArtifact('陶泥儿.app.tar.gz', (artifact) => {
|
|
assert.equal(
|
|
selectReleaseArtifact(
|
|
['/tmp/win.exe', artifact, '/tmp/mac.dmg'],
|
|
context.target,
|
|
),
|
|
artifact,
|
|
);
|
|
const manifest = createUpdateManifest(artifact, {
|
|
...context,
|
|
downloadArtifact: createDmgFixture(
|
|
path.dirname(artifact),
|
|
context.target,
|
|
),
|
|
});
|
|
assert.deepEqual(Object.keys(manifest.platforms), [
|
|
'darwin-aarch64',
|
|
]);
|
|
assert.match(
|
|
manifest.platforms['darwin-aarch64'].url,
|
|
/\/dev-mac\//,
|
|
);
|
|
});
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
assert.equal(calls.length, 1, 'Mac 不应读取 Windows 迁移指针');
|
|
assert.equal(seenContexts.length, 3);
|
|
assert.ok(seenContexts.every((context) => context === seenContexts[0]));
|
|
});
|
|
|
|
for (const target of ['aarch64-apple-darwin', 'x86_64-apple-darwin']) {
|
|
test(`real manifest writer publishes the ${target} updater and first installer separately`, async () => {
|
|
const root = mkdtempSync(path.join(os.tmpdir(), 'agc-mac-manifest-'));
|
|
try {
|
|
const artifact = path.join(root, '陶泥儿.app.tar.gz');
|
|
writeFileSync(artifact, 'mac package');
|
|
writeFileSync(`${artifact}.sig`, 'mac signature');
|
|
writeFileSync(path.join(root, 'windows.exe'), 'wrong platform');
|
|
const downloadArtifact = createDmgFixture(root, target);
|
|
const context = {
|
|
...resolveReleaseContext([`--target=${target}`], {}),
|
|
bundleRoot: root,
|
|
};
|
|
const result = await withStubbedFetch(
|
|
(url) => {
|
|
assert.match(url, /\/dev-mac\/latest\.json$/);
|
|
return jsonResponse({}, 404);
|
|
},
|
|
() => generateUpdateManifest(context),
|
|
);
|
|
assert.equal(result.artifact, artifact);
|
|
assert.equal(result.downloadArtifact, downloadArtifact);
|
|
assert.equal(result.manifestPath, path.join(root, 'latest.json'));
|
|
assert.equal(result.legacyManifestPath, null);
|
|
const key = target.startsWith('aarch64')
|
|
? 'darwin-aarch64'
|
|
: 'darwin-x86_64';
|
|
assert.deepEqual(Object.keys(result.manifest.platforms), [key]);
|
|
assert.deepEqual(Object.keys(result.manifest.downloads), [key]);
|
|
assert.match(
|
|
result.manifest.platforms[key].url,
|
|
/\/dev-mac\/.*\.app\.tar\.gz$/,
|
|
);
|
|
assert.equal(
|
|
decodeURIComponent(
|
|
new URL(result.manifest.downloads[key].url).pathname,
|
|
),
|
|
`/agc/dev-mac/${packageVersion}/${path.basename(downloadArtifact)}`,
|
|
);
|
|
assert.deepEqual(
|
|
JSON.parse(readFileSync(result.manifestPath, 'utf8')),
|
|
result.manifest,
|
|
);
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
}
|
|
|
|
test('DMG selection ignores other versions and architectures but rejects missing, empty and ambiguous current packages', () => {
|
|
const root = mkdtempSync(path.join(os.tmpdir(), 'agc-dmg-selection-'));
|
|
try {
|
|
const target = 'aarch64-apple-darwin';
|
|
const options = {
|
|
target,
|
|
version: '2.3.4',
|
|
artifact: path.join(root, '陶泥儿.app.tar.gz'),
|
|
};
|
|
const oldVersion = createDmgFixture(root, target, '2.3.3');
|
|
const wrongArchitecture = createDmgFixture(
|
|
root,
|
|
'x86_64-apple-darwin',
|
|
'2.3.4',
|
|
);
|
|
assert.throws(() => selectFirstInstallArtifact([], options), /找到 0 个/u);
|
|
assert.throws(
|
|
() =>
|
|
selectFirstInstallArtifact([oldVersion, wrongArchitecture], options),
|
|
/找到 0 个/u,
|
|
);
|
|
const current = createDmgFixture(root, target, '2.3.4');
|
|
assert.equal(
|
|
selectFirstInstallArtifact(
|
|
[oldVersion, wrongArchitecture, current],
|
|
options,
|
|
),
|
|
current,
|
|
);
|
|
writeFileSync(current, '');
|
|
assert.throws(
|
|
() => selectFirstInstallArtifact([current], options),
|
|
/不存在或为空/u,
|
|
);
|
|
writeFileSync(current, 'valid dmg');
|
|
const second = path.join(root, '另一包_2.3.4_aarch64.dmg');
|
|
writeFileSync(second, 'ambiguous dmg');
|
|
assert.throws(
|
|
() => selectFirstInstallArtifact([current, second], options),
|
|
/找到 2 个/u,
|
|
);
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('manifest writer refuses to create latest when the current Mac DMG is missing', async () => {
|
|
const root = mkdtempSync(path.join(os.tmpdir(), 'agc-missing-dmg-'));
|
|
try {
|
|
const artifact = path.join(root, '陶泥儿.app.tar.gz');
|
|
writeFileSync(artifact, 'updater archive');
|
|
writeFileSync(`${artifact}.sig`, 'signature');
|
|
const context = {
|
|
...resolveReleaseContext(['--target=aarch64-apple-darwin'], {}),
|
|
bundleRoot: root,
|
|
};
|
|
await assert.rejects(
|
|
() => generateUpdateManifest(context),
|
|
/首装 DMG 必须唯一匹配/u,
|
|
);
|
|
assert.throws(() => readFileSync(path.join(root, 'latest.json')), {
|
|
code: 'ENOENT',
|
|
});
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('invalid target or platform used as channel fails before any release side effect', async () => {
|
|
let touched = false;
|
|
const sideEffects = {
|
|
prepareVersion: () => {
|
|
touched = true;
|
|
},
|
|
build: () => {
|
|
touched = true;
|
|
},
|
|
generateManifest: () => {
|
|
touched = true;
|
|
},
|
|
};
|
|
await assert.rejects(
|
|
() => buildRelease(['--target', 'unknown'], sideEffects),
|
|
/不支持的发布目标/,
|
|
);
|
|
await withEnv({ AGC_UPDATE_CHANNEL: 'dev-win' }, () =>
|
|
assert.rejects(
|
|
() => buildRelease(['--target=aarch64-apple-darwin'], sideEffects),
|
|
/发布渠道无效/,
|
|
),
|
|
);
|
|
assert.equal(touched, false);
|
|
});
|
|
|
|
test('universal uses the Mac channel and the same signed artifact for both architectures', () => {
|
|
const context = resolveReleaseContext(['--target', universalTarget], {
|
|
AGC_BUILD_TARGET: windowsTarget,
|
|
});
|
|
// 渠道本身不含系统:分区由渠道 + 目标推导,二者不能混为一谈。
|
|
assert.equal(context.channel, 'dev');
|
|
assert.equal(
|
|
resolveReleasePartition(context.channel, context.target),
|
|
'dev-mac',
|
|
);
|
|
assert.ok(context.bundleRoot.includes(universalTarget));
|
|
withSignedArtifact('陶泥儿.app.tar.gz', (artifact) => {
|
|
const manifest = createUpdateManifest(artifact, {
|
|
...context,
|
|
downloadArtifact: createDmgFixture(
|
|
path.dirname(artifact),
|
|
universalTarget,
|
|
),
|
|
});
|
|
assert.deepEqual(Object.keys(manifest.platforms), [
|
|
'darwin-aarch64',
|
|
'darwin-x86_64',
|
|
]);
|
|
assert.deepEqual(
|
|
manifest.platforms['darwin-aarch64'],
|
|
manifest.platforms['darwin-x86_64'],
|
|
);
|
|
assert.match(manifest.platforms['darwin-aarch64'].url, /\/dev-mac\//);
|
|
// 两个平台键共用同一个 universal 首装包,不能要求出两份架构 DMG。
|
|
assert.deepEqual(
|
|
manifest.downloads['darwin-aarch64'].url,
|
|
manifest.downloads['darwin-x86_64'].url,
|
|
);
|
|
assert.match(manifest.downloads['darwin-aarch64'].url, /_universal\.dmg$/u);
|
|
});
|
|
});
|
|
|
|
test('Windows remains the default and explicit Windows overrides macOS environment', () => {
|
|
const files = ['/tmp/mac.app.tar.gz', '/tmp/windows.exe', '/tmp/mac.dmg'];
|
|
for (const context of [
|
|
resolveReleaseContext([], {}),
|
|
resolveReleaseContext(['--target', windowsTarget], {
|
|
AGC_BUILD_TARGET: 'aarch64-apple-darwin',
|
|
}),
|
|
]) {
|
|
assert.equal(context.channel, 'dev');
|
|
assert.equal(
|
|
selectReleaseArtifact(files, context.target),
|
|
'/tmp/windows.exe',
|
|
);
|
|
runTauriBuild(
|
|
['--target', windowsTarget, '--config', 'user-config.json'],
|
|
context,
|
|
{
|
|
stageRuntime: () => {},
|
|
spawn: (_binary, command) => {
|
|
assert.ok(
|
|
command.includes(
|
|
'--features=cocos-editor-execute,unity-editor-execute,godot-editor-execute',
|
|
),
|
|
);
|
|
assert.ok(command.includes('user-config.json'));
|
|
const configIndex = command.lastIndexOf('--config');
|
|
const config = JSON.parse(
|
|
readFileSync(command[configIndex + 1], 'utf8'),
|
|
);
|
|
assert.match(
|
|
config.plugins.updater.endpoints[0],
|
|
/\/dev-win\/latest\.json$/,
|
|
);
|
|
return { status: 0 };
|
|
},
|
|
},
|
|
);
|
|
}
|
|
});
|
|
|
|
test('no-bundle smoke skips version writes and manifest generation', async () => {
|
|
const steps = [];
|
|
await withEnv({ AGC_UPDATE_CHANNEL: undefined }, () =>
|
|
buildRelease(['--no-bundle', '--target=aarch64-apple-darwin'], {
|
|
prepareVersion: () => {
|
|
steps.push('version');
|
|
},
|
|
build: (_args, context) => {
|
|
steps.push(context.channel);
|
|
},
|
|
generateManifest: () => {
|
|
steps.push('manifest');
|
|
},
|
|
}),
|
|
);
|
|
assert.deepEqual(steps, ['dev']);
|
|
});
|
|
|
|
test('Windows 打包在 Tauri 构建前预置 NSIS 工具链', async () => {
|
|
const events = [];
|
|
await buildRelease(['--target', windowsTarget], {
|
|
prepareVersion: () => {
|
|
events.push('version');
|
|
},
|
|
prepareToolset: (context, options) => {
|
|
events.push(`toolset:${context.target}:${options.bundling}`);
|
|
},
|
|
build: () => {
|
|
events.push('build');
|
|
},
|
|
generateManifest: () => {
|
|
events.push('manifest');
|
|
},
|
|
});
|
|
assert.deepEqual(events, [
|
|
'version',
|
|
`toolset:${windowsTarget}:true`,
|
|
'build',
|
|
'manifest',
|
|
]);
|
|
});
|
|
|
|
test('NSIS 工具链预置失败即失败关闭,不进入 Tauri 构建', async () => {
|
|
const events = [];
|
|
await assert.rejects(
|
|
buildRelease(['--target', windowsTarget], {
|
|
prepareVersion: () => {
|
|
events.push('version');
|
|
},
|
|
prepareToolset: () => {
|
|
throw new Error('NSIS 工具链预置失败:下载 nsis-3.11.zip 失败');
|
|
},
|
|
build: () => {
|
|
events.push('build');
|
|
},
|
|
generateManifest: () => {
|
|
events.push('manifest');
|
|
},
|
|
}),
|
|
/NSIS 工具链预置失败/u,
|
|
);
|
|
assert.deepEqual(events, ['version']);
|
|
});
|
|
|
|
test('--no-bundle 不预置 NSIS 工具链', async () => {
|
|
const steps = [];
|
|
await buildRelease(['--no-bundle', '--target', windowsTarget], {
|
|
prepareVersion: () => {
|
|
steps.push('version');
|
|
},
|
|
prepareToolset: () => {
|
|
steps.push('toolset');
|
|
},
|
|
build: () => {
|
|
steps.push('build');
|
|
},
|
|
generateManifest: () => {
|
|
steps.push('manifest');
|
|
},
|
|
});
|
|
assert.deepEqual(steps, ['build']);
|
|
});
|
|
|
|
test('release stages Node before Tauri and injects its resource mapping only for bundles', () => {
|
|
const context = resolveReleaseContext(['--target', windowsTarget]);
|
|
const events = [];
|
|
runTauriBuild(['--target', windowsTarget], context, {
|
|
stageRuntime(target) {
|
|
assert.equal(target, windowsTarget);
|
|
events.push('stage');
|
|
},
|
|
spawn(_binary, args) {
|
|
events.push('build');
|
|
const config = JSON.parse(
|
|
readFileSync(args[args.lastIndexOf('--config') + 1], 'utf8'),
|
|
);
|
|
assert.deepEqual(config.bundle.resources, {
|
|
'resources/node-runtime': 'game-runtime/node',
|
|
});
|
|
return { status: 0 };
|
|
},
|
|
});
|
|
assert.deepEqual(events, ['stage', 'build']);
|
|
runTauriBuild(['--no-bundle', '--target', windowsTarget], context, {
|
|
stageRuntime() {
|
|
assert.fail('no-bundle must not stage resources');
|
|
},
|
|
spawn(_binary, args) {
|
|
const config = JSON.parse(
|
|
readFileSync(args[args.lastIndexOf('--config') + 1], 'utf8'),
|
|
);
|
|
assert.equal(config.bundle, undefined);
|
|
return { status: 0 };
|
|
},
|
|
});
|
|
assert.throws(
|
|
() =>
|
|
runTauriBuild(['--target', windowsTarget], context, {
|
|
stageRuntime() {
|
|
throw new Error('missing runtime');
|
|
},
|
|
spawn() {
|
|
assert.fail('invalid runtime must prevent build');
|
|
},
|
|
}),
|
|
/missing runtime/,
|
|
);
|
|
});
|
|
|
|
test('channel manifest carries version, platform keys and signature', () => {
|
|
withSignedArtifact('陶泥儿_0.1.48_x64-setup.exe', (artifact) => {
|
|
withEnv({ AGC_UPDATE_RELEASE_NOTES: '修复与改进' }, () => {
|
|
const manifest = createUpdateManifest(artifact, {
|
|
channel: 'dev',
|
|
target: windowsTarget,
|
|
publishedAt: '2026-09-17T00:00:00.000Z',
|
|
});
|
|
assert.match(manifest.version, /^\d+\.\d+\.\d+$/u);
|
|
assert.equal(manifest.notes, '修复与改进');
|
|
assert.equal(manifest.pub_date, '2026-09-17T00:00:00.000Z');
|
|
assert.deepEqual(Object.keys(manifest.platforms), ['windows-x86_64']);
|
|
assert.deepEqual(manifest.downloads, {
|
|
'windows-x86_64': { url: manifest.platforms['windows-x86_64'].url },
|
|
});
|
|
assert.equal(
|
|
manifest.platforms['windows-x86_64'].signature,
|
|
'signature-content',
|
|
);
|
|
assert.match(
|
|
manifest.platforms['windows-x86_64'].url,
|
|
new RegExp(`/agc/dev-win/${manifest.version}/`, 'u'),
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
test('missing signature fails the channel manifest closed', () => {
|
|
const directory = mkdtempSync(path.join(os.tmpdir(), 'agc-release-test-'));
|
|
try {
|
|
const artifact = path.join(directory, '陶泥儿_0.1.48_x64-setup.exe');
|
|
writeFileSync(artifact, 'installation package');
|
|
assert.throws(
|
|
() =>
|
|
createUpdateManifest(artifact, {
|
|
channel: 'dev',
|
|
target: windowsTarget,
|
|
}),
|
|
/缺少更新包签名/u,
|
|
);
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('legacy manifest keeps the sha256 contract of published clients', () => {
|
|
withSignedArtifact('陶泥儿_0.1.48_x64-setup.exe', (artifact) => {
|
|
const legacy = createLegacyUpdateManifest(artifact, {
|
|
channel: 'dev',
|
|
});
|
|
assert.match(legacy.version, /^\d+\.\d+\.\d+$/u);
|
|
assert.equal(legacy.sha256.length, 64);
|
|
assert.equal(legacy.size, 'installation package'.length);
|
|
assert.match(legacy.downloadUrl, /\/agc\/dev-win\/[\d.]+\//u);
|
|
});
|
|
});
|
|
|
|
test('next release version follows the higher local or channel version', () => {
|
|
assert.equal(compareVersions('0.1.15', '0.1.12'), 1);
|
|
assert.equal(nextPatchVersion('0.1.12', '0.1.15'), '0.1.16');
|
|
assert.equal(nextPatchVersion('0.1.18', '0.1.15'), '0.1.19');
|
|
assert.equal(nextPatchVersion('0.1.12', null), '0.1.13');
|
|
});
|
|
|
|
for (const channel of ['release', 'beta-2']) {
|
|
for (const target of [windowsTarget, 'aarch64-apple-darwin']) {
|
|
test(`${channel} ${target} freezes its endpoint, version source and published objects`, async () => {
|
|
const root = mkdtempSync(path.join(os.tmpdir(), 'agc-channel-manifest-'));
|
|
try {
|
|
const windows = target === windowsTarget;
|
|
const partition = `${channel}-${windows ? 'win' : 'mac'}`;
|
|
const artifact = path.join(
|
|
root,
|
|
windows ? '陶泥儿_x64-setup.exe' : '陶泥儿.app.tar.gz',
|
|
);
|
|
writeFileSync(artifact, 'updater package');
|
|
writeFileSync(`${artifact}.sig`, 'updater signature');
|
|
if (!windows) createDmgFixture(root, target);
|
|
const context = {
|
|
...resolveReleaseContext([`--target=${target}`], {
|
|
AGC_UPDATE_CHANNEL: channel,
|
|
}),
|
|
bundleRoot: root,
|
|
};
|
|
const requests = [];
|
|
const result = await withStubbedFetch(
|
|
(url) => {
|
|
requests.push(url);
|
|
assert.ok(url.endsWith(`/agc/${partition}/latest.json`));
|
|
return jsonResponse({
|
|
version: '2.3.4',
|
|
commit: 'abcdef1234567890',
|
|
});
|
|
},
|
|
async () => {
|
|
assert.equal(
|
|
await resolveRemoteHighWaterVersion(
|
|
context.channel,
|
|
context.target,
|
|
),
|
|
'2.3.4',
|
|
);
|
|
runTauriBuild([`--target=${target}`], context, {
|
|
stageRuntime: () => {},
|
|
spawn: (_binary, command) => {
|
|
const config = JSON.parse(
|
|
readFileSync(
|
|
command[command.lastIndexOf('--config') + 1],
|
|
'utf8',
|
|
),
|
|
);
|
|
assert.ok(
|
|
config.plugins.updater.endpoints[0].endsWith(
|
|
`/agc/${partition}/latest.json`,
|
|
),
|
|
);
|
|
return { status: 0 };
|
|
},
|
|
});
|
|
return generateUpdateManifest(context);
|
|
},
|
|
);
|
|
assert.equal(result.channel, channel);
|
|
assert.equal(result.target, target);
|
|
assert.equal(result.manifest.version, packageVersion);
|
|
assert.equal(result.legacyManifestPath, null);
|
|
assert.equal(result.legacyManifest, null);
|
|
assert.equal(requests.length, 2);
|
|
for (const entry of [
|
|
...Object.values(result.manifest.platforms),
|
|
...Object.values(result.manifest.downloads),
|
|
]) {
|
|
assert.ok(entry.url.includes(`/agc/${partition}/${packageVersion}/`));
|
|
}
|
|
assert.throws(
|
|
() => createLegacyUpdateManifest(artifact, { channel, target }),
|
|
/只属于 dev 渠道/u,
|
|
);
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
test('version high water keeps the legacy pointer during the migration window', async () => {
|
|
await withStubbedFetch(
|
|
(url) =>
|
|
url.endsWith('/agc/dev-win/latest.json')
|
|
? jsonResponse({}, 404)
|
|
: jsonResponse({ version: '0.1.57' }),
|
|
async () => {
|
|
assert.equal(
|
|
await resolveRemoteHighWaterVersion('dev', windowsTarget),
|
|
'0.1.57',
|
|
);
|
|
// 旧指针 0.1.57 已是高水位,下一次发布必须是 0.1.58,不能退回渠道本地版本。
|
|
assert.equal(nextPatchVersion('0.1.47', '0.1.57'), '0.1.58');
|
|
},
|
|
);
|
|
});
|
|
|
|
test('version high water takes the higher of channel and legacy pointer', async () => {
|
|
await withStubbedFetch(
|
|
(url) =>
|
|
url.endsWith('/agc/dev-win/latest.json')
|
|
? jsonResponse({ version: '0.1.60' })
|
|
: jsonResponse({ version: '0.1.57' }),
|
|
async () => {
|
|
assert.equal(
|
|
await resolveRemoteHighWaterVersion('dev', windowsTarget),
|
|
'0.1.60',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
test('version high water ignores the windows migration pointer for other channels', async () => {
|
|
await withStubbedFetch(
|
|
(url) => {
|
|
assert.ok(
|
|
!url.endsWith('/agc/latest.json'),
|
|
'non-windows channel must not read the windows migration pointer',
|
|
);
|
|
return jsonResponse({ version: '0.1.12' });
|
|
},
|
|
async () => {
|
|
for (const [channel, target] of [
|
|
['dev', 'aarch64-apple-darwin'],
|
|
['release', windowsTarget],
|
|
['beta-2', windowsTarget],
|
|
]) {
|
|
assert.equal(
|
|
await resolveRemoteHighWaterVersion(channel, target),
|
|
'0.1.12',
|
|
);
|
|
}
|
|
},
|
|
);
|
|
});
|
|
|
|
test('release notes anchor prefers the explicit commit and falls back to the manifest', async () => {
|
|
await withStubbedFetch(
|
|
() => jsonResponse({ version: '0.1.61', commit: 'abcdef1234567890' }),
|
|
async () => {
|
|
assert.equal(
|
|
await resolvePreviousReleaseCommit('dev', {
|
|
override: '6017d46088c04199e99cf89f347b12d67591475e',
|
|
}),
|
|
'6017d46088c04199e99cf89f347b12d67591475e',
|
|
);
|
|
// 覆盖值非法时忽略,继续用清单里的 commit。
|
|
assert.equal(
|
|
await resolvePreviousReleaseCommit('dev', {
|
|
override: 'not-a-sha',
|
|
}),
|
|
'abcdef1234567890',
|
|
);
|
|
assert.equal(
|
|
await resolvePreviousReleaseCommit('dev', { override: ' ' }),
|
|
'abcdef1234567890',
|
|
);
|
|
},
|
|
);
|
|
|
|
await withStubbedFetch(
|
|
() => jsonResponse({ version: '0.1.61' }),
|
|
async () => {
|
|
assert.equal(
|
|
await resolvePreviousReleaseCommit('dev', { override: undefined }),
|
|
null,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
test('release notes anchor degrades to null when the manifest cannot be read', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async () => {
|
|
throw new Error('fetch failed');
|
|
};
|
|
try {
|
|
assert.equal(
|
|
await resolvePreviousReleaseCommit('dev', { override: undefined }),
|
|
null,
|
|
);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test('recent commit fallback marks that entries may repeat the previous release', () => {
|
|
const directory = mkdtempSync(path.join(os.tmpdir(), 'agc-recent-git-'));
|
|
const git = (...args) =>
|
|
execFileSync('git', args, { cwd: directory, encoding: 'utf8' });
|
|
try {
|
|
git('init', '--quiet');
|
|
git('config', 'user.email', 'release@example.test');
|
|
git('config', 'user.name', 'release test');
|
|
mkdirSync(path.join(directory, 'apps/ai-game-creator-shell'), {
|
|
recursive: true,
|
|
});
|
|
for (const name of ['one', 'two']) {
|
|
writeFileSync(
|
|
path.join(directory, `apps/ai-game-creator-shell/${name}.rs`),
|
|
`fn ${name}() {}\n`,
|
|
);
|
|
git('add', '.');
|
|
git('commit', '--quiet', '-m', `客户端:${name}`);
|
|
}
|
|
writeFileSync(path.join(directory, 'README.md'), '# 文档\n');
|
|
git('add', '.');
|
|
git('commit', '--quiet', '-m', '文档:说明');
|
|
|
|
const recent = collectRecentReleaseCommits({ cwd: directory, limit: 5 });
|
|
assert.deepEqual(
|
|
recent.map((entry) => entry.subject),
|
|
['客户端:two', '客户端:one'],
|
|
);
|
|
const notes = formatRecentReleaseNotes(recent);
|
|
assert.match(
|
|
notes,
|
|
/^最近客户端改动(未定位到上一次发布提交,可能与上一版重复):\n- 客户端:two/u,
|
|
);
|
|
assert.equal(formatRecentReleaseNotes([]), '');
|
|
assert.equal(formatRecentReleaseNotes(null), '');
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('release entry forwards the built artifacts and dry-run mode to the uploader', () => {
|
|
const source = readFileSync(
|
|
new URL('./release-upload.mjs', import.meta.url),
|
|
'utf8',
|
|
);
|
|
assert.match(
|
|
source,
|
|
/const release = await buildRelease\(process\.argv\.slice\(2\)\)/u,
|
|
);
|
|
assert.match(source, /uploadReleaseArtifacts\(release, \{/u);
|
|
assert.match(source, /const dryRun = readReleaseDryRun\(\);/u);
|
|
assert.ok(source.includes('\n dryRun,\n'));
|
|
});
|
|
|
|
test('release notes list client commit subjects only and bound their size', () => {
|
|
const notes = formatReleaseNotes([
|
|
{ sha: 'a5fd25f1', subject: '客户端更新切换到官方更新插件' },
|
|
{ sha: '55af6014', subject: '修'.repeat(120) },
|
|
]);
|
|
const lines = notes.split('\n');
|
|
assert.equal(lines.length, 2);
|
|
assert.equal(lines[0], '- 客户端更新切换到官方更新插件');
|
|
const truncatedSubject = lines[1].replace(/^- /u, '');
|
|
assert.equal(truncatedSubject.length, 80, `主题应截断到 80 字:${lines[1]}`);
|
|
assert.match(truncatedSubject, /…$/u);
|
|
|
|
const many = formatReleaseNotes(
|
|
Array.from({ length: 20 }, (_, index) => ({
|
|
sha: `sha${index}`,
|
|
subject: `改动 ${index}`,
|
|
})),
|
|
);
|
|
assert.match(many, /- 其余 8 项客户端改动省略$/u);
|
|
assert.equal(
|
|
formatReleaseNotes([
|
|
{
|
|
sha: 'ignored',
|
|
subject: '提交标题\n不应展示的说明一\n不应展示的说明二',
|
|
},
|
|
]),
|
|
'- 提交标题',
|
|
);
|
|
assert.equal(formatReleaseNotes([]), '');
|
|
assert.equal(formatReleaseNotes(null), '');
|
|
});
|
|
|
|
test('release commits cover only client paths and skip merge commits', () => {
|
|
const directory = mkdtempSync(path.join(os.tmpdir(), 'agc-changelog-git-'));
|
|
const git = (...args) =>
|
|
execFileSync('git', args, { cwd: directory, encoding: 'utf8' });
|
|
try {
|
|
git('init', '--quiet');
|
|
git('config', 'user.email', 'release@example.test');
|
|
git('config', 'user.name', 'release test');
|
|
git('commit', '--allow-empty', '--quiet', '-m', '基点');
|
|
const base = git('rev-parse', 'HEAD').trim();
|
|
|
|
mkdirSync(path.join(directory, 'apps/ai-game-creator-shell'), {
|
|
recursive: true,
|
|
});
|
|
mkdirSync(path.join(directory, 'docs'), { recursive: true });
|
|
writeFileSync(
|
|
path.join(directory, 'apps/ai-game-creator-shell/main.rs'),
|
|
'fn main() {}\n',
|
|
);
|
|
const commitMessagePath = path.join(
|
|
directory,
|
|
'.git',
|
|
'commit-message.txt',
|
|
);
|
|
writeFileSync(
|
|
commitMessagePath,
|
|
'客户端:新增更新插件接入\n补充更新插件接入的详细说明\n',
|
|
);
|
|
git('add', '.');
|
|
git('commit', '--quiet', '-F', commitMessagePath);
|
|
|
|
writeFileSync(path.join(directory, 'docs/readme.md'), '# 文档\n');
|
|
git('add', '.');
|
|
git('commit', '--quiet', '-m', '文档:补充说明');
|
|
|
|
writeFileSync(
|
|
path.join(directory, 'apps/ai-game-creator-shell/other.rs'),
|
|
'fn other() {}\n',
|
|
);
|
|
git('add', '.');
|
|
git('commit', '--quiet', '-m', '客户端:修复版本回退');
|
|
|
|
git('checkout', '--quiet', '-b', 'side');
|
|
writeFileSync(
|
|
path.join(directory, 'apps/ai-game-creator-shell/side.rs'),
|
|
'fn side() {}\n',
|
|
);
|
|
git('add', '.');
|
|
git('commit', '--quiet', '-m', '客户端:侧分支改动');
|
|
git('checkout', '--quiet', 'master');
|
|
git('merge', '--quiet', '--no-ff', '--no-edit', 'side');
|
|
|
|
const commits = collectReleaseCommits(base, 'HEAD', { cwd: directory });
|
|
assert.ok(commits, '应能在临时仓库里收集提交');
|
|
const subjects = commits.map((entry) => entry.subject);
|
|
// 标题后没有空行时,git %s 会把说明行拼进标题;摘要必须取原始首行。
|
|
// 合并提交本身被 --no-merges 排除,但它带入的客户端改动仍然计入。
|
|
assert.deepEqual(subjects, [
|
|
'客户端:侧分支改动',
|
|
'客户端:修复版本回退',
|
|
'客户端:新增更新插件接入',
|
|
]);
|
|
assert.ok(commits.every((entry) => /^[0-9a-f]{7,}$/u.test(entry.sha)));
|
|
|
|
assert.equal(
|
|
collectReleaseCommits('1234567890abcdef', 'HEAD', { cwd: directory }),
|
|
null,
|
|
);
|
|
assert.equal(collectReleaseCommits(null, 'HEAD', { cwd: directory }), null);
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('scheduler path filter stays in sync with client release paths', () => {
|
|
const jenkinsfile = readFileSync(
|
|
new URL(
|
|
'../../../jenkins/Jenkinsfile.scheduled-revision-trigger',
|
|
import.meta.url,
|
|
),
|
|
'utf8',
|
|
);
|
|
const filterLine = jenkinsfile
|
|
.split('\n')
|
|
.find((line) => line.includes('apps/ai-game-creator-shell/*|'));
|
|
assert.ok(filterLine, '调度管线里应存在发布范围过滤模式');
|
|
for (const pattern of agcReleasePathPatterns) {
|
|
const bashPattern = pattern.includes('/') ? `${pattern}*` : pattern;
|
|
assert.ok(
|
|
filterLine.includes(bashPattern),
|
|
`调度管线过滤缺少 ${bashPattern}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('scheduler skips the full build only for non-deploy paths', () => {
|
|
const jenkinsfile = readFileSync(
|
|
new URL(
|
|
'../../../jenkins/Jenkinsfile.scheduled-revision-trigger',
|
|
import.meta.url,
|
|
),
|
|
'utf8',
|
|
);
|
|
const skipLine = jenkinsfile
|
|
.split('\n')
|
|
.find((line) => line.includes('docs/*|.codex/*|jenkins/*'));
|
|
assert.ok(skipLine, '调度管线里应存在 Full Build 跳过模式');
|
|
for (const pattern of [
|
|
'docs/*',
|
|
'.codex/*',
|
|
'jenkins/*',
|
|
'apps/ai-game-creator-shell/*',
|
|
'apps/mobile-shell/*',
|
|
'apps/desktop-shell/*',
|
|
'apps/preview-deployer-web/*',
|
|
'tools/*',
|
|
'*.md',
|
|
]) {
|
|
assert.ok(skipLine.includes(pattern), `Full Build 跳过模式缺少 ${pattern}`);
|
|
}
|
|
});
|