Skip to content
Merged
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
94 changes: 94 additions & 0 deletions .github/workflows/merge-sync-to-experimental.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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:

# Shared with sync-main-to-experimental.yml: see the note there.
concurrency:
group: chore-sync-branch
cancel-in-progress: false

permissions:
contents: write
pull-requests: write

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 fetch --no-tags --force --prune origin
Comment thread
pravusjif marked this conversation as resolved.

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."
# It is fully merged, so a leftover branch is only a failed cleanup.
git push origin --delete chore/sync || true
exit 0
Comment thread
pravusjif marked this conversation as resolved.
fi

PR_NUMBER="$(gh pr list --base experimental --head chore/sync --state open --json number --jq '.[0].number // empty')"

# The sync PR is opened as a draft so it cannot be squashed. Take it out of
# draft before the merge lands, so GitHub applies its normal
# merged-by-reachability handling to it.
if [ -n "$PR_NUMBER" ]; then
gh pr ready "$PR_NUMBER" \
|| echo "::warning::Could not mark PR #$PR_NUMBER ready; it may close as Closed rather than Merged."
fi

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. Deleting a head branch also closes its PR, so wait for
# GitHub to register the merge first — otherwise the PR reads Closed, not Merged.
STATE=""
if [ -n "$PR_NUMBER" ]; then
for _ in $(seq 1 10); do
STATE="$(gh pr view "$PR_NUMBER" --json state --jq .state)"
[ "$STATE" = "MERGED" ] && break
sleep 3
done
if [ "$STATE" != "MERGED" ]; then
echo "::warning::PR #$PR_NUMBER still reads $STATE, so chore/sync is left in place rather than closing the PR unmerged. Re-run this workflow to clean it up."
exit 0
fi
fi

# Non-fatal: experimental is already pushed, so a failed cleanup is not a
# failed sync. The no-op path above finishes the job on any later run.
git push origin --delete chore/sync \
|| echo "::warning::Could not delete chore/sync; re-run this workflow to clean it up."
106 changes: 100 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,129 @@ on:
branches: [ main ]
workflow_dispatch:

# Shared with merge-sync-to-experimental.yml: both workflows mutate chore/sync, and
# interleaving them either rejects this push or recreates a branch that one just
# deleted. One group serializes every writer.
concurrency:
group: chore-sync-branch
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

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. Skip it when there is
# nothing to propose either, so a no-op run does not leave a branch behind
# that later runs would read as a sync in flight.
REMOTE_HEAD="$(git rev-parse --verify --quiet refs/remotes/origin/chore/sync || echo absent)"
if [ "$(git rev-parse HEAD)" = "$REMOTE_HEAD" ] \
|| git merge-base --is-ancestor HEAD origin/experimental; then
echo "Nothing to push: chore/sync is current, or experimental already contains it."
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*

**This PR is a draft on purpose: it is landed by a workflow, not by a button.**

### How to land it

1. Test the `@dcl/protocol` tarball from the **build-deploy** comment below in
the Explorer, and review the diff as usual.
2. Go to **Actions -> Merge sync PR into experimental -> Run workflow**, from
`main`.
3. That workflow marks this PR ready, merges `chore/sync` into `experimental`
as a real merge commit, and this PR closes as **Merged**.

Further commits on `main` are merged onto this same branch while the PR is
open, so it stays current on its own.

> [!IMPORTANT]
> Do not take this PR out of draft in order to squash it. Squash is the only
> merge this repo allows, and squashing replaces `main`'s commits with a single
> new one: `main` stops being an ancestor of `experimental`, the merge base
> freezes, and every later sync re-applies changes `experimental` already has,
> conflicting within a couple of runs.
>
> 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.
# The tree check also covers a net-zero round of main changes, where commits
# exist but there is nothing left to propose.
if git merge-base --is-ancestor HEAD origin/experimental \
|| git diff --quiet origin/experimental HEAD; then
echo "experimental already has everything chore/sync would 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..."
# --draft: squash is the only merge the repo allows, and squashing this PR
# breaks the ancestry. A draft has no merge button at all.
gh pr create \
--draft \
--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