Der Host steht fest im Manifest und muss beim Bauen mitgegeben werden. Das Publish-Skript rief Gradle bisher ohne ihn auf, ein so veroeffentlichtes APK haette also den Platzhalter aus build.gradle.kts getragen und geteilte Links waeren im Browser gelandet statt in der App. NOVASONIC_HOST (und optional NOVASONIC_HOST2) kommen aus scripts/.env.release; fehlt der Wert, warnt das Skript deutlich, statt still das Falsche zu bauen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
144 lines
7.8 KiB
JavaScript
144 lines
7.8 KiB
JavaScript
#!/usr/bin/env node
|
|
// Build the NovaSonic Android APK and upload it to the fixed Gitea release ("apk-latest"), replacing
|
|
// the previous .apk asset — so the web "Android App" download button always serves the latest build.
|
|
//
|
|
// One command after your changes: npm run publish-apk
|
|
//
|
|
// Config (env, or a gitignored scripts/.env.release file with KEY=VALUE lines):
|
|
// GITEA_URL internal/LAN Gitea base reachable from THIS machine, e.g. http://192.168.1.10:3000
|
|
// GITEA_REPO owner/repo, e.g. Docker/NovaSonic
|
|
// GITEA_TOKEN Gitea token with write access to the repo (repo/releases)
|
|
// GITEA_APK_TAG fixed release tag (default: apk-latest)
|
|
// APK_GRADLE_TASK gradle task (default: assembleDebug — installable; release is unsigned here)
|
|
// APK_FILE override the built APK path if you use a custom task
|
|
//
|
|
// The debug build is used by default because the release build type has no signingConfig yet, so a
|
|
// release APK would be unsigned and refuse to install. Same dev machine ⇒ stable debug key ⇒ updates
|
|
// install over each other. Add a keystore + signingConfig later to switch APK_GRADLE_TASK=assembleRelease.
|
|
|
|
import { spawnSync } from 'node:child_process';
|
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join, resolve } from 'node:path';
|
|
|
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
|
// ── tiny .env.release loader (never committed) ───────────────────────────────
|
|
const envFile = join(root, 'scripts', '.env.release');
|
|
if (existsSync(envFile)) {
|
|
for (const line of readFileSync(envFile, 'utf8').split('\n')) {
|
|
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*)\s*$/i);
|
|
if (m && !(m[1] in process.env)) process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
}
|
|
}
|
|
|
|
const GITEA_URL = (process.env.GITEA_URL || '').replace(/\/$/, '');
|
|
const GITEA_REPO = process.env.GITEA_REPO || '';
|
|
const GITEA_TOKEN = process.env.GITEA_TOKEN || '';
|
|
const TAG = process.env.GITEA_APK_TAG || 'apk-latest';
|
|
const TASK = process.env.APK_GRADLE_TASK || 'assembleDebug';
|
|
// Host fuer die App-Links geteilter Songs (https://<host>/?song=…). Steht fest im Manifest, muss
|
|
// also beim BAUEN mitgegeben werden — ohne ihn traegt das APK den Platzhalter aus build.gradle.kts
|
|
// und eingehende Links landen im Browser statt in der App. NOVASONIC_HOST2 optional fuer einen
|
|
// Domainwechsel (beide Adressen oeffnen dann in der App), siehe docs/share-links.md.
|
|
const HOST = process.env.NOVASONIC_HOST || '';
|
|
const HOST2 = process.env.NOVASONIC_HOST2 || '';
|
|
|
|
function die(msg) { console.error(`\n✖ ${msg}`); process.exit(1); }
|
|
if (!GITEA_URL || !GITEA_REPO || !GITEA_TOKEN)
|
|
die('GITEA_URL, GITEA_REPO und GITEA_TOKEN müssen gesetzt sein (env oder scripts/.env.release).');
|
|
|
|
const API = `${GITEA_URL}/api/v1/repos/${GITEA_REPO}`;
|
|
const authHeaders = { Authorization: `token ${GITEA_TOKEN}` };
|
|
|
|
// ── bump versionCode + read versionName from the Gradle config ───────────────
|
|
// The in-app updater compares versionCode, so every published build MUST increment it — we do that
|
|
// here automatically (pass --version X.Y to also set the versionName). Commit the changed
|
|
// build.gradle.kts afterwards so the repo reflects what's live.
|
|
const gradlePath = join(root, 'android', 'app', 'build.gradle.kts');
|
|
let gradle = readFileSync(gradlePath, 'utf8');
|
|
const curCode = parseInt(gradle.match(/versionCode\s*=\s*(\d+)/)?.[1] || '0', 10);
|
|
const versionCode = curCode + 1;
|
|
const versionArg = process.argv.slice(2).find(a => a.startsWith('--version='))?.split('=')[1]
|
|
|| (process.argv.includes('--version') ? process.argv[process.argv.indexOf('--version') + 1] : null);
|
|
let version = versionArg || gradle.match(/versionName\s*=\s*"([^"]+)"/)?.[1] || `0.${versionCode}`;
|
|
gradle = gradle
|
|
.replace(/versionCode\s*=\s*\d+/, `versionCode = ${versionCode}`)
|
|
.replace(/versionName\s*=\s*"[^"]*"/, `versionName = "${version}"`);
|
|
writeFileSync(gradlePath, gradle);
|
|
console.log(`▶ versionCode → ${versionCode}, versionName → ${version} (build.gradle.kts danach committen!)`);
|
|
|
|
const relBody = `versionCode=${versionCode}\nversionName=${version}`;
|
|
|
|
// ── 1) build ─────────────────────────────────────────────────────────────────
|
|
const isWin = process.platform === 'win32';
|
|
const gradlew = join(root, 'android', isWin ? 'gradlew.bat' : 'gradlew');
|
|
console.log(`▶ Baue APK: ${TASK} …`);
|
|
if (!HOST) {
|
|
console.warn('⚠ NOVASONIC_HOST ist nicht gesetzt — das APK bekommt den Platzhalter-Host aus');
|
|
console.warn(' build.gradle.kts, geteilte Links oeffnen dann NICHT in der App.');
|
|
console.warn(' In scripts/.env.release eintragen: NOVASONIC_HOST=music.deine-domain.de');
|
|
}
|
|
const gradleArgs = [`:app:${TASK}`];
|
|
if (HOST) gradleArgs.push(`-PnovasonicHost=${HOST}`);
|
|
if (HOST2) gradleArgs.push(`-PnovasonicHost2=${HOST2}`);
|
|
const build = spawnSync(gradlew, gradleArgs, {
|
|
cwd: join(root, 'android'), stdio: 'inherit', shell: isWin,
|
|
});
|
|
if (build.status !== 0) die('Gradle-Build fehlgeschlagen.');
|
|
|
|
// derive the APK path from the task (assembleDebug → debug/app-debug.apk)
|
|
const variant = TASK.replace(/^assemble/, '').toLowerCase() || 'debug';
|
|
const apkPath = process.env.APK_FILE
|
|
|| join(root, 'android', 'app', 'build', 'outputs', 'apk', variant, `app-${variant}.apk`);
|
|
if (!existsSync(apkPath)) die(`APK nicht gefunden: ${apkPath} (APK_FILE setzen, falls Task abweicht).`);
|
|
const apkBytes = readFileSync(apkPath);
|
|
console.log(`✓ Gebaut: ${apkPath} (${(apkBytes.length / 1048576).toFixed(1)} MB)`);
|
|
|
|
// ── 2) get-or-create the fixed release ───────────────────────────────────────
|
|
async function api(path, opts = {}) {
|
|
const r = await fetch(`${API}${path}`, { ...opts, headers: { ...authHeaders, ...(opts.headers || {}) } });
|
|
return r;
|
|
}
|
|
|
|
let rel;
|
|
{
|
|
const r = await api(`/releases/tags/${encodeURIComponent(TAG)}`);
|
|
if (r.status === 200) {
|
|
rel = await r.json();
|
|
} else if (r.status === 404) {
|
|
console.log(`▶ Release "${TAG}" existiert nicht — lege sie an …`);
|
|
const cr = await api('/releases', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ tag_name: TAG, target: 'main', name: `NovaSonic ${version}`, body: relBody }),
|
|
});
|
|
if (!cr.ok) die(`Release anlegen fehlgeschlagen: ${cr.status} ${await cr.text()}`);
|
|
rel = await cr.json();
|
|
} else {
|
|
die(`Release-Abfrage fehlgeschlagen: ${r.status} ${await r.text()}`);
|
|
}
|
|
}
|
|
|
|
// ── 3) delete old .apk assets, upload the new one ────────────────────────────
|
|
for (const a of rel.assets || []) {
|
|
if (/\.apk$/i.test(a.name)) {
|
|
const d = await api(`/releases/${rel.id}/assets/${a.id}`, { method: 'DELETE' });
|
|
console.log(d.ok ? `✓ Altes Asset gelöscht: ${a.name}` : `⚠ Löschen von ${a.name} fehlgeschlagen (${d.status})`);
|
|
}
|
|
}
|
|
|
|
const form = new FormData();
|
|
form.append('attachment', new Blob([apkBytes], { type: 'application/vnd.android.package-archive' }), 'NovaSonic.apk');
|
|
const up = await api(`/releases/${rel.id}/assets?name=NovaSonic.apk`, { method: 'POST', body: form });
|
|
if (!up.ok) die(`Upload fehlgeschlagen: ${up.status} ${await up.text()}`);
|
|
|
|
// keep the release title + versionCode body in sync
|
|
await api(`/releases/${rel.id}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: `NovaSonic ${version}`, body: relBody }),
|
|
}).catch(() => {});
|
|
|
|
console.log(`\n✔ NovaSonic ${version} hochgeladen → Release "${TAG}". Der Web-Button serviert jetzt diese APK.`);
|