#!/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 { statSync } from "node:fs"; import { execFileSync } from "node:child_process"; 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])); for (const file of files) { const name = basename(file); const filePath = join(distDir, file); const size = statSync(filePath).size; const already = existing.get(name); // Resume: skip assets already uploaded at the same size (lets a re-run // finish where it left off instead of re-uploading everything). if (already && already.size === size) { console.log(`Skipping ${name} (already uploaded)`); continue; } if (already) { await api(`/repos/${OWNER}/${REPO}/releases/${release.id}/assets/${already.id}`, { method: "DELETE" }); } process.stdout.write(`Uploading ${name} (${(size / 1e6).toFixed(0)} MB)... `); // Upload via curl: Node's built-in fetch (undici) times out on large files // (UND_ERR_HEADERS_TIMEOUT). curl streams from disk with no such limit. execFileSync( "curl", [ "-sS", "--fail-with-body", "-X", "POST", "-H", `Authorization: token ${token}`, "-F", `attachment=@${filePath}`, `${API}/repos/${OWNER}/${REPO}/releases/${release.id}/assets?name=${encodeURIComponent(name)}`, ], { stdio: ["ignore", "ignore", "inherit"] }, ); console.log("done"); } console.log(`\nDone. Updater feed: https://codeberg.org/${OWNER}/${REPO}/releases/latest/download/latest.yml`); } await main();