Playbook — main → dev back-merge (and SOPS drift remediation) — DEPRECATED
DEPRECATED (#2551, 2026-07-31). This playbook assumes the retired
feat/* → dev → rc/vX.Y.Z → mainbranching model. Current model (CLAUDE.md§Branching): all PRs targetmaindirectly,devis frozen and kept for historical reference only — no new work targets it, and nothing should ever back-merge into it again.sops-drift-check.ymlno longer comparesmainagainstdevautomatically for the same reason. Kept for historical context only; do not follow the “Fix” section below on a live repo.
Symptom class: a credential rotation or hotfix landed directly on main and never
propagated back to dev, so dev silently holds stale secret values. The next dev-side
secrets change then redeploys those stale values to production and re-fires alerts.
Canonical instances: #2269 rotation reached
main (commit 391b96f) but not dev; #2483
reconciled the specific keys; #2495 is the
systemic finding this playbook exists to close.
Why it happens
The intended flow is feat/* → dev → rc/vX.Y.Z → main. Everything reaches main through
dev, so dev is always an ancestor of main. Two things break that invariant:
- Direct-to-
mainhotfixes/rotations. Emergency credential rotations are sometimes committed straight tomain(or via anrc/emergency-*branch that merges tomainonly). That change now exists onmainbut not ondev. mainis an orphan-root relative todev.391b96fonmainhas no parent —main’s history was re-rooted with a bulk snapshot at some point, sogit merge/rebasebetween the two sees no common ancestor and cannot fast-forward. (Re-parenting the two branches onto a shared base is a destructive, human-only decision — see “Open architectural question” below. It is deliberately NOT automated.)
Because secrets-sync.yml runs on push to both dev and main, a later dev-side edit
redeploys dev’s stale copy over the freshly-rotated production value.
Detect
sops-drift-check.yml runs daily, on every push to main that touches secrets/*.env.sops, and
on manual dispatch. It decrypts every secrets file on both refs and compares value-hashes only
(never values). On divergence it opens/updates the sops-drift issue “[Infra] SOPS secret drift:
dev ↔ main”.
Run it locally the same way (needs an age key that can decrypt the files — same key material as
secrets-sync.yml):
git fetch origin main dev
bash scripts/check-sops-drift.sh origin/main origin/dev # exit 0 = in sync, 1 = drift, 2 = errorThe output lists only key names and a status (value hash mismatch / only on <ref>). It never
prints a secret value.
Fix — mandatory back-merge after any direct-to-main change
Rule: immediately after any hotfix or rotation that lands on main without going through dev,
open a main → dev back-merge PR. Do not consider the rotation “done” until dev is reconciled.
-
Create the back-merge branch from
dev:git fetch origin git checkout -b sync/main-to-dev origin/dev -
Bring
main’s content in. Becausemainmay be an orphan-root, an ordinarygit mergecan report “unrelated histories” — allow it, then resolve in favour ofmainfor rotated secret values (production truth), keepingdev’s newer application changes:git merge origin/main --allow-unrelated-histories --no-commit || true # Resolve conflicts: take main's secrets/*.env.sops values; keep dev's non-secret changes.For a secrets-only reconciliation it is often cleaner to check out just the affected files from
mainand re-encrypt them ondev(never print values; follow sops-edit-operations.md):git checkout origin/main -- secrets/<file>.env.sops -
Verify drift is gone before committing:
bash scripts/check-sops-drift.sh origin/main HEAD # expect: no drift -
Open a PR to
dev(never push todevdirectly), reference the originating issue, and let CI (includingsops-drift-check.yml) confirm green.
If dev intentionally holds a newer value than main (a rotation that started on dev), do
the opposite: promote it forward through the normal dev → rc → main flow rather than back-merging.
Open architectural question (human decision — do NOT automate)
Whether to re-parent main and dev onto a common base (eliminating the orphan-root and letting
git merge fast-forward cleanly) is a history-rewriting operation with blast radius across every
open branch. It is tracked as a human decision on
#2495 and must not be performed by an autonomous
worker.
Related
- gh-secret-sops-drift.md — the other drift axis (SOPS ↔ GH Actions Secret store)
- sops-edit-operations.md — safe SOPS edit/decrypt
.github/workflows/sops-drift-check.yml·scripts/check-sops-drift.sh