import { spawnSync } from 'node:child_process'; import path from 'node:path'; /** * 发布上传的 OSS 命令行整理:把 ossutil 参数与凭据整理成可执行或可打印的形式, * 便于在 dry-run 下核对将要执行的上传,同时保证任何输出都不回显凭据明文。 */ const redactedCredential = ''; export function readReleaseDryRun(env = process.env) { const value = env.AGC_RELEASE_DRY_RUN?.trim().toLowerCase(); return value === '1' || value === 'true'; } function quoteArgument(value) { return /[\s"']/u.test(value) ? JSON.stringify(value) : value; } export function formatOssutilCommand({ binary, args, endpoint, credentials }) { const parts = [binary, ...args, '--endpoint', endpoint]; if (credentials) { parts.push( '--access-key-id', redactedCredential, '--access-key-secret', redactedCredential, ); } return parts.map(quoteArgument).join(' '); } export function createReleaseUploadPlan( { artifact, downloadArtifact, channel, manifest, manifestPath, legacyManifestPath, }, bucket, ) { if (!artifact || !downloadArtifact || !manifestPath || !manifest?.version) { throw new Error('发布结果缺少更新包、首装包或清单'); } const prefix = `oss://${bucket}/agc/${channel}`; const artifacts = [ ...new Set( [artifact, `${artifact}.sig`, downloadArtifact].map((file) => path.resolve(file), ), ), ]; const plan = artifacts.map((source) => ({ source, destination: `${prefix}/${manifest.version}/${path.basename(source)}`, })); plan.push({ source: manifestPath, destination: `${prefix}/latest.json` }); if (legacyManifestPath) { plan.push({ source: legacyManifestPath, destination: `oss://${bucket}/agc/latest.json`, }); } return plan; } export function uploadReleaseArtifacts( release, { bucket, endpoint, binary = 'ossutil', accessKeyId, accessKeySecret, dryRun = false, spawn = spawnSync, log = console.log, }, ) { if (Boolean(accessKeyId) !== Boolean(accessKeySecret)) { throw new Error('OSS AccessKey ID 和 Secret 必须同时提供'); } const plan = createReleaseUploadPlan(release, bucket); for (const { source, destination } of plan) { // 全部安装对象成功后才执行 latest 指针;失败立即终止,不发布悬空链接。 const args = ['cp', '--force', source, destination]; if (dryRun) { log( `[dry-run] ${formatOssutilCommand({ binary, args, endpoint, credentials: Boolean(accessKeyId) })}`, ); continue; } const credentials = accessKeyId ? ['--access-key-id', accessKeyId, '--access-key-secret', accessKeySecret] : []; const result = spawn( binary, [...args, '--endpoint', endpoint, ...credentials], { stdio: 'inherit', shell: false, }, ); if (result.error) throw new Error(`无法执行 ${binary},请先安装并配置 ossutil`); if (result.status !== 0) { throw new Error( `OSS 上传失败(退出码 ${result.status ?? 1}):${destination}`, ); } log(`[ai-game-creator-shell] 已上传 ${destination}`); } if (dryRun) log('[ai-game-creator-shell] dry-run:未写入任何 OSS 对象'); return plan; }