diff --git a/scripts/release.js b/scripts/release.js new file mode 100755 index 0000000..406f35f --- /dev/null +++ b/scripts/release.js @@ -0,0 +1,99 @@ +#!/usr/bin/env node +// Minimal safe release script. +// Usage: node scripts/release.js [major|minor|patch|] +const { execSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +const root = path.resolve(__dirname, ".."); +const pkgPath = path.join(root, "package.json"); + +function run(cmd, opts = {}) { + return execSync(cmd, { stdio: "inherit", cwd: root, ...opts }); +} +function runOutput(cmd) { + return execSync(cmd, { cwd: root }).toString().trim(); +} +function bumpSemver(current, spec) { + if (["major","minor","patch"].includes(spec)) { + const [maj, min, patch] = current.split(".").map(n=>parseInt(n,10)); + if (spec==="major") return `${maj+1}.0.0`; + if (spec==="minor") return `${maj}.${min+1}.0`; + return `${maj}.${min}.${patch+1}`; + } + if (!/^\d+\.\d+\.\d+$/.test(spec)) throw new Error("Invalid version spec"); + return spec; +} + +(async () => { + const arg = process.argv[2] || "patch"; + const pkgRaw = fs.readFileSync(pkgPath, "utf8"); + const pkg = JSON.parse(pkgRaw); + const oldVersion = pkg.version; + const newVersion = bumpSemver(oldVersion, arg); + let committed = false; + let tagged = false; + let pushedTags = false; + try { + // refuse to run if there are unstaged/uncommitted changes + const status = runOutput("git status --porcelain"); + if (status) throw new Error("Repository has uncommitted changes; please commit or stash before releasing."); + + console.log("Running tests..."); + run("npm run test:ci"); + + console.log("Building..."); + run("npm run build"); + + // write new version + pkg.version = newVersion; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8"); + console.log(`Bumped version: ${oldVersion} -> ${newVersion}`); + + // commit + run(`git add ${pkgPath}`); + run(`git commit -m "chore(release): v${newVersion} - bump from v${oldVersion}"`); + committed = true; + + // tag + run(`git tag -a v${newVersion} -m "Release v${newVersion}"`); + tagged = true; + + // push commit and tags + run("git push"); + run("git push --tags"); + pushedTags = true; + + // publish + console.log("Publishing to npm..."); + const publishCmd = pkg.name && pkg.name.startsWith("@") ? "npm publish --access public" : "npm publish"; + run(publishCmd); + + console.log(`Release v${newVersion} succeeded.`); + process.exit(0); + } catch (err) { + console.error("Release failed:", err.message || err); + try { + // delete local tag + if (tagged) { + try { run(`git tag -d v${newVersion}`); } catch {} + if (pushedTags) { + try { run(`git push origin :refs/tags/v${newVersion}`); } catch {} + } + } + // undo commit if made + if (committed) { + try { run("git reset --hard HEAD~1"); } catch { + // fallback: restore package.json content + fs.writeFileSync(pkgPath, pkgRaw, "utf8"); + } + } else { + // restore package.json + fs.writeFileSync(pkgPath, pkgRaw, "utf8"); + } + } catch (rbErr) { + console.error("Rollback error:", rbErr.message || rbErr); + } + process.exit(1); + } +})();