Files
xplane-cockpit/scripts/release-gitea.mjs
T
karim ebc33a78b7 Initial commit: X-Plane G1000 web cockpit + bridge + Tauri desktop app
- server/: Node bridge (datarefs/commands, navdata, CIFP procedures, flight plan)
- web/: React cockpit (PFD/MFD/Map, VFR six-pack, AFCS, FMS CDU), PWA, collapsible sidebar
- desktop/: Tauri 2 launcher (Bun sidecar, system tray, updater) + Linux build via Docker
- scripts/: prep-desktop, build-linux, Gitea release + latest.json

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:07:03 +02:00

120 lines
5.5 KiB
JavaScript

#!/usr/bin/env node
// Publish a release to Gitea and (re)write the updater's latest.json.
//
// Reads the built artifacts (macOS .app.tar.gz + .sig, Linux .AppImage + .sig,
// plus .dmg / .deb for manual install), creates a versioned release with those
// assets, then builds latest.json (Tauri v2 updater format) pointing at the
// release download URLs and uploads it to a fixed-tag "updater" release so the
// app's updater endpoint URL stays constant.
//
// Env: GITEA_URL (e.g. https://git.kgva.ch), GITEA_REPO (owner/name),
// GITEA_TOKEN (or a token file at /tmp/gitea_token).
import fs from 'node:fs';
import path from 'node:path';
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
const GITEA_URL = (process.env.GITEA_URL || 'https://git.kgva.ch').replace(/\/$/, '');
const REPO = process.env.GITEA_REPO || 'karim/xplane-cockpit';
const TOKEN = process.env.GITEA_TOKEN
|| (fs.existsSync('/tmp/gitea_token') ? fs.readFileSync('/tmp/gitea_token', 'utf8').trim() : '');
const VERSION = JSON.parse(fs.readFileSync(path.join(ROOT, 'desktop/src-tauri/tauri.conf.json'), 'utf8')).version;
const UPDATER_TAG = 'updater';
if (!TOKEN) { console.error('No GITEA_TOKEN (env or /tmp/gitea_token).'); process.exit(1); }
const api = (p, opts = {}) => fetch(`${GITEA_URL}/api/v1${p}`, {
...opts,
headers: { Authorization: `token ${TOKEN}`, Accept: 'application/json', ...(opts.headers || {}) },
});
// Collect built artifacts that exist (mac and/or linux builds may have run).
function findArtifacts() {
const macBundle = path.join(ROOT, 'desktop/src-tauri/target/aarch64-apple-darwin/release/bundle');
const linBundle = path.join(ROOT, 'target-linux/x86_64-unknown-linux-gnu/release/bundle');
const out = { assets: [], updater: {} };
const add = (file, platformKey) => {
if (!fs.existsSync(file)) return;
out.assets.push(file);
if (platformKey) {
const sig = file + '.sig';
if (fs.existsSync(sig)) out.updater[platformKey] = { file, sig: fs.readFileSync(sig, 'utf8').trim() };
}
};
const glob1 = (dir, re) => fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => re.test(f)).map((f) => path.join(dir, f)) : [];
// macOS
glob1(path.join(macBundle, 'macos'), /\.app\.tar\.gz$/).forEach((f) => add(f, 'darwin-aarch64'));
glob1(path.join(macBundle, 'dmg'), /\.dmg$/).forEach((f) => out.assets.push(f));
// Linux
glob1(path.join(linBundle, 'appimage'), /\.AppImage$/).forEach((f) => add(f, 'linux-x86_64'));
glob1(path.join(linBundle, 'deb'), /\.deb$/).forEach((f) => out.assets.push(f));
return out;
}
async function getReleaseByTag(tag) {
const r = await api(`/repos/${REPO}/releases/tags/${encodeURIComponent(tag)}`);
return r.ok ? r.json() : null;
}
async function ensureRelease(tag, name, body) {
let rel = await getReleaseByTag(tag);
if (rel) return rel;
const r = await api(`/repos/${REPO}/releases`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tag_name: tag, name, body, draft: false, prerelease: false }),
});
if (!r.ok) throw new Error(`create release ${tag}: ${r.status} ${await r.text()}`);
return r.json();
}
async function uploadAsset(rel, file, asName) {
const name = asName || path.basename(file);
// delete an existing asset with the same name first (Gitea won't overwrite)
for (const a of rel.assets || []) {
if (a.name === name) await api(`/repos/${REPO}/releases/${rel.id}/assets/${a.id}`, { method: 'DELETE' });
}
const blob = new Blob([fs.readFileSync(file)]);
const fd = new FormData();
fd.append('attachment', blob, name);
const r = await api(`/repos/${REPO}/releases/${rel.id}/assets?name=${encodeURIComponent(name)}`, { method: 'POST', body: fd });
if (!r.ok) throw new Error(`upload ${name}: ${r.status} ${await r.text()}`);
console.log(' ↑', name);
return r.json();
}
const dlUrl = (tag, name) => `${GITEA_URL}/${REPO}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(name)}`;
async function main() {
const { assets, updater } = findArtifacts();
if (!assets.length) { console.error('No build artifacts found — run the builds first.'); process.exit(1); }
console.log(`Release v${VERSION}${GITEA_URL}/${REPO}`);
console.log('Artifacts:', assets.map((a) => path.basename(a)).join(', '));
const verTag = `v${VERSION}`;
const rel = await ensureRelease(verTag, `X-Plane Cockpit ${verTag}`, `Automated release ${verTag}.`);
for (const f of assets) await uploadAsset(rel, f);
const relFresh = await getReleaseByTag(verTag); // refresh asset list
// Build latest.json referencing this release's updater artifacts.
const platforms = {};
for (const [key, { file, sig }] of Object.entries(updater)) {
platforms[key] = { signature: sig, url: dlUrl(verTag, path.basename(file)) };
}
const latest = {
version: VERSION,
notes: `X-Plane Cockpit ${verTag}`,
pub_date: new Date().toISOString(),
platforms,
};
const latestPath = path.join(ROOT, 'desktop/latest.json');
fs.writeFileSync(latestPath, JSON.stringify(latest, null, 2));
console.log('latest.json platforms:', Object.keys(platforms).join(', ') || '(none)');
// Upload latest.json to the fixed "updater" release (constant endpoint URL).
const upd = await ensureRelease(UPDATER_TAG, 'Updater channel', 'Rolling pointer used by the in-app updater.');
await uploadAsset(upd, latestPath, 'latest.json');
console.log('Updater endpoint:', dlUrl(UPDATER_TAG, 'latest.json'));
}
main().catch((e) => { console.error(e); process.exit(1); });