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).
Branches
Section titled “Branches”| 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--> prodThe core invariant
Section titled “The core invariant”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
maindirectly. - 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.
staging
Section titled “staging”- PR required.
- Required status checks: the full build/test/validate suite — so broken code
can’t reach
staging, and therefore can’t be promoted.
Promotion (staging → main)
Section titled “Promotion (staging → main)”Owned by .github/workflows/auto-deploy-main.yml (daily cron + manual dispatch):
- Promotes only when
mainis an ancestor ofstaging, via a fast-forward push — no merge commit, no squash. - If
mainhas any commit not onstaging(divergence), the job fails loudly and opens a tracking issue instead of silently skipping. (Hardened in #1317.)
Hotfixes
Section titled “Hotfixes”- Normal: fix on a branch → PR to
staging→ rides the next promotion tomain. - Emergency (prod-only, can’t wait): break-glass — a repo admin temporarily
relaxes
mainprotection, lands the fix, restores protection, and immediately back-merges tostagingso the branches don’t diverge.
Rollout status
Section titled “Rollout status”-
mainbaseline protection — PR required, force-push/deletion blocked, enforce-admins. - Reconcile
main→stagingso promotion can resume — PR #1317 (keystone; must land first so the workflow changes below can reachmainvia the first promotion). - Fast-forward promotion in
auto-deploy-main.yml+ restrictmainpushes to the promotion automation. - Required status checks on
mainandstaging. - Prod deploy-time approval (GitHub Environment required reviewer).