diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e91f8b8d..55b5679e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,62 +68,33 @@ concurrency: # bypass. # MERGE-QUEUE REUSE: a push to main whose exact SHA already passed this # workflow in the merge queue (scripts/ci-reuse-merge-group.py, run by -# clippy) only compiles, to refresh the main-only caches, and skips the -# test steps and test-only jobs the queue already ran. hosted-e2e still -# runs. The full tier (e2e-full, yarn-berry-full, cargo-vex-matrix-full) +# release-readiness) only compiles, to refresh the main-only caches, and +# skips the test steps and test-only jobs the queue already ran. hosted-e2e +# still runs. The full tier (e2e-full, yarn-berry-full, cargo-vex-matrix-full) # never runs on push unless CI_SCOPE is `full`; it is nightly and on # dispatch. API errors, missing evidence and direct pushes run everything # else. # MERGE-MAIN REUSE: a pull_request push that only merged the base branch, # with the PR's own diff unchanged and the previous head green -# (scripts/ci-reuse-pr-head.py, run by clippy), runs only clippy, the cheap -# preflight jobs, test-release and hosted-e2e. The merge queue re-tests the -# PR on the latest main before it lands; PRs into other branches and the -# release/v5-prerelease PR never reuse. +# (scripts/ci-reuse-pr-head.py, run by release-readiness), runs only +# clippy, the cheap preflight jobs, test-release and hosted-e2e. The merge +# queue re-tests the PR on the latest main before it lands; PRs into other +# branches and the release/v5-prerelease PR never reuse. jobs: # Required independently of ci-ok so the merge queue sees a compile/lint - # failure immediately. Expensive jobs also depend on this preflight. + # failure immediately. Every expensive job except coverage and e2e-build + # (#1681, see release-readiness) also waits for it, the e2e and cargo-vex + # legs through their own `needs`. clippy: if: github.event.pull_request.draft != true runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-16' }} timeout-minutes: 20 - permissions: - contents: read - actions: read - outputs: - reuse: ${{ steps.merge-queue.outputs.reuse }} - pr_unchanged: ${{ steps.pr-head.outputs.unchanged }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - # Main remains the cache writer. Skip duplicate test execution only - # when this workflow passed in the merge queue on the identical SHA. - - name: Check merge-queue validation - id: merge-queue - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - env: - GH_TOKEN: ${{ github.token }} - run: python3 scripts/ci-reuse-merge-group.py - - # A PR push that only merged the base branch, with the PR's own diff - # unchanged and the previous head green, skips the test jobs: the - # merge queue re-tests every entry on the latest main anyway (#1373). - # This job and hosted-e2e still run. Only PRs into main (the only - # queue), and never the v5 integration PR, whose full tier the queue - # does not run. - - name: Check for an unchanged PR diff - id: pr-head - if: github.event_name == 'pull_request' && github.base_ref == 'main' && github.head_ref != 'release/v5-prerelease' - env: - GH_TOKEN: ${{ github.token }} - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - PR_BASE_REF: ${{ github.base_ref }} - PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} - run: python3 scripts/ci-reuse-pr-head.py - - name: Install Rust # rustup is pre-installed on GitHub-hosted runners. `toolchain # install` with no name reads rust-toolchain.toml in the repo root, @@ -153,13 +124,15 @@ jobs: # Clippy above checks the shipped targets. This catches errors in # cfg(test), integration tests and feature-gated targets without # linking hundreds of executables or waiting for the E2E fan-out. + # coverage and e2e-build start alongside it (#1681); the legs that + # consume e2e-build's binaries still wait for it. run: cargo check --locked --workspace --all-targets --all-features # The napi addon is only ever loaded by Node, so cargo's own tests never # exercise its JS loader or the engine/provider boundary. node-addon: - if: (github.event.pull_request.draft != true) && needs.clippy.outputs.pr_unchanged != 'true' - needs: clippy + if: (github.event.pull_request.draft != true) && needs.release-readiness.outputs.pr_unchanged != 'true' + needs: [release-readiness, clippy] runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 30 steps: @@ -188,7 +161,7 @@ jobs: run: node crates/socket-patch-node/npm/scripts/build-addon.mjs - name: Smoke-test addon - if: needs.clippy.outputs.reuse != 'true' + if: needs.release-readiness.outputs.reuse != 'true' run: node --test crates/socket-patch-node/npm/test/smoke.mjs # Check the standalone installer, release scripts, and native installer @@ -285,15 +258,57 @@ jobs: # - any other PR that bumps the workspace version: the full gate — # CHANGELOG has a dated, non-empty section for the new version and the # tag doesn't already exist. + # + # This ~10 s job also owns the two reuse checks (`reuse`, `pr_unchanged`) + # every gated job reads. They used to live in clippy, so coverage and + # e2e-build, the two critical-path chains, sat ~1.4 min waiting for + # clippy only to read outputs set in its first second. Both now start + # off this job instead; clippy failed in none of 256 PR runs in 7 days, + # and merge-queue fail-fast still cancels the queue run on a red clippy + # (#1681). A failed release lint now also skips them, as a red clippy + # does the rest; ci-ok fails either way. Kept here rather than in a new + # job so a run starts no extra runner. release-readiness: if: github.event.pull_request.draft != true runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} + timeout-minutes: 10 + permissions: + contents: read + actions: read + outputs: + reuse: ${{ steps.merge-queue.outputs.reuse }} + pr_unchanged: ${{ steps.pr-head.outputs.unchanged }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false + # Main remains the cache writer. Skip duplicate test execution only + # when this workflow passed in the merge queue on the identical SHA. + - name: Check merge-queue validation + id: merge-queue + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + GH_TOKEN: ${{ github.token }} + run: python3 scripts/ci-reuse-merge-group.py + + # A PR push that only merged the base branch, with the PR's own diff + # unchanged and the previous head green, skips the test jobs: the + # merge queue re-tests every entry on the latest main anyway (#1373). + # clippy, this job and hosted-e2e still run. Only PRs into main (the only + # queue), and never the v5 integration PR, whose full tier the queue + # does not run. + - name: Check for an unchanged PR diff + id: pr-head + if: github.event_name == 'pull_request' && github.base_ref == 'main' && github.head_ref != 'release/v5-prerelease' + env: + GH_TOKEN: ${{ github.token }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_BASE_REF: ${{ github.base_ref }} + PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + run: python3 scripts/ci-reuse-pr-head.py + - name: Lint release readiness env: EVENT_NAME: ${{ github.event_name }} @@ -328,8 +343,8 @@ jobs: bash scripts/release-lint.sh --sync-only test: - if: ((github.event.pull_request.draft != true) && (vars.CI_SCOPE == 'full' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) && needs.clippy.outputs.pr_unchanged != 'true' - needs: clippy + if: ((github.event.pull_request.draft != true) && (vars.CI_SCOPE == 'full' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) && needs.release-readiness.outputs.pr_unchanged != 'true' + needs: [release-readiness, clippy] # No ubuntu-latest leg: `coverage` runs this same `cargo test # --workspace` (debug, default features, plus Go and vexctl) on ubuntu, # instrumented, and fails on any test failure. A plain ubuntu leg ran @@ -349,7 +364,7 @@ jobs: - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} # One compile-only cache writer per OS is enough for an already # tested SHA. - - shard: ${{ needs.clippy.outputs.reuse == 'true' && 2 || 0 }} + - shard: ${{ needs.release-readiness.outputs.reuse == 'true' && 2 || 0 }} runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS != 'true' && (matrix.os == 'ubuntu-latest' && 'depot-ubuntu-24.04-4' || matrix.os == 'ubuntu-22.04' && 'depot-ubuntu-22.04-4') || matrix.os }} timeout-minutes: 50 env: @@ -382,11 +397,11 @@ jobs: run: cargo build --workspace - name: Warm the test cache after merge-queue validation - if: needs.clippy.outputs.reuse == 'true' + if: needs.release-readiness.outputs.reuse == 'true' run: cargo test --locked --workspace --no-run - name: Install Go (for vexctl) - if: needs.clippy.outputs.reuse != 'true' + if: needs.release-readiness.outputs.reuse != 'true' id: go # The `vex` subcommand emits OpenVEX documents; tests/e2e_vex.rs # validates the output with vexctl when it's on PATH. vexctl is @@ -410,14 +425,14 @@ jobs: # version, the Go toolchain and the runner, so it is cached under # exactly those. Saved from main only, like the cargo cache. id: vexctl-cache - if: needs.clippy.outputs.reuse != 'true' && runner.os == 'macOS' + if: needs.release-readiness.outputs.reuse != 'true' && runner.os == 'macOS' uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 with: path: ${{ runner.temp }}/vexctl-bin key: vexctl-${{ env.VEXCTL_VERSION }}-go${{ steps.go.outputs.go-version }}-${{ runner.os }}-${{ runner.arch }} - name: Install vexctl - if: needs.clippy.outputs.reuse != 'true' + if: needs.release-readiness.outputs.reuse != 'true' # Linux / Windows: the v0.3.0 release binary, checked against the # sha256 pinned here (from the release's vexctl_checksums.txt). # Compiling it took ~80s on ubuntu and ~180s on windows, the @@ -481,7 +496,7 @@ jobs: - name: Save vexctl (macOS) if: >- - needs.clippy.outputs.reuse != 'true' + needs.release-readiness.outputs.reuse != 'true' && runner.os == 'macOS' && github.ref == 'refs/heads/main' && steps.vexctl-cache.outputs.cache-hit != 'true' @@ -491,7 +506,7 @@ jobs: key: ${{ steps.vexctl-cache.outputs.cache-primary-key }} - name: Run tests - if: needs.clippy.outputs.reuse != 'true' + if: needs.release-readiness.outputs.reuse != 'true' # Default features only: `--all-features` would also RUN the # docker-e2e suites, which soft-skip as "ok" here (no images, and # macOS/Windows have no Docker) — fake greens hiding a broken @@ -565,8 +580,8 @@ jobs: run: python3 scripts/ci-test-shard.py "$TEST_SHARD" "$TEST_SHARD_COUNT" --locked --profile ci-release coverage: - if: (github.event.pull_request.draft != true) && needs.clippy.outputs.pr_unchanged != 'true' - needs: clippy + if: (github.event.pull_request.draft != true) && needs.release-readiness.outputs.pr_unchanged != 'true' + needs: release-readiness # Code coverage via cargo-llvm-cov (LLVM source-based instrumentation). # Reports as a markdown table in the job summary and uploads the raw # lcov.info file as a workflow artifact. No threshold gating — the @@ -606,14 +621,14 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - name: Warm the coverage cache after merge-queue validation - if: needs.clippy.outputs.reuse == 'true' + if: needs.release-readiness.outputs.reuse == 'true' run: | cargo llvm-cov show-env --sh > "$RUNNER_TEMP/coverage-env.sh" source "$RUNNER_TEMP/coverage-env.sh" cargo test --locked --workspace --no-run - name: Install Go (for vexctl and the real-go suites) - if: needs.clippy.outputs.reuse != 'true' + if: needs.release-readiness.outputs.reuse != 'true' # This job is the Linux leg of `test` (see there): same Go pin. uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -621,7 +636,7 @@ jobs: cache: false - name: Install vexctl - if: needs.clippy.outputs.reuse != 'true' + if: needs.release-readiness.outputs.reuse != 'true' # The v0.3.0 Linux release binary, as in `test`'s vexctl step. env: VEXCTL_VERSION: v0.3.0 @@ -638,7 +653,7 @@ jobs: echo "$dir" >> "$GITHUB_PATH" - name: Run tests with coverage - if: needs.clippy.outputs.reuse != 'true' + if: needs.release-readiness.outputs.reuse != 'true' # Two-step pattern: `--no-report` runs instrumented tests and # collects raw profiles, then one `report` exports LCOV. The # summary is derived from that file (scripts/ci-lcov-summary.py) @@ -676,7 +691,7 @@ jobs: python3 scripts/ci-lcov-summary.py coverage-host.lcov | tee coverage-summary.txt - name: Publish coverage summary to job summary - if: needs.clippy.outputs.reuse != 'true' + if: needs.release-readiness.outputs.reuse != 'true' # Render the per-file LCOV line/function/branch totals as a fenced # block in the GitHub Actions job summary so reviewers don't # need to crack open the artifact for a quick look. @@ -693,7 +708,7 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" - name: Upload host LCOV artifact - if: needs.clippy.outputs.reuse != 'true' + if: needs.release-readiness.outputs.reuse != 'true' uses: ./.github/actions/upload-artifact with: name: coverage-host @@ -704,8 +719,8 @@ jobs: # Dockerfile.base compiles the full-LTO release binary inside Docker, with # no cache. Build it once per run and hand the image to every docker leg. docker-base: - if: ((github.event.pull_request.draft != true && needs.clippy.outputs.reuse != 'true') && (vars.CI_SCOPE == 'full' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.head_ref == 'release/v5-prerelease')) && needs.clippy.outputs.pr_unchanged != 'true' - needs: clippy + if: ((github.event.pull_request.draft != true && needs.release-readiness.outputs.reuse != 'true') && (vars.CI_SCOPE == 'full' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.head_ref == 'release/v5-prerelease')) && needs.release-readiness.outputs.pr_unchanged != 'true' + needs: [release-readiness, clippy] runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-22.04' || 'depot-ubuntu-22.04-4' }} timeout-minutes: 30 permissions: @@ -994,10 +1009,10 @@ jobs: # test binaries directly from the same checkout path, so `CARGO_BIN_EXE_*` # and `CARGO_MANIFEST_DIR` resolve as they did under `cargo test`. e2e-build: - if: (github.event.pull_request.draft != true) && needs.clippy.outputs.pr_unchanged != 'true' - needs: clippy + if: (github.event.pull_request.draft != true) && needs.release-readiness.outputs.pr_unchanged != 'true' + needs: release-readiness outputs: - reuse: ${{ needs.clippy.outputs.reuse }} + reuse: ${{ needs.release-readiness.outputs.reuse }} strategy: fail-fast: false matrix: @@ -1032,14 +1047,14 @@ jobs: - name: Bundle the binaries the legs run # A reused push runs no leg, except e2e-full and cargo-vex-matrix-full # on ubuntu when CI_SCOPE is `full`. - if: needs.clippy.outputs.reuse != 'true' || (matrix.os == 'ubuntu-latest' && vars.CI_SCOPE == 'full') + if: needs.release-readiness.outputs.reuse != 'true' || (matrix.os == 'ubuntu-latest' && vars.CI_SCOPE == 'full') shell: bash env: BUNDLE_OS: ${{ matrix.os }} run: python3 scripts/ci-e2e-bundle.py --os "$BUNDLE_OS" --cargo-json target-build.json --dest target/e2e-bin - name: Compress the e2e binaries - if: needs.clippy.outputs.reuse != 'true' || (matrix.os == 'ubuntu-latest' && vars.CI_SCOPE == 'full') + if: needs.release-readiness.outputs.reuse != 'true' || (matrix.os == 'ubuntu-latest' && vars.CI_SCOPE == 'full') # Compress the bundle as one stream so identical Rust code across # test binaries shares a dictionary. Zstd is on every runner image. # The 128 MiB window cuts the Linux artifact from ~200 MB to ~94 MB; @@ -1050,7 +1065,7 @@ jobs: tar -C target/e2e-bin -cf - . | zstd -q -f -10 --long=27 -o target/e2e-bin.tar.zst - uses: ./.github/actions/upload-artifact - if: needs.clippy.outputs.reuse != 'true' || (matrix.os == 'ubuntu-latest' && vars.CI_SCOPE == 'full') + if: needs.release-readiness.outputs.reuse != 'true' || (matrix.os == 'ubuntu-latest' && vars.CI_SCOPE == 'full') with: name: e2e-bin-${{ matrix.os }} path: target/e2e-bin.tar.zst @@ -1097,10 +1112,10 @@ jobs: # Builds the Windows test binaries for e2e-windows and # cargo-vex-matrix-windows. The lean merge queue runs # windows-compile-check instead. - if: ((github.event.pull_request.draft != true) && (vars.CI_SCOPE == 'full' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) && needs.clippy.outputs.pr_unchanged != 'true' - needs: clippy + if: ((github.event.pull_request.draft != true) && (vars.CI_SCOPE == 'full' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) && needs.release-readiness.outputs.pr_unchanged != 'true' + needs: [release-readiness, clippy] outputs: - reuse: ${{ needs.clippy.outputs.reuse }} + reuse: ${{ needs.release-readiness.outputs.reuse }} strategy: fail-fast: false matrix: @@ -1112,9 +1127,9 @@ jobs: e2e-build-macos: if: (github.event_name != 'pull_request') && (vars.CI_SCOPE == 'full' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') - needs: clippy + needs: [release-readiness, clippy] outputs: - reuse: ${{ needs.clippy.outputs.reuse }} + reuse: ${{ needs.release-readiness.outputs.reuse }} strategy: fail-fast: false matrix: @@ -1127,7 +1142,9 @@ jobs: e2e: if: needs.e2e-build.outputs.reuse != 'true' # These jobs consume e2e-build's binaries and can run alongside unit tests. - needs: [e2e-build] + # e2e-build no longer waits for clippy (#1681), so wait for it here: a + # red clippy still skips the fan-out. + needs: [e2e-build, clippy] strategy: fail-fast: false matrix: @@ -1847,7 +1864,7 @@ jobs: # LEAN SCOPE note at the top of `jobs:`. e2e-extended: if: needs.e2e-build.outputs.reuse != 'true' && (vars.CI_SCOPE == 'full' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') - needs: [e2e-build] + needs: [e2e-build, clippy] strategy: fail-fast: false matrix: @@ -2067,7 +2084,7 @@ jobs: # Nightly, on dispatch (release-qa) and on v5 landings only, like # e2e-docker; CI_SCOPE=full also runs it on pushes to main. if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && vars.CI_SCOPE == 'full') || github.head_ref == 'release/v5-prerelease' - needs: [e2e-build] + needs: [e2e-build, clippy] strategy: fail-fast: false matrix: @@ -2216,9 +2233,9 @@ jobs: # queue's critical path. # Dropping that `needs` also dropped the draft skip it inherited, so # gate on draft here directly (push/merge_group/schedule still run). - # Only wait for the clippy preflight. - if: (github.event.pull_request.draft != true && needs.clippy.outputs.reuse != 'true') && needs.clippy.outputs.pr_unchanged != 'true' - needs: clippy + # Only wait for the clippy and release-readiness preflights. + if: (github.event.pull_request.draft != true && needs.release-readiness.outputs.reuse != 'true') && needs.release-readiness.outputs.pr_unchanged != 'true' + needs: [release-readiness, clippy] runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 40 strategy: @@ -2260,9 +2277,9 @@ jobs: # queue's critical path. # Dropping that `needs` also dropped the draft skip it inherited, so # gate on draft here directly (push/merge_group/schedule still run). - # Only wait for the clippy preflight. - if: (github.event.pull_request.draft != true && needs.clippy.outputs.reuse != 'true') && needs.clippy.outputs.pr_unchanged != 'true' - needs: clippy + # Only wait for the clippy and release-readiness preflights. + if: (github.event.pull_request.draft != true && needs.release-readiness.outputs.reuse != 'true') && needs.release-readiness.outputs.pr_unchanged != 'true' + needs: [release-readiness, clippy] strategy: fail-fast: false matrix: @@ -2302,9 +2319,9 @@ jobs: # nightly, not on every PR push, so PRs stop queueing on the small # macOS runner pool. yarn-berry-e2e-macos: - if: (github.event_name != 'pull_request' && needs.clippy.outputs.reuse != 'true') && (vars.CI_SCOPE == 'full' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') + if: (github.event_name != 'pull_request' && needs.release-readiness.outputs.reuse != 'true') && (vars.CI_SCOPE == 'full' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') name: yarn-berry ${{ matrix.yarn }} (${{ matrix.os }}) - needs: clippy + needs: [release-readiness, clippy] strategy: fail-fast: false matrix: @@ -2345,7 +2362,7 @@ jobs: name: cargo ${{ matrix.toolchain }} lock-v${{ matrix.lock || 'own' }} (${{ matrix.os }}) # e2e-build only (its binaries; only the matching OS build is needed); # see yarn-classic-matrix on test/coverage. - needs: [e2e-build] + needs: [e2e-build, clippy] runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS != 'true' && (matrix.os == 'ubuntu-latest' && 'depot-ubuntu-24.04-4' || matrix.os == 'ubuntu-22.04' && 'depot-ubuntu-22.04-4') || matrix.os }} timeout-minutes: 40 # What .cargo/config.toml's [env] gives processes cargo launches; these @@ -2461,7 +2478,7 @@ jobs: name: cargo ${{ matrix.toolchain }} lock-v${{ matrix.lock || 'own' }} (${{ matrix.os }}) # Same gate as e2e-full. if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && vars.CI_SCOPE == 'full') || github.head_ref == 'release/v5-prerelease' - needs: [e2e-build] + needs: [e2e-build, clippy] runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS != 'true' && (matrix.os == 'ubuntu-latest' && 'depot-ubuntu-24.04-4' || matrix.os == 'ubuntu-22.04' && 'depot-ubuntu-22.04-4') || matrix.os }} timeout-minutes: 40 # What .cargo/config.toml's [env] gives processes cargo launches; these @@ -2499,9 +2516,9 @@ jobs: # queue's critical path. # Dropping that `needs` also dropped the draft skip it inherited, so # gate on draft here directly (push/merge_group/schedule still run). - # Only wait for the clippy preflight. - if: ((github.event.pull_request.draft != true) && (vars.CI_SCOPE == 'full' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) && needs.clippy.outputs.pr_unchanged != 'true' - needs: clippy + # Only wait for the clippy and release-readiness preflights. + if: ((github.event.pull_request.draft != true) && (vars.CI_SCOPE == 'full' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) && needs.release-readiness.outputs.pr_unchanged != 'true' + needs: [release-readiness, clippy] runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 30 steps: @@ -2512,7 +2529,7 @@ jobs: - name: Install Rust run: scripts/rustup-retry.sh toolchain install - name: Pull the old cargos under test - if: needs.clippy.outputs.reuse != 'true' + if: needs.release-readiness.outputs.reuse != 'true' run: | docker pull rust:1.41-slim docker pull rust:1.56-slim @@ -2522,10 +2539,10 @@ jobs: shared-key: dev-ubuntu-latest save-if: ${{ github.ref == 'refs/heads/main' }} - name: Warm the shared Linux dev cache after merge-queue validation - if: needs.clippy.outputs.reuse == 'true' + if: needs.release-readiness.outputs.reuse == 'true' run: cargo test --locked -p socket-patch-cli --test e2e_vendor_cargo_build --no-run - name: Vendored manifest [patch] on old cargo - if: needs.clippy.outputs.reuse != 'true' + if: needs.release-readiness.outputs.reuse != 'true' shell: bash env: SOCKET_PATCH_CARGO_E2E_REQUIRED: '1' diff --git a/scripts/tests/test_ci_scheduling.py b/scripts/tests/test_ci_scheduling.py index 790ea0013..e49271e89 100644 --- a/scripts/tests/test_ci_scheduling.py +++ b/scripts/tests/test_ci_scheduling.py @@ -45,9 +45,15 @@ def test_expensive_jobs_cannot_start_after_a_failed_preflight(self): # on its critical path, so it starts in parallel with clippy; # merge-queue fail-fast cancels it when clippy fails. parallel = {"windows-compile-check"} + # coverage and e2e-build head the two critical-path chains and wait + # only for the ~10 s release-readiness job, not clippy (#1681). + # Everything downstream of them still waits for clippy. + early = {"coverage", "e2e-build"} + for job in early: + self.assertEqual(dependencies(job), {"release-readiness"}) for job in set(JOBS) - cheap: with self.subTest(job=job): - if job not in parallel: + if job not in parallel | early: self.assertIn("clippy", ancestors(job)) # Job-level always() would defeat failure/skip propagation. self.assertFalse(any(line.startswith(" if:") and "always()" in line @@ -63,10 +69,13 @@ def test_consumers_wait_only_for_their_own_os_build(self): job = family + suffix builder = "e2e-build" + (suffix if suffix in ("-windows", "-macos") else "") with self.subTest(job=job): - self.assertEqual(dependencies(job), {builder}) + # The ubuntu build skips clippy (#1681), so its consumers + # wait for clippy themselves. + extra = {"clippy"} if builder == "e2e-build" else set() + self.assertEqual(dependencies(job), {builder} | extra) self.assertEqual({row["os"] for row in reader.matrix_include(JOBS[job])}, {os_name}) self.assertIn(f" os: [{os_name}]", JOBS[builder]) - self.assertEqual(ancestors(job), {builder, "clippy"}) + self.assertEqual(ancestors(job), {builder, "release-readiness", "clippy"}) def test_os_builds_use_the_same_artifact_contract(self): producer = "\n".join(JOBS["e2e-build"]) @@ -107,7 +116,7 @@ def test_lean_queue_type_checks_windows_and_full_scope_builds_it(self): build = next(line for line in JOBS["e2e-build-windows"] if line.startswith(" if:")) self.assertEqual(build, " if: ((github.event.pull_request.draft != true) && (vars.CI_SCOPE == 'full'" " || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))" - " && needs.clippy.outputs.pr_unchanged != 'true'") + " && needs.release-readiness.outputs.pr_unchanged != 'true'") def test_reused_push_keeps_cache_writers_and_unqueued_jobs(self): # A push whose SHA passed the merge queue still compiles (main is the @@ -124,31 +133,40 @@ def test_reused_push_keeps_cache_writers_and_unqueued_jobs(self): if name.startswith(("Bundle ", "Compress ")) or "upload-artifact" in body] self.assertEqual(len(ship), 3) for body in ship: - self.assertIn("if: needs.clippy.outputs.reuse != 'true'" + self.assertIn("if: needs.release-readiness.outputs.reuse != 'true'" " || (matrix.os == 'ubuntu-latest' && vars.CI_SCOPE == 'full')", body) for job in ("docker-base", "yarn-classic-matrix", "yarn-berry-e2e", "yarn-berry-e2e-macos"): with self.subTest(job=job): condition = next(line for line in JOBS[job] if line.startswith(" if:")) - self.assertIn("needs.clippy.outputs.reuse != 'true'", condition) + self.assertIn("needs.release-readiness.outputs.reuse != 'true'", condition) for family in ("e2e", "cargo-vex-matrix"): for suffix in ("", "-windows", "-macos"): build = "e2e-build" + suffix with self.subTest(job=family + suffix): condition = next(line for line in JOBS[family + suffix] if line.startswith(" if:")) self.assertIn(f"needs.{build}.outputs.reuse != 'true'", condition) - self.assertIn(" reuse: ${{ needs.clippy.outputs.reuse }}", JOBS[build]) + self.assertIn(" reuse: ${{ needs.release-readiness.outputs.reuse }}", JOBS[build]) self.assertIn("needs.e2e-build.outputs.reuse != 'true'", next(line for line in JOBS["e2e-extended"] if line.startswith(" if:"))) for job in ("test", "coverage", "cargo-old-toolchains"): with self.subTest(job=job): warm = [body for name, body in reader.steps(JOBS[job]) if name.startswith("Warm ")] self.assertEqual(len(warm), 1) - self.assertIn("if: needs.clippy.outputs.reuse == 'true'", warm[0]) + self.assertIn("if: needs.release-readiness.outputs.reuse == 'true'", warm[0]) self.assertIn("--no-run", warm[0]) - clippy = "\n".join(JOBS["clippy"]) - self.assertIn("actions: read", clippy) - self.assertIn("reuse: ${{ steps.merge-queue.outputs.reuse }}", clippy) - self.assertIn("python3 scripts/ci-reuse-merge-group.py", clippy) + owner = "\n".join(JOBS["release-readiness"]) + self.assertIn("actions: read", owner) + self.assertIn("reuse: ${{ steps.merge-queue.outputs.reuse }}", owner) + self.assertIn("python3 scripts/ci-reuse-merge-group.py", owner) + self.assertNotIn("ci-reuse-", "\n".join(JOBS["clippy"])) + + def test_every_needs_output_reader_needs_that_job(self): + # `needs..outputs` is empty unless is a direct `needs:` + # entry, which would silently turn every reuse gate off. + for job in JOBS: + for parent in set(re.findall(r"needs\.([a-z0-9-]+)\.outputs", "\n".join(JOBS[job]))): + with self.subTest(job=job, parent=parent): + self.assertIn(parent, dependencies(job)) def test_unchanged_pr_merge_skips_only_what_the_queue_re_tests(self): # A PR push that only merged main keeps clippy and the cheap preflight @@ -167,11 +185,11 @@ def test_unchanged_pr_merge_skips_only_what_the_queue_re_tests(self): for job in JOBS: condition = next((line for line in JOBS[job] if line.startswith(" if:")), "") with self.subTest(job=job): - self.assertEqual("needs.clippy.outputs.pr_unchanged != 'true'" in condition, + self.assertEqual("needs.release-readiness.outputs.pr_unchanged != 'true'" in condition, job in gated) if job in gated: - self.assertTrue(condition.endswith(") && needs.clippy.outputs.pr_unchanged != 'true'")) - pr_tier = {job for job in JOBS if "clippy" in ancestors(job)} + self.assertTrue(condition.endswith(") && needs.release-readiness.outputs.pr_unchanged != 'true'")) + pr_tier = {job for job in JOBS if ancestors(job) & {"clippy", "release-readiness"}} for job in sorted(pr_tier - gated - {"hosted-e2e", "test-release"}): condition = next(line for line in JOBS[job] if line.startswith(" if:")) with self.subTest(job=job): @@ -179,11 +197,11 @@ def test_unchanged_pr_merge_skips_only_what_the_queue_re_tests(self): # only on the v5 integration PR, which never reuses (below). self.assertTrue(ancestors(job) & gated or condition == full_tier or condition.startswith(never_on_pr) and "head_ref" not in condition, job) - clippy = "\n".join(JOBS["clippy"]) - self.assertIn("pr_unchanged: ${{ steps.pr-head.outputs.unchanged }}", clippy) - self.assertIn("python3 scripts/ci-reuse-pr-head.py", clippy) + owner = "\n".join(JOBS["release-readiness"]) + self.assertIn("pr_unchanged: ${{ steps.pr-head.outputs.unchanged }}", owner) + self.assertIn("python3 scripts/ci-reuse-pr-head.py", owner) self.assertIn("if: github.event_name == 'pull_request' && github.base_ref == 'main'" - " && github.head_ref != 'release/v5-prerelease'", clippy) + " && github.head_ref != 'release/v5-prerelease'", owner) def test_row_reader_preserves_both_os_siblings(self): jobs = reader.jobs("""jobs: