Skip to main content

Releasing

Green Goods ships once a month, at the start of the month, in step with our two-week cycles (two cycles per release). This is the high-level runbook; branch and commit conventions live in CLAUDE.md § Git Workflow.

The monthly rhythm

Work lands on develop (staging) all month and is QA'd there as it goes. At the beginning of each month, once staging is green, we turn what's on develop into a release:

  1. Cut a release/<month>-<version> branch from develop — this captures the tested snapshot while develop keeps moving.
  2. Finalize on the branch — version bump, changelog entry, a last run of the gates.
  3. Ship — tag vX.Y.0, merge into main (production); a GitHub Release is generated automatically.
  4. Back-merge main into develop so the release returns to staging.
  5. Announce to gardeners.

The release-prep routine opens each cycle with a readiness brief so you cut with a clear picture of what's shipping.

Versioning & naming

  • Semver, minor per month (1.1.01.2.0); patch for hotfixes (1.2.1); major for breaking changes. bun run version:bump <x.y.z> keeps all packages on one version and updates the supported release in SECURITY.md.
  • A release is named for the month it ships in — branch release/july-2026-v1.2.0, tag v1.2.0, GitHub Release July 2026 — v1.2.0.

GitHub Actions supply-chain policy

Every external action and reusable workflow reference must use a full 40-character commit SHA. Tags such as @v6, branches such as @main, and abbreviated SHAs are rejected by the repository's Actions policy. When updating an action, review the upstream change and pin the exact commit that provides it.

Cutting a release

git switch -c release/july-2026-v1.2.0 origin/develop # cut the tested snapshot
bun run version:bump 1.2.0 # unify packages + SECURITY.md
bun run version:check 1.2.0 # fail if any release marker drifted
# add the curated changelog entry, then run the gates:
bun run format:check && bun run lint && bun run test && bun run build
git commit -am "chore(release): v1.2.0 (July 2026)" && git push -u origin release/july-2026-v1.2.0
gh pr create --base main --title "Release July 2026 — v1.2.0"
# once CI is green and the PR merges, tag the merged main HEAD:
git switch main && git pull && git tag -a v1.2.0 -m "July 2026" && git push origin v1.2.0

Pushing the tag triggers the GitHub Release (auto-generated notes). Then back-merge.

Hotfixes

A production bug mid-month branches from main (not develop), bumps a patch (bun run version:bump 1.2.1), and ships the same way with tag v1.2.1. Then back-merge — this is exactly the step that, when skipped, strands a fix on main.

Back-merge — every time

After every release and every hotfix, merge main back into develop. Use the direct-push fast-path — as a maintainer you push straight to develop:

git fetch origin # refresh origin/main after the release merged
git switch develop && git pull && git merge origin/main && git push

This keeps main an ancestor of develop — the check that it worked is an empty git log origin/develop..origin/main.

Never open a back-merge PR whose head is main

The repo has delete_branch_on_merge: true, so GitHub deletes a PR's head branch when the PR merges. A back-merge PR with main as the head therefore deletes main on merge — the org-admin bypass sails straight past main's restrict-deletions rule. This is exactly what deleted main during this release's back-merge; it had to be restored from the reflog.

If the direct push above is somehow unavailable and you must go through a PR, push main to a throwaway branch and PR that into develop — never main itself:

DATE=$(date +%Y-%m-%d)
git fetch origin # copy the current main, not a stale local ref
git push origin origin/main:refs/heads/backmerge/main-$DATE
gh pr create --repo greenpill-dev-guild/green-goods \
--base develop --head "backmerge/main-$DATE" \
--title "Back-merge main → develop"

The changelog

GitHub Releases is the canonical, auto-generated changelog. The docs Changelog is a short, curated summary for gardeners — not a full mirror.