Release script
This commit is contained in:
99
scripts/release.js
Executable file
99
scripts/release.js
Executable file
@@ -0,0 +1,99 @@
|
|||||||
|
#!/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;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user