Single command, idempotent. Run pnpm release at any point - it figures out where you are and does the next step.
- Run
pnpm release, select Regular, confirm -> creates prerelease PR (develop -> release) - Merge prerelease PR on GitHub
- Run
pnpm releaseagain -> generates AI release notes, creates release PR (release -> main) - Merge release PR on GitHub when CI passes
- Run
pnpm releaseagain -> tags main with version, creates private sync PR (main -> private) - Merge private sync PR on GitHub
- Run
pnpm releaseagain -> "done, nothing to do"
- Run
pnpm release, select Hotfix, pick commits -> creates hotfix PR (hotfix/vX.Y.Z -> main) - Merge hotfix PR on GitHub
- Run
pnpm releaseagain -> tags, creates private sync PR + backmerge PR (main -> develop) - Merge both PRs on GitHub
The script derives its state from observable git/GitHub state (branch SHAs, tags, open PRs) rather than tracking state in a file. This makes it idempotent - you can run it as many times as you want without creating duplicates or re-tagging.
idle (no prerelease) -> create develop -> release PR
prerelease_pr_open -> waiting for merge on GitHub
idle (prerelease merged) -> create release -> main PR with AI notes
release_pr_open -> waiting for merge on GitHub
merged_untagged -> tag main, create main -> private PR
tagged_private_stale -> waiting for private sync merge on GitHub
done -> nothing to do
idle -> cherry-pick commits, create hotfix -> main PR
hotfix_pr_open -> waiting for merge on GitHub
merged_untagged -> tag main, create private sync + backmerge PRs
tagged_private_stale -> waiting for PR merges on GitHub
done -> nothing to do
All four branches are protected - no direct pushes:
- main: production
- develop: development
- release: staging between develop and main
- private: tracks main with different env vars (Cloudflare deployment)
The release PR body is AI-generated using Claude CLI. It groups commits by feature domain, separates production changes from dev-only (feature-flagged) changes, and includes testing notes. Falls back to a raw commit list if Claude is unavailable.