feat: ship portal and android release updates
This commit is contained in:
210
scripts/deploy-android-release.mjs
Normal file
210
scripts/deploy-android-release.mjs
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
import {
|
||||
buildObjectKey,
|
||||
createAwsV4Headers,
|
||||
encodeObjectKey,
|
||||
getCacheControl,
|
||||
normalizeEndpoint,
|
||||
parseSimpleEnv,
|
||||
requestDogeCloudTemporaryS3Session,
|
||||
} from './oss-deploy-lib.mjs';
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const envFilePath = path.join(repoRoot, '.env.oss.local');
|
||||
const apkSourcePath = path.join(repoRoot, 'front', 'android', 'app', 'build', 'outputs', 'apk', 'debug', 'app-debug.apk');
|
||||
|
||||
function parseArgs(argv) {
|
||||
return {
|
||||
dryRun: argv.includes('--dry-run'),
|
||||
};
|
||||
}
|
||||
|
||||
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 getAndroidReleaseVersion() {
|
||||
const versionCode = requireEnv('YOYUZH_ANDROID_VERSION_CODE').trim();
|
||||
const versionName = requireEnv('YOYUZH_ANDROID_VERSION_NAME').trim();
|
||||
return {versionCode, versionName};
|
||||
}
|
||||
|
||||
function getAndroidReleasePrefix() {
|
||||
return (process.env.YOYUZH_ANDROID_RELEASE_PREFIX || 'android/releases').trim().replace(/^\/+|\/+$/g, '');
|
||||
}
|
||||
|
||||
function getAndroidReleaseScope() {
|
||||
return (process.env.YOYUZH_DOGECLOUD_ANDROID_SCOPE || process.env.YOYUZH_DOGECLOUD_STORAGE_SCOPE || '').trim();
|
||||
}
|
||||
|
||||
function getAndroidReleaseApkObjectKey(versionName) {
|
||||
const safeVersionName = versionName.replace(/[^0-9A-Za-z._-]/g, '-');
|
||||
return buildObjectKey(getAndroidReleasePrefix(), `yoyuzh-portal-${safeVersionName}.apk`);
|
||||
}
|
||||
|
||||
function getAndroidReleaseMetadataObjectKey() {
|
||||
return buildObjectKey(getAndroidReleasePrefix(), 'latest.json');
|
||||
}
|
||||
|
||||
function buildAndroidReleaseMetadata() {
|
||||
const {versionCode, versionName} = getAndroidReleaseVersion();
|
||||
const fileName = path.posix.basename(getAndroidReleaseApkObjectKey(versionName));
|
||||
return {
|
||||
versionCode,
|
||||
versionName,
|
||||
fileName,
|
||||
objectKey: getAndroidReleaseApkObjectKey(versionName),
|
||||
publishedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async function uploadFile({
|
||||
bucket,
|
||||
endpoint,
|
||||
region,
|
||||
objectKey,
|
||||
filePath,
|
||||
contentType,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
sessionToken,
|
||||
}) {
|
||||
const body = await fs.readFile(filePath);
|
||||
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 main() {
|
||||
const {dryRun} = parseArgs(process.argv.slice(2));
|
||||
await loadEnvFileIfPresent();
|
||||
|
||||
const androidScope = getAndroidReleaseScope();
|
||||
if (!androidScope) {
|
||||
throw new Error('Missing required environment variable: YOYUZH_DOGECLOUD_ANDROID_SCOPE or YOYUZH_DOGECLOUD_STORAGE_SCOPE');
|
||||
}
|
||||
|
||||
await fs.access(apkSourcePath);
|
||||
|
||||
const apiAccessKey = requireEnv('YOYUZH_DOGECLOUD_API_ACCESS_KEY');
|
||||
const apiSecretKey = requireEnv('YOYUZH_DOGECLOUD_API_SECRET_KEY');
|
||||
const apiBaseUrl = process.env.YOYUZH_DOGECLOUD_API_BASE_URL || 'https://api.dogecloud.com';
|
||||
const region = process.env.YOYUZH_DOGECLOUD_S3_REGION || 'automatic';
|
||||
const ttlSeconds = Number(process.env.YOYUZH_DOGECLOUD_ANDROID_TTL_SECONDS || process.env.YOYUZH_DOGECLOUD_STORAGE_TTL_SECONDS || '3600');
|
||||
|
||||
const {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
sessionToken,
|
||||
endpoint,
|
||||
bucket,
|
||||
} = await requestDogeCloudTemporaryS3Session({
|
||||
apiBaseUrl,
|
||||
accessKey: apiAccessKey,
|
||||
secretKey: apiSecretKey,
|
||||
scope: androidScope,
|
||||
ttlSeconds,
|
||||
});
|
||||
|
||||
const metadata = buildAndroidReleaseMetadata();
|
||||
const tempMetadataPath = path.join(repoRoot, '.tmp-android-release.json');
|
||||
await fs.writeFile(tempMetadataPath, JSON.stringify(metadata, null, 2) + '\n', 'utf-8');
|
||||
|
||||
try {
|
||||
const uploads = [
|
||||
{
|
||||
objectKey: metadata.objectKey,
|
||||
filePath: apkSourcePath,
|
||||
contentType: 'application/vnd.android.package-archive',
|
||||
},
|
||||
{
|
||||
objectKey: getAndroidReleaseMetadataObjectKey(),
|
||||
filePath: tempMetadataPath,
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
},
|
||||
];
|
||||
|
||||
for (const upload of uploads) {
|
||||
if (dryRun) {
|
||||
console.log(`[dry-run] upload ${upload.filePath} -> ${upload.objectKey}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await uploadFile({
|
||||
bucket,
|
||||
endpoint,
|
||||
region,
|
||||
objectKey: upload.objectKey,
|
||||
filePath: upload.filePath,
|
||||
contentType: upload.contentType,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
sessionToken,
|
||||
});
|
||||
console.log(`uploaded ${upload.objectKey}`);
|
||||
}
|
||||
} finally {
|
||||
await fs.rm(tempMetadataPath, {force: true});
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user