MovixOpenSource/scripts/publish-app.mjs
Movix 1401dc20f3 tird release
mon compte github a été détecté comme du spam je crois
2026-04-28 17:55:00 +02:00

201 lines
6.6 KiB
JavaScript

#!/usr/bin/env node
/* eslint-disable no-console */
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '..');
const PATHS = {
buildGradle: path.join(REPO_ROOT, 'app/android/app/build.gradle'),
keystoreProps: path.join(REPO_ROOT, 'app/android/app/keystore.properties'),
gradleWrapper: path.join(REPO_ROOT, 'app/android/gradlew'),
apkOutput: path.join(
REPO_ROOT,
'app/android/app/build/outputs/apk/release/app-release.apk',
),
publishedApk: path.join(REPO_ROOT, 'app/movix-android.apk'),
manifest: path.join(REPO_ROOT, 'app/version.json'),
};
const APK_URL =
'https://github.com/movixcorp/MovixOpenSource/raw/refs/heads/main/app/movix-android.apk';
function die(msg) {
console.error(`\n[publish-app] ${msg}\n`);
process.exit(1);
}
function log(step, msg) {
console.log(`[${step}] ${msg}`);
}
function readBuildGradle() {
const txt = fs.readFileSync(PATHS.buildGradle, 'utf8');
const vc = txt.match(/versionCode\s+(\d+)/);
const vn = txt.match(/versionName\s+"([^"]+)"/);
if (!vc || !vn) {
die('Could not parse versionCode/versionName from build.gradle');
}
return { versionCode: Number(vc[1]), versionName: vn[1] };
}
function readCurrentManifest() {
try {
const raw = fs.readFileSync(PATHS.manifest, 'utf8');
return JSON.parse(raw);
} catch {
return null;
}
}
async function promptMultiline(rl, label) {
console.log(`${label} (ligne vide + Enter pour finir) :`);
const lines = [];
for (;;) {
const line = await rl.question('> ');
if (line === '') break;
lines.push(line);
}
return lines.join('\n');
}
async function promptYesNo(rl, label, defaultNo = true) {
const suffix = defaultNo ? '(y/N)' : '(Y/n)';
const answer = (await rl.question(`${label} ${suffix} : `)).trim().toLowerCase();
if (answer === '') return !defaultNo;
return answer === 'y' || answer === 'yes' || answer === 'o' || answer === 'oui';
}
function runGradle() {
const args = ['assembleRelease'];
const wrapper = process.platform === 'win32' ? 'gradlew.bat' : './gradlew';
const cwd = path.dirname(PATHS.gradleWrapper);
const res = spawnSync(wrapper, args, {
cwd,
stdio: 'inherit',
shell: process.platform === 'win32',
});
if (res.status !== 0) die(`gradle assembleRelease failed (exit ${res.status})`);
}
function verifyApkSigned() {
const cmd = process.platform === 'win32' ? 'apksigner.bat' : 'apksigner';
const res = spawnSync(cmd, ['verify', '--print-certs', PATHS.apkOutput], {
encoding: 'utf8',
shell: process.platform === 'win32',
});
if (res.status !== 0) {
console.warn(
'[publish-app] apksigner not found or verify failed — skipping signature check.\n' +
' Install Android build-tools or run apksigner manually to confirm release-signed.',
);
return;
}
if (/does not verify/i.test(res.stdout) || /debug/i.test(res.stdout)) {
die(`APK appears debug-signed or invalid:\n${res.stdout}`);
}
}
function computeSha256(filePath) {
const hash = createHash('sha256');
const fd = fs.openSync(filePath, 'r');
try {
const buf = Buffer.alloc(65536);
let bytesRead;
while ((bytesRead = fs.readSync(fd, buf, 0, buf.length, null)) > 0) {
hash.update(buf.subarray(0, bytesRead));
}
} finally {
fs.closeSync(fd);
}
return hash.digest('hex');
}
async function main() {
log('1/7', 'Vérifications préalables…');
if (!fs.existsSync(PATHS.keystoreProps)) {
die(
'app/android/keystore.properties absent — APK serait debug-signed (install = échec signature).\n' +
' Configure la release keystore avant de publier.',
);
}
const { versionCode, versionName } = readBuildGradle();
const current = readCurrentManifest();
if (current && typeof current.buildNumber === 'number' && versionCode <= current.buildNumber) {
die(
`versionCode (${versionCode}) doit être > current.buildNumber (${current.buildNumber}).\n` +
' Bump `versionCode` dans app/android/app/build.gradle d\'abord.',
);
}
console.log(
` ✓ keystore.properties présent\n` +
` ✓ versionCode=${versionCode}, versionName="${versionName}"` +
(current ? ` (current JSON buildNumber=${current.buildNumber})` : ''),
);
const rl = readline.createInterface({ input, output });
try {
log('2/7', 'Release notes');
const notesFr = await promptMultiline(rl, 'Locale FR (markdown)');
const notesEn = await promptMultiline(rl, 'Locale EN (markdown)');
log('3/7', 'Flag mandatory');
const mandatory = await promptYesNo(rl, 'Update obligatoire ?', true);
log('4/7', 'Build APK release (gradle assembleRelease)…');
runGradle();
if (!fs.existsSync(PATHS.apkOutput)) {
die(`APK introuvable à ${PATHS.apkOutput}`);
}
log('5/7', 'Vérification de la signature APK…');
verifyApkSigned();
log('6/7', 'Copie de l\'APK + hashes…');
fs.mkdirSync(path.dirname(PATHS.publishedApk), { recursive: true });
fs.copyFileSync(PATHS.apkOutput, PATHS.publishedApk);
const stat = fs.statSync(PATHS.publishedApk);
const sha = computeSha256(PATHS.publishedApk);
console.log(
`${PATHS.publishedApk} (${(stat.size / (1024 * 1024)).toFixed(2)} MB)\n` +
` ✓ SHA256=${sha}`,
);
log('7/7', 'Écriture de app/version.json…');
const manifest = {
version: versionName,
buildNumber: versionCode,
apkUrl: APK_URL,
apkSizeBytes: stat.size,
apkSha256: sha,
mandatory,
releasedAt: new Date().toISOString(),
releaseNotes: { fr: notesFr, en: notesEn },
};
fs.writeFileSync(PATHS.manifest, JSON.stringify(manifest, null, 2) + '\n');
console.log(
'\n─────────────────────────────────────────────\n' +
'✓ Publish ready. Next steps:\n\n' +
' git add app/version.json app/movix-android.apk app/android/app/build.gradle\n' +
` git commit -m "release(app): v${versionName} (build ${versionCode})"\n` +
' git push\n' +
'─────────────────────────────────────────────\n',
);
} finally {
rl.close();
}
}
main().catch(err => {
console.error('\n[publish-app] fatal', err);
process.exit(1);
});