diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcb173795..6dedff584 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: @@ -1706,7 +1712,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 @@ -1927,7 +1934,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 @@ -2019,7 +2027,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 @@ -2081,7 +2090,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 @@ -2093,8 +2103,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. @@ -2113,15 +2123,17 @@ 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 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 }} @@ -2309,3 +2321,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)' 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)