From 9fe5a85a87548eb67403baac9f749227afa4dac3 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Fri, 24 Jul 2026 17:08:51 +0200 Subject: [PATCH] ci: auto-sync dev from main after every prod merge Enforces AGENTS.md fast-forward rule. Every push on main triggers: 1. ff dev to main if possible 2. real merge commit if ff isn't possible but git can resolve 3. auto-PR sync/main-to-dev- if git can't resolve conflicts Stops the divergence-drift pattern that put dev 111 commits behind main. --- .github/workflows/sync-dev-from-main.yml | 109 +++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 .github/workflows/sync-dev-from-main.yml diff --git a/.github/workflows/sync-dev-from-main.yml b/.github/workflows/sync-dev-from-main.yml new file mode 100644 index 00000000..afce609b --- /dev/null +++ b/.github/workflows/sync-dev-from-main.yml @@ -0,0 +1,109 @@ +name: Sync dev from main + +# Purpose +# ------- +# The workflow enforces the AGENTS.md rule "immediately fast-forward dev +# from main after a hotfix". Every hotfix that lands on main used to +# require a manual `git checkout dev && git merge --ff-only main && git +# push` step; that step was skipped often enough for dev to drift 100+ +# commits behind main, silently rotting the staging environment. +# +# On every push to main this job: +# 1. Tries a fast-forward of dev to main. If it succeeds, dev is now +# identical to main and the staging deploy fires as usual (its +# workflow triggers on push to dev). +# 2. If a fast-forward isn't possible (dev has commits main doesn't), +# the job attempts a real merge commit. Clean auto-merge = pushed +# straight to dev. +# 3. Only when git itself can't resolve the merge (real conflicts) do +# we fall back to opening a "sync/main-to-dev-" branch + a PR +# against dev. A human resolves those conflicts by merging the PR. +# +# The job is intentionally lenient: pushing to dev directly is normally +# forbidden (AGENTS.md), but this bot is the one exception because it +# only carries commits that main has already accepted. + +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: sync-dev-from-main + cancel-in-progress: false + +jobs: + sync: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout full history + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: dev + + - name: Configure bot identity + run: | + git config user.name "ocb-sync-bot" + git config user.email "ocb-sync-bot@users.noreply.github.com" + + - name: Try fast-forward dev → main + id: ff + run: | + set -e + git fetch origin main:refs/remotes/origin/main + if git merge-base --is-ancestor origin/main HEAD; then + echo "already-in-sync=true" >> "$GITHUB_OUTPUT" + echo "dev already contains origin/main; nothing to do." + exit 0 + fi + if git merge --ff-only origin/main; then + git push origin dev + echo "ff-succeeded=true" >> "$GITHUB_OUTPUT" + echo "dev fast-forwarded to $(git rev-parse --short HEAD)" + exit 0 + fi + echo "ff-failed=true" >> "$GITHUB_OUTPUT" + + - name: Attempt clean merge commit + if: steps.ff.outputs.ff-failed == 'true' + id: merge + run: | + set -e + if git merge --no-edit --no-ff origin/main -m "sync: main → dev (auto-merge $(git rev-parse --short origin/main))"; then + git push origin dev + echo "merge-pushed=true" >> "$GITHUB_OUTPUT" + echo "dev auto-merged main at $(git rev-parse --short HEAD)" + exit 0 + fi + git merge --abort + echo "merge-failed=true" >> "$GITHUB_OUTPUT" + + - name: Open sync PR on conflict + if: steps.merge.outputs.merge-failed == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -e + SHORT=$(git rev-parse --short origin/main) + BRANCH="sync/main-to-dev-$SHORT" + git checkout -B "$BRANCH" + # Force a merge commit that stops on conflicts. We DO NOT + # push conflict markers to dev; instead we push a branch that + # a human can pull, resolve locally, and merge into dev. + git reset --hard dev + git merge --no-edit --no-ff origin/main || true + git add -A + git commit -m "sync: main → dev (CONFLICTS — resolve before merge)" || true + git push -f origin "$BRANCH" + gh pr create \ + --repo "$GITHUB_REPOSITORY" \ + --base dev \ + --head "$BRANCH" \ + --title "sync: main → dev (auto, needs conflict resolution)" \ + --body "Automated by \`.github/workflows/sync-dev-from-main.yml\`. Fast-forward + auto-merge both failed; a human needs to resolve conflicts and merge this PR into \`dev\`. Base main SHA: $SHORT." \ + || echo "PR already exists for $BRANCH; leaving it alone."