From bdab8f1bea35ce5529b55b001f55e2bbbf16c157 Mon Sep 17 00:00:00 2001 From: ilitteri Date: Mon, 24 Aug 2026 17:12:08 -0300 Subject: [PATCH 1/5] Make the merge queue enforce the required integration checks instead of skipping them. `Integration Test` and `Integration Test L2` are required status checks on main, but both gate jobs bailed out whenever a dependency was skipped: if: ${{ ... && needs.run-hive.result != 'skipped' ... }} Hive, assertoor and the L2 suites are all excluded from `merge_group` to keep the queue cheap, so inside the queue that condition was always false and the gate job was skipped. GitHub counts a skipped check run as satisfying a required status check, so every merge group satisfied both requirements without running or even consulting the suites they exist to enforce. A pull request queued while its hive run was red, or while a re-run was still in flight, merged on that vacuous green. Two of the three commits that landed on main on the day this was found had a red `Hive - Devp2p tests` and a failed `Integration Test` on their own head, and main went red on devp2p immediately afterwards. The gate jobs now always run when the workflow is not skipped wholesale, and on `merge_group` they read the queued pull request's own results for those suites via `check-queued-pr-checks.sh`. That keeps the queue's cost profile unchanged while making the requirement real, and because it runs at merge time it also catches a suite that turned red or was re-triggered after the pull request was added to the queue: a still-running suite blocks the group rather than passing. The script resolves the queued pull requests from the merge group's commit subjects, so a batched group is covered rather than only its last pull request, and it keeps the most recently started check run per name so a superseded red run cannot block a head that is now green. A skipped suite still counts as satisfied, which is what an L2-only pull request looks like to the L1 workflow. Finding no matching check run at all fails: a gate that cannot see what it is verifying must not report success. Not addressed here: `check-cargo-locks` is in the L1 gate's `needs` but its result is still never inspected, so `Check Cargo.lock` remains unenforced by the required check. That is a separate policy call. --- .github/scripts/check-queued-pr-checks.sh | 105 ++++++++++++++++++++++ .github/workflows/pr-main_l1.yaml | 30 ++++++- .github/workflows/pr-main_l2.yaml | 31 ++++++- 3 files changed, 161 insertions(+), 5 deletions(-) create mode 100755 .github/scripts/check-queued-pr-checks.sh diff --git a/.github/scripts/check-queued-pr-checks.sh b/.github/scripts/check-queued-pr-checks.sh new file mode 100755 index 00000000000..8c185f3d5a7 --- /dev/null +++ b/.github/scripts/check-queued-pr-checks.sh @@ -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 [ ...]" >&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 // "")) + | .[] + | [.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 + echo "FAILED ${name} (conclusion=${conclusion})" + failed=1 + else + echo "ok ${name} (${conclusion})" + fi + break + fi + done + done <<<"$all_checks" + + if [[ $matched -eq 0 ]]; then + 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." diff --git a/.github/workflows/pr-main_l1.yaml b/.github/workflows/pr-main_l1.yaml index 1cff24ea4ed..2a981cdce93 100644 --- a/.github/workflows/pr-main_l1.yaml +++ b/.github/workflows/pr-main_l1.yaml @@ -468,10 +468,32 @@ 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.outputs.run_tests == 'true' }} + permissions: + contents: read + checks: read + pull-requests: read steps: + - name: Checkout sources + uses: actions/checkout@v6 + + # 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 - " + - 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 +505,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 diff --git a/.github/workflows/pr-main_l2.yaml b/.github/workflows/pr-main_l2.yaml index d74edf45e89..9d2d06ccc92 100644 --- a/.github/workflows/pr-main_l2.yaml +++ b/.github/workflows/pr-main_l2.yaml @@ -972,10 +972,37 @@ 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.outputs.run_tests == 'true' }} + permissions: + contents: read + checks: read + pull-requests: read steps: + - 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 - " \ + "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" From f21035d39bd3e6318005dfa9e98e0b2ae26ba9b5 Mon Sep 17 00:00:00 2001 From: ilitteri Date: Mon, 24 Aug 2026 17:15:33 -0300 Subject: [PATCH 2/5] Fail the integration gate when change detection itself does not conclude. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same bug class as the parent commit, one dependency further up. The gate keys off `needs.detect-changes.outputs.run_tests`, and a job that failed publishes no outputs, so `'' == 'true'` was false and the gate skipped — which GitHub counts as satisfying the required check. A broken change-detection step therefore turned both `Integration Test` and `Integration Test L2` green without anything having been evaluated. The gate now also runs when `detect-changes` did not succeed, and fails immediately in that case: with no `run_tests` answer there is no way to tell whether the suites were required, and an unanswerable gate must not report success. --- .github/workflows/pr-main_l1.yaml | 10 +++++++++- .github/workflows/pr-main_l2.yaml | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-main_l1.yaml b/.github/workflows/pr-main_l1.yaml index 2a981cdce93..83555ed32e7 100644 --- a/.github/workflows/pr-main_l1.yaml +++ b/.github/workflows/pr-main_l1.yaml @@ -473,12 +473,20 @@ jobs: # 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.outputs.run_tests == 'true' }} + 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 diff --git a/.github/workflows/pr-main_l2.yaml b/.github/workflows/pr-main_l2.yaml index 9d2d06ccc92..222a3fc540b 100644 --- a/.github/workflows/pr-main_l2.yaml +++ b/.github/workflows/pr-main_l2.yaml @@ -977,12 +977,20 @@ jobs: # 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.outputs.run_tests == 'true' }} + 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 From 95a1f6f0201ba404fef36df97ecfa2d71ce218d0 Mon Sep 17 00:00:00 2001 From: ilitteri Date: Mon, 31 Aug 2026 17:48:00 -0300 Subject: [PATCH 3/5] Read the queued pull request's own gate verdict instead of matching suite check-run names. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matching check-run names on a commit was ambiguous in four ways, each of which let a real red result through: - A suite `skipped` because a dependency failed was indistinguishable from one skipped by design. Head e104cdbc had `Build Docker` = failure, which turns every hive and assertoor job into a skip, and the old loop reported it green. - `matched` was one counter across all prefixes, so renaming or deleting a single suite silently stopped enforcing it while the others kept the gate green. - `group_by(.name) | max_by(.started_at)` collapsed same-named check runs from different workflows. `daily_hive_report.yaml` publishes `Hive - ` on any pull request touching its trigger paths, with `continue-on-error: true`, so which run survived was runner scheduling. - `Engine EF tests` was in the gate's `needs` and skips in the queue exactly like hive, but was missing from the prefix list, and `Integration Test - ` matched `Integration Test - TDX`, which the pull_request side deliberately does not require. The gate now resolves the latest `pull_request` run of its own workflow on each queued pull request's head, taking the workflow path from `GITHUB_WORKFLOW_REF` so it cannot drift, and reads the verdict of this same gate job inside that run. That leaves one definition of what is required — the `Check if any job failed` step — so the two sides cannot disagree about TDX or about any suite added later, and an unrelated workflow's identically named job is out of scope by construction. `skipped` still passes, but now means something checkable: the gate itself was not required, which is what an L1-only pull request looks like to the L2 workflow. A dependency failure no longer produces it, because dropping the `needs..result != 'skipped'` guards makes the gate run and fail in that case. A run that is not yet completed fails, which is the case this exists for: re-running a suite bumps the run attempt, so a re-run in flight blocks the merge group instead of being bypassed. Resolving the pull request's live head rather than the squashed commit is deliberate and now documented in the script: the merge_group payload carries no pull request head to compare against, reading the newest head is what catches a result that turned red after queueing, and every way the two can disagree is fail-closed. --- .github/scripts/check-queued-pr-checks.sh | 146 +++++++++++++++------- .github/workflows/pr-main_l1.yaml | 12 +- .github/workflows/pr-main_l2.yaml | 17 +-- 3 files changed, 112 insertions(+), 63 deletions(-) diff --git a/.github/scripts/check-queued-pr-checks.sh b/.github/scripts/check-queued-pr-checks.sh index 8c185f3d5a7..fe1526a829c 100755 --- a/.github/scripts/check-queued-pr-checks.sh +++ b/.github/scripts/check-queued-pr-checks.sh @@ -1,27 +1,55 @@ #!/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. +# Assert that this workflow's required integration gate was genuinely green on +# the head of every pull request in the current merge group. # -# Usage: check-queued-pr-checks.sh "Hive - " "Assertoor - " ... +# Usage: check-queued-pr-checks.sh "" # -# 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. +# Why this exists: the expensive suites (hive, assertoor, the L2 integration +# tests) 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 a gate that skips is a gate that always passes, and +# the queue would merge a pull request whose suites were red or still running. +# GitHub also decides queue eligibility when the pull request is enqueued and +# never re-evaluates it afterwards, so a result that turns red later cannot +# evict it. +# +# What it reads: the latest `pull_request` run of *this* workflow on each queued +# pull request's head, and within that run the verdict of this same gate job. +# Reading the gate's own verdict rather than matching suite check-run names +# keeps one definition of what is required — the workflow's own +# `Check if any job failed` step — and cannot be confused by an unrelated +# workflow that happens to name a job the same way. +# +# How the three outcomes are read: +# - `success` passes, obviously. +# - `skipped` passes. That is what the gate looks like on a pull request whose +# changes did not require these suites at all, for example the L2 gate on an +# L1-only pull request. It is not the same as the suites being skipped for +# the wrong reason: when a dependency such as the docker build fails, the +# suites skip but the gate itself runs and fails. +# - A run that is not yet `completed` fails. That is the case this exists for: +# re-running a suite bumps the run attempt, so a re-run in flight blocks the +# merge group instead of being bypassed. set -euo pipefail -if [[ $# -eq 0 ]]; then - echo "usage: $0 [ ...]" >&2 +if [[ $# -ne 1 ]]; then + echo "usage: $0 " >&2 exit 2 fi +gate_name=$1 + : "${GITHUB_REPOSITORY:?}" : "${GITHUB_EVENT_PATH:?}" +: "${GITHUB_WORKFLOW_REF:?}" + +# "owner/repo/.github/workflows/pr-main_l1.yaml@refs/heads/..." — the middle is +# what the runs API reports as `.path`. Derived from the environment rather than +# passed in so it cannot drift if the workflow file is renamed. +workflow_path=${GITHUB_WORKFLOW_REF#"${GITHUB_REPOSITORY}/"} +workflow_path=${workflow_path%%@*} base_sha=$(jq -r '.merge_group.base_sha' "$GITHUB_EVENT_PATH") head_sha=$(jq -r '.merge_group.head_sha' "$GITHUB_EVENT_PATH") @@ -32,7 +60,9 @@ if [[ -z "$base_sha" || "$base_sha" == "null" || -z "$head_sha" || "$head_sha" = 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. +# them over the API rather than from a checkout so the job needs no deep clone, +# and from the commit subjects rather than the queue branch name because a +# batched group's ref names only its last pull request. mapfile -t pr_numbers < <( gh api "repos/${GITHUB_REPOSITORY}/compare/${base_sha}...${head_sha}" \ --jq '.commits[].commit.message | split("\n")[0]' | @@ -48,58 +78,82 @@ if [[ ${#pr_numbers[@]} -eq 0 ]]; then fi echo "Merge group covers pull request(s): ${pr_numbers[*]}" +echo "Gate: '${gate_name}' in ${workflow_path}" failed=0 for pr in "${pr_numbers[@]}"; do + # The pull request's live head, deliberately, rather than the commit that was + # squashed into the merge group: the merge_group payload carries no pull + # request head to compare against, and reading the newest head is what lets + # this catch a result that turned red after the pull request was queued. Every + # way the two can disagree is fail-closed — a head that moved after queueing + # has either no run at all or one still in flight, and both fail below. 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=$( + run=$( + gh api --paginate --slurp \ + "repos/${GITHUB_REPOSITORY}/actions/runs?head_sha=${pr_head}&event=pull_request&per_page=100" | + jq -r --arg path "$workflow_path" \ + '[.[].workflow_runs[] | select(.path == $path)] + | if length == 0 then empty + else max_by([.run_number, .id]) | [.id, .status, .html_url] | @tsv + end' + ) + + if [[ -z "$run" ]]; then + echo "No ${workflow_path} pull_request run on head ${pr_head}." + echo "Refusing to pass: a gate that cannot see what it is verifying must not report success." + failed=1 + echo "::endgroup::" + continue + fi + + IFS=$'\t' read -r run_id run_status run_url <<<"$run" + + if [[ "$run_status" != "completed" ]]; then + echo "PENDING the run is still '${run_status}', so this cannot be merged yet: ${run_url}" + failed=1 + echo "::endgroup::" + continue + fi + + # `/jobs` defaults to the latest run attempt, which is the one whose verdict + # counts. Every job carrying the gate's name is read, so a duplicate cannot + # hide behind a green sibling. + gate_jobs=$( 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 // "")) - | .[] - | [.name, .status, (.conclusion // "")] - | @tsv' + "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" | + jq -r --arg name "$gate_name" \ + '.[].jobs[] | select(.name == $name) | [.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 - echo "FAILED ${name} (conclusion=${conclusion})" - failed=1 - else - echo "ok ${name} (${conclusion})" - fi - break - fi - done - done <<<"$all_checks" + matched=$((matched + 1)) + if [[ "$status" != "completed" ]]; then + echo "PENDING ${name} is '${status}', so this cannot be merged yet: ${run_url}" + failed=1 + elif [[ "$conclusion" == "success" || "$conclusion" == "skipped" ]]; then + echo "ok ${name} (${conclusion}): ${run_url}" + else + echo "FAILED ${name} concluded '${conclusion}': ${run_url}" + failed=1 + fi + done <<<"$gate_jobs" if [[ $matched -eq 0 ]]; then - echo "No check runs matching [$*] on PR #${pr}." - echo "Refusing to pass: the suites this gate exists to enforce never reported." + echo "No job named '${gate_name}' in ${run_url}." + echo "Refusing to pass: a gate that cannot see what it is verifying must not report success." 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 + echo "The required gate was not green on the queued pull request head(s)." >&2 exit 1 fi -echo "All required suites passed on every queued pull request head." +echo "'${gate_name}' was green on every queued pull request head." diff --git a/.github/workflows/pr-main_l1.yaml b/.github/workflows/pr-main_l1.yaml index 83555ed32e7..c0b2b7994d8 100644 --- a/.github/workflows/pr-main_l1.yaml +++ b/.github/workflows/pr-main_l1.yaml @@ -476,7 +476,7 @@ jobs: if: ${{ always() && (needs.detect-changes.result != 'success' || needs.detect-changes.outputs.run_tests == 'true') }} permissions: contents: read - checks: read + actions: read pull-requests: read steps: - name: Fail if change detection did not conclude @@ -491,14 +491,14 @@ jobs: uses: actions/checkout@v6 # 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 + # there is no local result to inspect. Read this gate's own verdict on the + # queued pull request's head instead, which also catches a suite that + # turned red or was re-triggered after the pull request was queued. + - name: Check the queued pull request's gate if: ${{ github.event_name == 'merge_group' }} env: GH_TOKEN: ${{ github.token }} - run: ./.github/scripts/check-queued-pr-checks.sh "Hive - " "Assertoor - " + run: ./.github/scripts/check-queued-pr-checks.sh "Integration Test" - name: Check if any job failed if: ${{ github.event_name != 'merge_group' }} diff --git a/.github/workflows/pr-main_l2.yaml b/.github/workflows/pr-main_l2.yaml index 222a3fc540b..2144a040255 100644 --- a/.github/workflows/pr-main_l2.yaml +++ b/.github/workflows/pr-main_l2.yaml @@ -980,7 +980,7 @@ jobs: if: ${{ always() && (needs.detect-changes.result != 'success' || needs.detect-changes.outputs.run_tests == 'true') }} permissions: contents: read - checks: read + actions: read pull-requests: read steps: - name: Fail if change detection did not conclude @@ -995,19 +995,14 @@ jobs: 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 + # no local result to inspect. Read this gate's own verdict on the queued + # pull request's head instead, which also catches a suite that turned red + # or was re-triggered after the pull request was queued. + - name: Check the queued pull request's gate if: ${{ github.event_name == 'merge_group' }} env: GH_TOKEN: ${{ github.token }} - run: | - ./.github/scripts/check-queued-pr-checks.sh \ - "Integration Test - " \ - "State Reconstruction Tests" \ - "Uniswap Swap Token Flow" \ - "Integration Test Shared Bridge - " + run: ./.github/scripts/check-queued-pr-checks.sh "Integration Test L2" - name: Check if any job failed if: ${{ github.event_name != 'merge_group' }} From 35172eb57cdcdbe9114e1eec4daee9d0dee871e6 Mon Sep 17 00:00:00 2001 From: ilitteri Date: Mon, 31 Aug 2026 17:48:40 -0300 Subject: [PATCH 4/5] Run the integration gate in the merge queue whatever change detection decided. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `run_tests` on `merge_group` is `code_changed`, which matches only `**/*.rs`, `**/*.toml` and `**/*.lock`. Any pull request touching just workflows, scripts, fixtures or configs therefore still skipped the gate in the queue and turned both required checks green with nothing consulted — the exact hole this branch set out to close, and the class this branch itself belongs to. PR #7193 is the worked example: its merge group skipped every job, `Integration Test` included, and it merged, while its head carried seven green `Hive - *` results the queue never read. The gate now runs on `merge_group` regardless. A pull request whose changes genuinely required nothing still passes, because the verdict it reads on the head is then `skipped`. `!cancelled()` replaces `always()` at the same time. The old expression short-circuited on an empty `outputs.run_tests`, so a concurrency-cancelled run skipped the gate; with the condition widened it would instead run and fail, stamping a red required check on a commit that has already been superseded. --- .github/workflows/pr-main_l1.yaml | 9 +++++++-- .github/workflows/pr-main_l2.yaml | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr-main_l1.yaml b/.github/workflows/pr-main_l1.yaml index c0b2b7994d8..91537849caf 100644 --- a/.github/workflows/pr-main_l1.yaml +++ b/.github/workflows/pr-main_l1.yaml @@ -472,8 +472,13 @@ jobs: # 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') }} + # whose suites were red merge on a vacuous green. In the queue it therefore + # runs unconditionally, because `run_tests` there tracks only Rust changes + # and a pull request touching just workflows, scripts or fixtures would keep + # skipping the gate. `!cancelled()` rather than `always()` so a + # concurrency-cancelled run does not stamp a red required check on a + # superseded commit. + if: ${{ !cancelled() && (github.event_name == 'merge_group' || needs.detect-changes.result != 'success' || needs.detect-changes.outputs.run_tests == 'true') }} permissions: contents: read actions: read diff --git a/.github/workflows/pr-main_l2.yaml b/.github/workflows/pr-main_l2.yaml index 2144a040255..f7d3833dfa7 100644 --- a/.github/workflows/pr-main_l2.yaml +++ b/.github/workflows/pr-main_l2.yaml @@ -976,8 +976,13 @@ jobs: # 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') }} + # suites were red merge on a vacuous green. In the queue it therefore runs + # unconditionally, because `run_tests` there tracks only Rust changes and a + # pull request touching just workflows, scripts or fixtures would keep + # skipping the gate. `!cancelled()` rather than `always()` so a + # concurrency-cancelled run does not stamp a red required check on a + # superseded commit. + if: ${{ !cancelled() && (github.event_name == 'merge_group' || needs.detect-changes.result != 'success' || needs.detect-changes.outputs.run_tests == 'true') }} permissions: contents: read actions: read From ae2f3dd9c3e4d4977d5e7135f37cdc3ca4ed2bf7 Mon Sep 17 00:00:00 2001 From: ilitteri Date: Mon, 31 Aug 2026 17:49:21 -0300 Subject: [PATCH 5/5] Skip the gate's checkout outside the merge queue. Only the `merge_group` step uses the checked-out tree; the pull_request and push paths evaluate the `needs` context in inline shell and need nothing from disk. Both gates cloned the repository on every run regardless. --- .github/workflows/pr-main_l1.yaml | 3 +++ .github/workflows/pr-main_l2.yaml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/pr-main_l1.yaml b/.github/workflows/pr-main_l1.yaml index 91537849caf..0b6f3d0d4ae 100644 --- a/.github/workflows/pr-main_l1.yaml +++ b/.github/workflows/pr-main_l1.yaml @@ -492,7 +492,10 @@ jobs: echo "detect-changes concluded '${{ needs.detect-changes.result }}'" exit 1 + # Only the merge_group step below needs the tree; the pull_request and push + # paths read the `needs` context and nothing else. - name: Checkout sources + if: ${{ github.event_name == 'merge_group' }} uses: actions/checkout@v6 # Assertoor and hive are skipped in the merge queue to keep it cheap, so diff --git a/.github/workflows/pr-main_l2.yaml b/.github/workflows/pr-main_l2.yaml index f7d3833dfa7..7e702cf4705 100644 --- a/.github/workflows/pr-main_l2.yaml +++ b/.github/workflows/pr-main_l2.yaml @@ -996,7 +996,10 @@ jobs: echo "detect-changes concluded '${{ needs.detect-changes.result }}'" exit 1 + # Only the merge_group step below needs the tree; the pull_request and push + # paths read the `needs` context and nothing else. - name: Checkout sources + if: ${{ github.event_name == 'merge_group' }} uses: actions/checkout@v6 # These suites are skipped in the merge queue to keep it cheap, so there is