Skip to content

Commit dba91ca

Browse files
Skip merge queue for GHA CI (#4687)
* Move .buildkite/pipeline.yml to full_pipeline.yml * Add dispatch pipeline.yml and merge-queue skip scripts * Skip merge queue for GHA CI
1 parent 78dd3b0 commit dba91ca

3 files changed

Lines changed: 186 additions & 0 deletions

File tree

.github/should_skip_merge_queue.sh

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/bin/bash
2+
# GitHub Actions merge-queue fast path. Companion to
3+
# .buildkite/should_skip_merge_queue.sh, applying the same reasoning to the
4+
# GitHub-hosted checks: the unit-test matrix (.github/workflows/ci.yml) and the
5+
# doc build (.github/workflows/docs.yml).
6+
#
7+
# Exit 0 => the merge group is provably identical to an already-green PR head,
8+
# so this workflow's real jobs can be skipped and a trivial gate job
9+
# reports the required check green.
10+
# Exit non-zero => run the real jobs.
11+
#
12+
# Usage: should_skip_merge_queue.sh <check-name-prefix>
13+
# <check-name-prefix> selects which of the PR's checks must already be green
14+
# for this workflow, e.g. "ci " for the ci matrix or "docbuild" for the docs.
15+
#
16+
# All of the following must hold to skip. ANY uncertainty (not a merge-queue
17+
# build, unparseable ref, failed fetch/API call, missing or non-green check)
18+
# exits non-zero and runs the full jobs, so a wrong guess can never merge
19+
# untested code:
20+
#
21+
# 1. This is a GitHub merge-queue build: GITHUB_EVENT_NAME == "merge_group"
22+
# and the head ref has the form
23+
# gh-readonly-queue/main/pr-<pr_number>-<base_sha>
24+
#
25+
# 2. <base_sha> (from the merge_group payload) is a real commit on `main`
26+
# (git merge-base --is-ancestor), i.e. the merge group stacks exactly one
27+
# PR on `main`. Groups combining multiple PRs stack on a synthetic
28+
# gh-readonly-queue commit that never reaches `main` and fail this check.
29+
# (Same argument as the Buildkite script; see its header for detail.)
30+
#
31+
# 3. The merge group's tree equals the PR head's tree, i.e. the merged code is
32+
# byte-identical to what the PR already tested. This is the "up to date
33+
# with origin/main" condition: it holds only when `main` has not advanced
34+
# past what the PR contains. Compares tree content, so it holds for the
35+
# squash, rebase, and merge-commit queue methods alike.
36+
#
37+
# 4. Every one of the PR's own checks whose name starts with <prefix> already
38+
# concluded successfully (queried with `gh pr checks`, which reports the
39+
# checks GitHub shows for the PR head and is agnostic to how Actions
40+
# associates runs). This is the GitHub analogue of the Buildkite
41+
# "staged bundle exists" condition: it confirms the PR build actually ran
42+
# these checks to green on the identical code.
43+
set -euo pipefail
44+
45+
prefix="${1:?usage: should_skip_merge_queue.sh <check-name-prefix>}"
46+
47+
# (1) merge-queue build + parse the PR number from the head ref.
48+
[[ "${GITHUB_EVENT_NAME:-}" == "merge_group" ]] || exit 1
49+
head_ref="${MERGE_GROUP_HEAD_REF:-}"
50+
base_sha="${MERGE_GROUP_BASE_SHA:-}"
51+
[[ "$head_ref" =~ pr-([0-9]+)-[0-9a-fA-F]+$ ]] || exit 1
52+
pr_number="${BASH_REMATCH[1]}"
53+
[[ -n "$base_sha" ]] || exit 1
54+
55+
# (2) <base_sha> must be a real commit on `main` (single-PR delta).
56+
git fetch -q origin main || exit 1
57+
git merge-base --is-ancestor "$base_sha" FETCH_HEAD || exit 1
58+
59+
# (3) merge-group tree must equal the PR head tree (up to date, nothing new to
60+
# test). HEAD is the merge-group commit checked out by actions/checkout.
61+
git fetch -q origin "refs/pull/${pr_number}/head" || exit 1
62+
pr_head="$(git rev-parse FETCH_HEAD)" || exit 1
63+
git diff --quiet "$pr_head" HEAD || exit 1
64+
65+
# (4) every matching check on the PR already concluded success. `gh pr checks`
66+
# exits non-zero when checks are pending/failing, so read the JSON (still
67+
# emitted) and evaluate it ourselves; require >=1 match, all buckets "pass".
68+
checks="$(gh pr checks "$pr_number" --repo "$GITHUB_REPOSITORY" \
69+
--json name,bucket 2>/dev/null || true)"
70+
[[ -n "$checks" ]] || exit 1
71+
echo "$checks" | jq -e --arg p "$prefix" \
72+
'[.[] | select(.name | startswith($p))] as $m
73+
| ($m | length) > 0 and ($m | all(.bucket == "pass"))' >/dev/null || exit 1
74+
75+
exit 0

.github/workflows/ci.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,41 @@ permissions:
1616
contents: read
1717

