feat(scripts): add pnpm release for local end-to-end publish
Wraps the changesets flow into one command: preflight (branch/clean/up-to-date), apply changesets (version bump + CHANGELOG), build, commit, publish to npm, push tag. The tag push triggers release.yml for platform binary builds. Trade-off vs the version.yml CI workflow: no npm provenance (OIDC is CI-only). Use the CI workflow when provenance matters. Flags: --dry-run (no publish/push), --yes (skip confirmation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
132
scripts/release.mjs
Executable file
132
scripts/release.mjs
Executable file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env node
|
||||
// Local release: consume changesets, bump versions, publish to npm, push tag.
|
||||
//
|
||||
// This is a local-machine alternative to the `version.yml` CI workflow.
|
||||
// Trade-off: CI publishes with npm provenance via OIDC; this script does not.
|
||||
// If you want provenance, run the workflow manually instead of this script.
|
||||
//
|
||||
// Requirements:
|
||||
// - clean working tree on `main`, up to date with origin
|
||||
// - at least one pending changeset in .changeset/
|
||||
// - `npm login` already completed (publish uses the active npm token)
|
||||
//
|
||||
// Usage:
|
||||
// pnpm release # interactive, confirms before publish+tag
|
||||
// pnpm release --yes # skip confirmation prompt
|
||||
// pnpm release --dry-run # run through steps without publishing/pushing
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { stdin, stdout } from "node:process";
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const DRY_RUN = args.has("--dry-run");
|
||||
const AUTO_YES = args.has("--yes") || args.has("-y");
|
||||
|
||||
const color = (c, s) => `\x1b[${c}m${s}\x1b[0m`;
|
||||
const info = (s) => console.log(color(36, "▶ ") + s);
|
||||
const ok = (s) => console.log(color(32, "✓ ") + s);
|
||||
const warn = (s) => console.log(color(33, "! ") + s);
|
||||
const fail = (s) => {
|
||||
console.error(color(31, "✗ ") + s);
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
function run(cmd, { capture = false, allowFail = false } = {}) {
|
||||
const r = spawnSync(cmd, {
|
||||
shell: true,
|
||||
stdio: capture ? "pipe" : "inherit",
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (r.status !== 0 && !allowFail) fail(`Command failed: ${cmd}`);
|
||||
return { status: r.status, stdout: (r.stdout || "").trim() };
|
||||
}
|
||||
|
||||
async function confirm(prompt) {
|
||||
if (AUTO_YES || DRY_RUN) return true;
|
||||
const rl = createInterface({ input: stdin, output: stdout });
|
||||
const answer = (await rl.question(`${prompt} [y/N] `)).trim().toLowerCase();
|
||||
rl.close();
|
||||
return answer === "y" || answer === "yes";
|
||||
}
|
||||
|
||||
// --- Preflight ------------------------------------------------------------
|
||||
|
||||
info("Preflight checks…");
|
||||
|
||||
const branch = run("git rev-parse --abbrev-ref HEAD", { capture: true }).stdout;
|
||||
if (branch !== "main") fail(`Must be on 'main' (currently '${branch}').`);
|
||||
|
||||
const dirty = run("git status --porcelain", { capture: true }).stdout;
|
||||
if (dirty) fail("Working tree is not clean. Commit or stash first.");
|
||||
|
||||
run("git fetch origin main", { capture: true });
|
||||
const ahead = run("git rev-list --count origin/main..HEAD", { capture: true }).stdout;
|
||||
const behind = run("git rev-list --count HEAD..origin/main", { capture: true }).stdout;
|
||||
if (behind !== "0") fail(`Local main is behind origin/main by ${behind} commit(s). Pull first.`);
|
||||
if (ahead !== "0") warn(`Local main is ahead of origin/main by ${ahead} commit(s); they will be pushed.`);
|
||||
|
||||
const changesets = readdirSync(".changeset").filter(
|
||||
(f) => f.endsWith(".md") && f !== "README.md"
|
||||
);
|
||||
if (changesets.length === 0) {
|
||||
fail("No pending changesets in .changeset/. Run `pnpm changeset` first.");
|
||||
}
|
||||
ok(`${changesets.length} pending changeset(s).`);
|
||||
|
||||
info("Changeset summary:");
|
||||
run("pnpm changeset status");
|
||||
|
||||
if (!(await confirm("Proceed with version bump, build, publish, and tag?"))) {
|
||||
warn("Aborted by user.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// --- Version bump ---------------------------------------------------------
|
||||
|
||||
info("Applying changesets (version bump + CHANGELOG)…");
|
||||
run("pnpm release:version");
|
||||
|
||||
info("Updating lockfile…");
|
||||
run("pnpm install --no-frozen-lockfile");
|
||||
|
||||
const cliPkg = JSON.parse(readFileSync("packages/cli/package.json", "utf8"));
|
||||
const version = cliPkg.version;
|
||||
ok(`New version: ${version}`);
|
||||
|
||||
// --- Build ----------------------------------------------------------------
|
||||
|
||||
info("Building all packages…");
|
||||
run("pnpm build");
|
||||
|
||||
// --- Commit ---------------------------------------------------------------
|
||||
|
||||
info("Committing version bump…");
|
||||
run("git add -A");
|
||||
run(
|
||||
`git commit -m "chore(release): v${version}" -m "Version bump via changesets."`,
|
||||
{ allowFail: true }
|
||||
);
|
||||
|
||||
// --- Publish --------------------------------------------------------------
|
||||
|
||||
if (DRY_RUN) {
|
||||
warn("--dry-run: skipping npm publish, git push, and tag.");
|
||||
info(`Would publish, commit, and tag v${version}.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
info("Publishing to npm (non-private packages only)…");
|
||||
run("pnpm -r publish --access public --no-git-checks");
|
||||
|
||||
// --- Push + tag -----------------------------------------------------------
|
||||
|
||||
info("Pushing commit to origin/main…");
|
||||
run("git push origin main");
|
||||
|
||||
info(`Creating and pushing tag v${version}…`);
|
||||
run(`git tag v${version}`);
|
||||
run(`git push origin v${version}`);
|
||||
|
||||
ok(`Released v${version}. The 'v${version}' tag will trigger release.yml for binary builds.`);
|
||||
Reference in New Issue
Block a user