Files
my_site/scripts/deploy-front-oss.mjs
2026-04-05 13:57:13 +08:00

230 lines
5.3 KiB
JavaScript
Executable File

#!/usr/bin/env node
import fs from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import {spawnSync} from 'node:child_process';
import {
buildObjectKey,
createAwsV4Headers,
encodeObjectKey,
getFrontendSpaAliasContentType,
getFrontendSpaAliasKeys,
getCacheControl,
getContentType,
listFiles,
normalizeEndpoint,
parseSimpleEnv,
requestDogeCloudTemporaryS3Session,
} from './oss-deploy-lib.mjs';
const repoRoot = process.cwd();
const frontDir = path.join(repoRoot, 'front');
const distDir = path.join(frontDir, 'dist');
const envFilePath = path.join(repoRoot, '.env.oss.local');
function parseArgs(argv) {
return {
dryRun: argv.includes('--dry-run'),
skipBuild: argv.includes('--skip-build'),
};
}
async function loadEnvFileIfPresent() {
try {
const raw = await fs.readFile(envFilePath, 'utf-8');
const values = parseSimpleEnv(raw);
for (const [key, value] of Object.entries(values)) {
if (!process.env[key]) {
process.env[key] = value;
}
}
} catch (error) {
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
return;
}
throw error;
}
}
function requireEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
function runBuild() {
const result = spawnSync('npm', ['run', 'build'], {
cwd: frontDir,
stdio: 'inherit',
shell: process.platform === 'win32',
});
if (result.status !== 0) {
throw new Error('Frontend build failed');
}
}
async function uploadFile({
bucket,
endpoint,
region,
objectKey,
filePath,
contentTypeOverride,
accessKeyId,
secretAccessKey,
sessionToken,
}) {
const body = await fs.readFile(filePath);
const contentType = contentTypeOverride || getContentType(objectKey);
const amzDate = new Date().toISOString().replace(/[:-]|\.\d{3}/g, '');
const url = `https://${bucket}.${normalizeEndpoint(endpoint)}/${encodeObjectKey(objectKey)}`;
const signatureHeaders = createAwsV4Headers({
method: 'PUT',
endpoint,
region,
bucket,
objectKey,
headers: {
'Content-Type': contentType,
},
amzDate,
accessKeyId,
secretAccessKey,
sessionToken,
});
const response = await fetch(url, {
method: 'PUT',
headers: {
...signatureHeaders,
'Cache-Control': getCacheControl(objectKey),
'Content-Length': String(body.byteLength),
},
body,
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Upload failed for ${objectKey}: ${response.status} ${response.statusText}\n${text}`);
}
}
async function uploadSpaAliases({
bucket,
endpoint,
region,
distIndexPath,
accessKeyId,
secretAccessKey,
sessionToken,
remotePrefix,
dryRun,
}) {
const aliases = getFrontendSpaAliasKeys();
const contentType = getFrontendSpaAliasContentType();
for (const alias of aliases) {
const objectKey = buildObjectKey(remotePrefix, alias);
if (dryRun) {
console.log(`[dry-run] upload alias ${alias} -> ${objectKey}`);
continue;
}
await uploadFile({
bucket,
endpoint,
region,
objectKey,
filePath: distIndexPath,
contentTypeOverride: contentType,
accessKeyId,
secretAccessKey,
sessionToken,
});
console.log(`uploaded alias ${objectKey}`);
}
}
async function main() {
const {dryRun, skipBuild} = parseArgs(process.argv.slice(2));
await loadEnvFileIfPresent();
const apiAccessKey = requireEnv('YOYUZH_DOGECLOUD_API_ACCESS_KEY');
const apiSecretKey = requireEnv('YOYUZH_DOGECLOUD_API_SECRET_KEY');
const scope = requireEnv('YOYUZH_DOGECLOUD_FRONT_SCOPE');
const apiBaseUrl = process.env.YOYUZH_DOGECLOUD_API_BASE_URL || 'https://api.dogecloud.com';
const region = process.env.YOYUZH_DOGECLOUD_S3_REGION || 'automatic';
const remotePrefix = process.env.YOYUZH_DOGECLOUD_FRONT_PREFIX || '';
const ttlSeconds = Number(process.env.YOYUZH_DOGECLOUD_FRONT_TTL_SECONDS || '3600');
const {
accessKeyId,
secretAccessKey,
sessionToken,
endpoint,
bucket,
} = await requestDogeCloudTemporaryS3Session({
apiBaseUrl,
accessKey: apiAccessKey,
secretKey: apiSecretKey,
scope,
ttlSeconds,
});
if (!skipBuild) {
runBuild();
}
const files = await listFiles(distDir);
if (files.length === 0) {
throw new Error('No files found in front/dist. Run the frontend build first.');
}
for (const filePath of files) {
const relativePath = path.relative(distDir, filePath).split(path.sep).join('/');
const objectKey = buildObjectKey(remotePrefix, relativePath);
if (dryRun) {
console.log(`[dry-run] upload ${relativePath} -> ${objectKey}`);
continue;
}
await uploadFile({
bucket,
endpoint,
region,
objectKey,
filePath,
accessKeyId,
secretAccessKey,
sessionToken,
});
console.log(`uploaded ${objectKey}`);
}
await uploadSpaAliases({
bucket,
endpoint,
region,
distIndexPath: path.join(distDir, 'index.html'),
accessKeyId,
secretAccessKey,
sessionToken,
remotePrefix,
dryRun,
});
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});