-
Notifications
You must be signed in to change notification settings - Fork 224
ci(l1,l2): stop the merge queue satisfying the required integration checks with a skipped job #7213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
bdab8f1
f21035d
95a1f6f
35172eb
ae2f3dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| #!/usr/bin/env bash | ||
| # | ||
| # Assert that every check run matching one of the given name prefixes concluded | ||
| # successfully on the head commit of each pull request in the current merge group. | ||
| # | ||
| # Usage: check-queued-pr-checks.sh "Hive - " "Assertoor - " ... | ||
| # | ||
| # Why this exists: the expensive suites (hive, assertoor) are deliberately not | ||
| # re-run inside the merge queue, so the required gate job has nothing of its own | ||
| # to inspect there. Skipping the gate instead is not a safe substitute — GitHub | ||
| # counts a skipped check run as satisfying a required status check, so the queue | ||
| # would merge a pull request whose suites were red or still running. Reading the | ||
| # pull request's own results here keeps the queue cheap while making the | ||
| # requirement real, and because it runs at merge time it also catches a result | ||
| # that turned red (or was re-triggered) after the pull request was queued. | ||
| set -euo pipefail | ||
|
|
||
| if [[ $# -eq 0 ]]; then | ||
| echo "usage: $0 <check-name-prefix> [<check-name-prefix> ...]" >&2 | ||
| exit 2 | ||
| fi | ||
|
|
||
| : "${GITHUB_REPOSITORY:?}" | ||
| : "${GITHUB_EVENT_PATH:?}" | ||
|
|
||
| base_sha=$(jq -r '.merge_group.base_sha' "$GITHUB_EVENT_PATH") | ||
| head_sha=$(jq -r '.merge_group.head_sha' "$GITHUB_EVENT_PATH") | ||
|
|
||
| if [[ -z "$base_sha" || "$base_sha" == "null" || -z "$head_sha" || "$head_sha" == "null" ]]; then | ||
| echo "No merge_group payload found; this script only runs on merge_group events." >&2 | ||
| exit 2 | ||
| fi | ||
|
|
||
| # One squashed commit per queued pull request, each titled "... (#1234)". Read | ||
| # them over the API rather than from a checkout so the job needs no clone. | ||
| mapfile -t pr_numbers < <( | ||
| gh api "repos/${GITHUB_REPOSITORY}/compare/${base_sha}...${head_sha}" \ | ||
| --jq '.commits[].commit.message | split("\n")[0]' | | ||
| grep -oE '\(#[0-9]+\)$' | | ||
| tr -d '(#)' | | ||
| sort -u | ||
| ) | ||
|
|
||
| if [[ ${#pr_numbers[@]} -eq 0 ]]; then | ||
| echo "Could not identify any pull request in merge group ${base_sha}..${head_sha}." >&2 | ||
| echo "Refusing to pass: a gate that cannot find what to verify must not report success." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "Merge group covers pull request(s): ${pr_numbers[*]}" | ||
|
|
||
| failed=0 | ||
| for pr in "${pr_numbers[@]}"; do | ||
| pr_head=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr}" --jq '.head.sha') | ||
| echo "::group::PR #${pr} (head ${pr_head})" | ||
|
|
||
| # One row per check name, tab separated, keeping only the most recently started | ||
| # run of that name. A single commit can carry several check suites (a re-trigger | ||
| # creates a new suite rather than updating the old one), and without this a | ||
| # superseded red run would keep blocking a head that is now green. | ||
| all_checks=$( | ||
| gh api --paginate --slurp \ | ||
| "repos/${GITHUB_REPOSITORY}/commits/${pr_head}/check-runs?per_page=100" | | ||
| jq -r '[.[].check_runs[]] | ||
| | group_by(.name) | ||
| | map(max_by(.started_at // "")) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This collapses same-named check runs from different workflows, keeping whichever started later.
Which one survives is runner scheduling. Across ten recent daily runs its hive jobs concluded 104 success, 1 failure, 5 cancelled, so this can mask a red L1 hive, or fail the gate on a Two smaller points on the same expression: the comment above attributes the dedupe to re-triggers, but
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Check runs are no longer read. Jobs come from one workflow run resolved by |
||
| | .[] | ||
| | [.name, .status, (.conclusion // "")] | ||
| | @tsv' | ||
| ) | ||
|
|
||
| matched=0 | ||
| while IFS=$'\t' read -r name status conclusion; do | ||
| [[ -z "$name" ]] && continue | ||
| for prefix in "$@"; do | ||
| if [[ "$name" == "$prefix"* ]]; then | ||
| matched=$((matched + 1)) | ||
| if [[ "$status" != "completed" ]]; then | ||
| echo "PENDING ${name} (status=${status}) is still running, so it cannot be merged yet" | ||
| failed=1 | ||
| elif [[ "$conclusion" != "success" && "$conclusion" != "skipped" && "$conclusion" != "neutral" ]]; then | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Real head
Rejecting
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The prefix loop is gone — the gate now reads its own verdict on the queued head, so
|
||
| echo "FAILED ${name} (conclusion=${conclusion})" | ||
| failed=1 | ||
| else | ||
| echo "ok ${name} (${conclusion})" | ||
| fi | ||
| break | ||
| fi | ||
| done | ||
| done <<<"$all_checks" | ||
|
|
||
| if [[ $matched -eq 0 ]]; then | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Reproduced under bash with the L2 caller's four prefixes: rename Worth noting that none of the six suite jobs carry the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No prefix list remains. There is one gate name, and finding no job carrying it fails closed, so a rename cannot silently drop enforcement — and a suite added later is covered without touching the script. |
||
| echo "No check runs matching [$*] on PR #${pr}." | ||
| echo "Refusing to pass: the suites this gate exists to enforce never reported." | ||
| failed=1 | ||
| fi | ||
| echo "::endgroup::" | ||
| done | ||
|
|
||
| if [[ $failed -ne 0 ]]; then | ||
| echo "Required suites did not pass on the queued pull request head(s)." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "All required suites passed on every queued pull request head." | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -468,10 +468,40 @@ jobs: | |
| name: Integration Test | ||
| runs-on: ubuntu-latest | ||
| needs: [detect-changes, run-assertoor, run-hive, check-cargo-locks, engine-ef-tests] | ||
| # Make sure this job runs even if the previous jobs failed or were skipped | ||
| if: ${{ needs.detect-changes.outputs.run_tests == 'true' && always() && needs.run-assertoor.result != 'skipped' && needs.run-hive.result != 'skipped' }} | ||
| # Runs even when a dependency failed, and deliberately does not bail out on a | ||
| # skipped one. GitHub counts a skipped check run as satisfying a required | ||
| # status check, so a gate that skips is a gate that always passes: inside the | ||
| # merge queue, where assertoor and hive do not run, that let a pull request | ||
| # whose suites were red merge on a vacuous green. | ||
| if: ${{ always() && (needs.detect-changes.result != 'success' || needs.detect-changes.outputs.run_tests == 'true') }} | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This still skips the gate whenever PR #7193 (CI-only) is the worked example: merge group run This pull request changes only Separately,
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The gate now runs on
|
||
| permissions: | ||
| contents: read | ||
| checks: read | ||
| pull-requests: read | ||
| steps: | ||
| - name: Fail if change detection did not conclude | ||
| if: ${{ needs.detect-changes.result != 'success' }} | ||
| run: | | ||
| # Without a run_tests answer there is no way to tell whether the suites | ||
| # below were required, and an unanswerable gate must not report success. | ||
| echo "detect-changes concluded '${{ needs.detect-changes.result }}'" | ||
| exit 1 | ||
|
|
||
| - name: Checkout sources | ||
| uses: actions/checkout@v6 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only the merge_group step uses the checked-out tree; the other two steps are inline shell over the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Gated on |
||
|
|
||
| # Assertoor and hive are skipped in the merge queue to keep it cheap, so | ||
| # there is no local result to inspect. Read the queued pull request's own | ||
| # results instead, which also catches a suite that turned red or was | ||
| # re-triggered after the pull request was added to the queue. | ||
| - name: Check the queued pull request's suites | ||
| if: ${{ github.event_name == 'merge_group' }} | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: ./.github/scripts/check-queued-pr-checks.sh "Hive - " "Assertoor - " | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Both halves are subsumed rather than patched. The gate reads its own verdict, which already covers everything in its |
||
|
|
||
| - name: Check if any job failed | ||
| if: ${{ github.event_name != 'merge_group' }} | ||
| run: | | ||
| if [ "${{ needs.run-assertoor.result }}" != "success" ]; then | ||
| echo "Job Assertoor Tx Check failed" | ||
|
|
@@ -483,7 +513,9 @@ jobs: | |
| exit 1 | ||
| fi | ||
|
|
||
| # engine-ef-tests is skipped in the merge queue (merge_group), which is OK. | ||
| # Tolerating a skipped engine-ef-tests is defensive rather than load | ||
| # bearing: outside the merge queue it only skips when run_tests is | ||
| # false, and then this job does not run either. | ||
| if [ "${{ needs.engine-ef-tests.result }}" != "success" ] && [ "${{ needs.engine-ef-tests.result }}" != "skipped" ]; then | ||
| echo "Job Engine EF tests failed" | ||
| exit 1 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -972,10 +972,45 @@ jobs: | |
| uniswap-swap, | ||
| integration-test-shared-bridge, | ||
| ] | ||
| # Make sure this job runs even if the previous jobs failed or were skipped | ||
| if: ${{ needs.detect-changes.outputs.run_tests == 'true' && always() && needs.integration-test.result != 'skipped' && needs.state-diff-test.result != 'skipped' && needs.integration-test-tdx.result != 'skipped' && needs.uniswap-swap.result != 'skipped' && needs.integration-test-shared-bridge.result != 'skipped' }} | ||
| # Runs even when a dependency failed, and deliberately does not bail out on a | ||
| # skipped one. GitHub counts a skipped check run as satisfying a required | ||
| # status check, so a gate that skips is a gate that always passes: inside the | ||
| # merge queue, where none of these suites run, that let a pull request whose | ||
| # suites were red merge on a vacuous green. | ||
| if: ${{ always() && (needs.detect-changes.result != 'success' || needs.detect-changes.outputs.run_tests == 'true') }} | ||
| permissions: | ||
| contents: read | ||
| checks: read | ||
| pull-requests: read | ||
| steps: | ||
| - name: Fail if change detection did not conclude | ||
| if: ${{ needs.detect-changes.result != 'success' }} | ||
| run: | | ||
| # Without a run_tests answer there is no way to tell whether the suites | ||
| # below were required, and an unanswerable gate must not report success. | ||
| echo "detect-changes concluded '${{ needs.detect-changes.result }}'" | ||
| exit 1 | ||
|
|
||
| - name: Checkout sources | ||
| uses: actions/checkout@v6 | ||
|
|
||
| # These suites are skipped in the merge queue to keep it cheap, so there is | ||
| # no local result to inspect. Read the queued pull request's own results | ||
| # instead, which also catches a suite that turned red or was re-triggered | ||
| # after the pull request was added to the queue. | ||
| - name: Check the queued pull request's suites | ||
| if: ${{ github.event_name == 'merge_group' }} | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| ./.github/scripts/check-queued-pr-checks.sh \ | ||
| "Integration Test - " \ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So a red TDX leaves Either add the tdx branch to the step below or narrow the prefix, but the two sides should agree on what is required.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
TDX stays not required, and the two sides now agree by construction: the merge group reads the pull_request gate's verdict, and that step is the only definition of what is required. Promoting a job that concluded failure or cancelled in 5 of your 12 sampled runs to a merge blocker felt like a policy change rather than a fix to this hole, so it is recorded under Not addressed here instead. |
||
| "State Reconstruction Tests" \ | ||
| "Uniswap Swap Token Flow" \ | ||
| "Integration Test Shared Bridge - " | ||
|
|
||
| - name: Check if any job failed | ||
| if: ${{ github.event_name != 'merge_group' }} | ||
| run: | | ||
| if [ "${{ needs.integration-test.result }}" != "success" ]; then | ||
| echo "Job Integration Tests failed" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This resolves the pull request's head at gate time, not the SHA that was squashed into the merge group, and the
merge_grouppayload carries no PR-head SHA to compare it against.Every branch I could construct is fail-closed: a force-pushed head has no check runs, so
matched == 0; a fresh push leaves themin_progress, so PENDING. What is lost is the property this file's header advertises, catching a result that turned red after queueing, during that window. Low priority, but a comment saying the live head is deliberate would help.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
95a1f6f02Documented in the per-pull-request loop, including why every way the live head and the squashed commit can disagree is fail-closed.