Files
cardy/mobile-client/scripts/publish-android-update.mjs

65 lines
3.0 KiB
JavaScript

// Publishes a live update of the Android app's web part:
// 1. builds the Capacitor web bundle (its version comes from the build time),
// 2. zips it into updates/android/bundle-<version>.zip,
// 3. writes updates/android/version.json (version, zip, SHA-256, minimum native build).
// The mobile-client dev/preview server serves updates/android at /m/android/, i.e.
// https://dev.aggtaa.com/m/android/version.json. Installed apps pick the update up on the next check.
//
// minNativeBuild is the versionCode of the latest published APK (updates/android/apk.json, see
// publish-android-apk.mjs): after an APK release, web updates go only to that APK or newer, older
// installations are offered the APK instead. Publish an APK whenever the native part changes
// (plugins, AndroidManifest, Java code).
import { execSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { zipSync } from 'fflate';
const root = path.resolve(import.meta.dirname, '..');
const distDir = path.join(root, 'dist-capacitor');
const outDir = path.join(root, 'updates', 'android');
const KEEP_BUNDLES = 3;
execSync('pnpm exec vite build --mode capacitor', { cwd: root, stdio: 'inherit' });
const { version } = JSON.parse(readFileSync(path.join(distDir, 'cardy-version.json'), 'utf8'));
function collect(dir, prefix = '') {
const files = {};
for (const name of readdirSync(dir)) {
const full = path.join(dir, name);
const rel = prefix ? `${prefix}/${name}` : name;
if (statSync(full).isDirectory()) Object.assign(files, collect(full, rel));
else files[rel] = readFileSync(full);
}
return files;
}
const zip = Buffer.from(zipSync(collect(distDir), { level: 6 }));
const checksum = createHash('sha256').update(zip).digest('hex');
let minNativeBuild = 1;
try {
minNativeBuild = Number(JSON.parse(readFileSync(path.join(outDir, 'apk.json'), 'utf8')).versionCode) || 1;
} catch {
// no APK published yet: any installed APK can take the bundle
}
mkdirSync(outDir, { recursive: true });
const zipName = `bundle-${version}.zip`;
writeFileSync(path.join(outDir, zipName), zip);
// version.json last: clients never see a version whose zip is not there yet.
writeFileSync(
path.join(outDir, 'version.json'),
JSON.stringify({ version, url: zipName, checksum, minNativeBuild, publishedAt: new Date().toISOString() }, null, 2),
);
// Keep a few recent bundles (clients may be mid-download of the previous one).
const bundles = readdirSync(outDir)
.filter((f) => /^bundle-.+\.zip$/.test(f))
.sort((a, b) => statSync(path.join(outDir, b)).mtimeMs - statSync(path.join(outDir, a)).mtimeMs);
for (const old of bundles.slice(KEEP_BUNDLES)) unlinkSync(path.join(outDir, old));
console.log(`\nPublished Android update ${version} (${(zip.length / 1024).toFixed(0)} KiB, native build >= ${minNativeBuild})`);
console.log(` ${path.join(outDir, zipName)}`);
console.log('Served at /m/android/version.json by the mobile-client dev/preview server.');