Files
Genarrative/apps/ai-game-creator-shell/scripts/release-oss.mjs
T
kdletters e3682fd06f
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m31s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m32s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m33s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m40s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m21s
Project CI / AI game creator shell Rust crates (push) Successful in 3m11s
Project CI / Native shell tests (push) Successful in 16m15s
Project CI / Frontend tests (push) Successful in 14m57s
Project CI / Repository checks (push) Successful in 14m48s
Project CI / Backend tests (push) Successful in 20m7s
Project CI / AI game creator shell web tests (push) Successful in 6m20s
固定客户端dev服务并支持官网多平台下载
移除服务器选择并按来源隔离登录凭据
新增官网客户端下载入口和匿名平台聚合接口
根据发布清单自动展示Windows与macOS首装包
补齐Mac首装元数据和上传顺序校验
同步定向测试与下载发布规范
2026-09-19 16:19:28 +08:00

117 lines
3.3 KiB
JavaScript

import { spawnSync } from 'node:child_process';
import path from 'node:path';
/**
* 发布上传的 OSS 命令行整理:把 ossutil 参数与凭据整理成可执行或可打印的形式,
* 便于在 dry-run 下核对将要执行的上传,同时保证任何输出都不回显凭据明文。
*/
const redactedCredential = '<redacted>';
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;
}