feat(android): in-app updater (backend/Gitea APK), auto-bump versionCode

The app checks {backend}/api/apk/info on launch and, if a newer versionCode
than BuildConfig.VERSION_CODE is offered, shows an update dialog that
downloads {backend}/api/apk into a single wiped-each-time cache file and
hands it to the system installer (FileProvider + REQUEST_INSTALL_PACKAGES).
No APK hoarding; in-place install preserves data (stable signing key needed).

- backend /api/apk/info now returns versionCode (parsed from the release body).
- publish-apk.mjs auto-increments versionCode, sets versionName, and writes
  versionCode/versionName into the Gitea release body.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-09-02 00:46:11 +02:00
1 parent 845f125d76
commit 2fd4d978d0
7 files changed
+175 -14

No files matched your search

+22 -11
View File
@@ -17,7 +17,7 @@
// install over each other. Add a keystore + signingConfig later to switch APK_GRADLE_TASK=assembleRelease.
import { spawnSync } from 'node:child_process';
import { readFileSync, existsSync } from 'node:fs';
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
@@ -45,13 +45,24 @@ if (!GITEA_URL || !GITEA_REPO || !GITEA_TOKEN)
const API = `${GITEA_URL}/api/v1/repos/${GITEA_REPO}`;
const authHeaders = { Authorization: `token ${GITEA_TOKEN}` };
// ── version name from the Gradle config (for the release title) ──────────────
let version = new Date().toISOString().slice(0, 10);
try {
const g = readFileSync(join(root, 'android', 'app', 'build.gradle.kts'), 'utf8');
const m = g.match(/versionName\s*=\s*"([^"]+)"/);
if (m) version = m[1];
} catch {}
// ── 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';
@@ -86,7 +97,7 @@ let rel;
const cr = await api('/releases', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tag_name: TAG, target: 'main', name: `NovaSonic ${version}`, body: 'NovaSonic Android App — neueste Version.' }),
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();
@@ -108,11 +119,11 @@ form.append('attachment', new Blob([apkBytes], { type: 'application/vnd.android.
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 in sync with the version
// 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: JSON.stringify({ name: `NovaSonic ${version}`, body: relBody }),
}).catch(() => {});
console.log(`\n✔ NovaSonic ${version} hochgeladen → Release "${TAG}". Der Web-Button serviert jetzt diese APK.`);