From 69cb33944fcb8a8b8334f2cdd50ca4b97abf20ab Mon Sep 17 00:00:00 2001 From: John McCall Date: Wed, 2 Sep 2026 14:41:12 -0400 Subject: [PATCH 1/3] chore: detect vnext release merge by ancestry, not branch name vnext-compat.yaml and rebase-vnext.yaml both special-cased head_ref == 'vnext' to skip their normal checks for the vnext release merge. That missed OvertureMaps/schema#709, which stages the release on a 2_0_0 branch built on top of vnext's tip instead of merging vnext directly. Detect the release merge by ancestry instead: origin/vnext being an ancestor of the PR head (vnext-compat) or the new main HEAD (rebase-vnext) means the branch already carries everything on vnext, so any staging branch is recognized the same way a PR literally named vnext was. Document the pattern in CONTRIBUTING.md. Fixes OvertureMaps/schema#713 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: John McCall --- .github/workflows/rebase-vnext.yaml | 38 ++++++++++++++----------- .github/workflows/vnext-compat.yaml | 43 +++++++++++++++++++++++++---- CONTRIBUTING.md | 9 ++++++ 3 files changed, 68 insertions(+), 22 deletions(-) diff --git a/.github/workflows/rebase-vnext.yaml b/.github/workflows/rebase-vnext.yaml index 7e64bf462..38319b26a 100644 --- a/.github/workflows/rebase-vnext.yaml +++ b/.github/workflows/rebase-vnext.yaml @@ -3,8 +3,11 @@ name: Rebase vnext onto main # Triggered on every push to main. Force-rebases vnext onto the new main HEAD # using the overture-pull-requester GitHub App (which has branch-protection bypass). # -# Skipped for vnext→main release merges — vnext is already equal to main at that -# point so a rebase would be a no-op, and the GitHub API check catches this. +# Skipped for a vnext release merge — detected by ancestry (origin/vnext is +# already an ancestor of the new main HEAD), not by branch name, so a staging +# branch built on top of vnext's tip (e.g. `2_0_0`) is skipped the same as a +# PR literally named `vnext`. vnext is already equal to main at that point so +# a rebase would be a no-op. # # If the rebase fails a GitHub issue is opened and assigned to the PR author # so the conflict can be resolved manually. @@ -41,23 +44,34 @@ jobs: permission-pull-requests: read # read PR on merge commit to detect vnext→main release permission-workflows: write # vnext commits may touch .github/workflows/** - # Detect whether this push was a vnext→main release merge. - # The GitHub API returns the PR(s) associated with the merge commit. - - name: Detect vnext→main release merge + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + # Use the app token so the subsequent force-push is authenticated. + token: ${{ steps.app-token.outputs.token }} + persist-credentials: true + + # Detect whether this push was a vnext release merge, by ancestry rather + # than branch name: if origin/vnext is already an ancestor of the new + # main HEAD, all of vnext's commits are in main and rebasing is a no-op. + # The GitHub API additionally returns the PR associated with the merge + # commit, so a failure issue can still be filed against its author. + - name: Detect vnext release merge id: skip env: GH_TOKEN: ${{ steps.app-token.outputs.token }} MAIN_SHA: ${{ github.sha }} run: | + git fetch origin vnext + PR_JSON=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${MAIN_SHA}/pulls" \ --jq '.[0] // empty' 2>/dev/null || true) - HEAD_REF=$(echo "$PR_JSON" | jq -r '.head.ref // empty' 2>/dev/null || true) PR_NUMBER=$(echo "$PR_JSON" | jq -r '.number // empty' 2>/dev/null || true) PR_AUTHOR=$(echo "$PR_JSON" | jq -r '.user.login // empty' 2>/dev/null || true) - if [ "$HEAD_REF" = "vnext" ]; then - echo "Skipping: this is a vnext→main release merge." + if git merge-base --is-ancestor origin/vnext "$MAIN_SHA"; then + echo "Skipping: origin/vnext is already an ancestor of main, this is a release merge." echo "skip=true" >> "$GITHUB_OUTPUT" else echo "skip=false" >> "$GITHUB_OUTPUT" @@ -65,14 +79,6 @@ jobs: echo "pr_author=${PR_AUTHOR}" >> "$GITHUB_OUTPUT" fi - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - if: steps.skip.outputs.skip != 'true' - with: - fetch-depth: 0 - # Use the app token so the subsequent force-push is authenticated. - token: ${{ steps.app-token.outputs.token }} - persist-credentials: true - - name: Rebase vnext onto new main HEAD if: steps.skip.outputs.skip != 'true' id: rebase diff --git a/.github/workflows/vnext-compat.yaml b/.github/workflows/vnext-compat.yaml index 2d5425496..25323ed9f 100644 --- a/.github/workflows/vnext-compat.yaml +++ b/.github/workflows/vnext-compat.yaml @@ -7,9 +7,12 @@ name: vnext compatibility check # exact commands to resolve it. # # Only PRs targeting main do that real work — compat-check is a no-op for -# anything else (including vnext→main release PRs, head_ref == 'vnext'). The -# noop job covers that inverse case, and both paths feed vnext-status so -# there's always a single, consistently-named check to require. +# anything else, including a vnext release merge. A release merge is detected +# by ancestry (origin/vnext is an ancestor of the PR head), not by branch name, +# so a staging branch built on top of vnext's tip (e.g. `2_0_0`) is recognized +# the same as a PR literally named `vnext`. The noop job covers that inverse +# case, and both paths feed vnext-status so there's always a single, +# consistently-named check to require. on: pull_request: @@ -23,9 +26,36 @@ concurrency: cancel-in-progress: true jobs: + detect-release: + name: Detect vnext release merge + if: github.event.pull_request.base.ref == 'main' + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + is_release: ${{ steps.check.outputs.is_release }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Check whether origin/vnext is an ancestor of this PR's head + id: check + env: + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + git fetch origin vnext + if git merge-base --is-ancestor origin/vnext "$PR_HEAD_SHA"; then + echo "is_release=true" >> "$GITHUB_OUTPUT" + else + echo "is_release=false" >> "$GITHUB_OUTPUT" + fi + compat-check: name: Check vnext compatibility - if: github.event.pull_request.base.ref == 'main' && github.head_ref != 'vnext' + needs: detect-release + if: github.event.pull_request.base.ref == 'main' && needs.detect-release.outputs.is_release != 'true' runs-on: ubuntu-slim permissions: contents: read @@ -160,10 +190,11 @@ jobs: noop: name: Skip vnext compatibility check - if: github.event.pull_request.base.ref != 'main' || github.head_ref == 'vnext' + needs: detect-release + if: github.event.pull_request.base.ref != 'main' || needs.detect-release.outputs.is_release == 'true' runs-on: ubuntu-slim steps: - - run: echo "Base isn't main, or this is the vnext release PR — nothing to check here." + - run: echo "Base isn't main, or this is the vnext release merge — nothing to check here." vnext-status: name: vnext compatibility status diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f52792ea..93f4ebf77 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -109,6 +109,15 @@ merge to `main`, CI cuts a published GitHub Release tagged `-v` with those notes. See [docs/versioning.md](docs/versioning.md). +The PR that carries the release merge doesn't have to be `vnext` itself: you +can stage it on a branch built on top of `vnext`'s tip (for example `2_0_0`) +instead, useful when `vnext` needs to stay open for more breaking work while +the release stabilizes. Either way it still has to land on `main` as a regular +merge commit, not a squash. [vnext-compat.yaml](.github/workflows/vnext-compat.yaml) +and [rebase-vnext.yaml](.github/workflows/rebase-vnext.yaml) detect the +release merge by ancestry (`git merge-base --is-ancestor origin/vnext `) +rather than by branch name, so either path is recognized the same way. + ## Opening a PR Two CI checks may comment on your PR: From bd0c138a5888327e4c3dd0b7f3890138677460d9 Mon Sep 17 00:00:00 2001 From: John McCall Date: Wed, 2 Sep 2026 14:44:41 -0400 Subject: [PATCH 2/3] chore: surface skip reasons as GH Actions notice annotations Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: John McCall --- .github/workflows/rebase-vnext.yaml | 2 +- .github/workflows/vnext-compat.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rebase-vnext.yaml b/.github/workflows/rebase-vnext.yaml index 38319b26a..fc3291a9c 100644 --- a/.github/workflows/rebase-vnext.yaml +++ b/.github/workflows/rebase-vnext.yaml @@ -71,7 +71,7 @@ jobs: PR_AUTHOR=$(echo "$PR_JSON" | jq -r '.user.login // empty' 2>/dev/null || true) if git merge-base --is-ancestor origin/vnext "$MAIN_SHA"; then - echo "Skipping: origin/vnext is already an ancestor of main, this is a release merge." + echo "::notice::Skipping rebase: origin/vnext is already an ancestor of main, this is a release merge." echo "skip=true" >> "$GITHUB_OUTPUT" else echo "skip=false" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/vnext-compat.yaml b/.github/workflows/vnext-compat.yaml index 25323ed9f..ee3ab2dfd 100644 --- a/.github/workflows/vnext-compat.yaml +++ b/.github/workflows/vnext-compat.yaml @@ -194,7 +194,7 @@ jobs: if: github.event.pull_request.base.ref != 'main' || needs.detect-release.outputs.is_release == 'true' runs-on: ubuntu-slim steps: - - run: echo "Base isn't main, or this is the vnext release merge — nothing to check here." + - run: echo "::notice::Base isn't main, or this is the vnext release merge — nothing to check here." vnext-status: name: vnext compatibility status From 6f32fdc41d2ad67495a52a45404a9bdab15c1365 Mon Sep 17 00:00:00 2001 From: John McCall Date: Wed, 2 Sep 2026 14:52:55 -0400 Subject: [PATCH 3/3] fix: run detect-release unconditionally to avoid needs-skip propagation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: John McCall --- .github/workflows/vnext-compat.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/vnext-compat.yaml b/.github/workflows/vnext-compat.yaml index ee3ab2dfd..8ca3e7c06 100644 --- a/.github/workflows/vnext-compat.yaml +++ b/.github/workflows/vnext-compat.yaml @@ -28,14 +28,17 @@ concurrency: jobs: detect-release: name: Detect vnext release merge - if: github.event.pull_request.base.ref == 'main' runs-on: ubuntu-slim permissions: contents: read outputs: is_release: ${{ steps.check.outputs.is_release }} steps: + # Runs unconditionally so noop/compat-check (which `need` this job) never + # get skipped by dependency propagation for non-main bases; skip the + # actual check with a fast is_release=false instead. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + if: github.event.pull_request.base.ref == 'main' with: fetch-depth: 0 persist-credentials: false @@ -44,7 +47,13 @@ jobs: id: check env: PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} run: | + if [ "$PR_BASE_REF" != "main" ]; then + echo "is_release=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + git fetch origin vnext if git merge-base --is-ancestor origin/vnext "$PR_HEAD_SHA"; then echo "is_release=true" >> "$GITHUB_OUTPUT"