1818
jobs:
19+
# Merge-queue fast path: decide whether the matrix below can be reused from an
20+
# already-green PR head (see .github/should_skip_merge_queue.sh). Always runs;
21+
# only ever returns skip=true for a merge_group build that is provably
22+
# identical to a PR whose "ci " checks already passed.
23+
decide:
24+
name: decide
25+
runs-on: ubuntu-latest
26+
permissions:
27+
contents: read
28+
checks: read
29+
pull-requests: read
30+
statuses: read
31+
outputs:
32+
skip: ${{ steps.decide.outputs.skip }}
33+
steps:
34+
- uses: actions/checkout@v7
35+
with:
36+
fetch-depth: 0
37+
- id: decide
38+
env:
39+
GH_TOKEN: ${{ github.token }}
40+
MERGE_GROUP_HEAD_REF: ${{ github.event.merge_group.head_ref }}
41+
MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
42+
run: |
43+
if .github/should_skip_merge_queue.sh "ci "; then
44+
echo "Merge-queue CI skipped: reusing this PR's green checks."
45+
echo "skip=true" >> "$GITHUB_OUTPUT"
46+
else
47+
echo "skip=false" >> "$GITHUB_OUTPUT"
48+
fi
49+
1950
test:
2051
name: ci ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.test_group }}
52+
needs: decide
53+
if: needs.decide.outputs.skip != 'true'
2154

2255
runs-on: ${{ matrix.os }}
2356
timeout-minutes: 60
@@ -59,3 +92,26 @@ jobs:
5992
with:
6093
file: lcov.info
6194
token: ${{secrets.CODECOV_TOKEN}}
95+
96+
# Single required status check for branch protection / the merge queue. Set
97+
# THIS ("ci-required") as the required check instead of the individual matrix
98+
# jobs, so a reused merge-queue build (matrix skipped) still reports green.
99+
ci-required:
100+
name: ci-required
101+
needs: [decide, test]
102+
if: always()
103+
runs-on: ubuntu-latest
104+
steps:
105+
- run: |
106+
if [[ "${{ needs.decide.result }}" != "success" ]]; then
107+
echo "decide job did not succeed"; exit 1
108+
fi
109+
if [[ "${{ needs.test.result }}" == "success" ]]; then
110+
echo "ci matrix passed"; exit 0
111+
fi
112+
if [[ "${{ needs.test.result }}" == "skipped" && \
113+
"${{ needs.decide.outputs.skip }}" == "true" ]]; then
114+
echo "ci matrix reused from an already-green PR head"; exit 0
115+
fi
116+
echo "ci matrix did not pass (result=${{ needs.test.result }})"
117+
exit 1

.github/workflows/docs.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,39 @@ concurrency:
1313
cancel-in-progress: true
1414

1515
jobs:
16+
# Merge-queue fast path: reuse the doc build from an already-green PR head
17+
# when the merge group is byte-identical to it (see
18+
# .github/should_skip_merge_queue.sh).
19+
decide:
20+
name: decide
21+
runs-on: ubuntu-latest
22+
permissions:
23+
contents: read
24+
checks: read
25+
pull-requests: read
26+
statuses: read
27+
outputs:
28+
skip: ${{ steps.decide.outputs.skip }}
29+
steps:
30+
- uses: actions/checkout@v7
31+
with:
32+
fetch-depth: 0
33+
- id: decide
34+
env:
35+
GH_TOKEN: ${{ github.token }}
36+
MERGE_GROUP_HEAD_REF: ${{ github.event.merge_group.head_ref }}
37+
MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
38+
run: |
39+
if .github/should_skip_merge_queue.sh "docbuild"; then
40+
echo "Merge-queue doc build skipped: reusing this PR's green check."
41+
echo "skip=true" >> "$GITHUB_OUTPUT"
42+
else
43+
echo "skip=false" >> "$GITHUB_OUTPUT"
44+
fi
45+
1646
docbuild:
47+
needs: decide
48+
if: needs.decide.outputs.skip != 'true'
1749
runs-on: ubuntu-latest
1850
timeout-minutes: 30
1951
steps:
@@ -61,3 +93,26 @@ jobs:
6193
} else {
6294
await github.rest.issues.createComment({ owner, repo, issue_number: pr, body });
6395
}
96+
97+
# Single required status check for the doc build. Set THIS ("docs-required")
98+
# as the required check instead of "docbuild", so a reused merge-queue build
99+
# (docbuild skipped) still reports green.
100+
docs-required:
101+
name: docs-required
102+
needs: [decide, docbuild]
103+
if: always()
104+
runs-on: ubuntu-latest
105+
steps:
106+
- run: |
107+
if [[ "${{ needs.decide.result }}" != "success" ]]; then
108+
echo "decide job did not succeed"; exit 1
109+
fi
110+
if [[ "${{ needs.docbuild.result }}" == "success" ]]; then
111+
echo "doc build passed"; exit 0
112+
fi
113+
if [[ "${{ needs.docbuild.result }}" == "skipped" && \
114+
"${{ needs.decide.outputs.skip }}" == "true" ]]; then
115+
echo "doc build reused from an already-green PR head"; exit 0
116+
fi
117+
echo "doc build did not pass (result=${{ needs.docbuild.result }})"
118+
exit 1

0 commit comments

Comments
 (0)