Skip to content

ci(l1,l2): stop the merge queue satisfying the required integration checks with a skipped job - #7213

Open
ilitteri wants to merge 2 commits into
mainfrom
ci/merge-queue-required-checks
Open

ci(l1,l2): stop the merge queue satisfying the required integration checks with a skipped job#7213
ilitteri wants to merge 2 commits into
mainfrom
ci/merge-queue-required-checks

Conversation

@ilitteri

Copy link
Copy Markdown
Collaborator

Motivation

A pull request whose Hive run was red, or whose re-run was still in flight, could merge
through the merge queue without that result ever being consulted.

Integration Test and Integration Test L2 are required status checks on main, and both
gate jobs bailed out whenever a dependency was skipped:

if: ${{ ... && needs.run-assertoor.result != 'skipped' && 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.

Merge when ready compounds it: GitHub decides queue eligibility when the pull request is
enqueued and does not re-evaluate the head afterwards. A stale green Integration Test is
enough to get in, a later red result cannot evict it, and the merge group's own gate was
vacuous. So re-running a failed Hive job did not protect main — the merge went ahead while
the re-run was still going, and the re-run's failure landed after the merge.

Observed on three merges the same day:

pull request Integration Test on its head outcome
#7200 failure at 16:49:47 merge group started 16:50:00, merged
#7201 failure at 17:03:50 merged at 17:52

Both had a red Hive - Devp2p tests, and main went red on devp2p immediately afterwards
until #7204 fixed the underlying discv5 bug.

Description

No suite is added to the merge queue. All fourteen github.event_name != 'merge_group'
exclusions are untouched, so hive, assertoor, reorg, engine-EF and the L2 suites still do not
run there, and the queue's cost profile is unchanged. What changes is what the gate does:

  • The gate jobs no longer bail out on a skipped dependency, because a gate that skips is a
    gate that always passes.
  • On merge_group they run .github/scripts/check-queued-pr-checks.sh, which reads the
    queued pull request's own check runs for those suites. A red suite fails the group; a suite
    that is not yet completed also fails it, which is the case this PR is really about — a
    Hive re-run in flight now blocks the merge group instead of being bypassed. Because this
    runs at merge time rather than at enqueue time, it also catches a result that turned red
    after the pull request was queued.
  • A second commit closes the same hole one dependency up: a failed detect-changes publishes
    no outputs, so '' == 'true' skipped the gate and turned both required checks green with
    nothing evaluated. The gate now runs and fails in that case.

Details in the script that are easy to get wrong, and why it is written this way:

  • Queued pull requests are resolved from the merge group's commit subjects via the compare
    API, not from the queue branch name, because a batched group's ref names only its last
    pull request.
  • Only the most recently started check run per name is considered. One commit can carry
    several check suites, and without this a superseded red run would keep blocking a head that
    is now green.
  • skipped counts as satisfied. That is what an L2-only pull request looks like to the L1
    workflow, and what a docs-only one looks like to both.
  • Finding no matching check run at all fails. A gate that cannot see what it is verifying
    must not report success.

Verification

The gate's decision logic is not something CI can exercise, so it was checked directly:
ten cases through a stubbed API (all green, all skipped, in_progress, queued, failure,
cancelled, neutral, no match, and both stale-run orderings), then against live data —
the #7204 merge group passes, a group containing #7201 blocks on its red Hive - Devp2p tests, an L2-only pull request's skipped L1 suites pass, and a pull request with three Hive
jobs genuinely in flight blocks.

Known limitation

If the merge group's gate has already reported green and a Hive job is re-run after that, the
merge can still complete. Closing that would need the suites to run in the queue, which is
the trade this PR deliberately does not make: it would add roughly twenty minutes per queued
pull request and let a hive flake evict pull requests from the queue.

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.

Checklist

  • Updated STORE_SCHEMA_VERSION (crates/storage/lib.rs) if the PR includes breaking changes to the Store requiring a re-sync.

…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.
…ude.

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.
@ilitteri
ilitteri requested a review from a team as a code owner August 24, 2026 20:30
@github-actions

Copy link
Copy Markdown

⚠️ Known Issues — intentionally skipped tests

Source: docs/known_issues.md

rpc-compat log-bearing cases excluded

Where: KNOWN_EXCLUDED_TESTS in .github/scripts/check-hive-results.sh counts out
eight hive rpc-compat cases — the four eth_getLogs cases, eth_getBlockReceipts/get-block-receipts-latest,
and three eth_getTransactionReceipt cases. They are exactly the cases whose recorded
response contains at least one log object; every case with an empty log array still runs.
Note this leaves eth_getLogs with no rpc-compat coverage at all, since all four of its
cases are in the set.

Why: ethrex populates blockTimestamp on log objects, as geth, besu, nethermind, reth
and erigon all do. hive's rpc-compat compares responses byte-exactly (jsondiff.FullMatch;
the lenient checkJSONStructure path applies only to cases upstream marks speconly), and
the corpus is pinned to execution-apis d08382ae (2025-02-10), whose recordings predate the
field — it entered the schema in execution-apis#639 and the fixtures in #846 (2026-07-22).
So the extra key cannot match, and this is a property of the pin rather than of the response.

The pin cannot move, and this is not temporary. The pin sits one commit before
execution-apis#627, which moved the test chain to a pre-merge genesis: the current corpus has
~36 proof-of-work blocks before its terminal total difficulty. ethrex does not support
pre-merge chains and will not, so importing that chain.rlp fails at block 1 —
validate_block_header has no pre-London base-fee path. Every revision carrying
blockTimestamp in its fixtures also carries that chain, so there is no revision that
satisfies both. Nor can the corpus be patched locally: rpc-compat's Dockerfile clones
ethereum/execution-apis by hard-coded URL, so the branch buildarg cannot point at a fork.

Coverage: the field itself is pinned by
block_timestamp_is_on_the_log_and_not_on_the_receipt in
crates/networking/rpc/types/receipt.rs, which asserts it is present on each log and absent
from the receipt level.

Removal: delete the entries if ethrex ever gains pre-merge chain import, or if upstream
marks these cases speconly so they are type-checked instead of compared byte-for-byte.


The stateless schema id does not identify the encoding

Where: STATELESS_INPUT_SCHEMA_ID in crates/common/types/stateless_ssz.rs.

Upstream keeps the stateless input schema id at 0x1501
(fork_index 0x15 << 8 | revision 0x01) across incompatible body changes. Three
encodings have now shipped under it: tests-zkevm@v0.6.2, then #3248 + #3278,
then #3356, which moved state, codes and public_keys from SszList to
ProgressiveList. ethrex speaks the last one.

The consequence is that the 2-byte prefix cannot be used to detect a stale or
mismatched bundle. A wrong-dialect input is accepted by the id check and then
fails later — in SSZ decode, or on a root that does not match — rather than being
rejected up front for what it is. only_amsterdam_schema_id_decodes therefore
proves less than its name suggests.

Worth raising upstream: a revision field that does not move across a body change
provides no version negotiation at all.


ZisK guest program hash changes with the unsync_cell gate

Where: crates/common/types/block.rs, transaction.rs.

The gate on the single-threaded unsync_cell::OnceCell moved from
all(feature = "eip-8025", target_arch = "riscv64") to
all(feature = "zisk", target_arch = "riscv64") when the eip-8025 feature was removed.

The guest ELFs were previously built --features "<zkvm>-build-elf,ci", which never enabled
eip-8025, so they compiled the atomic once_cell variant. bin/zisk/Cargo.toml does enable
ethrex-common/zisk, so the ZisK guest now compiles the unsafe impl Sync cell instead.
That changes the ELF bytes and therefore the program hash and verification key.

This is intended (the guest is single-threaded, so the unsync cell is sound and cheaper), but it
is a VK change rather than a no-op refactor, and the diffstat presents it as a file rename
(eip8025_cell.rsunsync_cell.rs). Anyone pinning a ZisK VK across this change must
re-register it. The stateless-validator crate now forwards ethrex-common/zisk from its own
zisk feature so the two ZisK guests do not disagree on the cell type.


Release signing key is an unprotected repository secret

Where: .github/workflows/tag_release.yaml.

MINISIGN_SECRET_KEY is a plain repository secret. There is no environment: on
finalize-release or dry-run-release-assets, and gh api repos/lambdaclass/ethrex/rulesets
shows only branch-targeted rulesets, so the github.ref_type == 'tag' condition is a workflow
check rather than an enforced boundary: anyone who can push a tag can reach the signing key.

This is a repository-settings change, not a code change, so it is recorded here rather than
fixed in the tree. Recommended:

  1. Move MINISIGN_SECRET_KEY / MINISIGN_PASSWORD into a GitHub Environment with required
    reviewers, and add environment: to the two jobs that sign.
  2. Add a ruleset targeting refs/tags/v* restricting who may create release tags.

Until then, the compromise of that key is silent and durable: signatures would still verify
against the committed .github/minisign.pub.

@github-actions github-actions Bot added L1 Ethereum client L2 Rollup client labels Aug 24, 2026
@github-actions

Copy link
Copy Markdown

🤖 Kimi Code Review

This PR correctly implements a fail-closed gate for GitHub merge queues to prevent merging PRs with failing/skipped expensive test suites (Hive, Assertoor, L2 integration tests). The approach of reading the original PR's check runs rather than relying on skipped jobs in the merge group is the correct solution to GitHub's behavior where skipped checks satisfy required status checks.

.github/scripts/check-queued-pr-checks.sh

Security & Correctness:

  • Line 24-28: The regex extraction of PR numbers from commit messages assumes GitHub's squash-merge format (Title (#1234)). This is robust for the intended workflow, but consider adding a comment noting this dependency on squash-merge conventions.
  • Line 38: Unquoted variable in pr_head=$(gh api ...). While SHA values are alphanumeric, defensive quoting is preferred: pr_head="$(gh api ...)".
  • Line 40-41: The --slurp flag loads all paginated responses into memory. For repositories with extensive check run histories, this could be memory-intensive, but likely acceptable for typical PRs.

Robustness:

  • Line 30-33: Excellent defensive programming—refusing to pass when no PRs are identified prevents false positives.
  • Line 65-68: Good handling of the case where expected check suites never reported (fail-closed behavior).

Minor:

  • Line 53: The empty check [[ -z "$name" ]] handles jq's potential empty output, but since mapfile with -t removes trailing newlines and the while loop processes the here-string, this is safe.

.github/workflows/pr-main_l1.yaml & .github/workflows/pr-main_l2.yaml

Architecture:

  • Line 471 (L1) / Line 975 (L2): The condition always() && (needs.detect-changes.result != 'success' || ...) correctly ensures the gate runs even when dependencies fail, allowing the job to report the actual failure reason rather than being silently skipped.
  • Line 490-493 (L1) / Line 1000-1003 (L2): The explicit failure on detect-changes non-success is critical—an unanswerable gate must not report success.

Permissions:

  • Line 476-479 (L1) / Line 980-983 (L2): The permissions block correctly scopes to minimum required (contents: read, checks: read, pull-requests: read). This follows least-privilege principles.

Logic:

  • Line 495 (L1) / Line 1005 (L2): Using actions/checkout@v6 is up-to-date.
  • Line 502-505 (L1): The script invocation correctly targets the expensive suite prefixes. Note that in L2 (line 1007), "State Reconstruction Tests" and "Uniswap Swap Token Flow" are passed as exact matches (prefixes without wildcards), which works correctly due to the == "$prefix"* pattern in the script.

Nitpick:

  • Line 513 (L1): Comment mentions engine-ef-tests being skipped in merge queue, but the script doesn't verify engine-ef-tests results from the PR. This appears intentional (defensive comment), but verify this aligns with your security model—if engine-ef-tests is required but skipped in merge queue, it should likely be added to the script arguments or documented why it's exempt.

Summary

The implementation is secure and correct. The fail-closed approach (exiting 1 when uncertain) properly mitigates the risk of merging red builds. The bash script follows best practices (set -euo pipefail, proper array handling with mapfile).

Recommendation: Accept with optional minor quoting fix on line 38 of the shell script.


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Codex Code Review

Findings

  1. High: .github/scripts/check-queued-pr-checks.sh only checks that there was at least one matching check overall, not at least one match for each requested prefix. If Hive - ... exists but every Assertoor - ... run is missing, matched is still nonzero and the gate passes. The same bug applies to the L2 invocation if one whole suite family never reported. Track matches per prefix and fail any prefix with zero hits.

  2. High: .github/workflows/pr-main_l1.yaml verifies only Hive - and Assertoor - in merge-group runs, but the aggregate job still includes engine-ef-tests in needs at .github/workflows/pr-main_l1.yaml, and that job is explicitly skipped on merge_group at .github/workflows/pr-main_l1.yaml. Result: a PR with failing Engine EF tests can still merge through the queue. Add Engine EF tests to the PR-head verification path.

  3. Medium: .github/scripts/check-queued-pr-checks.sh treats skipped and neutral as passing conclusions. That weakens the exact invariant this change is trying to restore: a required suite on the PR head can still be non-successful and yet allow the merge queue to go green. Unless there is a concrete, intended neutral/skipped mode for these suites, require conclusion == success.

This PR only touches CI/workflow code, so I did not find any Rust/EVM/consensus-path changes to review here.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Review: PR 7213 — merge queue enforcement of required integration checks

This is a CI/workflow change rather than core protocol code, so the review focuses on the correctness of the gating logic and the new shell script rather than EVM/consensus concerns.

Findings

1. engine-ef-tests reintroduces the exact vacuous-pass hole this PR is fixing (high confidence)

.github/workflows/pr-main_l1.yaml:470 still lists engine-ef-tests as a dependency of the required Integration Test gate, and per the PR's own description, engine-ef-tests is one of the suites excluded from merge_group (same as Hive and Assertoor). On merge_group:

  • The Check if any job failed step that used to tolerate needs.engine-ef-tests.result == 'skipped' now only runs if: github.event_name != 'merge_group' (line 504), so it never executes in the queue.
  • The new Check the queued pull request's suites step (line 497-501) only checks "Hive - " and "Assertoor - " prefixes — "Engine EF tests" (the check run name at line 143) is never passed to check-queued-pr-checks.sh.

The result: in the merge queue, engine-ef-tests is neither directly inspected (it's skipped) nor verified against the queued PR's own head via the script. It silently passes exactly the way Hive/Assertoor used to before this fix. Unlike check-cargo-locks, this gap isn't called out in the "Not addressed here" section, so it reads as an oversight rather than a deliberate scope decision. Suggest adding "Engine EF tests" to the prefix list at line 501.

2. check-queued-pr-checks.sh verifies "at least one prefix matched", not "every prefix matched" (medium confidence)

In check-queued-pr-checks.sh, matched is a single counter incremented across all supplied prefixes (lines ~59-72), and the failure condition is matched -eq 0 (line 74). If, for some reason, one requested suite (e.g. "Assertoor - ") never produces a matching check run on the PR's head — while another suite (e.g. "Hive - ") does — the script reports success for the whole call, because the aggregate matched count is non-zero. In the current L1/L2 workflows this is likely masked because GitHub Actions still creates a named check run per static matrix entry even when the job-level if is false, so every expected name should normally appear with a skipped/success/failure conclusion. But this makes the safety property depend on that implicit behavior rather than something the script itself verifies. Tracking per-prefix match counts (e.g. a match-count map, failing if any prefix has zero matches) would make the "a gate that cannot see what it verifies must not report success" guarantee (which the script explicitly aims for) hold per-suite instead of in aggregate.

3. Reliance on squash-commit-subject convention for identifying queued PRs (low severity, flagged for awareness)

grep -oE '\(#[0-9]+\)$' on the first line of each commit in the compare diff assumes every queued commit is a GitHub squash-merge with the default Title (#1234) format. If the repo ever allows merge or rebase commits, or a commit message happens to end with (#NNN) referencing something unrelated (e.g., a manually written commit subject citing an issue number), the script could either silently drop a real PR (safe: fails closed via the pr_numbers empty check) or misattribute a commit to the wrong PR number (unsafe, though very unlikely given GitHub's squash format). Given this was validated against live merge groups per the PR description, this is more a documented fragility than a bug — but it may be worth a comment noting the assumption is enforced by requiring squash-merge in repo settings, if that's the case.

Positive notes

  • Permissions are scoped down to contents: read, checks: read, pull-requests: read at the job level rather than widened globally — good least-privilege practice.
  • The script fails closed in every ambiguous case (no PR identified, no matching check run, change detection inconclusive) rather than defaulting to pass — the right posture for a required-check gate.
  • Good use of max_by(.started_at // "") per check name to avoid a stale/superseded run blocking a now-green head.
  • The detect-changes non-success case (Item in commit 2) is a correct and easy-to-miss fix — '' == 'true' silently skipping the gate is a real footgun in GitHub Actions conditionals.
  • Test coverage described in the PR body (stubbed API cases + live merge groups) is appropriately thorough for logic that "CI cannot exercise" otherwise.

Automated review by Claude (Anthropic) · sonnet · custom prompt

@MegaRedHand MegaRedHand left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the merge-queue gate change against live repo data. The problem it targets is real and I confirmed it end to end: PR #7209 merged on 2026-08-24 with Hive - Devp2p tests red, and its merge group reported Integration Test as skipped. Reading the queued pull request's own check runs is the right shape for a fix, and the bash is careful.

Two things stop it from actually closing the hole. Both are inline, both verified against real commits.

1. skipped is accepted, and that state is reachable with the suites genuinely not run. Head e104cdbc really exists: Build Docker = failure, Hive - ${{ matrix.name }} = skipped, Assertoor - ${{ matrix.name }} = skipped. Replaying this script's loop over that head prints All required suites passed.

The natural defense is that such a pull request cannot enter the queue because its own Integration Test is red. That does not hold: Integration Test is declared by three workflows (pr-main_l1.yaml, pr-main_l1_l2_dev.yaml, pr-main_levm.yaml), and on PR #7209's head the L1 one was failure while the L2-Dev one was success. It merged.

2. The gate still skips entirely in the queue for any non-Rust change. On merge_group both workflows set run_tests = code_changed, which matches only **/*.rs, **/*.toml, **/*.lock. PR #7193 (CI-only) merged with every job in its merge group skipped, Integration Test included, while its head carried seven real green Hive - * results the queue never read. This pull request is in that same class, so it would merge under the behavior it is fixing.

What it gets right. Dropping the needs.<job>.result != 'skipped' guards is a genuine fix on the pull_request side: run-hive and run-assertoor both need docker_build, so a broken image used to skip the gate into a green required check. I also checked and found no problems with injection (PR numbers are digits after grep -oE/tr, names are quoted, jq @tsv escapes), set -euo pipefail behavior (the empty-array guard catches the grep miss, [[ -z ... ]] && continue does not trip set -e, the here-string keeps matched/failed in scope), the --paginate --slurp shape, the (#N) title heuristic (the repo is squash-only with PR_TITLE), and the permissions: blocks, which are minimal and sufficient.

Remaining inline notes are smaller.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skipped is accepted unconditionally, and it is reachable with the suites genuinely not run.

Real head e104cdbccb2aad1c771ea4ab397fff8a10520f5b carries:

Build Docker                    failure
Hive - ${{ matrix.name }}       skipped
Assertoor - ${{ matrix.name }}  skipped

run-hive and run-assertoor both declare needs: [detect-changes, docker_build], so a broken image turns the whole suite into skips. Feeding that head's check runs through this loop prints All required suites passed on every queued pull request head.

Rejecting skipped outright is not the fix: an L1-only pull request legitimately shows all four L2 suites skipped (PR #7194 is exactly that), so it would become unmergeable. Telling the two apart needs a second signal, for example the head's own Integration Test / Integration Test L2 check run, or the pull_request run conclusion from /actions/runs?head_sha=....

done
done <<<"$all_checks"

if [[ $matched -eq 0 ]]; then

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

matched is one counter across every prefix, so this net only fires when all prefixes come up empty. Rename or delete a single suite and it silently stops being enforced while the others keep the gate green.

Reproduced under bash with the L2 caller's four prefixes: rename state-diff-test's job, leave it red, and the script still prints All required suites passed and exits 0.

Worth noting that none of the six suite jobs carry the # "..." is a required check, don't change the name comment that all-tests has, and the prefix strings live in a third file with no link back to them. A per-prefix counter closes this.

"repos/${GITHUB_REPOSITORY}/commits/${pr_head}/check-runs?per_page=100" |
jq -r '[.[].check_runs[]]
| group_by(.name)
| map(max_by(.started_at // ""))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

daily_hive_report.yaml:24 declares name: Hive - ${{ matrix.test.name }} with a Rpc Compat tests matrix entry and continue-on-error: true, and it triggers on pull_request for .github/workflows/daily_hive_report.yaml and .github/scripts/publish_hive.sh. On PR #7170's head both workflows reported:

Hive - Rpc Compat tests   success   2026-08-20T17:35:44Z   (daily_hive_report)
Hive - Rpc Compat tests   success   2026-08-20T17:53:25Z   (pr-main_l1)

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 cancelled that blocks nothing.

Two smaller points on the same expression: the comment above attributes the dedupe to re-triggers, but commits/{sha}/check-runs already defaults to filter=latest, so that is not what it is doing. And started_at has second granularity, so ties resolve by API array order; .id descending would at least be deterministic. Matching on the workflow or .app.id alongside the name would be better than either.


failed=0
for pr in "${pr_numbers[@]}"; do
pr_head=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr}" --jq '.head.sha')

Copy link
Copy Markdown
Collaborator

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_group payload 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 them in_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.

# 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') }}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still skips the gate whenever run_tests is false, and on merge_group both workflows set run_tests = code_changed, which matches only **/*.rs, **/*.toml and **/*.lock. Any pull request touching only workflows, scripts, fixtures or configs keeps the old behavior.

PR #7193 (CI-only) is the worked example: merge group run 32502079792 skipped every job including Integration Test, and it merged. Its head was not untested; it carried seven green Hive - * runs, because the pull_request filter is !crates/l2/** and .github/** passes it. The queue just never read them.

This pull request changes only .github/** .yaml and .sh, so it is in that class itself.

Separately, always() also runs the job when the run is cancelled. The old expression short-circuited on outputs.run_tests == 'true' being empty, so a concurrency-cancelled run skipped the gate; now it runs and the step below exits 1, stamping a red Integration Test on superseded commits. !cancelled() && (...) avoids that.

exit 1

- name: Checkout sources
uses: actions/checkout@v6

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 needs context. if: ${{ github.event_name == 'merge_group' }} here would skip a full clone on every pull_request and push run of both gates.

if: ${{ github.event_name == 'merge_group' }}
env:
GH_TOKEN: ${{ github.token }}
run: ./.github/scripts/check-queued-pr-checks.sh "Hive - " "Assertoor - "

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

engine-ef-tests is in this job's needs, skips in the merge queue exactly like hive and assertoor, and is not a required context on its own, but it is missing from this prefix list. PR #7194's head carries a real Engine EF tests success run, so adding the prefix closes it in one line.

The Hive - prefix also over-matches: daily_hive_report.yaml emits check runs under that same prefix on any pull request touching its trigger paths. See the note on the jq dedupe in the script.

GH_TOKEN: ${{ github.token }}
run: |
./.github/scripts/check-queued-pr-checks.sh \
"Integration Test - " \

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integration Test - matches Integration Test - TDX (integration-test-tdx, line 509), but the Check if any job failed step below checks only integration-test, state-diff-test, uniswap-swap and integration-test-shared-bridge. TDX is in needs and has no continue-on-error.

So a red TDX leaves Integration Test L2 green on the pull request, the pull request is queueable, and the merge group then rejects it with nothing the author can turn green by re-running. Sampling twelve recent pr-main_l2 pull_request runs, TDX concluded 2 failure, 3 cancelled, 4 skipped, 1 success, so this is not a rare state.

Either add the tdx branch to the step below or narrow the prefix, but the two sides should agree on what is required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client L2 Rollup client

Projects

Status: No status
Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants