From c5eeff849190b860247696b5aaacc909d373ef22 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Wed, 7 Oct 2026 09:15:24 -0400 Subject: [PATCH 1/3] Run CI on the merge queue and stop cancelling main push runs Add a merge_group trigger and a single aggregate `ci-ok` job that needs every CI job, so a repository ruleset can require one check and turn on GitHub's merge queue. The queue tests each PR merged onto the current tip of main before it lands, which closes the semantic merge race that left main red (two PRs that each passed on their own, #955 and #690, broke the two-sided PENDING_INLINE_DIGESTS ratchet once both merged). Group push runs per commit. A concurrency group keeps only one pending run, so the shared refs/heads/main group cancelled every queued push but the newest during a merge burst; most main commits never got a verdict. The old comment claimed push runs "always finish", which was false. Also correct the hosted-e2e comment: no branch protection or ruleset registers it as a required check today; ci-ok needs it instead. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 49 ++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0b14d8d4..e92ded516 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,11 @@ on: # workflow (cache-poisoning). branches: [main] pull_request: + # The merge queue tests each queued PR merged onto the tip of main (plus + # the PRs ahead of it) before it lands. `ci-ok` below is the required + # check, so this event has to run the same pull_request-tier job set. + merge_group: + types: [checks_requested] schedule: # Nightly on main: the `full` tier (e2e-full, cargo-vex-matrix-full, # yarn-berry-full) and e2e-docker, which pull_request runs skip. @@ -27,13 +32,14 @@ permissions: contents: read # A newer push to the same PR supersedes its older run; nothing else is -# cancelled. Push, dispatch and schedule runs always finish: main runs are -# the ONLY rust-cache writers (save-if) and must not die mid-save, and a -# dispatched base-branch run is the base's only CI verdict. The nightly -# gets its own group: a group holds one pending run, so sharing main's would -# let a queued push and the nightly cancel each other. +# cancelled. Push runs are grouped per commit: a concurrency group holds only +# ONE pending run, so a shared `refs/heads/main` group silently cancelled every +# queued push but the newest during a merge burst, and most main commits never +# got a verdict. Merge-group refs (gh-readonly-queue/...) are unique per queue +# entry already. The nightly keeps its own group so it never queues behind (or +# cancels) a push. concurrency: - group: ci-${{ github.event.pull_request.number || github.ref }}${{ github.event_name == 'schedule' && '-nightly' || '' }} + group: ci-${{ github.event.pull_request.number || (github.event_name == 'push' && github.sha) || github.ref }}${{ github.event_name == 'schedule' && '-nightly' || '' }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: @@ -2068,7 +2074,8 @@ jobs: cargo test -p socket-patch-cli --test e2e_vendor_cargo_build -- old_toolchain --nocapture # ---------------------------------------------------------------------- - # Hosted-mode production e2e — REQUIRED status check, with a kill switch. + # Hosted-mode production e2e — merge-blocking through `ci-ok`, with a kill + # switch. # # Drives `scan --mode hosted` against the REAL production endpoints # (patches-api.socket.dev + patch.socket.dev) and the REAL upstream @@ -2080,8 +2087,8 @@ jobs: # The suite itself is `#[ignore]`-gated, so it stays OUT of the `test` and # `e2e` jobs and only runs where it is explicitly asked for — here. # - # INVARIANTS (this job is registered in branch protection as a required - # check named exactly `hosted-e2e`): + # INVARIANTS (`ci-ok` needs this job, and older rulesets may still name the + # check `hosted-e2e` directly): # * NO job-level `if:` — a *skipped* required check is ambiguous to branch # protection and can wedge a PR at "Expected — waiting for status". # The kill switch gates the STEPS, never the job. @@ -2100,7 +2107,7 @@ jobs: # hosted_e2e = force (ignore the variable) | skip (bypass this run). # ---------------------------------------------------------------------- hosted-e2e: - name: hosted-e2e # registered in branch protection; do not rename + name: hosted-e2e # may be a required check; do not rename runs-on: ubuntu-latest permissions: contents: read @@ -2296,3 +2303,25 @@ jobs: done echo "::error title=hosted-e2e::the vendored vlt production proof failed on all 3 attempts" exit 1 + + # The single required status check for the merge queue (and PRs). It needs + # every job above, so adding a job here is how it becomes merge-blocking. + # Skipped jobs (the nightly `full` tier) pass; failed or cancelled ones fail. + ci-ok: + name: ci-ok # registered as a required check; do not rename + if: always() + needs: [clippy, node-addon, lint-ecosystems, release-readiness, test, test-release, coverage, docker-base, coverage-docker, coverage-merge, dispatch-tests, e2e-build, e2e, e2e-full, e2e-docker, yarn-classic-matrix, yarn-berry-e2e, yarn-berry-full, cargo-vex-matrix, cargo-vex-matrix-full, cargo-old-toolchains, hosted-e2e] + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check every needed job + env: + RESULTS: ${{ toJSON(needs) }} + run: | + echo "$RESULTS" | python3 -c ' + import json, sys + bad = {k: v["result"] for k, v in json.load(sys.stdin).items() + if v["result"] not in ("success", "skipped")} + for k, v in sorted(bad.items()): + print(f"::error::{k}: {v}") + sys.exit(1 if bad else 0)' From 717b1c7234624d485b64a87966392cf11594d0ea Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 16:17:41 +0000 Subject: [PATCH 2/3] Keep merge-queue runs on the PR tier and main hosted-e2e uncancelled merge_group fell through the 'not pull_request' gate on e2e-full, yarn-berry-full and cargo-vex-matrix-full, so every queue entry ran the nightly matrices and could fail or overrun the queue wait. hosted-e2e still grouped main pushes on github.ref; a group keeps one pending job, so burst pushes cancelled each other and ci-ok counted that as failure. Group main pushes per commit, matching the workflow-level group. Co-Authored-By: Claude --- .github/workflows/ci.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e92ded516..2db99bcaa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1699,7 +1699,8 @@ jobs: # v5 landings, the nightly schedule and dispatch run them with the `e2e` # steps. e2e-full: - if: github.event_name != 'pull_request' || github.head_ref == 'release/v5-prerelease' + # merge_group runs the pull_request tier: the queue gates on ci-ok. + if: (github.event_name != 'pull_request' && github.event_name != 'merge_group') || github.head_ref == 'release/v5-prerelease' needs: [test, coverage, e2e-build] strategy: fail-fast: false @@ -1920,7 +1921,8 @@ jobs: # Skipped on pull_request (except v5 landings), like e2e-full. yarn-berry-full: name: yarn-berry ${{ matrix.yarn }} (${{ matrix.os }}) - if: github.event_name != 'pull_request' || github.head_ref == 'release/v5-prerelease' + # merge_group runs the pull_request tier: the queue gates on ci-ok. + if: (github.event_name != 'pull_request' && github.event_name != 'merge_group') || github.head_ref == 'release/v5-prerelease' needs: [test, coverage] strategy: fail-fast: false @@ -2012,7 +2014,8 @@ jobs: cargo-vex-matrix-full: name: cargo ${{ matrix.toolchain }} lock-v${{ matrix.lock || 'own' }} (${{ matrix.os }}) - if: github.event_name != 'pull_request' || github.head_ref == 'release/v5-prerelease' + # merge_group runs the pull_request tier: the queue gates on ci-ok. + if: (github.event_name != 'pull_request' && github.event_name != 'merge_group') || github.head_ref == 'release/v5-prerelease' needs: [test, coverage, e2e-build] runs-on: ${{ matrix.os }} timeout-minutes: 40 @@ -2114,8 +2117,10 @@ jobs: timeout-minutes: 30 concurrency: # These are real requests against a real production service — keep it to - # one run per ref rather than one per push. - group: hosted-e2e-${{ github.ref }} + # one run per PR ref rather than one per push. Main pushes group per + # commit like the workflow: a group holds ONE pending job, so a shared + # main group cancelled queued pushes and ci-ok failed them. + group: hosted-e2e-${{ (github.event_name == 'push' && github.sha) || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: HOSTED_E2E_DISABLED: ${{ vars.HOSTED_E2E_DISABLED }} From ae720dcde250568cfbf024f26b4d594d5d258746 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Wed, 7 Oct 2026 13:51:41 -0400 Subject: [PATCH 3/3] Expect the full CI tier to skip merge-queue runs too Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/tests/test_ci_e2e_tiers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/tests/test_ci_e2e_tiers.py b/scripts/tests/test_ci_e2e_tiers.py index e058af77f..37d8195ed 100644 --- a/scripts/tests/test_ci_e2e_tiers.py +++ b/scripts/tests/test_ci_e2e_tiers.py @@ -64,7 +64,10 @@ def test_full_jobs_skip_pull_requests_and_share_steps(self): ("cargo-vex-matrix-full", "cargo-vex-steps")): with self.subTest(job=job): text = job_text(job) - self.assertIn("if: github.event_name != 'pull_request'", text) + # The merge queue runs the pull_request tier, so the full tier + # skips merge_group too. + self.assertIn("if: (github.event_name != 'pull_request' && github.event_name != 'merge_group')", + text) self.assertIn(f"steps: *{anchor}", text) self.assertIn(f"steps: &{anchor}", TEXT)