import fs from "node:fs"; import path from "node:path"; import { spawnSync } from "node:child_process"; const NPM_EXECUTABLE = process.platform === "win32" ? "npm.cmd" : "npm"; const BASE_URL = String(process.env.GITEA_BASE_URL || "https://git.24-music.de").replace(/\/+$/, ""); const OWNER = String(process.env.GITEA_REPO_OWNER || "Administrator").trim(); const REPO = String(process.env.GITEA_REPO_NAME || "Twitch-VOD-Manager").trim(); function run(command, args, options = {}) { const result = spawnSync(command, args, { cwd: process.cwd(), encoding: "utf8", input: options.input, stdio: options.capture ? ["pipe", "pipe", "pipe"] : "inherit" }); if (result.status !== 0) { const stderr = String(result.stderr || "").trim(); const stdout = String(result.stdout || "").trim(); const details = [stderr, stdout].filter(Boolean).join("\n"); throw new Error(`Command failed: ${command} ${args.join(" ")}${details ? `\n${details}` : ""}`); } return String(result.stdout || ""); } function parseArgs(argv) { const args = argv.slice(2); if (args.includes("--help") || args.includes("-h")) { return { help: true }; } const dryRun = args.includes("--dry-run"); const version = args.find((arg) => arg !== "--dry-run") || ""; const notes = args.filter((arg) => arg !== "--dry-run").slice(1).join(" ").trim(); return { help: false, dryRun, version, notes }; } function ensureVersion(version) { if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(String(version || "").trim())) { throw new Error("Invalid version format. Expected e.g. 4.2.0"); } return String(version).trim(); } function getAuthHeader() { const token = String(process.env.GITEA_TOKEN || process.env.FORGEJO_TOKEN || "").trim(); if (token) { return `token ${token}`; } const output = run("git", ["credential", "fill"], { capture: true, input: `protocol=https\nhost=${new URL(BASE_URL).host}\n\n` }); const map = new Map(); for (const line of output.split(/\r?\n/)) { if (!line.includes("=")) continue; const [key, value] = line.split("=", 2); map.set(key, value); } const username = map.get("username"); const password = map.get("password"); if (!username || !password) { throw new Error("Missing Gitea credentials. Set GITEA_TOKEN or configure git credential helper."); } return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`; } async function apiRequest(method, url, authHeader, body, contentType = "application/json") { const headers = { Accept: "application/json", Authorization: authHeader }; if (body !== undefined) headers["Content-Type"] = contentType; const response = await fetch(url, { method, headers, body }); const text = await response.text(); let parsed = null; try { parsed = text ? JSON.parse(text) : null; } catch { parsed = text; } return { ok: response.ok, status: response.status, body: parsed }; } function ensureAssets(version) { const releaseDir = path.join(process.cwd(), "release"); const files = [ `Twitch-VOD-Manager-Setup-${version}.exe`, `Twitch-VOD-Manager-Setup-${version}.exe.blockmap`, "latest.yml" ]; for (const file of files) { const fullPath = path.join(releaseDir, file); if (!fs.existsSync(fullPath)) { throw new Error(`Missing release artifact: ${fullPath}`); } } return { releaseDir, files }; } async function createOrGetRelease(baseApi, tag, authHeader, notes) { const existing = await apiRequest("GET", `${baseApi}/releases/tags/${encodeURIComponent(tag)}`, authHeader); if (existing.ok) return existing.body; const payload = { tag_name: tag, target_commitish: "main", name: tag, body: notes || `Release ${tag}`, draft: false, prerelease: false }; const created = await apiRequest("POST", `${baseApi}/releases`, authHeader, JSON.stringify(payload)); if (!created.ok) { throw new Error(`Failed to create release (${created.status}): ${JSON.stringify(created.body)}`); } return created.body; } async function uploadAssets(baseApi, releaseId, authHeader, releaseDir, files) { for (const fileName of files) { const filePath = path.join(releaseDir, fileName); const fileData = fs.readFileSync(filePath); const uploadUrl = `${baseApi}/releases/${releaseId}/assets?name=${encodeURIComponent(fileName)}`; const response = await apiRequest("POST", uploadUrl, authHeader, fileData, "application/octet-stream"); if (response.ok || response.status === 409 || response.status === 422) { continue; } throw new Error(`Asset upload failed for ${fileName} (${response.status}): ${JSON.stringify(response.body)}`); } } async function main() { const args = parseArgs(process.argv); if (args.help) { process.stdout.write("Usage: npm run release:gitea -- [release notes] [--dry-run]\n"); process.stdout.write("Env: GITEA_BASE_URL, GITEA_REPO_OWNER, GITEA_REPO_NAME, GITEA_TOKEN\n"); return; } const version = ensureVersion(args.version); const tag = `v${version}`; const authHeader = getAuthHeader(); const baseApi = `${BASE_URL}/api/v1/repos/${OWNER}/${REPO}`; run("git", ["fetch", "--tags"]); if (!args.dryRun) { run("git", ["push", "origin", "main"]); run("git", ["push", "origin", tag]); } run(NPM_EXECUTABLE, ["run", "dist:win"]); const assets = ensureAssets(version); if (args.dryRun) { process.stdout.write(`Dry run complete for ${tag}\n`); return; } const release = await createOrGetRelease(baseApi, tag, authHeader, args.notes); await uploadAssets(baseApi, release.id, authHeader, assets.releaseDir, assets.files); process.stdout.write(`Release published: ${release.html_url || `${BASE_URL}/${OWNER}/${REPO}/releases/tag/${tag}`}\n`); } main().catch((error) => { process.stderr.write(`${String(error?.message || error)}\n`); process.exit(1); });