Skip to content

Branching & Promotion Model

How code flows to production, and the guardrails that keep main and staging from diverging. Adopted 2026-05-30 after repeated direct-to-main merges silently broke the daily promotion (see PR #1317).

BranchRole
mainProduction. Prod images build from main. No one merges to main directly — it is updated only by the automated fast-forward promotion from staging.
stagingIntegration. All feature work lands here via PR; continuously deployed to staging.
feature/*, claude/*, …Short-lived; PR into staging.
feature --PR + CI + review--> staging --automated fast-forward--> main --approved deploy--> prod

main is always a fast-forward of staging — i.e. main is an exact, already-CI’d staging commit (main ⊆ staging). Because promotion is a fast-forward (never a squash or a fresh merge commit on main), the SHAs match and divergence is structurally impossible from the promotion side. Combined with “no direct merges to main,” main can never drift from staging again.

git merge-base --is-ancestor origin/main origin/staging should therefore always succeed. If it doesn’t, something pushed to main out-of-band.

  • PR required; force-push and deletion blocked; enforced for admins.
  • Pushes/merges restricted to the promotion automation only — humans never update main directly.
  • Required status checks: the full build/test/validate suite.
  • The production human gate is at deploy time (a GitHub Environment required reviewer on the prod deploy job), not at branch-merge time.
  • PR required.
  • Required status checks: the full build/test/validate suite — so broken code can’t reach staging, and therefore can’t be promoted.

Owned by .github/workflows/auto-deploy-main.yml (daily cron + manual dispatch):

  • Promotes only when main is an ancestor of staging, via a fast-forward push — no merge commit, no squash.
  • If main has any commit not on staging (divergence), the job fails loudly and opens a tracking issue instead of silently skipping. (Hardened in #1317.)
  • Normal: fix on a branch → PR to staging → rides the next promotion to main.
  • Emergency (prod-only, can’t wait): break-glass — a repo admin temporarily relaxes main protection, lands the fix, restores protection, and immediately back-merges to staging so the branches don’t diverge.
  • main baseline protection — PR required, force-push/deletion blocked, enforce-admins.
  • Reconcile mainstaging so promotion can resume — PR #1317 (keystone; must land first so the workflow changes below can reach main via the first promotion).
  • Fast-forward promotion in auto-deploy-main.yml + restrict main pushes to the promotion automation.
  • Required status checks on main and staging.
  • Prod deploy-time approval (GitHub Environment required reviewer).