feat: ship portal and android release updates
This commit is contained in:
112
scripts/deploy-android-apk.mjs
Normal file
112
scripts/deploy-android-apk.mjs
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/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 {fileURLToPath} from 'node:url';
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, '..');
|
||||
const frontDir = path.join(repoRoot, 'front');
|
||||
const androidDir = path.join(frontDir, 'android');
|
||||
const capacitorPluginsGradlePath = path.join(androidDir, 'capacitor-cordova-android-plugins', 'build.gradle');
|
||||
const capacitorAppGradlePath = path.join(frontDir, 'node_modules', '@capacitor', 'app', 'android', 'build.gradle');
|
||||
|
||||
const googleMirrorValue = 'https://maven.aliyun.com/repository/google';
|
||||
|
||||
function runCommand(command, args, cwd, extraEnv = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
...extraEnv,
|
||||
},
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`Command failed: ${command} ${args.join(' ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
function createAndroidBuildVersion() {
|
||||
const now = new Date();
|
||||
const year = String(now.getFullYear()).slice(-2);
|
||||
const startOfYear = new Date(now.getFullYear(), 0, 1);
|
||||
const dayOfYear = String(Math.floor((now - startOfYear) / 86400000) + 1).padStart(3, '0');
|
||||
const hour = String(now.getHours()).padStart(2, '0');
|
||||
const minute = String(now.getMinutes()).padStart(2, '0');
|
||||
|
||||
return {
|
||||
versionCode: `${year}${dayOfYear}${hour}${minute}`,
|
||||
versionName: `${now.getFullYear()}.${String(now.getMonth() + 1).padStart(2, '0')}.${String(now.getDate()).padStart(2, '0')}.${hour}${minute}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureAndroidGoogleMirror() {
|
||||
await Promise.all([
|
||||
patchCapacitorPluginGradle(capacitorPluginsGradlePath),
|
||||
patchCapacitorPluginGradle(capacitorAppGradlePath),
|
||||
]);
|
||||
}
|
||||
|
||||
async function patchCapacitorPluginGradle(gradlePath) {
|
||||
try {
|
||||
const original = await fs.readFile(gradlePath, 'utf-8');
|
||||
let next = original;
|
||||
|
||||
if (!next.includes(`def googleMirror = '${googleMirrorValue}'`)) {
|
||||
next = next.replace(
|
||||
'buildscript {\n',
|
||||
`buildscript {\n def googleMirror = '${googleMirrorValue}'\n`,
|
||||
);
|
||||
}
|
||||
|
||||
next = next.replace(
|
||||
/buildscript \{\n\s+def googleMirror = 'https:\/\/maven\.aliyun\.com\/repository\/google'\n\s+repositories \{\n\s+google\(\)\n/,
|
||||
`buildscript {\n def googleMirror = '${googleMirrorValue}'\n repositories {\n maven { url googleMirror }\n`,
|
||||
);
|
||||
|
||||
next = next.replace(
|
||||
/repositories \{\n\s+google\(\)\n\s+mavenCentral\(\)/g,
|
||||
`repositories {\n maven { url '${googleMirrorValue}' }\n mavenCentral()`,
|
||||
);
|
||||
|
||||
if (next !== original) {
|
||||
await fs.writeFile(gradlePath, next, 'utf-8');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const buildVersion = createAndroidBuildVersion();
|
||||
const buildEnv = {
|
||||
YOYUZH_ANDROID_VERSION_CODE: buildVersion.versionCode,
|
||||
YOYUZH_ANDROID_VERSION_NAME: buildVersion.versionName,
|
||||
};
|
||||
|
||||
console.log(`Android versionCode=${buildVersion.versionCode}`);
|
||||
console.log(`Android versionName=${buildVersion.versionName}`);
|
||||
|
||||
runCommand('npm', ['run', 'build'], frontDir, buildEnv);
|
||||
runCommand('npx', ['cap', 'sync', 'android'], frontDir, buildEnv);
|
||||
await ensureAndroidGoogleMirror();
|
||||
|
||||
const gradleCommand = process.platform === 'win32' ? 'gradlew.bat' : './gradlew';
|
||||
runCommand(gradleCommand, ['assembleDebug'], androidDir, buildEnv);
|
||||
runCommand('node', ['scripts/deploy-front-oss.mjs', '--skip-build'], repoRoot, buildEnv);
|
||||
runCommand('node', ['scripts/deploy-android-release.mjs'], repoRoot, buildEnv);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
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);
|
||||
});
|
||||
@@ -23,8 +23,6 @@ const repoRoot = process.cwd();
|
||||
const frontDir = path.join(repoRoot, 'front');
|
||||
const distDir = path.join(frontDir, 'dist');
|
||||
const envFilePath = path.join(repoRoot, '.env.oss.local');
|
||||
const apkSourcePath = path.join(frontDir, 'android', 'app', 'build', 'outputs', 'apk', 'debug', 'app-debug.apk');
|
||||
const apkObjectPath = 'downloads/yoyuzh-portal.apk';
|
||||
|
||||
function parseArgs(argv) {
|
||||
return {
|
||||
@@ -155,48 +153,6 @@ async function uploadSpaAliases({
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadApkIfPresent({
|
||||
bucket,
|
||||
endpoint,
|
||||
region,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
sessionToken,
|
||||
remotePrefix,
|
||||
dryRun,
|
||||
}) {
|
||||
try {
|
||||
await fs.access(apkSourcePath);
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
||||
console.warn(`skip apk upload: not found at ${apkSourcePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const objectKey = buildObjectKey(remotePrefix, apkObjectPath);
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`[dry-run] upload ${apkObjectPath} -> ${objectKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await uploadFile({
|
||||
bucket,
|
||||
endpoint,
|
||||
region,
|
||||
objectKey,
|
||||
filePath: apkSourcePath,
|
||||
contentTypeOverride: 'application/vnd.android.package-archive',
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
sessionToken,
|
||||
});
|
||||
console.log(`uploaded ${objectKey}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const {dryRun, skipBuild} = parseArgs(process.argv.slice(2));
|
||||
|
||||
@@ -265,17 +221,6 @@ async function main() {
|
||||
remotePrefix,
|
||||
dryRun,
|
||||
});
|
||||
|
||||
await uploadApkIfPresent({
|
||||
bucket,
|
||||
endpoint,
|
||||
region,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
sessionToken,
|
||||
remotePrefix,
|
||||
dryRun,
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
Reference in New Issue
Block a user