feat(ci): auto-sync main → staging to keep staging-as-superset invariant - #2209
Merged
Merged
Conversation
Background `auto-promote-staging.yml` advances main via `git merge --ff-only` + `git push origin main` — clean fast-forward, no merge commit. But manual `staging → main` merges via the GitHub UI / API create a merge commit on main that staging doesn't have. The next `staging → main` PR then evaluates as "BEHIND" because staging is missing that merge commit, requiring a manual `gh pr update-branch` round-trip. This pattern bit twice on 2026-04-28 (PRs #2202 and #2205, both manual bridges to land pipeline fixes themselves). Each needed update-branch + re-CI before they could merge. Annoying and avoidable. What this workflow does Triggered on every push to main (regardless of source: auto-promote, UI merge, API merge, direct push): 1. Check whether main is already in staging's ancestry. If yes, no-op — auto-promote-staging keeps them aligned via ff push, and the no-op case is the steady state. 2. If not (manual merge commit on main, or direct main hotfix): try `git merge --ff-only origin/main` first. Works when staging hasn't diverged with its own commits. 3. If ff fails (staging has its own in-flight feature work): `git merge --no-ff origin/main -m "chore: sync main → staging"`. Absorbs main's tip while keeping staging's own history. 4. Push staging. Loop safety Pushing the synced staging triggers auto-promote-staging.yml, which checks gates on staging's new tip and, if green, ff-pushes staging to main. Since staging now ⊇ main, the resulting push to main is either a no-op (no ref change → no push event fires → auto-sync doesn't re-trigger) or advances main further. In the latter case auto-sync fires once more, sees main already in staging's ancestry, no-ops. Bounded. Conflict handling If the merge step hits conflicts (staging and main diverged with incompatible changes), the workflow fails with a clear summary pointing to manual resolution. This shouldn't happen in practice — staging is the integration branch; conflicts indicate a direct main hotfix touching the same code as in-flight staging work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HongmingWang-Rabbit
requested a review
from hongmingwang-moleculeai
as a code owner
April 28, 2026 21:44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Eliminates the bridge-PR-thrash class that bit us twice today (#2202, #2205). Whenever main moves, staging absorbs the new tip automatically — no more manual
gh pr update-branchround-trips on follow-up PRs.Mechanism
Triggered on every `push: branches: [main]`:
No-op case (steady state): `origin/main` is already in staging's ancestry. Workflow logs and exits. This is the common case after `auto-promote-staging.yml` ff-advanced main from staging.
Manual main merge case (the bug we hit twice today): main has a merge commit not on staging. Try ff first; if staging hasn't diverged it succeeds and staging fast-forwards to main.
Diverged case (staging has in-flight feature work main doesn't): create a merge commit `chore: sync main → staging (auto)` so staging absorbs main's tip while keeping its own history.
Push staging.
Loop safety
Pushing synced staging triggers auto-promote-staging.yml, which ff-pushes staging to main. main now equals staging; that push is a no-op ref-update so no push event fires → auto-sync doesn't re-trigger. Bounded.
Conflict handling
If `merge --no-ff main` hits conflicts (staging and main diverged with incompatible changes), workflow fails with a clear summary pointing to manual resolution. Shouldn't happen in practice — conflicts indicate a direct main hotfix touching the same code as in-flight staging work, which is itself an anti-pattern (hotfixes should land on staging first).
Test plan
Out-of-scope (track separately)
🤖 Generated with Claude Code