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).

Branch Role
main Production. Prod images build from main. No one merges to main directly β€” it is updated only by the automated fast-forward promotion from staging.
staging Integration. 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 schedule: Runs a divergence check only (does not auto-promote staging to main).
  • Manual workflow_dispatch (gh workflow run auto-deploy-main.yml --ref staging): Promotes staging β†’ main 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 main β†’ staging 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 (manual workflow_dispatch of auto-deploy-main.yml --ref staging).