93af29314c
Project CI / AI game creator shell Rust smoke (push) Successful in 1m21s
Project CI / AI game creator shell Rust crates (push) Successful in 1m37s
Project CI / Backend tests (push) Successful in 3m49s
Project CI / Frontend tests (push) Successful in 1m37s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m20s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m39s
Project CI / Native shell tests (push) Successful in 5m50s
Project CI / AI game creator shell web tests (push) Successful in 2m18s
Project CI / Repository checks (push) Successful in 2m49s
- check:agc-update-channel-manifests:非 dev 的 Windows 渠道核对时断言 agc/latest.json 仍指向 dev-win 分区(全局单对象只属于 dev) - 渠道隔离里程碑与 decision-log 记录线上证据:release-win 核对通过,指针指向 dev-win 0.1.155
527 lines
19 KiB
JavaScript
527 lines
19 KiB
JavaScript
// 只读核对 OSS 上**已发布**的 AGC 更新渠道清单(不写任何远端对象)。
|
||
//
|
||
// 对应里程碑「AGC 更新发布管线渠道化」第 3 条与「AGC macOS 渠道更新落地」第 1 条:
|
||
// 清单内地址指向已存在的对象、签名对象与清单一致;macOS 渠道按 2026-09-21 决策只登记
|
||
// `darwin-aarch64`(不再登记 `darwin-x86_64`);允许下载时用产物里烘焙的 updater 公钥
|
||
// 验证「签名 ↔ 安装包」,并解出 mac 包内 `Info.plist` 核对版本与渠道身份。
|
||
//
|
||
// 用法:
|
||
// npm run check:agc-update-channel-manifests
|
||
// AGC_UPDATE_CHANNELS=dev-win,dev-mac # 要核对的渠道分区,默认两个 dev 分区
|
||
// AGC_UPDATE_OSS_BASE_URL=https://.../agc # 覆盖 OSS 基址
|
||
// AGC_UPDATE_VERIFY_DOWNLOAD=1 # 额外下载产物验签(默认只 HEAD 与读 .sig)
|
||
// 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';
|
||
import os from 'node:os';
|
||
import path from 'node:path';
|
||
import { Readable } from 'node:stream';
|
||
import { pipeline } from 'node:stream/promises';
|
||
|
||
import * as tar from 'tar';
|
||
|
||
import { compareVersions } from '../apps/ai-game-creator-shell/scripts/agc-global-version.mjs';
|
||
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,
|
||
} from '../apps/ai-game-creator-shell/scripts/verify-updater-signature.mjs';
|
||
import { readPortableExecutableVersionInfo } from './pe-version-info.mjs';
|
||
|
||
const OSS_BASE_URL = (
|
||
process.env.AGC_UPDATE_OSS_BASE_URL?.trim() ||
|
||
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc'
|
||
).replace(/\/+$/u, '');
|
||
const CHANNELS = (process.env.AGC_UPDATE_CHANNELS?.trim() || 'dev-win,dev-mac')
|
||
.split(',')
|
||
.map((value) => value.trim())
|
||
.filter(Boolean);
|
||
const VERIFY_DOWNLOAD = /^(1|true)$/iu.test(
|
||
process.env.AGC_UPDATE_VERIFY_DOWNLOAD?.trim() ?? '',
|
||
);
|
||
const DOWNLOAD_LIMIT_BYTES =
|
||
Number(process.env.AGC_UPDATE_DOWNLOAD_LIMIT_MB?.trim() || '600') *
|
||
1024 *
|
||
1024;
|
||
|
||
// 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;
|
||
let skipped = 0;
|
||
/** 下载过的更新包摘要:用于「不同渠道不能共用同一份产物字节」这条隔离断言。 */
|
||
const downloadedArtifacts = [];
|
||
/** 各渠道清单版本:用于和统一总号(`agc/global-version.json`)比对。 */
|
||
const channelVersions = new Map();
|
||
function check(name, ok, detail = '') {
|
||
if (!ok) failures += 1;
|
||
console.log(
|
||
`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` :: ${detail}` : ''}`,
|
||
);
|
||
}
|
||
function skip(name, detail) {
|
||
skipped += 1;
|
||
console.log(`SKIP ${name}${detail ? ` :: ${detail}` : ''}`);
|
||
}
|
||
|
||
function decodeMinisignText(value, label) {
|
||
const trimmed = String(value ?? '').trim();
|
||
if (trimmed.length === 0) throw new Error(`${label} 为空`);
|
||
if (trimmed.startsWith('untrusted comment:')) return trimmed;
|
||
const decoded = Buffer.from(trimmed, 'base64').toString('utf8').trim();
|
||
if (!decoded.startsWith('untrusted comment:')) {
|
||
throw new Error(`${label} 不是 minisign 内容`);
|
||
}
|
||
return decoded;
|
||
}
|
||
|
||
async function head(url) {
|
||
const response = await fetch(url, { method: 'HEAD' });
|
||
const length = Number(response.headers.get('content-length') ?? '0');
|
||
return { status: response.status, length };
|
||
}
|
||
|
||
async function download(url, destination) {
|
||
const response = await fetch(url);
|
||
if (!response.ok || !response.body) {
|
||
throw new Error(`下载失败 ${response.status}:${url}`);
|
||
}
|
||
await pipeline(
|
||
Readable.fromWeb(response.body),
|
||
createWriteStream(destination),
|
||
);
|
||
}
|
||
|
||
async function sha256Of(filePath) {
|
||
const hash = createHash('sha256');
|
||
hash.update(await readFile(filePath));
|
||
return hash.digest('hex');
|
||
}
|
||
|
||
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}`,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Windows 安装包同理:PE 版本资源里的产品名与版本必须对得上清单。
|
||
* 只核对文件名等于相信文件没被换过——文件名可以改,PE 资源是产物自己写的。
|
||
*/
|
||
async function verifyWindowsBundleIdentity(
|
||
channel,
|
||
key,
|
||
manifest,
|
||
artifactPath,
|
||
) {
|
||
const channelName = channel.replace(/-win$/u, '');
|
||
const expected = resolveChannelInstallIdentity(channelName);
|
||
let info;
|
||
try {
|
||
info = readPortableExecutableVersionInfo(await readFile(artifactPath));
|
||
} catch (error) {
|
||
check(`${channel}/${key} 安装包可读出 PE 版本资源`, false, String(error));
|
||
return;
|
||
}
|
||
const manifestVersion = String(manifest.version);
|
||
check(
|
||
`${channel}/${key} 安装包版本与清单一致`,
|
||
info.fileVersion === `${manifestVersion}.0` ||
|
||
info.strings.FileVersion === manifestVersion,
|
||
`pe=${info.fileVersion}/${info.strings.FileVersion ?? ''} manifest=${manifestVersion}`,
|
||
);
|
||
check(
|
||
`${channel}/${key} 安装包身份属于本渠道`,
|
||
info.strings.ProductName === expected.productName,
|
||
`pe=${info.strings.ProductName ?? ''} expected=${expected.productName}`,
|
||
);
|
||
}
|
||
|
||
async function verifyChannel(channel, tempDir) {
|
||
console.log(`\n--- 渠道 ${channel} ---`);
|
||
const manifestUrl = `${OSS_BASE_URL}/${channel}/latest.json`;
|
||
const response = await fetch(manifestUrl);
|
||
check(
|
||
`${channel} 渠道清单可读`,
|
||
response.status === 200,
|
||
`status=${response.status} ${manifestUrl}`,
|
||
);
|
||
if (response.status !== 200) return;
|
||
const raw = await response.text();
|
||
let manifest;
|
||
try {
|
||
manifest = JSON.parse(raw);
|
||
} catch (error) {
|
||
check(`${channel} 渠道清单是合法 JSON`, false, String(error));
|
||
return;
|
||
}
|
||
const version = String(manifest.version ?? '');
|
||
channelVersions.set(channel, version);
|
||
check(
|
||
`${channel} 清单版本与发布元数据齐备`,
|
||
/^\d+\.\d+\.\d+$/u.test(version) &&
|
||
!Number.isNaN(Date.parse(String(manifest.pub_date ?? ''))) &&
|
||
typeof manifest.commit === 'string' &&
|
||
manifest.commit.trim().length > 0,
|
||
`version=${version} pub_date=${manifest.pub_date ?? ''} commit=${String(manifest.commit ?? '').slice(0, 10)}`,
|
||
);
|
||
|
||
const platforms = manifest.platforms ?? {};
|
||
const platformKeys = Object.keys(platforms);
|
||
check(
|
||
`${channel} 清单至少有一个平台条目`,
|
||
platformKeys.length > 0,
|
||
`platforms=${platformKeys.join(',')}`,
|
||
);
|
||
|
||
const expectedPrefix = `${OSS_BASE_URL}/${channel}/${version}/`;
|
||
for (const key of platformKeys) {
|
||
const entry = platforms[key] ?? {};
|
||
const url = String(entry.url ?? '');
|
||
check(
|
||
`${channel}/${key} 地址指向本渠道版本目录`,
|
||
url.startsWith(expectedPrefix),
|
||
`url=${url}`,
|
||
);
|
||
const artifactHead = await head(url);
|
||
const sizeOk = artifactHead.status === 200 && artifactHead.length > 0;
|
||
check(
|
||
`${channel}/${key} 安装包对象存在`,
|
||
sizeOk,
|
||
`status=${artifactHead.status} bytes=${artifactHead.length}`,
|
||
);
|
||
const signatureHead = await head(`${url}.sig`);
|
||
check(
|
||
`${channel}/${key} 签名对象存在`,
|
||
signatureHead.status === 200 && signatureHead.length > 0,
|
||
`status=${signatureHead.status} bytes=${signatureHead.length}`,
|
||
);
|
||
if (signatureHead.status === 200) {
|
||
const signatureText = await (await fetch(`${url}.sig`)).text();
|
||
const manifestSignature = decodeMinisignText(
|
||
entry.signature,
|
||
`${channel}/${key} 清单签名`,
|
||
);
|
||
const objectSignature = decodeMinisignText(
|
||
signatureText,
|
||
`${channel}/${key} 签名对象`,
|
||
);
|
||
check(
|
||
`${channel}/${key} 清单签名与签名对象一致`,
|
||
manifestSignature === objectSignature,
|
||
);
|
||
}
|
||
const downloadUrl = String(manifest.downloads?.[key]?.url ?? '');
|
||
check(
|
||
`${channel}/${key} 首装下载地址指向已存在对象`,
|
||
downloadUrl.startsWith(expectedPrefix) &&
|
||
(await head(downloadUrl)).status === 200,
|
||
`url=${downloadUrl}`,
|
||
);
|
||
}
|
||
|
||
if (channel.endsWith('-mac')) {
|
||
check(
|
||
`${channel} 只登记 darwin-aarch64(2026-09-21 单架构决策)`,
|
||
Boolean(platforms[MACOS_ARM64_PLATFORM_KEY]) &&
|
||
!platforms[MACOS_INTEL_PLATFORM_KEY],
|
||
`platforms=${platformKeys.join(',')}`,
|
||
);
|
||
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} 平台键`,
|
||
Boolean(platforms[WINDOWS_PLATFORM_KEY]),
|
||
`platforms=${platformKeys.join(',')}`,
|
||
);
|
||
// 渠道身份会写进产物文件名的产品名,这里先用清单里的地址核对(下载后再验签与摘要)。
|
||
const expectedIdentity = resolveChannelInstallIdentity(
|
||
channel.replace(/-win$/u, ''),
|
||
);
|
||
const artifactNames = platformKeys.map((key) =>
|
||
decodeURIComponent(new URL(String(platforms[key].url ?? '')).pathname),
|
||
);
|
||
check(
|
||
`${channel} 安装包文件名带本渠道产品名`,
|
||
artifactNames.every((name) =>
|
||
name.includes(expectedIdentity.productName),
|
||
),
|
||
`names=${artifactNames.map((name) => path.basename(name)).join(',')} expected=${expectedIdentity.productName}`,
|
||
);
|
||
}
|
||
|
||
if (channel === 'dev-win') {
|
||
const bridge = await fetch(`${OSS_BASE_URL}/latest.json`);
|
||
const bridgeOk = bridge.status === 200;
|
||
check(`${channel} 旧协议迁移指针可读`, bridgeOk);
|
||
if (bridgeOk) {
|
||
const bridgeJson = await bridge.json();
|
||
const windowsEntry = platforms[WINDOWS_PLATFORM_KEY] ?? {};
|
||
check(
|
||
`${channel} 旧协议指针指向同一批已发布对象`,
|
||
String(bridgeJson.downloadUrl ?? '') ===
|
||
String(windowsEntry.url ?? '') &&
|
||
(await head(String(bridgeJson.downloadUrl ?? ''))).status === 200,
|
||
`sha256=${String(bridgeJson.sha256 ?? '').slice(0, 12)} size=${bridgeJson.size ?? ''}`,
|
||
);
|
||
}
|
||
} else if (channel.endsWith('-win')) {
|
||
// 旧协议迁移指针是**全局单对象**,只属于 dev 渠道的 Windows 系统;其它渠道发布
|
||
// 不得改写它,否则 dev 用户会被推去装别的渠道的包。
|
||
const bridge = await fetch(`${OSS_BASE_URL}/latest.json`);
|
||
if (bridge.status === 200) {
|
||
const bridgeJson = await bridge.json();
|
||
const bridgeUrl = String(bridgeJson.downloadUrl ?? '');
|
||
check(
|
||
`${channel} 没有改写 dev 的旧协议迁移指针`,
|
||
bridgeUrl.includes('/agc/dev-win/'),
|
||
`bridge=${bridgeUrl}`,
|
||
);
|
||
} else {
|
||
skip(
|
||
`${channel} 没有改写 dev 的旧协议迁移指针`,
|
||
`旧协议指针不可读:status=${bridge.status}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
// 可选:下载产物,核对 sha256/size 并用产物内公钥验签。
|
||
for (const key of platformKeys) {
|
||
const entry = platforms[key] ?? {};
|
||
const url = String(entry.url ?? '');
|
||
const artifactHead = await head(url);
|
||
if (!VERIFY_DOWNLOAD) {
|
||
skip(
|
||
`${channel}/${key} 签名 ↔ 安装包匹配`,
|
||
'未设置 AGC_UPDATE_VERIFY_DOWNLOAD=1,只做了对象存在性核对',
|
||
);
|
||
continue;
|
||
}
|
||
if (artifactHead.length > DOWNLOAD_LIMIT_BYTES) {
|
||
skip(
|
||
`${channel}/${key} 签名 ↔ 安装包匹配`,
|
||
`产物 ${artifactHead.length} 字节超过下载上限 ${DOWNLOAD_LIMIT_BYTES}`,
|
||
);
|
||
continue;
|
||
}
|
||
const fileName = `${channel}-${key}-${path.basename(decodeURIComponent(url))}`;
|
||
const artifactPath = path.join(tempDir, fileName);
|
||
const signaturePath = `${artifactPath}.sig`;
|
||
await download(url, artifactPath);
|
||
await writeFile(
|
||
signaturePath,
|
||
decodeMinisignText(entry.signature, `${channel}/${key} 清单签名`),
|
||
'utf8',
|
||
);
|
||
const size = await fileSize(artifactPath);
|
||
check(
|
||
`${channel}/${key} 下载对象字节数与 HEAD 一致`,
|
||
size === artifactHead.length,
|
||
`downloaded=${size} head=${artifactHead.length}`,
|
||
);
|
||
const sha256 = await sha256Of(artifactPath);
|
||
downloadedArtifacts.push({
|
||
channel,
|
||
platformKey: key,
|
||
sha256,
|
||
size,
|
||
url,
|
||
});
|
||
if (channel === 'dev-win' && key === WINDOWS_PLATFORM_KEY) {
|
||
const bridgeJson = await (
|
||
await fetch(`${OSS_BASE_URL}/latest.json`)
|
||
).json();
|
||
check(
|
||
`${channel} 旧协议指针 sha256/size 与实际产物一致`,
|
||
String(bridgeJson.sha256 ?? '').toLowerCase() === sha256 &&
|
||
Number(bridgeJson.size ?? 0) === size,
|
||
`sha256=${sha256.slice(0, 12)} size=${size}`,
|
||
);
|
||
}
|
||
try {
|
||
const result = verifyUpdaterSignature({
|
||
artifactPath,
|
||
signaturePath,
|
||
pubkey: readUpdaterPubkey(),
|
||
});
|
||
check(
|
||
`${channel}/${key} 签名 ↔ 安装包匹配(产物内公钥验签)`,
|
||
true,
|
||
`alg=${result.algorithm} keyId=${result.keyId}`,
|
||
);
|
||
} catch (error) {
|
||
check(
|
||
`${channel}/${key} 签名 ↔ 安装包匹配(产物内公钥验签)`,
|
||
false,
|
||
String(error),
|
||
);
|
||
}
|
||
if (key.startsWith('darwin-') && artifactPath.endsWith('.app.tar.gz')) {
|
||
await verifyMacosBundleIdentity(channel, key, manifest, artifactPath);
|
||
}
|
||
if (key.startsWith('windows-') && artifactPath.endsWith('.exe')) {
|
||
await verifyWindowsBundleIdentity(channel, key, manifest, artifactPath);
|
||
}
|
||
await rm(artifactPath, { force: true });
|
||
await rm(signaturePath, { force: true });
|
||
}
|
||
}
|
||
|
||
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'agc-channel-check-'));
|
||
try {
|
||
for (const channel of CHANNELS) await verifyChannel(channel, tempDir);
|
||
} finally {
|
||
await rm(tempDir, { recursive: true, force: true });
|
||
}
|
||
|
||
// 渠道隔离:同一次核对里如果下载到多个渠道的更新包,它们不能是同一份字节——
|
||
// 不同渠道的 productName / identifier 不同,产物就不会相同(2026-09-28 的 dev-mac
|
||
// 事故正是「dev 分区里放的其实是 release 渠道那份包」,字节级完全相同)。
|
||
const channelNames = [
|
||
...new Set(
|
||
downloadedArtifacts.map((item) => item.channel.replace(/-(win|mac)$/u, '')),
|
||
),
|
||
];
|
||
if (!VERIFY_DOWNLOAD) {
|
||
skip(
|
||
'不同渠道的更新包互不相同(渠道隔离)',
|
||
'需要 AGC_UPDATE_VERIFY_DOWNLOAD=1 才能比对产物字节',
|
||
);
|
||
} else if (channelNames.length < 2) {
|
||
skip(
|
||
'不同渠道的更新包互不相同(渠道隔离)',
|
||
`本次只核对到 ${channelNames.length} 个渠道(${channelNames.join(',')});用 AGC_UPDATE_CHANNELS=dev-win,dev-mac,release-win,release-mac 可覆盖全量`,
|
||
);
|
||
} else {
|
||
const byHash = new Map();
|
||
for (const item of downloadedArtifacts) {
|
||
const bucket = byHash.get(item.sha256) ?? [];
|
||
bucket.push(item);
|
||
byHash.set(item.sha256, bucket);
|
||
}
|
||
const collisions = [...byHash.entries()].filter(
|
||
([, items]) =>
|
||
new Set(items.map((item) => item.channel.replace(/-(win|mac)$/u, '')))
|
||
.size > 1,
|
||
);
|
||
check(
|
||
'不同渠道的更新包互不相同(渠道隔离)',
|
||
collisions.length === 0,
|
||
collisions
|
||
.map(
|
||
([hash, items]) =>
|
||
`${hash.slice(0, 12)}: ${items.map((item) => `${item.channel}/${item.platformKey}`).join(' = ')}`,
|
||
)
|
||
.join(';'),
|
||
);
|
||
}
|
||
|
||
// 统一总号:`agc/global-version.json` 是发号唯一事实源,任何渠道清单都不允许高于它;
|
||
// 总号高于渠道是正常的(渠道可能正在构建或尚未发布),这里只提示,不判失败。
|
||
{
|
||
const response = await fetch(`${OSS_BASE_URL}/global-version.json`);
|
||
check(
|
||
'统一总号对象可读',
|
||
response.status === 200,
|
||
`status=${response.status} ${OSS_BASE_URL}/global-version.json`,
|
||
);
|
||
if (response.status === 200) {
|
||
const global = await response.json();
|
||
const globalVersion = String(global.version ?? '');
|
||
check(
|
||
'统一总号字段齐备',
|
||
/^\d+\.\d+\.\d+$/u.test(globalVersion) &&
|
||
!Number.isNaN(Date.parse(String(global.updatedAt ?? ''))) &&
|
||
String(global.channel ?? '').trim().length > 0,
|
||
`version=${globalVersion} updatedAt=${global.updatedAt ?? ''} channel=${global.channel ?? ''}`,
|
||
);
|
||
for (const [channel, version] of channelVersions) {
|
||
if (!/^\d+\.\d+\.\d+$/u.test(version)) continue;
|
||
check(
|
||
`${channel} 清单版本不高于统一总号`,
|
||
compareVersions(version, globalVersion) <= 0,
|
||
`channel=${version} global=${globalVersion}`,
|
||
);
|
||
if (compareVersions(version, globalVersion) < 0) {
|
||
console.log(
|
||
`INFO ${channel} 还没发到总号版本(渠道 ${version} < 总号 ${globalVersion};总号最近由 ${global.channel} 在 ${global.updatedAt} 取号,构建可能仍在进行)`,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(
|
||
failures === 0
|
||
? `\n全部通过${skipped > 0 ? `(${skipped} 项跳过)` : ''}`
|
||
: `\n${failures} 项失败${skipped > 0 ? `,${skipped} 项跳过` : ''}`,
|
||
);
|
||
process.exit(failures === 0 ? 0 : 1);
|