feat(scripts): one-command APK build + upload to the fixed Gitea release
`npm run publish-apk` builds the APK (assembleDebug by default — the release build type is unsigned) and uploads it to the apk-latest Gitea release, replacing the old .apk asset, so the web download button always serves the latest build. Config via env or a gitignored scripts/.env.release (GITEA_URL/REPO/TOKEN, APK_GRADLE_TASK). Release title tracks versionName. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
33d24c6ed4
commit
3000dc1622
3 files changed
+121
-1
No files matched your search
@@ -0,0 +1,118 @@
|
||||
#!/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, 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';
|
||||
|
||||
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}` };
|
||||
|
||||
// ── 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 {}
|
||||
|
||||
// ── 1) build ─────────────────────────────────────────────────────────────────
|
||||
const isWin = process.platform === 'win32';
|
||||
const gradlew = join(root, 'android', isWin ? 'gradlew.bat' : 'gradlew');
|
||||
console.log(`▶ Baue APK: ${TASK} …`);
|
||||
const build = spawnSync(gradlew, [`:app:${TASK}`], {
|
||||
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: 'NovaSonic Android App — neueste Version.' }),
|
||||
});
|
||||
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 in sync with the version
|
||||
await api(`/releases/${rel.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: `NovaSonic ${version}` }),
|
||||
}).catch(() => {});
|
||||
|
||||
console.log(`\n✔ NovaSonic ${version} hochgeladen → Release "${TAG}". Der Web-Button serviert jetzt diese APK.`);
|
||||
Reference in new issue
Block a user