Added release script

This commit is contained in:
2026-03-11 18:00:10 +01:00
parent 121aa9d1ad
commit 94c96ebf15

115
scripts/release.js Executable file
View File

@@ -0,0 +1,115 @@
#!/usr/bin/env node
// Minimal safe release script.
// Usage: node scripts/release.js [major|minor|patch|<version>]
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;
// ensure tag doesn't already exist locally
let localTagExists = false;
try {
runOutput(`git rev-parse --verify refs/tags/v${newVersion}`);
localTagExists = true;
} catch (_) {
localTagExists = false;
}
if (localTagExists) throw new Error(`Tag v${newVersion} already exists locally — aborting to avoid overwrite.`);
// ensure tag doesn't exist on remote
const remoteTagInfo = (() => {
try { return runOutput(`git ls-remote --tags origin v${newVersion}`); } catch (_) { return ""; }
})();
if (remoteTagInfo) throw new Error(`Tag v${newVersion} already exists on remote — aborting to avoid overwrite.`);
// 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);
}
})();