#!/usr/bin/env node /** * Publie les artefacts du launcher sur deux releases Gitea : * * 1. Release à tag fixe "latest" — electron-updater (provider generic) lit * latest.yml à cette URL pour proposer les mises à jour automatiques. * * 2. Release versionnée "vX.Y.Z" — archive permanente consultable dans l'UI * Gitea, utile pour le suivi des versions et les téléchargements manuels. * * Config par variables d'env : * GITEA_URL base de l'instance (def. https://gitea.ldpt.fr) * GITEA_OWNER propriétaire du repo (def. zertus) * GITEA_REPO nom du repo launcher (def. OFLauncher) * GITEA_TOKEN token write:repository (obligatoire) */ import { readdir, readFile } from 'node:fs/promises' import { join } from 'node:path' const BASE = process.env.GITEA_URL ?? 'https://gitea.ldpt.fr' const OWNER = process.env.GITEA_OWNER ?? 'zertus' const REPO = process.env.GITEA_REPO ?? 'OFLauncher' const TOKEN = process.env.GITEA_TOKEN const DIST = join(process.cwd(), 'dist') const API = `${BASE}/api/v1` if (!TOKEN) { console.error('GITEA_TOKEN manquant (scope write:repository).') process.exit(1) } /** Lit la version courante depuis package.json. */ async function readVersion() { const pkg = JSON.parse(await readFile(join(process.cwd(), 'package.json'), 'utf8')) return pkg.version // ex. "3.1.0" } /** * Fichiers de dist/ à publier. * - Windows : latest.yml + installeur NSIS (+ .blockmap) * - Linux : latest-linux.yml + AppImage (+ .blockmap) + .deb */ function isUpdateArtifact(name) { return ( name === 'latest.yml' || name === 'latest-linux.yml' || name.endsWith('-setup.exe') || name.endsWith('-setup.exe.blockmap') || name.endsWith('.AppImage') || name.endsWith('.AppImage.blockmap') || name.endsWith('.deb') ) } async function api(path, init = {}) { const res = await fetch(`${API}${path}`, { ...init, headers: { Authorization: `token ${TOKEN}`, Accept: 'application/json', ...(init.headers ?? {}) } }) if (!res.ok) { const body = await res.text().catch(() => '') throw new Error(`Gitea ${init.method ?? 'GET'} ${path} → ${res.status} ${body}`) } return res.status === 204 ? null : res.json() } /** Récupère la release par tag, ou la crée avec les options données. */ async function getOrCreateRelease(tag, createBody) { const res = await fetch(`${API}/repos/${OWNER}/${REPO}/releases/tags/${tag}`, { headers: { Authorization: `token ${TOKEN}`, Accept: 'application/json' } }) if (res.ok) return res.json() if (res.status !== 404) { throw new Error(`Gitea GET release/${tag} → ${res.status} ${await res.text()}`) } console.log(`Création de la release "${tag}"…`) return api(`/repos/${OWNER}/${REPO}/releases`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(createBody) }) } async function deleteExistingAssets(releaseId) { const assets = await api(`/repos/${OWNER}/${REPO}/releases/${releaseId}/assets`) for (const a of assets) { console.log(` Suppression de l'ancien asset "${a.name}"…`) await api(`/repos/${OWNER}/${REPO}/releases/${releaseId}/assets/${a.id}`, { method: 'DELETE' }) } } async function uploadAssets(releaseId, files) { for (const name of files) { const buf = await readFile(join(DIST, name)) const form = new FormData() form.append('attachment', new Blob([buf]), name) console.log(` Upload "${name}" (${(buf.length / 1e6).toFixed(1)} Mo)…`) await api( `/repos/${OWNER}/${REPO}/releases/${releaseId}/assets?name=${encodeURIComponent(name)}`, { method: 'POST', body: form } ) } } async function publishRelease(tag, createBody, files) { console.log(`\n── Release "${tag}" ──`) const release = await getOrCreateRelease(tag, createBody) await deleteExistingAssets(release.id) await uploadAssets(release.id, files) console.log(` ✓ Release "${tag}" publiée.`) } async function main() { const version = await readVersion() const versionTag = `v${version}` const files = (await readdir(DIST)).filter(isUpdateArtifact) if (!files.some((f) => f === 'latest.yml')) { throw new Error('dist/latest.yml introuvable — lance d'abord `npm run build:win`.') } console.log(`Version : ${versionTag}`) console.log(`Artefacts : ${files.join(', ')}`) console.log(`Dépôt : ${OWNER}/${REPO}`) // 1. Release fixe "latest" — point d'ancrage de l'auto-update. await publishRelease( 'latest', { tag_name: 'latest', name: 'Auto-update (dernière version)', body: 'Artefacts d'auto-update écrasés à chaque publication. Version courante : ' + versionTag, prerelease: false }, files ) // 2. Release versionnée — archive permanente par version. await publishRelease( versionTag, { tag_name: versionTag, name: `OFLauncher ${versionTag}`, body: '', prerelease: false }, files ) console.log('\n✓ Publication terminée.') } main().catch((e) => { console.error(e.message) process.exit(1) })