From 97d5883e76ece0c99886030168d3b1fb22e203a3 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Tue, 28 Apr 2026 14:59:23 -0700 Subject: [PATCH] fix(ci): auto-sync concurrency + cleanup follow-ups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small fixes from the self-review of #2209: 1. **Required: concurrency group.** Two pushes to main in quick succession (manual UI merge then auto-promote-staging's ff-push, or any back-to-back main pushes) would race two auto-sync runs against the same staging branch — second `git push origin staging` fails non-fast-forward, surfacing as a red CI alert for what should be a no-op. Add `concurrency: { group: auto-sync-main-to-staging, cancel-in-progress: false }` so the second run waits for the first and sees its result. 2. **Hygiene: `git merge --abort` on conflict.** The conflict-error path exits 1 with the work tree in a half-merged state. Doesn't affect future runs (each gets a fresh checkout) but is an unpleasant artifact for anyone who shells into the runner. Abort first, then exit. 3. **Doc accuracy: "Loop safety" comment.** The original said the chain terminates because "main is either a no-op or advances further." That's true but understates the actual safety: GitHub Actions explicitly does NOT trigger downstream workflow runs from `GITHUB_TOKEN`-authored pushes. So the loop is impossible by construction, not just by happy coincidence of ref state. Updated the comment to reflect the actual mechanism. Plus a step-name nit: "Fast-forward staging → main" reads as if main is the target. Renamed to "Fast-forward staging to main" for consistency with the workflow's name (main → staging). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../workflows/auto-sync-main-to-staging.yml | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/workflows/auto-sync-main-to-staging.yml b/.github/workflows/auto-sync-main-to-staging.yml index 83156254..278c3428 100644 --- a/.github/workflows/auto-sync-main-to-staging.yml +++ b/.github/workflows/auto-sync-main-to-staging.yml @@ -33,13 +33,19 @@ name: Auto-sync main → 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 (ff case) or ⊇ main -# (merge case where promote then advances), the resulting push to -# main is either a no-op (no actual ref change → no push event) or -# advances main further. In the latter case auto-sync fires again, -# sees main already in staging's ancestry, no-ops. No infinite loop. +# `GITHUB_TOKEN`-authored pushes do NOT trigger downstream workflow +# runs by default (GitHub Actions safety). So when this workflow +# pushes the synced staging, `auto-promote-staging.yml` is NOT +# triggered by that push. The next developer push to staging triggers +# auto-promote normally. No loop is even theoretically possible. +# +# Concurrency: +# +# Two pushes to main in quick succession (e.g., manual UI merge +# immediately followed by auto-promote-staging's ff-merge) would +# otherwise race two auto-sync runs against the same staging branch +# — second push fails non-fast-forward. The concurrency group +# serializes them so the second run sees the first's result. on: push: @@ -48,6 +54,10 @@ on: permissions: contents: write +concurrency: + group: auto-sync-main-to-staging + cancel-in-progress: false + jobs: sync-staging: runs-on: ubuntu-latest @@ -82,7 +92,7 @@ jobs: echo "::notice::staging is missing main's tip — sync needed" fi - - name: Fast-forward staging → main + - name: Fast-forward staging to main if: steps.check.outputs.needs_sync == 'true' id: ff run: | @@ -96,15 +106,18 @@ jobs: fi - name: Merge main into staging (when ff fails) - if: | - steps.check.outputs.needs_sync == 'true' && - steps.ff.outputs.did_ff != 'true' + if: steps.check.outputs.needs_sync == 'true' && steps.ff.outputs.did_ff != 'true' run: | set -euo pipefail # ff failed because staging has commits main doesn't — typical # in-flight feature work. Create a merge commit so staging # absorbs main's tip while keeping its own history. if ! git merge --no-ff origin/main -m "chore: sync main → staging (auto)"; then + # Hygiene: leave the work tree clean before failing. Doesn't + # affect future runs (each gets a fresh checkout) but a + # half-merged tree is an unpleasant artifact to debug if + # anyone ever shells into the runner. + git merge --abort || true { echo "## ❌ Conflict" echo