From bd6fa797d4dbb41f1ace9c56e205839c221e03cc Mon Sep 17 00:00:00 2001 From: Pravus Date: Fri, 4 Sep 2026 14:23:50 +0200 Subject: [PATCH 1/4] fix: sync-main-to-experimental action corret base branch --- .../workflows/sync-main-to-experimental.yml | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sync-main-to-experimental.yml b/.github/workflows/sync-main-to-experimental.yml index 705c862d..cc83f73b 100644 --- a/.github/workflows/sync-main-to-experimental.yml +++ b/.github/workflows/sync-main-to-experimental.yml @@ -5,6 +5,15 @@ on: branches: [ main ] workflow_dispatch: +# Two pushes to main must not race on force-pushing chore/sync. +concurrency: + group: sync-main-to-experimental + cancel-in-progress: false + +permissions: + contents: write + pull-requests: write + jobs: create-pr: runs-on: ubuntu-latest @@ -12,22 +21,66 @@ jobs: steps: - name: Check out the code uses: actions/checkout@v4 + with: + # full history, otherwise the merges below have no common ancestor + fetch-depth: 0 + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Create or update sync branch + id: sync + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - git fetch origin experimental - git checkout -B chore/sync + set -euo pipefail + + git fetch --no-tags --force origin main experimental + + PR_NUMBER="$(gh pr list --base experimental --head chore/sync --state open --json number --jq '.[0].number // empty')" + echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + + # Start over from experimental, unless a sync PR is already open — then keep + # the existing branch so any manual conflict resolution on it survives. + BASE=origin/experimental + if [ -n "$PR_NUMBER" ] && git fetch --no-tags --force origin chore/sync; then + BASE=origin/chore/sync + fi + git checkout -B chore/sync "$BASE" + + # chore/sync must be experimental + main: anything that only exists on + # experimental has to survive the sync, or the package built from the branch + # ships main's protos alone. + for REF in origin/experimental origin/main; do + if ! git merge --no-edit -m "chore: merge $REF into chore/sync" "$REF"; then + git merge --abort || true + echo "::error::Conflict merging $REF into chore/sync. Resolve it manually on the chore/sync branch." + exit 1 + fi + done + + if git diff --quiet origin/experimental HEAD; then + echo "Nothing to sync: experimental already contains everything in main." + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "changed=true" >> "$GITHUB_OUTPUT" + git push --force --set-upstream origin chore/sync - name: Create or update PR + if: steps.sync.outputs.changed == 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.sync.outputs.pr_number }} run: | - PR_NUMBER=$(gh pr list --base experimental --head chore/sync --state open --json number --jq '.[0].number') + set -euo pipefail if [ -n "$PR_NUMBER" ]; then echo "PR #$PR_NUMBER already exists, adding a comment..." - gh pr comment $PR_NUMBER --body "🔄 Updated with latest changes from **main** on $(date -u '+%Y-%m-%d %H:%M UTC')" + gh pr comment "$PR_NUMBER" --body "🔄 Updated with latest changes from **main** on $(date -u '+%Y-%m-%d %H:%M UTC')" else echo "Creating new PR..." gh pr create \ From 4a745edf9fc9afd77163e72985a0feaca51aac12 Mon Sep 17 00:00:00 2001 From: Pravus Date: Fri, 4 Sep 2026 14:35:18 +0200 Subject: [PATCH 2/4] fix --- .github/workflows/sync-main-to-experimental.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sync-main-to-experimental.yml b/.github/workflows/sync-main-to-experimental.yml index cc83f73b..457f069a 100644 --- a/.github/workflows/sync-main-to-experimental.yml +++ b/.github/workflows/sync-main-to-experimental.yml @@ -5,7 +5,7 @@ on: branches: [ main ] workflow_dispatch: -# Two pushes to main must not race on force-pushing chore/sync. +# Two pushes to main must not race on pushing chore/sync. concurrency: group: sync-main-to-experimental cancel-in-progress: false @@ -37,15 +37,20 @@ jobs: run: | set -euo pipefail - git fetch --no-tags --force origin main experimental + # A wildcard refspec is required: origin/experimental and origin/chore/sync + # are both read below, and --force-with-lease resolves its lease through + # this mapping. + git config --replace-all remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" + git fetch --no-tags --force --prune origin PR_NUMBER="$(gh pr list --base experimental --head chore/sync --state open --json number --jq '.[0].number // empty')" echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" # Start over from experimental, unless a sync PR is already open — then keep - # the existing branch so any manual conflict resolution on it survives. + # the existing branch, so commits that arrive on main while the PR is open + # accumulate on it and any manual conflict resolution survives. BASE=origin/experimental - if [ -n "$PR_NUMBER" ] && git fetch --no-tags --force origin chore/sync; then + if [ -n "$PR_NUMBER" ] && git rev-parse --verify --quiet refs/remotes/origin/chore/sync >/dev/null; then BASE=origin/chore/sync fi git checkout -B chore/sync "$BASE" @@ -68,7 +73,9 @@ jobs: fi echo "changed=true" >> "$GITHUB_OUTPUT" - git push --force --set-upstream origin chore/sync + # The lease guards a manual conflict resolution pushed to chore/sync + # since the fetch above. + git push --force-with-lease --set-upstream origin chore/sync - name: Create or update PR if: steps.sync.outputs.changed == 'true' From 5ece7fe7dbc773f62f2eed7ec3c02d257732d14b Mon Sep 17 00:00:00 2001 From: Pravus Date: Fri, 4 Sep 2026 14:41:55 +0200 Subject: [PATCH 3/4] fix --- .github/workflows/sync-main-to-experimental.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/sync-main-to-experimental.yml b/.github/workflows/sync-main-to-experimental.yml index 457f069a..b5c10dd5 100644 --- a/.github/workflows/sync-main-to-experimental.yml +++ b/.github/workflows/sync-main-to-experimental.yml @@ -13,6 +13,8 @@ concurrency: permissions: contents: write pull-requests: write + # gh pr comment and gh pr create --label reach a PR through the issues API + issues: write jobs: create-pr: From a364e956ed90d51c914011f484b6a8248bb99e30 Mon Sep 17 00:00:00 2001 From: Pravus Date: Sat, 5 Sep 2026 01:20:06 +0200 Subject: [PATCH 4/4] mini refactor based on PR feedback --- .../workflows/merge-sync-to-experimental.yml | 67 +++++++++++++++++ .../workflows/sync-main-to-experimental.yml | 73 +++++++++++-------- 2 files changed, 111 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/merge-sync-to-experimental.yml diff --git a/.github/workflows/merge-sync-to-experimental.yml b/.github/workflows/merge-sync-to-experimental.yml new file mode 100644 index 00000000..cbfdd1c8 --- /dev/null +++ b/.github/workflows/merge-sync-to-experimental.yml @@ -0,0 +1,67 @@ +name: Merge sync PR into experimental + +# The repo allows only squash merges, and squashing the sync PR would drop main's +# commits and freeze the merge base. This lands it as a real merge commit instead; +# GitHub closes the sync PR as merged once its commits are reachable from experimental. +on: + workflow_dispatch: + +concurrency: + group: merge-sync-to-experimental + cancel-in-progress: false + +permissions: + contents: write + pull-requests: read + +jobs: + merge: + runs-on: ubuntu-latest + + steps: + - name: Check out the code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Merge chore/sync into experimental + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + git config --replace-all remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" + git fetch --no-tags --force --prune origin + + if ! git rev-parse --verify --quiet refs/remotes/origin/chore/sync >/dev/null; then + echo "::error::chore/sync does not exist on the remote; there is no sync to land." + exit 1 + fi + + if git merge-base --is-ancestor origin/chore/sync origin/experimental; then + echo "experimental already contains chore/sync; nothing to merge." + exit 0 + fi + + PR_NUMBER="$(gh pr list --base experimental --head chore/sync --state open --json number --jq '.[0].number // empty')" + + git checkout -B experimental origin/experimental + # --no-ff so the sync always shows up as one labelled merge commit, the way the + # PR button would have recorded it. + if ! git merge --no-ff -m "chore: sync main to experimental${PR_NUMBER:+ (#$PR_NUMBER)}" origin/chore/sync; then + git merge --abort || true + echo "::error::Merging chore/sync into experimental failed. Resolve it on chore/sync, push, and re-run this workflow." + exit 1 + fi + + git push origin experimental + + # A PR closed by a push is not covered by delete_branch_on_merge, so remove the + # now fully-merged branch here: the sync job treats an existing chore/sync as a + # sync still in flight. + git push origin --delete chore/sync diff --git a/.github/workflows/sync-main-to-experimental.yml b/.github/workflows/sync-main-to-experimental.yml index b5c10dd5..b7b16e22 100644 --- a/.github/workflows/sync-main-to-experimental.yml +++ b/.github/workflows/sync-main-to-experimental.yml @@ -13,8 +13,6 @@ concurrency: permissions: contents: write pull-requests: write - # gh pr comment and gh pr create --label reach a PR through the issues API - issues: write jobs: create-pr: @@ -33,26 +31,20 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Create or update sync branch - id: sync - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail - # A wildcard refspec is required: origin/experimental and origin/chore/sync - # are both read below, and --force-with-lease resolves its lease through - # this mapping. + # The wildcard is what the bare fetch below and --force-with-lease resolve + # branch names through. git config --replace-all remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" git fetch --no-tags --force --prune origin - PR_NUMBER="$(gh pr list --base experimental --head chore/sync --state open --json number --jq '.[0].number // empty')" - echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" - - # Start over from experimental, unless a sync PR is already open — then keep - # the existing branch, so commits that arrive on main while the PR is open - # accumulate on it and any manual conflict resolution survives. + # An existing chore/sync is a sync still in flight: keep it, so commits + # arriving on main accumulate on it and a manual conflict resolution pushed + # to it survives. The branch is deleted when its PR merges, so the next sync + # starts from experimental again. BASE=origin/experimental - if [ -n "$PR_NUMBER" ] && git rev-parse --verify --quiet refs/remotes/origin/chore/sync >/dev/null; then + if git rev-parse --verify --quiet refs/remotes/origin/chore/sync >/dev/null; then BASE=origin/chore/sync fi git checkout -B chore/sync "$BASE" @@ -61,32 +53,55 @@ jobs: # experimental has to survive the sync, or the package built from the branch # ships main's protos alone. for REF in origin/experimental origin/main; do - if ! git merge --no-edit -m "chore: merge $REF into chore/sync" "$REF"; then + if ! git merge -m "chore: merge $REF into chore/sync" "$REF"; then git merge --abort || true - echo "::error::Conflict merging $REF into chore/sync. Resolve it manually on the chore/sync branch." + echo "::error::Merging $REF into chore/sync failed. To recover: branch chore/sync off experimental (or check out the existing one), merge main into it, resolve, push it, and open a PR against experimental. Later runs merge on top of that resolution." exit 1 fi done - if git diff --quiet origin/experimental HEAD; then - echo "Nothing to sync: experimental already contains everything in main." - echo "changed=false" >> "$GITHUB_OUTPUT" - exit 0 + # Push whenever the recomputed branch differs from the remote, so a stale + # chore/sync can never stay the head of an open PR. + REMOTE_HEAD="$(git rev-parse --verify --quiet refs/remotes/origin/chore/sync || echo absent)" + if [ "$(git rev-parse HEAD)" = "$REMOTE_HEAD" ]; then + echo "chore/sync on the remote is already up to date." + else + git push --force-with-lease origin chore/sync fi - echo "changed=true" >> "$GITHUB_OUTPUT" - - # The lease guards a manual conflict resolution pushed to chore/sync - # since the fetch above. - git push --force-with-lease --set-upstream origin chore/sync - name: Create or update PR - if: steps.sync.outputs.changed == 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ steps.sync.outputs.pr_number }} + # Backticks here are data, not shell source: they reach gh through the + # variable and are never re-evaluated. + PR_BODY: | + :crown: *An automated PR to keep experimental in sync with main* + + > [!IMPORTANT] + > **Do not merge this PR with the Squash button.** + > + > Squashing replaces `main`'s commits with a single new one, so `main` stops + > being an ancestor of `experimental` and the merge base freezes. Every later + > sync then re-applies changes `experimental` already has, and starts + > conflicting within a couple of runs. + > + > Land it by running the **Merge sync PR into experimental** workflow + > (Actions -> Merge sync PR into experimental -> Run workflow). It pushes a + > real merge commit, and this PR closes as merged. + > + > If it does get squashed, restore the ancestry with + > `git merge -s ours origin/main` on `experimental`. run: | set -euo pipefail + # gh pr create fails with "No commits between ..." when nothing is ahead. + if git merge-base --is-ancestor HEAD origin/experimental; then + echo "experimental already contains chore/sync; nothing to propose." + exit 0 + fi + + PR_NUMBER="$(gh pr list --base experimental --head chore/sync --state open --json number --jq '.[0].number // empty')" + if [ -n "$PR_NUMBER" ]; then echo "PR #$PR_NUMBER already exists, adding a comment..." gh pr comment "$PR_NUMBER" --body "🔄 Updated with latest changes from **main** on $(date -u '+%Y-%m-%d %H:%M UTC')" @@ -96,6 +111,6 @@ jobs: --base experimental \ --head chore/sync \ --title "chore: sync main to experimental" \ - --body ":crown: *An automated PR to keep experimental in sync with main*" \ + --body "$PR_BODY" \ --label "auto-pr" fi