Test deployment public URL (Linux) / test-linux (push) Canceled after 0s
Test start.ps1 (Windows) / test-windows (push) Canceled after 0s
Test deployment public URL (Linux) / test-linux (pull_request) Canceled after 0s
Test start.ps1 (Windows) / test-windows (pull_request) Canceled after 0s
117 lines
7.7 KiB
JavaScript
117 lines
7.7 KiB
JavaScript
#!/usr/bin/env node
|
||
import { execFileSync } from 'node:child_process';
|
||
import { readFileSync, writeFileSync, renameSync } from 'node:fs';
|
||
import { resolve, dirname } from 'node:path';
|
||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||
|
||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||
const VERSION = /^\d+\.\d+\.\d+\.\d{8}(?:\.\d+)?$/;
|
||
const TARGETS = {
|
||
'server-main': ['registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server','latest',''],
|
||
web: ['registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai','latest',''],
|
||
'web-no-ssr': ['registry.cn-shanghai.aliyuncs.com/comfy-ai/one-ai','latest-no-ssr','-no-ssr'],
|
||
'ws-gateway': ['registry.cn-shanghai.aliyuncs.com/easyaigc/wsgateway','latest',''],
|
||
'model-conversion-worker': ['registry.cn-shanghai.aliyuncs.com/easyaigc/model-conversion-worker','latest',''],
|
||
updater: ['registry.cn-shanghai.aliyuncs.com/easyaigc/easyai-updater','latest',''],
|
||
'service-governance': ['registry.cn-shanghai.aliyuncs.com/easyaigc/agent-governance','latest',''],
|
||
'memory-service': ['registry.cn-shanghai.aliyuncs.com/easyaigc/agent-memory','latest',''],
|
||
};
|
||
const CORE = ['server-main','web','ws-gateway'];
|
||
const docker = args => execFileSync('docker', args, {encoding:'utf8', stdio:['ignore','pipe','pipe']});
|
||
const inspectArgs = ref => ['buildx','imagetools','inspect',ref,'--format','{{json .Manifest}}'];
|
||
|
||
export function promotionPlan(manifest, version, confirmedVersion) {
|
||
if (!VERSION.test(version ?? '') || version !== confirmedVersion) throw new Error('必须人工指定完整 --version 并用相同 --confirm-version 确认,不能自动选择最新版');
|
||
if (manifest.schemaVersion !== 1 || manifest.channel !== 'versioned' || manifest.productVersion !== version || manifest.platform !== 'linux/amd64') throw new Error('候选版本记录与指定版本/平台不一致');
|
||
if (typeof manifest.releaseId !== 'string' || !manifest.releaseId.trim() || manifest.releaseId.length > 200 || !Array.isArray(manifest.components)) throw new Error('候选版本记录缺少有效 releaseId/components');
|
||
if (!Number.isFinite(Date.parse(manifest.publishedAt)) || ['managedUpdateAllowed','scheduledUpdateAllowed','rollbackSafe'].some(key=>typeof manifest[key]!=='boolean')) throw new Error('候选版本记录缺少有效发布时间或更新兼容性设置');
|
||
const seen = new Set();
|
||
const images = manifest.components.map(component => {
|
||
const target = TARGETS[component.key];
|
||
if (!target || seen.has(component.key) || component.image !== target[0] || !/^sha256:[a-f0-9]{64}$/.test(component.digest ?? '')) throw new Error(`无效或重复组件: ${component.key}`);
|
||
seen.add(component.key);
|
||
const tag = component.tag ?? `${version}${target[2]}`;
|
||
const base = target[2] && tag.endsWith(target[2]) ? tag.slice(0,-target[2].length) : tag;
|
||
if (!VERSION.test(base) || (target[2] && !tag.endsWith(target[2]))) throw new Error(`组件 ${component.key} 必须引用固定版本标签`);
|
||
return {key:component.key, image:component.image, digest:component.digest, source:`${component.image}:${tag}`, stable:`${component.image}:${target[1]}`};
|
||
});
|
||
for (const key of [...CORE,'web-no-ssr']) if (!seen.has(key)) throw new Error(`候选记录缺少必要组件: ${key}`);
|
||
return images;
|
||
}
|
||
|
||
export function promoteStableRelease({manifest, version, confirmedVersion, outputPath, dryRun=false, run=docker}) {
|
||
const plan = promotionPlan(manifest, version, confirmedVersion);
|
||
// 所有固定标签先核验完成,再发生任何稳定标签写入。
|
||
for (const image of plan) {
|
||
const descriptor = JSON.parse(run(inspectArgs(image.source)));
|
||
let platforms = descriptor.manifests?.map(item=>item.platform) ?? [descriptor.platform];
|
||
if (!platforms.some(Boolean)) {
|
||
const verbose = JSON.parse(run(['manifest','inspect','--verbose',image.source]));
|
||
platforms = (Array.isArray(verbose) ? verbose : [verbose]).map(item=>item.Descriptor?.platform);
|
||
}
|
||
if (descriptor.digest !== image.digest || !platforms.some(platform=>platform?.os==='linux' && platform?.architecture==='amd64')) throw new Error(`固定产物核验失败: ${image.key};稳定标签未修改`);
|
||
}
|
||
if (dryRun) return {status:'DRY_RUN',version,images:plan};
|
||
if (!outputPath) throw new Error('缺少稳定清单输出路径');
|
||
const previous = new Map();
|
||
for (const image of plan) {
|
||
try { previous.set(image.stable, JSON.parse(run(inspectArgs(image.stable))).digest); }
|
||
catch (error) {
|
||
if (!/manifest unknown|not found|no such manifest/i.test(String(error.stderr ?? error.message))) throw error;
|
||
previous.set(image.stable,null);
|
||
}
|
||
}
|
||
const changed = [];
|
||
try {
|
||
for (const image of plan) {
|
||
// 先登记尝试;写入成功但回执丢失时也必须尝试恢复原稳定指针。
|
||
changed.push(image);
|
||
run(['buildx','imagetools','create','--prefer-index=false','--tag',image.stable,`${image.image}@${image.digest}`]);
|
||
const actual = JSON.parse(run(inspectArgs(image.stable)));
|
||
if (actual.digest !== image.digest) throw new Error(`稳定标签读回不一致: ${image.key}`);
|
||
}
|
||
const promoted = {...manifest, channel:'stable', publishedAt:new Date().toISOString(),
|
||
promotedFromPublishedAt:manifest.publishedAt, components:manifest.components.filter(item=>CORE.includes(item.key))};
|
||
const temporary = `${outputPath}.tmp`;
|
||
writeFileSync(temporary, JSON.stringify(promoted,null,2)+'\n');
|
||
renameSync(temporary,outputPath);
|
||
return {status:'PROMOTED',version,images:plan,manifest:outputPath};
|
||
} catch (error) {
|
||
const recovery = [];
|
||
for (const image of changed.reverse()) {
|
||
const oldDigest = previous.get(image.stable);
|
||
if (!oldDigest) { recovery.push(`${image.key}: 原稳定标签不存在,保留本次标签并记录未完成`); continue; }
|
||
try {
|
||
run(['buildx','imagetools','create','--prefer-index=false','--tag',image.stable,`${image.image}@${oldDigest}`]);
|
||
if (JSON.parse(run(inspectArgs(image.stable))).digest !== oldDigest) throw new Error('恢复读回不一致');
|
||
recovery.push(`${image.key}: 已恢复原稳定标签`);
|
||
} catch { recovery.push(`${image.key}: 恢复失败,需按原 digest 继续恢复`); }
|
||
}
|
||
throw new Error(`稳定提升未完成,未提交新的稳定清单:${error.message};${recovery.join(';')}`);
|
||
}
|
||
}
|
||
|
||
function main() {
|
||
const options = {};
|
||
const args = process.argv.slice(2);
|
||
for (let index=0; index<args.length; index++) {
|
||
const key=args[index];
|
||
if (key==='--dry-run') options.dryRun=true;
|
||
else if (['--version','--confirm-version','--manifest','--output'].includes(key)) {
|
||
const value=args[++index];
|
||
if (!value || value.startsWith('--')) throw new Error(`${key} 缺少参数`);
|
||
options[key.slice(2)]=value;
|
||
} else throw new Error(`未知选项: ${key}`);
|
||
}
|
||
// 在读取候选文件、调用 Docker 前先拒绝未确认或浮动版本。
|
||
if (!VERSION.test(options.version ?? '') || options.version!==options['confirm-version']) throw new Error('用法: node scripts/promote-stable-release.mjs --version X.Y.Z.YYYYMMDD[.N] --confirm-version 同一版本 [--dry-run];仅在用户明确指定版本转稳定版后执行');
|
||
const manifestPath=resolve(options.manifest ?? `${ROOT}/system-update/releases/${options.version}.json`);
|
||
const manifest=JSON.parse(readFileSync(manifestPath,'utf8'));
|
||
console.log(JSON.stringify(promoteStableRelease({manifest,version:options.version,confirmedVersion:options['confirm-version'],
|
||
outputPath:resolve(options.output ?? `${ROOT}/system-update/release-manifest.json`),dryRun:options.dryRun}),null,2));
|
||
}
|
||
|
||
if (process.argv[1] && import.meta.url===pathToFileURL(resolve(process.argv[1])).href) {
|
||
try { main(); } catch (error) { console.error(error.message); process.exitCode=1; }
|
||
}
|