#!/usr/bin/env node /** * Release script for tau * * Usage: node scripts/release.mjs * e.g. node scripts/release.mjs v0.0.4 * * What it does: * 1. Compute nix hashes from current HEAD * 2. Update flake.nix with rev + hashes * 3. Commit "Release ", create tag * 4. Push to origin * 5. Create Codeberg release * * Requires: CODEBERG_TOKEN (env or .env file), nix */ import { execSync } from "child_process"; import { readFileSync, writeFileSync, existsSync } from "fs"; const TAG = process.argv[2]; if (!TAG || !TAG.startsWith("v")) { console.error("Usage: node scripts/release.mjs (must use vx.x.x format)"); process.exit(1); } // Load .env if present if (existsSync(".env")) { for (const line of readFileSync(".env", "utf-8").split("\n")) { const m = line.match(/^\s*([^#=\s][^=]*)=(.*)$/); if (m) process.env[m[1].trim()] ??= m[2].trim().replace(/^['"]|['"]$/g, ""); } } const CODEBERG_TOKEN = process.env.CODEBERG_TOKEN; if (!CODEBERG_TOKEN) { console.error("Error: CODEBERG_TOKEN not set (checked env and .env)"); process.exit(1); } // Check nix available try { execSync("nix --version", { stdio: "pipe" }); } catch { console.error("Error: nix not in PATH"); process.exit(1); } function run(cmd, silent = false) { console.log(`$ ${cmd}`); return execSync(cmd, { encoding: "utf-8", stdio: silent ? "pipe" : "inherit" }); } function runSilent(cmd) { return execSync(cmd, { encoding: "utf-8", stdio: "pipe" }).trim(); } function getRepoUrl() { const remote = runSilent("git remote get-url origin"); // ssh://git@codeberg.org/owner/repo.git -> https://codeberg.org/owner/repo.git const m = remote.match(/codeberg\.org[:/](.+?)(?:\.git)?$/); if (!m) { console.error(`Can't parse Codeberg URL from: ${remote}`); process.exit(1); } return { slug: m[1], url: `https://codeberg.org/${m[1]}.git` }; } function computeHashes(rev) { const cwd = process.cwd(); console.log(`\nComputing hashes for ${rev}...`); const json = runSilent(`nix run nixpkgs#nix-prefetch-git -- --url "file://${cwd}" --rev ${rev} --quiet`); const srcHash = runSilent(`nix hash convert --hash-algo sha256 --to sri ${JSON.parse(json).sha256}`); console.log(` src: ${srcHash}`); const npmHash = runSilent("nix run nixpkgs#prefetch-npm-deps -- package-lock.json 2>/dev/null"); console.log(` npm: ${npmHash}`); return { srcHash, npmHash }; } function updateFlake(rev, srcHash, npmHash, repoUrl) { if (!existsSync("flake.nix")) return; let f = readFileSync("flake.nix", "utf-8"); f = f.replace(/url = "[^"]*";/, `url = "${repoUrl}";`); f = f.replace(/rev = "[^"]*";/, `rev = "${rev}";`); f = f.replace(/hash = "[^"]*";[^\n]*/, `hash = "${srcHash}";`); f = f.replace(/npmDepsHash = "[^"]*";[^\n]*/, `npmDepsHash = "${npmHash}";`); writeFileSync("flake.nix", f); console.log(`\nUpdated flake.nix`); } async function createRelease(slug, tag, commitish) { const res = await fetch(`https://codeberg.org/api/v1/repos/${slug}/releases`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `token ${CODEBERG_TOKEN}` }, body: JSON.stringify({ tag_name: tag, name: tag, target_commitish: commitish }), }); if (!res.ok) { console.error(`Codeberg API error: ${res.status} ${await res.text()}`); process.exit(1); } console.log(`\nRelease created: ${(await res.json()).html_url}`); } // --- Main --- console.log(`\n=== Release ${TAG} ===`); // Check clean const status = runSilent("git status --porcelain"); if (status) { console.error("Uncommitted changes:\n" + status); process.exit(1); } const { slug, url } = getRepoUrl(); const rev = runSilent("git rev-parse HEAD"); const { srcHash, npmHash } = computeHashes(rev); updateFlake(rev, srcHash, npmHash, url); // Commit + tag run("git add flake.nix"); run(`git commit -m "Release ${TAG}"`); run(`git tag ${TAG}`); // Push const branch = runSilent("git rev-parse --abbrev-ref HEAD"); run(`git push origin ${branch}`); run(`git push origin ${TAG}`); // Codeberg release const releaseCommit = runSilent("git rev-parse HEAD"); await createRelease(slug, TAG, releaseCommit); console.log(`\n=== Done: ${TAG} ===`);