#!/usr/bin/env node // Create a Gitea release and upload an asset. // Usage: // node scripts/gitea-release.mjs --tag v1.1.1 --name "Q-Edit v1.1.1" \ // --notes docs/RELEASE_NOTES_v1.1.1.md --asset q-edit_dist.zip // Env: // GITEA_TOKEN (required) // GITEA_BASE (optional, default parsed from git remote origin; fallback https://gitea.qortal.link) import { execSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; function parseArgs(argv) { const out = {}; for (let i = 2; i < argv.length; i++) { const a = argv[i]; if (a === "--tag") { out.tag = argv[++i]; } else if (a === "--name") { out.name = argv[++i]; } else if (a === "--notes") { out.notes = argv[++i]; } else if (a === "--asset") { out.asset = argv[++i]; } } return out; } function getOrigin() { try { const s = execSync("git config --get remote.origin.url", { encoding: "utf8" }).trim(); return s; } catch { return ""; } } function deriveRepo(originUrl) { try { const u = new URL(originUrl); const parts = u.pathname .replace(/\.git$/, "") .split("/") .filter(Boolean); const owner = parts[0]; const repo = parts[1]; const base = `${u.protocol}//${u.host}`; return { base, owner, repo }; } catch { return { base: "https://gitea.qortal.link", owner: "", repo: "" }; } } async function main() { const { tag, name, notes, asset } = parseArgs(process.argv); if (!tag || !name) { console.error("Usage: --tag vX.Y.Z --name 'Q-Edit vX.Y.Z' --notes [--asset ]"); process.exit(1); } const token = process.env.GITEA_TOKEN; if (!token) { console.error("GITEA_TOKEN env var is required to create a release."); process.exit(2); } const origin = getOrigin(); const parsed = deriveRepo(origin); const base = process.env.GITEA_BASE || parsed.base || "https://gitea.qortal.link"; const owner = process.env.GITEA_OWNER || parsed.owner; const repo = process.env.GITEA_REPO || parsed.repo; if (!owner || !repo) { console.error( "Could not determine owner/repo from git remote. Set GITEA_OWNER and GITEA_REPO." ); process.exit(3); } const body = notes ? fs.readFileSync(notes, "utf8") : ""; const createUrl = `${base}/api/v1/repos/${owner}/${repo}/releases`; const createRes = await fetch(createUrl, { method: "POST", headers: { "content-type": "application/json", Authorization: `token ${token}`, }, body: JSON.stringify({ tag_name: tag, name, body, draft: false, prerelease: false, }), }); if (!createRes.ok) { const t = await createRes.text(); console.error("Failed to create release:", createRes.status, t); process.exit(4); } const rel = await createRes.json(); console.log("Created release:", rel.html_url || rel.url || rel.id); if (asset && fs.existsSync(asset)) { const st = fs.statSync(asset); const assetUrl = `${base}/api/v1/repos/${owner}/${repo}/releases/${rel.id}/assets?name=${encodeURIComponent(path.basename(asset))}`; const buf = fs.readFileSync(asset); const blob = new Blob([buf], { type: "application/zip" }); const fd = new FormData(); fd.append("attachment", blob, path.basename(asset)); const upRes = await fetch(assetUrl, { method: "POST", headers: { Authorization: `token ${token}` }, body: fd, }); if (!upRes.ok) { const t = await upRes.text(); console.error("Failed to upload asset:", upRes.status, t); process.exit(5); } console.log(`Uploaded asset ${path.basename(asset)} (${st.size} bytes).`); } else if (asset) { console.warn(`Asset not found: ${asset} (skipping upload)`); } } main().catch((e) => { console.error("gitea-release failed:", e); process.exit(99); });