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
移除服务器选择并按来源隔离登录凭据 新增官网客户端下载入口和匿名平台聚合接口 根据发布清单自动展示Windows与macOS首装包 补齐Mac首装元数据和上传顺序校验 同步定向测试与下载发布规范
117 lines
3.3 KiB
JavaScript
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;
|
|
}
|