8a26cdca8c
Forgejo/Codeberg 404s the GitHub-style latest/download path, so the feed baked into app-update.yml never worked (1.0.0/1.0.1 never saw updates). Point it at a rolling release on the fixed tag "updates" instead, and make release-codeberg.mjs sync latest.yml + the Windows installer to that release on every ship (channel files always replaced; curl retries added). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
140 lines
5.9 KiB
JavaScript
140 lines
5.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/*
|
|
* Upload built desktop artifacts to a Gitea/Forgejo release (Codeberg IS Forgejo, and
|
|
* git.utn.lol is Gitea — same API) so electron-updater (generic provider ->
|
|
* .../releases/latest/download/) can find them.
|
|
*
|
|
* Run AFTER `pnpm run build` from apps/desktop. Defaults target Codeberg:
|
|
* CODEBERG_TOKEN=xxxx node scripts/release-codeberg.mjs [tag]
|
|
* Target a different forge (e.g. the git.utn.lol Gitea) via env overrides:
|
|
* FORGE_API=https://git.utn.lol/api/v1 FORGE_OWNER=enki FORGE_REPO=blap \
|
|
* FORGE_TOKEN=xxxx node scripts/release-codeberg.mjs [tag]
|
|
*
|
|
* - tag defaults to "v<version>" from package.json.
|
|
* - Uploads latest.yml / latest-linux.yml + installers + blockmaps.
|
|
* - Replaces assets of the same name so re-runs are idempotent.
|
|
* - The release is created non-draft / non-prerelease so the "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 = process.env.FORGE_OWNER ?? "Enkisu";
|
|
const REPO = process.env.FORGE_REPO ?? "Blap";
|
|
const API = process.env.FORGE_API ?? "https://codeberg.org/api/v1";
|
|
|
|
const token = process.env.FORGE_TOKEN ?? process.env.CODEBERG_TOKEN;
|
|
if (!token) {
|
|
console.error("A token is required: set FORGE_TOKEN (or CODEBERG_TOKEN) to an 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(tagName, { prerelease = false } = {}) {
|
|
let res = await api(`/repos/${OWNER}/${REPO}/releases/tags/${encodeURIComponent(tagName)}`);
|
|
if (res.status === 404) {
|
|
res = await api(`/repos/${OWNER}/${REPO}/releases`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ tag_name: tagName, name: tagName, draft: false, prerelease }),
|
|
});
|
|
console.log(`Created release ${tagName}`);
|
|
} else {
|
|
console.log(`Reusing existing release ${tagName}`);
|
|
}
|
|
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(tag);
|
|
await syncAssets(release, files);
|
|
|
|
// Forgejo/Codeberg has NO GitHub-style /releases/latest/download/ route, so
|
|
// app-update.yml points at a rolling release on the fixed tag "updates" —
|
|
// re-point its channel file + Windows installer at the build we just shipped.
|
|
const updaterFiles = files.filter((f) => f === "latest.yml" || /\.(exe|exe\.blockmap)$/.test(f));
|
|
const updates = await getOrCreateRelease("updates", { prerelease: true });
|
|
await syncAssets(updates, updaterFiles);
|
|
|
|
const webBase = API.replace(/\/api\/v1\/?$/, "");
|
|
console.log(`\nDone. Updater feed: ${webBase}/${OWNER}/${REPO}/releases/download/updates/latest.yml`);
|
|
}
|
|
|
|
async function syncAssets(release, files) {
|
|
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). Channel
|
|
// files are tiny and change meaning per build — always replace those.
|
|
if (already && already.size === size && !CHANNEL_FILES.has(name)) {
|
|
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",
|
|
"--retry",
|
|
"4",
|
|
"--retry-all-errors",
|
|
"-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");
|
|
}
|
|
}
|
|
|
|
await main();
|