#!/usr/bin/env node /* * Upload built desktop artifacts to a Codeberg (Forgejo) release so electron-updater * (generic provider -> .../releases/latest/download/) can find them. * * Run AFTER `pnpm run build` from apps/desktop: * CODEBERG_TOKEN=xxxx node scripts/release-codeberg.mjs [tag] * * - owner/repo are fixed to Enkisu/Blap (must match the electron-builder publish url). * - tag defaults to "v" from package.json. * - Uploads latest.yml / latest-linux.yml + installers (.exe/.AppImage) + blockmaps. * - Replaces assets of the same name so re-runs are idempotent. * - The release is created non-draft / non-prerelease so Forgejo's "latest" alias * resolves to it (that's what the updater feed URL points at). */ import { readFile, readdir } from "node:fs/promises"; import { basename, join } from "node:path"; import { fileURLToPath } from "node:url"; const OWNER = "Enkisu"; const REPO = "Blap"; const API = "https://codeberg.org/api/v1"; const token = process.env.CODEBERG_TOKEN; if (!token) { console.error("CODEBERG_TOKEN env var is required (a Codeberg access token with repo scope)."); process.exit(1); } const here = fileURLToPath(new URL(".", import.meta.url)); const pkg = JSON.parse(await readFile(join(here, "..", "package.json"), "utf8")); const tag = process.argv[2] ?? `v${pkg.version}`; const distDir = join(here, "..", "dist"); const authHeaders = { Authorization: `token ${token}` }; // Channel files electron-updater needs + the installers themselves. // electron-updater channel files (always the current build) — uploaded regardless. const CHANNEL_FILES = new Set(["latest.yml", "latest-linux.yml"]); // Installers for this version. The version filter avoids grabbing leftover // artifacts from previous builds sitting in dist/. const INSTALLER_EXT = /\.(exe|exe\.blockmap|deb|flatpak|pacman|tar\.gz)$/; async function api(path, init = {}) { const res = await fetch(`${API}${path}`, { ...init, headers: { ...authHeaders, ...(init.headers ?? {}) } }); if (!res.ok && res.status !== 404) { throw new Error(`${init.method ?? "GET"} ${path} -> ${res.status} ${await res.text()}`); } return res; } async function getOrCreateRelease() { let res = await api(`/repos/${OWNER}/${REPO}/releases/tags/${encodeURIComponent(tag)}`); if (res.status === 404) { res = await api(`/repos/${OWNER}/${REPO}/releases`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tag_name: tag, name: tag, draft: false, prerelease: false }), }); console.log(`Created release ${tag}`); } else { console.log(`Reusing existing release ${tag}`); } return res.json(); } async function main() { const version = pkg.version; const files = (await readdir(distDir)).filter( (f) => CHANNEL_FILES.has(f) || (f.includes(version) && INSTALLER_EXT.test(f)), ); if (files.length === 0) { console.error(`No matching artifacts in ${distDir}. Run \`pnpm run build\` first.`); process.exit(1); } const release = await getOrCreateRelease(); const existing = new Map((release.assets ?? []).map((a) => [a.name, a.id])); for (const file of files) { const name = basename(file); if (existing.has(name)) { await api(`/repos/${OWNER}/${REPO}/releases/${release.id}/assets/${existing.get(name)}`, { method: "DELETE", }); } const data = await readFile(join(distDir, file)); const form = new FormData(); form.append("attachment", new Blob([data]), name); const res = await fetch( `${API}/repos/${OWNER}/${REPO}/releases/${release.id}/assets?name=${encodeURIComponent(name)}`, { method: "POST", headers: authHeaders, body: form }, ); if (!res.ok) throw new Error(`upload ${name} -> ${res.status} ${await res.text()}`); console.log(`Uploaded ${name}`); } console.log(`\nDone. Updater feed: https://codeberg.org/${OWNER}/${REPO}/releases/latest/download/latest.yml`); } await main();