ZeroStarter

Releases

The automated canary-to-main flow that versions the repo and drafts the changelog.

Two long-lived branches carry the project: canary for development and main for production. Day-to-day work squash-merges into canary; when you're ready to cut a release you review and merge a single pull request that opens itself, and the changelog, version bump, tag, and GitHub release all follow automatically.

Turn on write permissions first

A fresh fork's Actions token is read-only

By default the release workflows get 403 Resource not accessible by integration, and no release PR opens until you grant write access.

Under Settings → Actions → General → Workflow permissions (the pre-push hook prints a direct link on your first push):

  1. Select Read and write permissions so workflows can branch, commit the changelog, and tag.
  2. Check Allow GitHub Actions to create and approve pull requests so the release PR can open.
  3. Click Save.

GitHub Workflow permissions: Read and write permissions selected and Allow GitHub Actions to create and approve pull requests checked

On a brand-new empty repo, push canary first (git push origin canary) so GitHub makes it the default branch. The pre-push hook (.github/scripts/ensure-remote-branches.ts) then seeds the release branches (main by default) on your next push (each a separate ref, so they never collide) and the release PR opens on its own. No gh and no repo-admin required.

Land work on canary with squash-merge

Feature PRs merge into canary as a squash: one commit per PR, then delete the branch. This matters because changelogen builds the changelog straight from commit subjects: one clean Conventional Commit per PR yields one clean changelog line, and a branch's messy work-in-progress history never leaks in. The auto-labeler.yml workflow posts this reminder on every PR opened against canary.

The lone exception is the canarymain release PR below, which merges with a merge commit so main keeps shared history with canary instead of diverging.

The release, one merge

A draft PR opens itself

Every push to canary runs auto-canary-into-main.yml, which opens a draft PR (canarymain, titled ci(release): 🚀 merge canary into main) if one isn't already open.

You review and merge with a merge commit

Mark the draft ready, review it, and merge with a merge commit. That merge is what triggers auto-release.yml.

Changelog, version, and release (automatic)

auto-release.yml checks out canary and:

  • generates CHANGELOG.md from the last v* tag,
  • bumps the patch version in package.json (or ships the exact version you hand-set, see Pinning a version),
  • refreshes the build-graph snapshot at .github/assets/graph-build.svg,
  • commits ci(changelog): update changelog and bump version directly onto canary,
  • tags v<version> and pushes branch and tag atomically,
  • publishes a GitHub release whose notes mirror the new CHANGELOG.md section.

If the commit range holds only filtered-out types (like ci), it skips the release.

The changelog commit lands directly on canary rather than through a second PR because it's mechanical, generated from PR titles you already reviewed. If a run tags the version but fails before publishing, the next run backfills the missing GitHub release.

Pinning a version

Ordinary releases bump the patch (0.1.0 -> 0.1.1 -> ...). To cut something bigger (a minor like 0.2.0 or a major like 1.0.0), set the version in the root package.json yourself on canary before you merge the release PR. auto-release.yml notices a hand-set version that is ahead of the last v* tag and ships that exact number (retargeting the generated changelog section to it), then resumes patch-bumping from it on the next release.

How it deploys

Both branches deploy through Vercel. Drizzle migrations run at API build time via .github/scripts/migrate-on-deploy.ts, but only when VERCEL_ENV=production or the ref is canary; every other deploy (including PR previews) logs a skip and exits, so an unmerged migration never touches the shared database. Those two builds therefore need a valid POSTGRES_URL.

Next