Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/merge-sync-to-experimental.yml
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Same as the sync workflow — git config --replace-all is redundant with fetch-depth: 0.


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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] When --is-ancestor is true (chore/sync already in experimental), the workflow exits without deleting the stale chore/sync branch. Consider:

Suggested change
exit 0
if git merge-base --is-ancestor origin/chore/sync origin/experimental; then
echo "experimental already contains chore/sync; cleaning up."
git push origin --delete chore/sync || true
exit 0

This handles the partial-failure recovery case (prior run pushed experimental but failed on branch delete).

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
89 changes: 83 additions & 6 deletions .github/workflows/sync-main-to-experimental.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,112 @@ on:
branches: [ main ]
workflow_dispatch:

# Two pushes to main must not race on 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

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
run: |
git fetch origin experimental
git checkout -B chore/sync
git push --force --set-upstream origin chore/sync
set -euo pipefail

# The wildcard is what the bare fetch below and --force-with-lease resolve

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] With fetch-depth: 0, actions/checkout@v4 already sets remote.origin.fetch to the wildcard refspec. This git config line is redundant (as noted by @charly-bg). The git fetch below is still useful for refreshing refs, but this config line can be removed.

# branch names through.
git config --replace-all remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git fetch --no-tags --force --prune origin

# 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 git rev-parse --verify --quiet refs/remotes/origin/chore/sync >/dev/null; 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 -m "chore: merge $REF into chore/sync" "$REF"; then
git merge --abort || true
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

# 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

- name: Create or update PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 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: |
PR_NUMBER=$(gh pr list --base experimental --head chore/sync --state open --json number --jq '.[0].number')
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')"
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 \
--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
Loading