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 → main branching model. Current model (CLAUDE.md §Branching): all PRs target main directly, dev is frozen and kept for historical reference only — no new work targets it, and nothing should ever back-merge into it again. sops-drift-check.yml no longer compares main against dev automatically 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:

  1. Direct-to-main hotfixes/rotations. Emergency credential rotations are sometimes committed straight to main (or via an rc/emergency-* branch that merges to main only). That change now exists on main but not on dev.
  2. main is an orphan-root relative to dev. 391b96f on main has no parent — main’s history was re-rooted with a bulk snapshot at some point, so git merge/rebase between 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 = error

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

  1. Create the back-merge branch from dev:

    git fetch origin
    git checkout -b sync/main-to-dev origin/dev
  2. Bring main’s content in. Because main may be an orphan-root, an ordinary git merge can report “unrelated histories” — allow it, then resolve in favour of main for rotated secret values (production truth), keeping dev’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 main and re-encrypt them on dev (never print values; follow sops-edit-operations.md):

    git checkout origin/main -- secrets/<file>.env.sops
  3. Verify drift is gone before committing:

    bash scripts/check-sops-drift.sh origin/main HEAD   # expect: no drift
  4. Open a PR to dev (never push to dev directly), reference the originating issue, and let CI (including sops-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.