From 38a09fdc5e9b7c5a0e8287763f8fa134c0baccce Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Fri, 9 Oct 2026 13:50:29 -0400 Subject: [PATCH 1/7] ci: reduce duplicate validation and shorten merge-queue checks Reuse successful same-SHA merge-group verdicts on main while retaining cache writers and the full tier. Keep Gradle PR coverage on the supported boundary lines, balance expensive hosted cases, and group short e2e jobs without losing filters or tool-version guards. Balance Windows tests from measured durations, retain line-table backtraces, avoid duplicate workspace builds and coverage reports, and trim the blocking sbt warm set. --- .github/workflows/ci.yml | 750 ++++++--------------- .github/workflows/gradle-compatibility.yml | 13 +- scripts/ci-e2e-groups.py | 132 ++++ scripts/ci-e2e-run.py | 114 ++++ scripts/ci-lcov-summary.py | 71 ++ scripts/ci-reuse-merge-group.py | 56 ++ scripts/ci-test-durations.json | 253 +++++++ scripts/ci-test-shard.py | 30 +- scripts/tests/test_ci_e2e_groups.py | 139 ++++ scripts/tests/test_ci_e2e_tiers.py | 39 +- scripts/tests/test_ci_gradle_prefixes.py | 2 +- scripts/tests/test_ci_lcov_summary.py | 52 ++ scripts/tests/test_ci_reuse_merge_group.py | 75 +++ scripts/tests/test_ci_scheduling.py | 40 +- scripts/tests/test_ci_test_shard.py | 18 +- scripts/tests/test_ci_vlt_rows.py | 26 +- tests/docker/Dockerfile.sbt | 8 +- 17 files changed, 1248 insertions(+), 570 deletions(-) create mode 100644 scripts/ci-e2e-groups.py create mode 100644 scripts/ci-e2e-run.py create mode 100644 scripts/ci-lcov-summary.py create mode 100644 scripts/ci-reuse-merge-group.py create mode 100644 scripts/ci-test-durations.json create mode 100644 scripts/tests/test_ci_e2e_groups.py create mode 100644 scripts/tests/test_ci_lcov_summary.py create mode 100644 scripts/tests/test_ci_reuse_merge_group.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b966b8d6..4a8f8fb2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,11 @@ on: permissions: contents: read +env: + # Keep backtrace file/line information without linking full debug symbols + # into every integration binary. Shared dev caches use the same setting. + CARGO_PROFILE_DEV_DEBUG: line-tables-only + # A newer push to the same PR supersedes its older run; nothing else is # 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 @@ -50,12 +55,26 @@ jobs: if: github.event.pull_request.draft != true runs-on: ubuntu-latest timeout-minutes: 20 + permissions: + contents: read + actions: read + outputs: + reuse: ${{ steps.merge-queue.outputs.reuse }} 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 + - name: Install Rust # rustup is pre-installed on GitHub-hosted runners. `rustup show` # reads rust-toolchain.toml in the repo root, then installs the @@ -119,6 +138,7 @@ jobs: run: node crates/socket-patch-node/npm/scripts/build-addon.mjs - name: Smoke-test addon + if: needs.clippy.outputs.reuse != 'true' run: node --test crates/socket-patch-node/npm/test/smoke.mjs # Check the standalone installer, release scripts, and native installer @@ -265,17 +285,20 @@ jobs: matrix: os: [macos-latest, windows-latest] # Two legs per OS (scripts/ci-test-shard.py): one leg linked ~240 - # test binaries and ran them serially for ~26 min, the merge queue's - # critical path. Shard 1 = unit tests + doctests + a third of the - # integration targets; shard 2 = the rest. + # test binaries and ran them serially for ~26 min. Windows balances + # measured runtime plus linking work; macOS keeps the count split. + # Shard 1 also owns unit tests and doctests. shard: [1, 2] exclude: # macOS legs run on main, the merge queue and nightly, not per PR push. - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} + # One compile-only writer per OS is enough for an already tested SHA. + - shard: ${{ needs.clippy.outputs.reuse == 'true' && 2 || 0 }} runs-on: ${{ matrix.os }} timeout-minutes: 50 env: VEXCTL_VERSION: v0.3.0 + CI_TEST_TIMINGS: ${{ matrix.os == 'windows-latest' && 'scripts/ci-test-durations.json' || '' }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -297,10 +320,18 @@ jobs: shared-key: dev-${{ matrix.os }} save-if: ${{ github.ref == 'refs/heads/main' }} - - name: Build - run: cargo build --workspace + # cargo test builds the CLI/bench executables itself. The addon is a + # cdylib with test=false, so retain its platform link check once per OS. + - name: Build Node addon + if: matrix.shard == 1 + run: cargo build --locked -p socket-patch-node + + - name: Warm the test cache after merge-queue validation + if: needs.clippy.outputs.reuse == 'true' + run: cargo test --locked --workspace --no-run - name: Install Go (for vexctl) + if: needs.clippy.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 @@ -324,13 +355,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: runner.os == 'macOS' + if: needs.clippy.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' # 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 @@ -394,7 +426,8 @@ jobs: - name: Save vexctl (macOS) if: >- - runner.os == 'macOS' + needs.clippy.outputs.reuse != 'true' + && runner.os == 'macOS' && github.ref == 'refs/heads/main' && steps.vexctl-cache.outputs.cache-hit != 'true' uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 @@ -403,6 +436,7 @@ jobs: key: ${{ steps.vexctl-cache.outputs.cache-primary-key }} - name: Run tests + if: needs.clippy.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 @@ -514,7 +548,15 @@ jobs: with: save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Warm the coverage cache after merge-queue validation + if: needs.clippy.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' # This job is the Linux leg of `test` (see there): same Go pin. uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -522,6 +564,7 @@ jobs: cache: false - name: Install vexctl + if: needs.clippy.outputs.reuse != 'true' # The v0.3.0 Linux release binary, as in `test`'s vexctl step. env: VEXCTL_VERSION: v0.3.0 @@ -538,10 +581,11 @@ jobs: echo "$dir" >> "$GITHUB_PATH" - name: Run tests with coverage + if: needs.clippy.outputs.reuse != 'true' # Two-step pattern: `--no-report` runs instrumented tests and - # collects the raw profile data, then the two `report` calls - # emit lcov + summary from the same data. Avoids re-running - # tests twice. The output filename matches the `*.lcov` + # collects raw profiles, then one `report` exports LCOV. Derive + # line/function/branch totals from that file instead of merging + # profiles and scanning objects a second time. The filename matches the `*.lcov` # gitignore pattern so a stray local run can't accidentally # commit a 600 KB report. # @@ -559,10 +603,11 @@ jobs: cargo llvm-cov --workspace --no-fail-fast \ --no-report cargo llvm-cov report --lcov --output-path coverage-host.lcov - cargo llvm-cov report --summary-only | tee coverage-summary.txt + python3 scripts/ci-lcov-summary.py coverage-host.lcov > coverage-summary.txt - name: Publish coverage summary to job summary - # Render the per-file table cargo-llvm-cov prints as a fenced + if: needs.clippy.outputs.reuse != 'true' + # Render the per-file LCOV 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. run: | @@ -578,6 +623,7 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" - name: Upload host LCOV artifact + if: needs.clippy.outputs.reuse != 'true' uses: ./.github/actions/upload-artifact with: name: coverage-host @@ -588,7 +634,7 @@ 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 + if: github.event.pull_request.draft != true && needs.clippy.outputs.reuse != 'true' needs: clippy runs-on: ubuntu-22.04 timeout-minutes: 30 @@ -709,6 +755,11 @@ jobs: file: tests/docker/Dockerfile.${{ matrix.ecosystem }} tags: socket-patch-test-${{ matrix.ecosystem }}:latest load: true + # Match the blocking agent_sbt_ slice below. The nightly and + # compatibility workflows retain Dockerfile.sbt's full defaults. + build-args: | + ${{ matrix.ecosystem == 'sbt' && 'SBT_WARM_VERSIONS=1.2.8 1.13.0' || '' }} + ${{ matrix.ecosystem == 'sbt' && 'SBT_WARM_TOOLS=0' || '' }} - name: Configure docker-e2e coverage hooks # Mount the instrumented socket-patch that the test step's own @@ -870,6 +921,8 @@ 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: + outputs: + reuse: ${{ needs.clippy.outputs.reuse }} if: github.event.pull_request.draft != true needs: clippy strategy: @@ -903,12 +956,14 @@ jobs: cargo test --locked -p socket-patch-cli --all-features --tests --no-run --message-format=json-render-diagnostics > target-build.json - name: Bundle the binaries the legs run + if: needs.clippy.outputs.reuse != 'true' || matrix.os == 'ubuntu-latest' 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' # 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; @@ -919,6 +974,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' with: name: e2e-bin-${{ matrix.os }} path: target/e2e-bin.tar.zst @@ -927,6 +983,8 @@ jobs: retention-days: 3 e2e-build-windows: + outputs: + reuse: ${{ needs.clippy.outputs.reuse }} if: github.event.pull_request.draft != true needs: clippy strategy: @@ -939,6 +997,8 @@ jobs: steps: *e2e-build-steps e2e-build-macos: + outputs: + reuse: ${{ needs.clippy.outputs.reuse }} if: github.event_name != 'pull_request' needs: clippy strategy: @@ -950,325 +1010,74 @@ jobs: env: *e2e-build-env steps: *e2e-build-steps + # Short cases share setup and artifact downloads. `cases` preserves each + # original suite/filter/toolchain row; ci-e2e-run.py isolates its environment + # and checks every result. Repack with scripts/ci-e2e-groups.py --write. e2e: + name: e2e (${{ matrix.os }}, ${{ matrix.ci_group }}) + if: needs.e2e-build.outputs.reuse != 'true' # These jobs consume e2e-build's binaries and can run alongside unit tests. needs: [e2e-build] strategy: fail-fast: false matrix: include: - # (No e2e_cargo / e2e_golang rows: neither suite has an - # `#[ignore]`-gated test or needs a real toolchain, so the `test` - # job already runs all of them. No e2e_maven / e2e_nuget / - # e2e_composer rows either: their tests are hermetic and not - # `#[ignore]`d, so `test` runs them on every OS.) - # Host build-proof capstones: fresh-checkout install + revert - # against the REAL composer/bundler toolchains, each ending in the - # manifest-less VEX matrix. `#[ignore]`-gated (the unpinned `test` - # job skips them); `composer:` / `bundler:` install that exact - # toolchain below, runs them via `--ignored`, and exports the - # suites' `_REQUIRED` + `_VERSION` gates so a leg hard-fails - # instead of skipping on a missing or wrong toolchain. - # ubuntu-latest only — they need the pinned toolchain, not per-OS - # coverage. - # - # composer: 1 (packagist stopped serving composer 1 on 2025-09-01, - # so the fixture resolves from an inline repository), 2.2 LTS and - # current 2. Every release from 1.x through 2.9 falls back to the - # git `source` when the dist download fails (the fallback the - # hosted redirect must drop); 2.10 does not. The exact-release - # matrix, 2.9.8 and macOS/Windows included, is - # composer-compatibility.yml. - # Both capstones share one leg per composer line: the setup is - # identical and the run step loops over `suite`. - - {os: ubuntu-latest, suite: e2e_vendor_composer_build e2e_redirect_composer_build, composer: '2'} - - {os: ubuntu-latest, suite: e2e_vendor_composer_build e2e_redirect_composer_build, composer: '2.2'} - - {os: ubuntu-latest, suite: e2e_vendor_composer_build e2e_redirect_composer_build, composer: '1'} - # Real-bundler gem capstones, one leg per bundler era. Boundaries: - # 1.17/2.1 merged GEM section, 2.2 separate sections, 2.5 last - # pre-CHECKSUMS, 2.6 CHECKSUMS, 4.0.15/4.0.21 before/after the - # strict frozen check (rubygems#9750). bundler <= 2.2 needs - # Ruby <= 3.3 and 1.17-2.1 need Ruby <= 3.1 (`untaint`). 2.7.2 - # (no boundary of its own) is in e2e-full. - # Hosted and vendored share each era's leg (one Ruby + bundler - # setup; the run step loops over `suite`). - - {os: ubuntu-latest, suite: e2e_redirect_gem_build e2e_vendor_gem_build, ruby: '3.1', bundler: '1.17.3'} - - {os: ubuntu-latest, suite: e2e_redirect_gem_build e2e_vendor_gem_build, ruby: '3.1', bundler: '2.1.4'} - - {os: ubuntu-latest, suite: e2e_redirect_gem_build e2e_vendor_gem_build, ruby: '3.1', bundler: '2.2.33'} - - {os: ubuntu-latest, suite: e2e_redirect_gem_build e2e_vendor_gem_build, ruby: '3.3', bundler: '2.5.23'} - - {os: ubuntu-latest, suite: e2e_redirect_gem_build e2e_vendor_gem_build, ruby: '3.3', bundler: '2.6.9'} - - {os: ubuntu-latest, suite: e2e_redirect_gem_build e2e_vendor_gem_build, ruby: '3.4', bundler: '4.0.15'} - - {os: ubuntu-latest, suite: e2e_redirect_gem_build e2e_vendor_gem_build, ruby: '3.4', bundler: '4.0.21'} - # The live-API smoke suites (e2e_npm, e2e_pypi, e2e_gem, - # e2e_scan) are intentionally NOT in the PR matrix — their - # `#[ignore]`-gated tests hit the real public proxy at - # patches-api.socket.dev, which intermittently returns - # 503 "Service temporarily over capacity" outside this - # repo's control. Run on demand: - # - # cargo test -p socket-patch-cli --test e2e_npm -- --ignored - # cargo test -p socket-patch-cli --test e2e_pypi -- --ignored - # cargo test -p socket-patch-cli --test e2e_gem -- --ignored - # cargo test -p socket-patch-cli --test e2e_scan -- --ignored - # - # Same policy for the self-update live smoke (hits real - # github.com releases; catches asset-naming/redirect/SUMS - # drift against the published pipeline — most useful right - # after a release): - # - # cargo test -p socket-patch-cli --test self_update_e2e -- --ignored - # - # PR-time coverage for the same code paths comes from the - # `e2e-docker` matrix below, which runs the same flow - # against a hermetic wiremock fixture. - # Safety-hardening e2e suites. The fast non-ignored ones - # (e2e_safety_lock, e2e_safety_yarn_pnp) run via the - # standard `test` job above on all three platforms, so no - # matrix entry is needed for them. e2e_safety_pnpm below needs a - # real pnpm and is #[ignore]-gated; e2e_safety_cargo_build runs in - # every cargo-vex-matrix leg (ubuntu, macOS and Windows). - - os: ubuntu-latest - suite: e2e_safety_pnpm - # pnpm-on-Windows uses junctions for symlinks and copies - # (not hardlinks) by default, so the CoW invariant holds - # vacuously. Test still runs to verify apply doesn't error - # on Windows — semantic Windows nlink coverage is a - # follow-up (`std::fs::Metadata` doesn't expose nlink on - # Windows; needs `GetFileInformationByHandle` via - # `windows-sys`). - # Wall-bound real-package-manager redirect capstones (~150s and - # ~70s of network installs + bootstrap resolutions — profile- - # insensitive, measured identical in debug and release). They ran - # inside the serial `test` job on every OS; #[ignore]-gated out of - # it and relocated here so they still run on every PR and every - # OS, but in parallel off the critical path. They use the runner's - # default node/corepack, exactly as they did inside `test` — no - # setup-node step, no version change. - # - # `npm_required` turns the npm suites' "npm not installed" soft-skip - # into a hard failure, on every OS: the suite resolves npm through - # PATHEXT, so Windows finds the `npm.cmd` shim instead of skipping. - - os: ubuntu-latest - suite: e2e_redirect_npm_build - npm_required: '1' - - os: ubuntu-latest - suite: e2e_redirect_rush_sim - # Hermetic real-bun capstones (wiremock patch service, real `bun - # install`): hosted (`e2e_redirect_bun_build`), vendored - # (`e2e_vendor_bun_build`) and the hosted⇄vendored takeover / - # scoped-rollback suite (`mode_migration_bun`). They are NOT - # `#[ignore]`-gated, so `test_filter: --include-ignored` is - # mandatory — the job default `-- --ignored` would select zero - # tests and pass vacuously (the e2e_composer trap above). The - # runner images ship no bun, so without the `bun:` key below the - # suites soft-skip; `bun:` installs that exact release via - # setup-bun and exports SOCKET_PATCH_BUN_E2E_REQUIRED=1 (+ the - # pinned version), under which the suites hard-fail instead of - # skipping when bun is missing, the wrong version, or the fixture - # install produces no text lock. - # - # Lock-era legs (ubuntu only): bun's text lock has three - # grammars — lockfileVersion 0 (opt-in `--save-text-lockfile`, - # 1.1.39–1.1.45; 2-tuple workspace entries), 1 (default from - # 1.2.0 through 1.3.x) and 2 (1.4.0+). Registry 4-tuples are - # identical across them but the workspace grammar, the lockb - # migration recipe and tarball digest enforcement (URL/local - # tarball sha512 checked only from 1.3.10) all differ, so the - # latest release alone cannot prove the rewrite + fresh install - # round-trip on the locks real projects commit. 1.1.45 = last v0 - # writer, 1.2.23 = v1, 1.3.14 = last pre-v2 default, 1.4.2 = v2. - # - # One leg per bun release runs all three suites (the run step - # loops over `suite`); e2e_bun_lockb keeps its own legs because - # the SOCKET_PATCH_BUN_LOCKB_* gates key on the row's suite. - - os: ubuntu-latest - suite: e2e_redirect_bun_build e2e_vendor_bun_build mode_migration_bun - bun: '1.4.2' - test_filter: --include-ignored - - os: ubuntu-latest - suite: e2e_redirect_bun_build e2e_vendor_bun_build mode_migration_bun - bun: '1.1.45' - test_filter: --include-ignored - - os: ubuntu-latest - suite: e2e_redirect_bun_build e2e_vendor_bun_build mode_migration_bun - bun: '1.2.23' - test_filter: --include-ignored - - os: ubuntu-latest - suite: e2e_redirect_bun_build e2e_vendor_bun_build mode_migration_bun - bun: '1.3.14' - test_filter: --include-ignored - # The binary bun.lockb era (1.0 / 1.1 lines) through e2e_bun_lockb, - # which reads the SOCKET_PATCH_BUN_LOCKB_* gates exported for it - # below. - - {os: ubuntu-latest, suite: e2e_bun_lockb, bun: '1.0.36', test_filter: --include-ignored} - - {os: ubuntu-latest, suite: e2e_bun_lockb, bun: '1.1.45', test_filter: --include-ignored} - # Bun >= 1.4 migrating a hosted workspace bun.lockb to bun.lock - # (#803; its reader must be 1.4+) and a vendored one, then - # reverting it (#784; skipped by the < 1.2 readers above). Both - # 1.4.2 and 1.3.14 also run the isolated-linker vendored re-run - # after a late dependent (#861); 1.3 re-hoists a frozen binary lock - # and refuses one whose trees changed. - - {os: ubuntu-latest, suite: e2e_bun_lockb, bun: '1.4.2', test_filter: --include-ignored text_migration workspace_late_dependent} - - {os: ubuntu-latest, suite: e2e_bun_lockb, bun: '1.3.14', test_filter: --include-ignored workspace_late_dependent} - # Real-vlt capstones (DESIGN §8.4): wiremock patch service and a local - # npm registry fed from npmjs, driven by the pinned vlt release - # (`node vlt.js`, installed below from a sha512-checked `npm pack`). - # Every test is `#[ignore]`d and named `vlt_pinned_matrix_*`, so the - # filter must be `--include-ignored vlt_pinned_matrix` (the job - # default `--ignored` selects nothing). The run pipes through - # scripts/check-vlt-legs.py, which fails on `0 passed` or any leg - # line the manifest does not predict. The eras: A0 0.0.0-16, A - # 0.0.0-32, B rc.12/rc.14 (rc.14 legs reach public npm), C rc.32, - # D 1.0.4/1.0.7, E 1.1.1, F 1.2.0. - - {os: ubuntu-latest, suite: e2e_redirect_vlt_build, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_redirect_vlt_build, vlt: '0.0.0-16', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_redirect_vlt_build, vlt: '0.0.0-32', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_redirect_vlt_build, vlt: '1.0.0-rc.14', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_redirect_vlt_build, vlt: '1.0.0-rc.32', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_redirect_vlt_build, vlt: '1.0.4', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_redirect_vlt_build, vlt: '1.1.1', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_vendor_vlt_build, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_vendor_vlt_build, vlt: '0.0.0-32', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_vendor_vlt_build, vlt: '1.0.0-rc.14', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_vendor_vlt_build, vlt: '1.0.0-rc.32', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_vendor_vlt_build, vlt: '1.0.4', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: mode_migration_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: mode_migration_vlt, vlt: '0.0.0-32', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: mode_migration_vlt, vlt: '1.0.0-rc.14', test_filter: --include-ignored vlt_pinned_matrix, vlt_upgrade: '1.2.0'} - # Linux `auto` hardlinks from the global store; every OS gets the - # explicit hardlink linker. - - {os: ubuntu-latest, suite: e2e_safety_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_safety_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix, vlt_store_linker: 'hardlink'} - # rc.12 gets the definite no-hook advisory; windows rc.14 runs the - # legacy DepIDs on NTFS with pre-junction symlinks. - - {os: ubuntu-latest, suite: e2e_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_vlt, vlt: '0.0.0-32', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_vlt, vlt: '1.0.0-rc.12', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_vlt, vlt: '1.0.0-rc.32', test_filter: --include-ignored vlt_pinned_matrix} - - {os: ubuntu-latest, suite: e2e_vlt, vlt: '1.0.7', test_filter: --include-ignored vlt_pinned_matrix} - # The named corepack pnpm hosted legs (pnpm 7-11, get-uuid, - # zero-touch, --trust-lockfile). `#[ignore]`d; the pinned matrix - # inside the same suite runs in pnpm-compatibility.yml, hence the - # skip. Node 24 (step below): the - # corepack pnpm@10/11 legs require it. - - {os: ubuntu-latest, suite: e2e_redirect_pnpm_build, test_filter: '--ignored --skip pnpm_pinned_matrix'} - # Real-uv hosted/vendored capstones ending in manifest-less VEX: - # the oldest and newest line + the 0.5.x boundary (0.5.4 still - # re-resolves a transitive override / rejects a repointed - # constraint under --locked; 0.5.5 keeps both). The other 0.N - # lines and 0.5.3/0.5.6 are in e2e-full. - - {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.1.45'} - - {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.5.4'} - - {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.5.5'} - - {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.12.17'} - - {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.1.45', test_filter: --include-ignored} - - {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.5.4', test_filter: --include-ignored} - - {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.5.5', test_filter: --include-ignored} - - {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.12.17', test_filter: --include-ignored} - # The Poetry / PDM / Hatch / Pipenv / pip / deno manifest-less VEX - # capstones share ONE test binary (`e2e_vex_build`, a module per - # tool — one optimized link in test-release instead of six), so - # each leg's `test_filter` names its tool's module and keeps - # `--ignored`. - # Real-Poetry hosted + vendored capstones (#[ignore]-gated, - # unix-only). One leg per major / lock format: 1.0 (lock 1.0), - # 1.1 (lock 1.1), 1.8 (lock 2.0), 2.0 (first lock 2.1 writer), - # current. - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '1.0.10'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '1.1.15'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '1.8.5'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '2.0.1'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '2.4.3'} - # Real PDM / Hatch capstones (wiremock Socket API that also serves - # the hosted wheel; PyPI for the tool bootstrap + six). - # PDM: lock 2 (1.4), refused 3.1 (1.15) and 4.2 (2.7), 4.3 (2.8), - # the hishel<1 bootstrap window (2.25) and current. - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pdm:: --ignored', pdm: '1.4.5'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pdm:: --ignored', pdm: '1.15.5'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pdm:: --ignored', pdm: '2.7.4'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pdm:: --ignored', pdm: '2.8.2'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pdm:: --ignored', pdm: '2.25.9'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pdm:: --ignored', pdm: '2.29.2'} - # Hatch: 1.0 (hatch.toml env vendoring refused, needs >= 1.2), - # 1.2, the virtualenv<21 window (1.9, 1.14) and current. - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'hatch:: --ignored', hatch: '1.0.0'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'hatch:: --ignored', hatch: '1.2.1'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'hatch:: --ignored', hatch: '1.9.7'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'hatch:: --ignored', hatch: '1.14.2'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'hatch:: --ignored', hatch: '1.18.1'} - # Real Pipenv / pip capstones (mock patch server; the tools are - # bootstrapped from PyPI). `pipenv:` / `pip:` hold one or more - # space-separated releases the suite loops over. The middle - # Pipenv releases are in e2e-full. - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pipenv:: --ignored', pipenv: '2022.12.19'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pipenv:: --ignored', pipenv: '2026.8.0'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pip:: --ignored', pip: '22 23 24 25 26'} - # Real-Maven hosted + vendored capstones, one leg per Maven line: - # 3.6 (pre http-blocker), 3.8 (resolver 1.6: no trusted checksums), - # 3.9 (trusted checksums), 4.0 rc. Hosted also runs both sides of - # the trusted-checksums floor: 3.9.3 (last release that ignores - # the .mvn/checksums pin) and 3.9.4 (first that enforces it). - # Lines that run both capstones run them in one leg (one Maven - # install; the run step loops over `suite`). - - {os: ubuntu-latest, suite: e2e_redirect_maven_build e2e_vendor_maven_build, jvm_tool: maven, maven: '3.6.3'} - - {os: ubuntu-latest, suite: e2e_redirect_maven_build e2e_vendor_maven_build, jvm_tool: maven, maven: '3.8.9'} - - {os: ubuntu-latest, suite: e2e_redirect_maven_build e2e_vendor_maven_build, jvm_tool: maven, maven: '3.9.16'} - - {os: ubuntu-latest, suite: e2e_redirect_maven_build e2e_vendor_maven_build, jvm_tool: maven, maven: '4.0.0-rc-6'} - - {os: ubuntu-latest, suite: e2e_redirect_maven_build, jvm_tool: maven, maven: '3.9.3'} - - {os: ubuntu-latest, suite: e2e_redirect_maven_build, jvm_tool: maven, maven: '3.9.4'} - - {os: ubuntu-latest, suite: e2e_vendor_jvm_build, jvm_tool: maven, maven: '3.6.3', test_filter: '--ignored maven_reactor'} - - {os: ubuntu-latest, suite: e2e_vendor_jvm_build, jvm_tool: maven, maven: '3.8.9', test_filter: '--ignored maven_reactor'} - - {os: ubuntu-latest, suite: e2e_vendor_jvm_build, jvm_tool: maven, maven: '3.9.2', test_filter: '--ignored maven_reactor'} - - {os: ubuntu-latest, suite: e2e_vendor_jvm_build, jvm_tool: maven, maven: '3.9.16', test_filter: '--ignored maven_reactor'} - - {os: ubuntu-latest, suite: e2e_vendor_jvm_build, jvm_tool: maven, maven: '4.0.0-rc-6', test_filter: '--ignored maven_reactor'} - # Real-Gradle capstones, PR tier: one leg per Gradle line x {agent + - # hosted, vendor + multi-project} on ubuntu, each on its line's LTS - # JDK. Every other OS x line x mode cell (and the JDK-ceiling, - # configuration-cache, Isolated Projects and real-Central rows) runs - # in gradle-compatibility.yml. `suite` lists every binary the leg - # runs (space-separated); the libtest filters select by the prefix - # contract ci-e2e-bundle.py enforces. Every suite has landed, so no - # row sets `allow_empty` (test_ci_gradle_prefixes.py keeps it that - # way): a missing suite fails the leg, and every suite must run at - # least one test on its own. Maven is installed only where a - # selected test needs it (gradle_vendor_395's mixed root). - # The hosted suite (~43 real-Gradle builds, ~31 min serially) is - # split into three legs per line so it stops being the merge-queue - # critical path: the agent suites plus `gradle_hosted_[345]`, the - # `gradle_hosted_[b-p]` names, and a catch-all that `--skip`s - # exactly those words, so a new test always lands in some leg - # (test_ci_gradle_prefixes.py keeps the skip list in sync). - - {os: ubuntu-latest, suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: gradle, gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5'} - - {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p'} - - {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p'} - - {os: ubuntu-latest, suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build', jvm_tool: gradle, gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_vendor_ gradle_multi_project'} - - {os: ubuntu-latest, suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: gradle, gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5'} - - {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p'} - - {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p'} - - {os: ubuntu-latest, suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build', jvm_tool: gradle, gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_vendor_ gradle_multi_project'} - - {os: ubuntu-latest, suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: gradle, gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5'} - - {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p'} - - {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p'} - - {os: ubuntu-latest, suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build', jvm_tool: gradle, gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_vendor_ gradle_multi_project'} - - {os: ubuntu-latest, suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: gradle, gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5'} - - {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p'} - - {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p'} - - {os: ubuntu-latest, suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build', jvm_tool: gradle, gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_vendor_ gradle_multi_project'} - # Real-sbt hosted (socket-patch.sbt) + vendored - # (socket-patch-vendor.sbt) capstones on the current 1.x line. The - # other sbt lines, JDK 21, the agent cells beyond coverage-docker's, - # Mill, scala-cli and macOS / Windows are sbt-compatibility.yml. - - {os: ubuntu-latest, suite: e2e_sbt_build, jvm_tool: sbt, sbt: '1.13.0', test_filter: '--ignored --test-threads=1'} - - {os: ubuntu-latest, suite: e2e_sbt_vendor_build, jvm_tool: sbt, sbt: '1.13.0', test_filter: '--ignored --test-threads=1'} - # Real .NET SDK capstones: hosted + vendored nuget, one leg per SDK - # major (the suite pins the major through a sandbox global.json): - # the oldest and newest here, 7-9 on ubuntu in e2e-full. - - {os: ubuntu-latest, suite: e2e_nuget_dotnet_build, dotnet: '6'} - - {os: ubuntu-latest, suite: e2e_nuget_dotnet_build, dotnet: '10'} - # Real deno negative capstone (no hosted/vendored wiring exists for - # deno; VEX must attest nothing), one leg per major. - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'deno:: --ignored', deno: '1.46.3'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'deno:: --ignored', deno: '2.9.7'} + - {os: 'ubuntu-latest', suite: 'e2e_vendor_composer_build e2e_redirect_composer_build', composer: '2', ci_group: 'e2e_vendor_composer_build e2e_redirect_composer_build-2-1'} + - {os: 'ubuntu-latest', suite: 'e2e_vendor_composer_build e2e_redirect_composer_build', composer: '2.2', ci_group: 'e2e_vendor_composer_build e2e_redirect_composer_build-2.2-2'} + - {os: 'ubuntu-latest', suite: 'e2e_vendor_composer_build e2e_redirect_composer_build', composer: '1', ci_group: 'e2e_vendor_composer_build e2e_redirect_composer_build-1-3'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gem_build e2e_vendor_gem_build', ruby: '3.1', bundler: '1.17.3', ci_group: 'e2e_redirect_gem_build e2e_vendor_gem_build-3.1-1.17.3-4'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gem_build e2e_vendor_gem_build', ruby: '3.1', bundler: '2.1.4', ci_group: 'e2e_redirect_gem_build e2e_vendor_gem_build-3.1-2.1.4-5'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gem_build e2e_vendor_gem_build', ruby: '3.1', bundler: '2.2.33', ci_group: 'e2e_redirect_gem_build e2e_vendor_gem_build-3.1-2.2.33-6'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gem_build e2e_vendor_gem_build', ruby: '3.3', bundler: '2.5.23', ci_group: 'e2e_redirect_gem_build e2e_vendor_gem_build-3.3-2.5.23-7'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gem_build e2e_vendor_gem_build', ruby: '3.3', bundler: '2.6.9', ci_group: 'e2e_redirect_gem_build e2e_vendor_gem_build-3.3-2.6.9-8'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gem_build e2e_vendor_gem_build', ruby: '3.4', bundler: '4.0.15', ci_group: 'e2e_redirect_gem_build e2e_vendor_gem_build-3.4-4.0.15-9'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gem_build e2e_vendor_gem_build', ruby: '3.4', bundler: '4.0.21', ci_group: 'e2e_redirect_gem_build e2e_vendor_gem_build-3.4-4.0.21-10'} + - {os: 'ubuntu-latest', bun: '1.4.2', suite: 'e2e_safety_pnpm e2e_redirect_npm_build e2e_redirect_rush_sim e2e_redirect_bun_build e2e_vendor_bun_build mode_migration_bun', cases: '[{"os":"ubuntu-latest","suite":"e2e_safety_pnpm"},{"os":"ubuntu-latest","suite":"e2e_redirect_npm_build","npm_required":"1"},{"os":"ubuntu-latest","suite":"e2e_redirect_rush_sim"},{"os":"ubuntu-latest","suite":"e2e_redirect_bun_build e2e_vendor_bun_build mode_migration_bun","bun":"1.4.2","test_filter":"--include-ignored"}]', ci_group: 'node20-1.4.2-11'} + - {os: 'ubuntu-latest', bun: '1.1.45', suite: 'e2e_redirect_bun_build e2e_vendor_bun_build mode_migration_bun e2e_bun_lockb', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_bun_build e2e_vendor_bun_build mode_migration_bun","bun":"1.1.45","test_filter":"--include-ignored"},{"os":"ubuntu-latest","suite":"e2e_bun_lockb","bun":"1.1.45","test_filter":"--include-ignored"}]', ci_group: 'node20-1.1.45-12'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_bun_build e2e_vendor_bun_build mode_migration_bun', bun: '1.2.23', test_filter: '--include-ignored', ci_group: 'node20-1.2.23-13'} + - {os: 'ubuntu-latest', bun: '1.3.14', suite: 'e2e_redirect_bun_build e2e_vendor_bun_build mode_migration_bun e2e_bun_lockb', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_bun_build e2e_vendor_bun_build mode_migration_bun","bun":"1.3.14","test_filter":"--include-ignored"},{"os":"ubuntu-latest","suite":"e2e_bun_lockb","bun":"1.3.14","test_filter":"--include-ignored workspace_late_dependent"}]', ci_group: 'node20-1.3.14-14'} + - {os: 'ubuntu-latest', suite: 'e2e_bun_lockb', bun: '1.0.36', test_filter: '--include-ignored', ci_group: 'node20-1.0.36-15'} + - {os: 'ubuntu-latest', suite: 'e2e_bun_lockb', bun: '1.4.2', test_filter: '--include-ignored text_migration workspace_late_dependent', ci_group: 'node20-1.4.2-16'} + - {os: 'ubuntu-latest', vlt: '1.2.0', suite: 'e2e_redirect_vlt_build e2e_vendor_vlt_build mode_migration_vlt e2e_safety_vlt e2e_vlt e2e_redirect_pnpm_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_vlt_build","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"ubuntu-latest","suite":"e2e_vendor_vlt_build","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"ubuntu-latest","suite":"mode_migration_vlt","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"ubuntu-latest","suite":"e2e_safety_vlt","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"ubuntu-latest","suite":"e2e_safety_vlt","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix","vlt_store_linker":"hardlink"},{"os":"ubuntu-latest","suite":"e2e_vlt","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"ubuntu-latest","suite":"e2e_redirect_pnpm_build","test_filter":"--ignored --skip pnpm_pinned_matrix"}]', ci_group: 'node24-1.2.0-17'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_vlt_build', vlt: '0.0.0-16', test_filter: '--include-ignored vlt_pinned_matrix', ci_group: 'node24-0.0.0-16-18'} + - {os: 'ubuntu-latest', vlt: '0.0.0-32', suite: 'e2e_redirect_vlt_build e2e_vendor_vlt_build mode_migration_vlt e2e_vlt', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_vlt_build","vlt":"0.0.0-32","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"ubuntu-latest","suite":"e2e_vendor_vlt_build","vlt":"0.0.0-32","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"ubuntu-latest","suite":"mode_migration_vlt","vlt":"0.0.0-32","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"ubuntu-latest","suite":"e2e_vlt","vlt":"0.0.0-32","test_filter":"--include-ignored vlt_pinned_matrix"}]', ci_group: 'node24-0.0.0-32-19'} + - {os: 'ubuntu-latest', vlt: '1.0.0-rc.14', vlt_upgrade: '1.2.0', suite: 'e2e_redirect_vlt_build e2e_vendor_vlt_build mode_migration_vlt', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_vlt_build","vlt":"1.0.0-rc.14","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"ubuntu-latest","suite":"e2e_vendor_vlt_build","vlt":"1.0.0-rc.14","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"ubuntu-latest","suite":"mode_migration_vlt","vlt":"1.0.0-rc.14","test_filter":"--include-ignored vlt_pinned_matrix","vlt_upgrade":"1.2.0"}]', ci_group: 'node24-1.0.0-rc.14-1.2.0-20'} + - {os: 'ubuntu-latest', vlt: '1.0.0-rc.32', suite: 'e2e_redirect_vlt_build e2e_vendor_vlt_build e2e_vlt', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_vlt_build","vlt":"1.0.0-rc.32","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"ubuntu-latest","suite":"e2e_vendor_vlt_build","vlt":"1.0.0-rc.32","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"ubuntu-latest","suite":"e2e_vlt","vlt":"1.0.0-rc.32","test_filter":"--include-ignored vlt_pinned_matrix"}]', ci_group: 'node24-1.0.0-rc.32-21'} + - {os: 'ubuntu-latest', vlt: '1.0.4', suite: 'e2e_redirect_vlt_build e2e_vendor_vlt_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_vlt_build","vlt":"1.0.4","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"ubuntu-latest","suite":"e2e_vendor_vlt_build","vlt":"1.0.4","test_filter":"--include-ignored vlt_pinned_matrix"}]', ci_group: 'node24-1.0.4-22'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_vlt_build', vlt: '1.1.1', test_filter: '--include-ignored vlt_pinned_matrix', ci_group: 'node24-1.1.1-23'} + - {os: 'ubuntu-latest', suite: 'e2e_vlt', vlt: '1.0.0-rc.12', test_filter: '--include-ignored vlt_pinned_matrix', ci_group: 'node24-1.0.0-rc.12-24'} + - {os: 'ubuntu-latest', suite: 'e2e_vlt', vlt: '1.0.7', test_filter: '--include-ignored vlt_pinned_matrix', ci_group: 'node24-1.0.7-25'} + - {os: 'ubuntu-latest', uv: '0.1.45', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_uv_build","uv":"0.1.45"},{"os":"ubuntu-latest","suite":"e2e_vendor_pypi_build","uv":"0.1.45","test_filter":"--include-ignored"}]', ci_group: 'uv-0.1.45-26'} + - {os: 'ubuntu-latest', uv: '0.5.4', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_uv_build","uv":"0.5.4"},{"os":"ubuntu-latest","suite":"e2e_vendor_pypi_build","uv":"0.5.4","test_filter":"--include-ignored"}]', ci_group: 'uv-0.5.4-27'} + - {os: 'ubuntu-latest', uv: '0.5.5', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_uv_build","uv":"0.5.5"},{"os":"ubuntu-latest","suite":"e2e_vendor_pypi_build","uv":"0.5.5","test_filter":"--include-ignored"}]', ci_group: 'uv-0.5.5-28'} + - {os: 'ubuntu-latest', uv: '0.12.17', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_uv_build","uv":"0.12.17"},{"os":"ubuntu-latest","suite":"e2e_vendor_pypi_build","uv":"0.12.17","test_filter":"--include-ignored"}]', ci_group: 'uv-0.12.17-29'} + - {os: 'ubuntu-latest', poetry: '1.0.10', pdm: '1.4.5', hatch: '1.0.0', suite: 'e2e_vex_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"poetry:: --ignored","poetry":"1.0.10"},{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"pdm:: --ignored","pdm":"1.4.5"},{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"hatch:: --ignored","hatch":"1.0.0"}]', ci_group: 'python-1.0.10-1.4.5-1.0.0-30'} + - {os: 'ubuntu-latest', pipenv: '2022.12.19', pip: '22 23 24 25 26', suite: 'e2e_vex_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"pipenv:: --ignored","pipenv":"2022.12.19"},{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"pip:: --ignored","pip":"22 23 24 25 26"}]', ci_group: 'python-installers-2022.12.19-22 23 24 25 26-31'} + - {os: 'ubuntu-latest', poetry: '1.1.15', pdm: '1.15.5', hatch: '1.2.1', suite: 'e2e_vex_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"poetry:: --ignored","poetry":"1.1.15"},{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"pdm:: --ignored","pdm":"1.15.5"},{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"hatch:: --ignored","hatch":"1.2.1"}]', ci_group: 'python-1.1.15-1.15.5-1.2.1-32'} + - {os: 'ubuntu-latest', suite: 'e2e_vex_build', test_filter: 'pipenv:: --ignored', pipenv: '2026.8.0', ci_group: 'python-installers-2026.8.0-33'} + - {os: 'ubuntu-latest', poetry: '1.8.5', pdm: '2.7.4', hatch: '1.9.7', suite: 'e2e_vex_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"poetry:: --ignored","poetry":"1.8.5"},{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"pdm:: --ignored","pdm":"2.7.4"},{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"hatch:: --ignored","hatch":"1.9.7"}]', ci_group: 'python-1.8.5-2.7.4-1.9.7-34'} + - {os: 'ubuntu-latest', poetry: '2.0.1', pdm: '2.8.2', hatch: '1.14.2', suite: 'e2e_vex_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"poetry:: --ignored","poetry":"2.0.1"},{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"pdm:: --ignored","pdm":"2.8.2"},{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"hatch:: --ignored","hatch":"1.14.2"}]', ci_group: 'python-2.0.1-2.8.2-1.14.2-35'} + - {os: 'ubuntu-latest', poetry: '2.4.3', pdm: '2.25.9', hatch: '1.18.1', suite: 'e2e_vex_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"poetry:: --ignored","poetry":"2.4.3"},{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"pdm:: --ignored","pdm":"2.25.9"},{"os":"ubuntu-latest","suite":"e2e_vex_build","test_filter":"hatch:: --ignored","hatch":"1.18.1"}]', ci_group: 'python-2.4.3-2.25.9-1.18.1-36'} + - {os: 'ubuntu-latest', suite: 'e2e_vex_build', test_filter: 'pdm:: --ignored', pdm: '2.29.2', ci_group: 'python-2.29.2-37'} + - {os: 'ubuntu-latest', jvm_tool: 'maven', maven: '3.6.3', dotnet: '6', suite: 'e2e_redirect_maven_build e2e_vendor_maven_build e2e_vendor_jvm_build e2e_nuget_dotnet_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_maven_build e2e_vendor_maven_build","jvm_tool":"maven","maven":"3.6.3"},{"os":"ubuntu-latest","suite":"e2e_vendor_jvm_build","jvm_tool":"maven","maven":"3.6.3","test_filter":"--ignored maven_reactor"},{"os":"ubuntu-latest","suite":"e2e_nuget_dotnet_build","dotnet":"6"}]', ci_group: 'jvm-dotnet-3.6.3-6-38'} + - {os: 'ubuntu-latest', jvm_tool: 'maven', maven: '3.8.9', dotnet: '10', suite: 'e2e_redirect_maven_build e2e_vendor_maven_build e2e_vendor_jvm_build e2e_nuget_dotnet_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_maven_build e2e_vendor_maven_build","jvm_tool":"maven","maven":"3.8.9"},{"os":"ubuntu-latest","suite":"e2e_vendor_jvm_build","jvm_tool":"maven","maven":"3.8.9","test_filter":"--ignored maven_reactor"},{"os":"ubuntu-latest","suite":"e2e_nuget_dotnet_build","dotnet":"10"}]', ci_group: 'jvm-dotnet-3.8.9-10-39'} + - {os: 'ubuntu-latest', jvm_tool: 'maven', maven: '3.9.16', suite: 'e2e_redirect_maven_build e2e_vendor_maven_build e2e_vendor_jvm_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_maven_build e2e_vendor_maven_build","jvm_tool":"maven","maven":"3.9.16"},{"os":"ubuntu-latest","suite":"e2e_vendor_jvm_build","jvm_tool":"maven","maven":"3.9.16","test_filter":"--ignored maven_reactor"}]', ci_group: 'jvm-dotnet-3.9.16-40'} + - {os: 'ubuntu-latest', jvm_tool: 'maven', maven: '4.0.0-rc-6', suite: 'e2e_redirect_maven_build e2e_vendor_maven_build e2e_vendor_jvm_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_maven_build e2e_vendor_maven_build","jvm_tool":"maven","maven":"4.0.0-rc-6"},{"os":"ubuntu-latest","suite":"e2e_vendor_jvm_build","jvm_tool":"maven","maven":"4.0.0-rc-6","test_filter":"--ignored maven_reactor"}]', ci_group: 'jvm-dotnet-4.0.0-rc-6-41'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_maven_build', jvm_tool: 'maven', maven: '3.9.3', ci_group: 'jvm-dotnet-3.9.3-42'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_maven_build', jvm_tool: 'maven', maven: '3.9.4', ci_group: 'jvm-dotnet-3.9.4-43'} + - {os: 'ubuntu-latest', suite: 'e2e_vendor_jvm_build', jvm_tool: 'maven', maven: '3.9.2', test_filter: '--ignored maven_reactor', ci_group: 'jvm-dotnet-3.9.2-44'} + - {os: 'ubuntu-latest', suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5', ci_group: '6.9.4-6.9.4-45'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin', ci_group: '6.9.4-6.9.4-46'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin --skip gradle_hosted_vendored_takeover_and_eject', ci_group: '6.9.4-6.9.4-47'} + - {os: 'ubuntu-latest', suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_vendor_ gradle_multi_project gradle_hosted_config_cache_second_row gradle_hosted_fallback_snippet_compiles_kotlin gradle_hosted_vendored_takeover_and_eject', ci_group: '6.9.4-6.9.4-48'} + - {os: 'ubuntu-latest', suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5', ci_group: '9.8.0-9.8.0-49'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin', ci_group: '9.8.0-9.8.0-50'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin --skip gradle_hosted_vendored_takeover_and_eject', ci_group: '9.8.0-9.8.0-51'} + - {os: 'ubuntu-latest', suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_vendor_ gradle_multi_project gradle_hosted_config_cache_second_row gradle_hosted_fallback_snippet_compiles_kotlin gradle_hosted_vendored_takeover_and_eject', ci_group: '9.8.0-9.8.0-52'} + - {os: 'ubuntu-latest', suite: 'e2e_sbt_build', jvm_tool: 'sbt', sbt: '1.13.0', test_filter: '--ignored --test-threads=1', ci_group: 'e2e_sbt_build-1.13.0-53'} + - {os: 'ubuntu-latest', suite: 'e2e_sbt_vendor_build', jvm_tool: 'sbt', sbt: '1.13.0', test_filter: '--ignored --test-threads=1', ci_group: 'e2e_sbt_vendor_build-1.13.0-54'} + - {os: 'ubuntu-latest', suite: 'e2e_vex_build', test_filter: 'deno:: --ignored', deno: '1.46.3', ci_group: 'e2e_vex_build-1.46.3-55'} + - {os: 'ubuntu-latest', suite: 'e2e_vex_build', test_filter: 'deno:: --ignored', deno: '2.9.7', ci_group: 'e2e_vex_build-2.9.7-56'} runs-on: ${{ matrix.os }} # The real-toolchain capstones loop several releases per leg (pip, # pipenv) or bootstrap a tool from PyPI before the suite (poetry, pdm, @@ -1320,13 +1129,13 @@ jobs: cp "target/e2e-bin/socket-patch$exe" "target/debug/socket-patch$exe" - name: Setup Node.js - if: matrix.suite == 'e2e_npm' || matrix.suite == 'e2e_scan' || matrix.suite == 'e2e_safety_pnpm' + if: contains(format(' {0} ', matrix.suite), ' e2e_npm ') || contains(format(' {0} ', matrix.suite), ' e2e_scan ') || contains(format(' {0} ', matrix.suite), ' e2e_safety_pnpm ') uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20.20.2' - name: Setup pnpm - if: matrix.suite == 'e2e_safety_pnpm' + if: contains(format(' {0} ', matrix.suite), ' e2e_safety_pnpm ') # Pin the major version so the store layout the test # asserts on stays stable. `npm install -g` is the simplest # cross-platform install path (works on ubuntu, macos, @@ -1335,13 +1144,13 @@ jobs: run: npm install -g pnpm@10 - name: Setup Node.js 24 (named pnpm legs) - if: matrix.suite == 'e2e_redirect_pnpm_build' + if: contains(format(' {0} ', matrix.suite), ' e2e_redirect_pnpm_build ') && matrix.vlt == '' uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '24.x' - name: Setup Python - if: matrix.suite == 'e2e_pypi' || matrix.uv != '' || matrix.poetry != '' || matrix.pdm != '' || matrix.hatch != '' + if: contains(format(' {0} ', matrix.suite), ' e2e_pypi ') || matrix.uv != '' || matrix.poetry != '' || matrix.pdm != '' || matrix.hatch != '' uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.12.x' @@ -1390,7 +1199,7 @@ jobs: run: uv python install 3.8 3.11 3.12 - name: Setup Ruby - if: matrix.suite == 'e2e_gem' || matrix.bundler != '' + if: contains(format(' {0} ', matrix.suite), ' e2e_gem ') || matrix.bundler != '' uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0 with: # setup-ruby does NOT support `3.2.x` wildcard pinning the @@ -1616,92 +1425,12 @@ jobs: node --version - name: Run e2e tests - if: matrix.vlt == '' - # Suites are `#[ignore]`-gated out of the unpinned `test` job by - # default, hence `--ignored`; an entry that sets `test_filter` - # overrides the selector for itself only. + # Every grouped member retains its own filters and required-tool + # guards; vlt members additionally pass the leg-manifest checker. + # The runner reports each failure and continues through later suites. env: - # Bun legs only: turn the bun suites' "bun not installed / no text - # lock" soft-skips into hard failures and make them assert the - # pinned release, so a leg can never report green on an - # unexercised toolchain. Both are the EMPTY string on non-bun legs, - # which the suites treat as unset. - SOCKET_PATCH_BUN_E2E_REQUIRED: ${{ matrix.bun != '' && '1' || '' }} - SOCKET_PATCH_BUN_E2E_VERSION: ${{ matrix.bun }} - # e2e_bun_lockb only. SOCKET_PATCH_BUN_LOCKB_VERSION is checked with - # `var().is_ok()`, so it must stay EMPTY on every other leg. - SOCKET_PATCH_BUN_LOCKB_REQUIRED: ${{ matrix.suite == 'e2e_bun_lockb' && '1' || '' }} - SOCKET_PATCH_BUN_LOCKB_VERSION: ${{ matrix.suite == 'e2e_bun_lockb' && matrix.bun || '' }} - SOCKET_PATCH_BUN_LOCKB_EXTENDED: ${{ matrix.suite == 'e2e_bun_lockb' && '1' || '' }} - SOCKET_PATCH_BUN_LOCKB_PRODUCTION: ${{ matrix.suite == 'e2e_bun_lockb' && '1' || '' }} - # The same fail-instead-of-skip + pinned-release gates for every - # other real-toolchain capstone. All EMPTY on legs that do not set - # the matching matrix key, which the suites treat as unset. - SOCKET_PATCH_NPM_E2E_REQUIRED: ${{ matrix.npm_required || '' }} - SOCKET_PATCH_CARGO_E2E_REQUIRED: ${{ matrix.suite == 'e2e_safety_cargo_build' && '1' || '' }} - SOCKET_PATCH_UV_E2E_REQUIRED: ${{ matrix.uv != '' && '1' || '' }} - SOCKET_PATCH_UV_E2E_VERSION: ${{ matrix.uv }} - SOCKET_PATCH_POETRY_E2E_REQUIRED: ${{ matrix.poetry != '' && '1' || '' }} - SOCKET_PATCH_POETRY_E2E_VERSION: ${{ matrix.poetry }} - SOCKET_PATCH_PDM_E2E_REQUIRED: ${{ matrix.pdm != '' && '1' || '' }} - SOCKET_PATCH_PDM_E2E_VERSION: ${{ matrix.pdm }} - SOCKET_PATCH_HATCH_E2E_REQUIRED: ${{ matrix.hatch != '' && '1' || '' }} - SOCKET_PATCH_HATCH_E2E_VERSION: ${{ matrix.hatch }} - SOCKET_PATCH_PIPENV_E2E_REQUIRED: ${{ matrix.pipenv != '' && '1' || '' }} - SOCKET_PATCH_PIPENV_E2E_VERSIONS: ${{ matrix.pipenv }} - SOCKET_PATCH_PIP_E2E_REQUIRED: ${{ matrix.pip != '' && '1' || '' }} - SOCKET_PATCH_PIP_E2E_VERSIONS: ${{ matrix.pip }} - SOCKET_PATCH_BUNDLER_E2E_REQUIRED: ${{ matrix.bundler != '' && '1' || '' }} - SOCKET_PATCH_BUNDLER_E2E_VERSION: ${{ matrix.bundler }} - SOCKET_PATCH_COMPOSER_E2E_REQUIRED: ${{ matrix.composer != '' && '1' || '' }} - SOCKET_PATCH_COMPOSER_E2E_VERSION: ${{ matrix.composer }} - SOCKET_PATCH_MAVEN_E2E_REQUIRED: ${{ steps.jvm.outputs.maven == 'true' && '1' || '' }} - SOCKET_PATCH_MAVEN_E2E_VERSION: ${{ steps.jvm.outputs.maven == 'true' && (matrix.maven || '3.9.16') || '' }} - SOCKET_PATCH_GRADLE_E2E_REQUIRED: ${{ matrix.gradle != '' && '1' || '' }} - SOCKET_PATCH_GRADLE_E2E_VERSION: ${{ matrix.gradle }} - SOCKET_PATCH_GRADLE_E2E_PROBE_DIR: ${{ matrix.gradle != '' && format('{0}/target/gradle-probe', github.workspace) || '' }} - SOCKET_PATCH_DOTNET_E2E_REQUIRED: ${{ matrix.dotnet != '' && '1' || '' }} - SOCKET_PATCH_DOTNET_E2E_VERSION: ${{ matrix.dotnet }} - SOCKET_PATCH_DENO_E2E_REQUIRED: ${{ matrix.deno != '' && '1' || '' }} - SOCKET_PATCH_DENO_E2E_VERSION: ${{ matrix.deno }} - SOCKET_PATCH_SBT_E2E_REQUIRED: ${{ matrix.sbt != '' && '1' || '' }} - SOCKET_PATCH_SBT_E2E_VERSION: ${{ matrix.sbt }} - E2E_SUITE: ${{ matrix.suite }} - E2E_TEST_FILTER: ${{ matrix.test_filter || '--ignored' }} - E2E_ALLOW_EMPTY: ${{ matrix.allow_empty }} - shell: bash - # Runs from the package root with CARGO_MANIFEST_DIR set, as - # `cargo test` would. `suite` may name several binaries; an - # `allow_empty` row skips the ones whose test file has not landed. - # Every suite a leg runs must run at least one test (per suite, so - # one suite's tests never hide another's empty filter): a renamed - # module, a dropped `#[ignore]` under the default `--ignored` - # selector or a stale `test_filter` would otherwise pass as 0/0. - run: | - set -uo pipefail - exe='' - if [ "$RUNNER_OS" = Windows ]; then exe=.exe; fi - cd crates/socket-patch-cli - export CARGO_MANIFEST_DIR="$PWD" - status=0 - for suite in $E2E_SUITE; do - if [ "$E2E_ALLOW_EMPTY" = true ] && [ ! -f "tests/$suite.rs" ] && [ ! -f "tests/$suite/main.rs" ]; then - echo "::notice::$suite has not landed yet; skipped (allow_empty)" - continue - fi - log="../../target/e2e-$suite.log" - # shellcheck disable=SC2086 # the filter is several libtest arguments - if ! "../../target/e2e-bin/$suite$exe" $E2E_TEST_FILTER 2>&1 | tee "$log"; then - status=1 - continue - fi - passed=$(sed -n 's/^test result: .* \([0-9][0-9]*\) passed;.*/\1/p' "$log" | head -n 1) - if [ "${passed:-0}" = 0 ]; then - echo "::error::$suite ran no test in this leg (filter: $E2E_TEST_FILTER). Fix the row's suite or test_filter so it selects the tests it is meant to run." - status=1 - fi - done - exit "$status" + E2E_ROW_JSON: ${{ toJSON(matrix) }} + run: python3 scripts/ci-e2e-run.py - name: Upload the Gradle probe reports if: always() && matrix.gradle != '' @@ -1712,32 +1441,6 @@ jobs: if-no-files-found: ignore retention-days: 14 - - name: Run vlt e2e tests - if: matrix.vlt != '' - # One capstone binary per row, through the leg checker: it fails on - # `0 passed`, a crashed binary, a missing `ran`, an unexpected skip or - # an unknown leg (crates/socket-patch-cli/tests/vlt-leg-manifest.json). - shell: bash - env: - SOCKET_PATCH_VLT_E2E_REQUIRED: ${{ matrix.vlt != '' && '1' || '' }} - VLT_SUITE: ${{ matrix.suite }} - VLT_TEST_FILTER: ${{ matrix.test_filter }} - run: | - set -uo pipefail - status=0 - exe='' - if [ "$RUNNER_OS" = Windows ]; then exe=.exe; fi - ( - cd crates/socket-patch-cli - export CARGO_MANIFEST_DIR="$PWD" - # shellcheck disable=SC2086 # the filter is several libtest arguments - "../../target/e2e-bin/$VLT_SUITE$exe" $VLT_TEST_FILTER - ) 2>&1 | tee vlt-leg.log || status=1 - py=$(command -v python3 || command -v python) - # No cargo `Running` line when the binary runs directly: name it. - "$py" scripts/check-vlt-legs.py --binary "$VLT_SUITE" --manifest crates/socket-patch-cli/tests/vlt-leg-manifest.json vlt-leg.log || status=1 - exit "$status" - # The PM-version legs with no boundary of their own: the middle uv lines, # Pipenv releases and .NET SDK majors, and bundler 2.7. Skipped # on pull_request (the `e2e` rows keep every named boundary, the oldest @@ -1747,34 +1450,42 @@ jobs: # Each OS consumes only its own build. A queued macOS runner must not # delay Linux or Windows, and a Windows compile must not delay Gradle. + # Middle Gradle lines are required in the merge queue; every PR still + # runs every suite on the oldest/newest line. Main reuses the queue verdict. + e2e-gradle-mid: + name: e2e-gradle-mid (${{ matrix.os }}, ${{ matrix.ci_group }}) + if: github.event_name != 'pull_request' && needs.e2e-build.outputs.reuse != 'true' + needs: [e2e-build] + strategy: + fail-fast: false + matrix: + include: + - {os: 'ubuntu-latest', suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5', ci_group: '7.6.6-7.6.6-1'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin', ci_group: '7.6.6-7.6.6-2'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin --skip gradle_hosted_vendored_takeover_and_eject', ci_group: '7.6.6-7.6.6-3'} + - {os: 'ubuntu-latest', suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_vendor_ gradle_multi_project gradle_hosted_config_cache_second_row gradle_hosted_fallback_snippet_compiles_kotlin gradle_hosted_vendored_takeover_and_eject', ci_group: '7.6.6-7.6.6-4'} + - {os: 'ubuntu-latest', suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5', ci_group: '8.14.3-8.14.3-5'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin', ci_group: '8.14.3-8.14.3-6'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin --skip gradle_hosted_vendored_takeover_and_eject', ci_group: '8.14.3-8.14.3-7'} + - {os: 'ubuntu-latest', suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_vendor_ gradle_multi_project gradle_hosted_config_cache_second_row gradle_hosted_fallback_snippet_compiles_kotlin gradle_hosted_vendored_takeover_and_eject', ci_group: '8.14.3-8.14.3-8'} + runs-on: ${{ matrix.os }} + timeout-minutes: 40 + env: *e2e-env + steps: *e2e-steps + e2e-windows: + name: e2e-windows (${{ matrix.os }}, ${{ matrix.ci_group }}) + if: needs.e2e-build-windows.outputs.reuse != 'true' needs: [e2e-build-windows] strategy: fail-fast: false matrix: include: - - os: windows-latest - suite: e2e_safety_pnpm - - os: windows-latest - suite: e2e_redirect_npm_build - npm_required: '1' - - os: windows-latest - suite: e2e_redirect_rush_sim - - os: windows-latest - suite: e2e_redirect_bun_build e2e_vendor_bun_build mode_migration_bun - bun: '1.4.2' - test_filter: --include-ignored - - {os: windows-latest, suite: e2e_redirect_vlt_build, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix} - - {os: windows-latest, suite: e2e_vendor_vlt_build, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix} - - {os: windows-latest, suite: e2e_vendor_vlt_build, vlt: '1.0.0-rc.14', test_filter: --include-ignored vlt_pinned_matrix} - - {os: windows-latest, suite: mode_migration_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix} - - {os: windows-latest, suite: mode_migration_vlt, vlt: '1.0.0-rc.14', test_filter: --include-ignored vlt_pinned_matrix} - - {os: windows-latest, suite: e2e_safety_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix, vlt_store_linker: 'hardlink'} - - {os: windows-latest, suite: e2e_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix} - - {os: windows-latest, suite: e2e_vlt, vlt: '1.0.0-rc.14', test_filter: --include-ignored vlt_pinned_matrix} - - {os: windows-latest, suite: e2e_redirect_pnpm_build, test_filter: '--ignored --skip pnpm_pinned_matrix'} - - {os: windows-latest, suite: e2e_vendor_jvm_build, jvm_tool: maven, maven: '3.9.16', test_filter: '--ignored maven_reactor'} - - {os: windows-latest, suite: e2e_vendor_jvm_build, jvm_tool: gradle, gradle: '8.14.3', java: '17', test_filter: '--ignored gradle_multi_project'} + - {os: 'windows-latest', bun: '1.4.2', suite: 'e2e_safety_pnpm e2e_redirect_npm_build e2e_redirect_rush_sim e2e_redirect_bun_build e2e_vendor_bun_build mode_migration_bun', cases: '[{"os":"windows-latest","suite":"e2e_safety_pnpm"},{"os":"windows-latest","suite":"e2e_redirect_npm_build","npm_required":"1"},{"os":"windows-latest","suite":"e2e_redirect_rush_sim"},{"os":"windows-latest","suite":"e2e_redirect_bun_build e2e_vendor_bun_build mode_migration_bun","bun":"1.4.2","test_filter":"--include-ignored"}]', ci_group: 'node20-1.4.2-1'} + - {os: 'windows-latest', vlt: '1.2.0', suite: 'e2e_redirect_vlt_build e2e_vendor_vlt_build mode_migration_vlt e2e_safety_vlt e2e_vlt e2e_redirect_pnpm_build', cases: '[{"os":"windows-latest","suite":"e2e_redirect_vlt_build","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"windows-latest","suite":"e2e_vendor_vlt_build","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"windows-latest","suite":"mode_migration_vlt","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"windows-latest","suite":"e2e_safety_vlt","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix","vlt_store_linker":"hardlink"},{"os":"windows-latest","suite":"e2e_vlt","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"windows-latest","suite":"e2e_redirect_pnpm_build","test_filter":"--ignored --skip pnpm_pinned_matrix"}]', ci_group: 'node24-1.2.0-2'} + - {os: 'windows-latest', vlt: '1.0.0-rc.14', suite: 'e2e_vendor_vlt_build mode_migration_vlt e2e_vlt', cases: '[{"os":"windows-latest","suite":"e2e_vendor_vlt_build","vlt":"1.0.0-rc.14","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"windows-latest","suite":"mode_migration_vlt","vlt":"1.0.0-rc.14","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"windows-latest","suite":"e2e_vlt","vlt":"1.0.0-rc.14","test_filter":"--include-ignored vlt_pinned_matrix"}]', ci_group: 'node24-1.0.0-rc.14-3'} + - {os: 'windows-latest', suite: 'e2e_vendor_jvm_build', jvm_tool: 'maven', maven: '3.9.16', test_filter: '--ignored maven_reactor', ci_group: 'jvm-dotnet-3.9.16-4'} + - {os: 'windows-latest', suite: 'e2e_vendor_jvm_build', jvm_tool: 'gradle', gradle: '8.14.3', java: '17', test_filter: '--ignored gradle_multi_project', ci_group: '8.14.3-8.14.3-5'} runs-on: ${{ matrix.os }} timeout-minutes: 40 env: *e2e-env @@ -1783,49 +1494,20 @@ jobs: # The macOS rows run on main, the merge queue and nightly, not on every # PR push, so PRs stop queueing on the small macOS runner pool. e2e-macos: - if: github.event_name != 'pull_request' + name: e2e-macos (${{ matrix.os }}, ${{ matrix.ci_group }}) + if: github.event_name != 'pull_request' && needs.e2e-build-macos.outputs.reuse != 'true' # These jobs consume e2e-build's binaries and can run alongside unit tests. needs: [e2e-build-macos] strategy: fail-fast: false matrix: include: - - os: macos-latest - suite: e2e_safety_pnpm - - os: macos-latest - suite: e2e_redirect_npm_build - npm_required: '1' - - os: macos-latest - suite: e2e_redirect_rush_sim - - os: macos-latest - suite: e2e_redirect_bun_build - bun: '1.4.2' - test_filter: --include-ignored - - os: macos-latest - suite: e2e_vendor_bun_build - bun: '1.4.2' - test_filter: --include-ignored - - os: macos-latest - suite: mode_migration_bun - bun: '1.4.2' - test_filter: --include-ignored - - {os: macos-latest, suite: e2e_redirect_vlt_build, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix} - - {os: macos-latest, suite: e2e_vendor_vlt_build, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix} - - {os: macos-latest, suite: mode_migration_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix} - - {os: macos-latest, suite: e2e_safety_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix, vlt_store_linker: 'hardlink'} - - {os: macos-latest, suite: e2e_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix} - - {os: macos-latest, suite: e2e_redirect_pnpm_build, test_filter: '--ignored --skip pnpm_pinned_matrix'} - - {os: macos-latest, suite: e2e_redirect_uv_build, uv: '0.12.17'} - - {os: macos-latest, suite: e2e_vendor_pypi_build, uv: '0.12.17', test_filter: --include-ignored} - - {os: macos-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '2.4.3'} - - {os: macos-latest, suite: e2e_vex_build, test_filter: 'pdm:: --ignored', pdm: '2.29.2'} - - {os: macos-latest, suite: e2e_vex_build, test_filter: 'hatch:: --ignored', hatch: '1.18.1'} - - {os: macos-latest, suite: e2e_vex_build, test_filter: 'pipenv:: --ignored', pipenv: '2022.12.19 2026.8.0'} - - {os: macos-latest, suite: e2e_vex_build, test_filter: 'pip:: --ignored', pip: '22 26'} - - {os: macos-latest, suite: e2e_redirect_maven_build, jvm_tool: maven, maven: '3.9.16'} - - {os: macos-latest, suite: e2e_vendor_maven_build, jvm_tool: maven, maven: '3.9.16'} - - {os: macos-latest, suite: e2e_vendor_jvm_build, jvm_tool: maven, maven: '3.9.16', test_filter: '--ignored maven_reactor'} - - {os: macos-latest, suite: e2e_nuget_dotnet_build, dotnet: '8'} + - {os: 'macos-latest', bun: '1.4.2', suite: 'e2e_safety_pnpm e2e_redirect_npm_build e2e_redirect_rush_sim e2e_redirect_bun_build e2e_vendor_bun_build mode_migration_bun', cases: '[{"os":"macos-latest","suite":"e2e_safety_pnpm"},{"os":"macos-latest","suite":"e2e_redirect_npm_build","npm_required":"1"},{"os":"macos-latest","suite":"e2e_redirect_rush_sim"},{"os":"macos-latest","suite":"e2e_redirect_bun_build","bun":"1.4.2","test_filter":"--include-ignored"},{"os":"macos-latest","suite":"e2e_vendor_bun_build","bun":"1.4.2","test_filter":"--include-ignored"},{"os":"macos-latest","suite":"mode_migration_bun","bun":"1.4.2","test_filter":"--include-ignored"}]', ci_group: 'node20-1.4.2-1'} + - {os: 'macos-latest', vlt: '1.2.0', suite: 'e2e_redirect_vlt_build e2e_vendor_vlt_build mode_migration_vlt e2e_safety_vlt e2e_vlt e2e_redirect_pnpm_build', cases: '[{"os":"macos-latest","suite":"e2e_redirect_vlt_build","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"macos-latest","suite":"e2e_vendor_vlt_build","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"macos-latest","suite":"mode_migration_vlt","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"macos-latest","suite":"e2e_safety_vlt","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix","vlt_store_linker":"hardlink"},{"os":"macos-latest","suite":"e2e_vlt","vlt":"1.2.0","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"macos-latest","suite":"e2e_redirect_pnpm_build","test_filter":"--ignored --skip pnpm_pinned_matrix"}]', ci_group: 'node24-1.2.0-2'} + - {os: 'macos-latest', uv: '0.12.17', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"macos-latest","suite":"e2e_redirect_uv_build","uv":"0.12.17"},{"os":"macos-latest","suite":"e2e_vendor_pypi_build","uv":"0.12.17","test_filter":"--include-ignored"}]', ci_group: 'uv-0.12.17-3'} + - {os: 'macos-latest', poetry: '2.4.3', pdm: '2.29.2', hatch: '1.18.1', suite: 'e2e_vex_build', cases: '[{"os":"macos-latest","suite":"e2e_vex_build","test_filter":"poetry:: --ignored","poetry":"2.4.3"},{"os":"macos-latest","suite":"e2e_vex_build","test_filter":"pdm:: --ignored","pdm":"2.29.2"},{"os":"macos-latest","suite":"e2e_vex_build","test_filter":"hatch:: --ignored","hatch":"1.18.1"}]', ci_group: 'python-2.4.3-2.29.2-1.18.1-4'} + - {os: 'macos-latest', pipenv: '2022.12.19 2026.8.0', pip: '22 26', suite: 'e2e_vex_build', cases: '[{"os":"macos-latest","suite":"e2e_vex_build","test_filter":"pipenv:: --ignored","pipenv":"2022.12.19 2026.8.0"},{"os":"macos-latest","suite":"e2e_vex_build","test_filter":"pip:: --ignored","pip":"22 26"}]', ci_group: 'python-installers-2022.12.19 2026.8.0-22 26-5'} + - {os: 'macos-latest', jvm_tool: 'maven', maven: '3.9.16', dotnet: '8', suite: 'e2e_redirect_maven_build e2e_vendor_maven_build e2e_vendor_jvm_build e2e_nuget_dotnet_build', cases: '[{"os":"macos-latest","suite":"e2e_redirect_maven_build","jvm_tool":"maven","maven":"3.9.16"},{"os":"macos-latest","suite":"e2e_vendor_maven_build","jvm_tool":"maven","maven":"3.9.16"},{"os":"macos-latest","suite":"e2e_vendor_jvm_build","jvm_tool":"maven","maven":"3.9.16","test_filter":"--ignored maven_reactor"},{"os":"macos-latest","suite":"e2e_nuget_dotnet_build","dotnet":"8"}]', ci_group: 'jvm-dotnet-3.9.16-8-6'} runs-on: ${{ matrix.os }} # The real-toolchain capstones loop several releases per leg (pip, # pipenv) or bootstrap a tool from PyPI before the suite (poetry, pdm, @@ -1841,6 +1523,7 @@ jobs: steps: *e2e-steps e2e-full: + name: e2e-full (${{ matrix.os }}, ${{ matrix.ci_group }}) # 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: [e2e-build] @@ -1848,35 +1531,24 @@ jobs: fail-fast: false matrix: include: - - {os: ubuntu-latest, suite: e2e_redirect_gem_build e2e_vendor_gem_build, ruby: '3.3', bundler: '2.7.2'} - - {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.2.37'} - - {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.3.5'} - - {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.4.30'} - - {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.5.3'} - - {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.5.6'} - - {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.6.17'} - - {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.7.22'} - - {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.8.24'} - - {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.9.30'} - - {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.10.12'} - - {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.11.33'} - - {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.2.37', test_filter: --include-ignored} - - {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.3.5', test_filter: --include-ignored} - - {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.4.30', test_filter: --include-ignored} - - {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.5.3', test_filter: --include-ignored} - - {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.5.6', test_filter: --include-ignored} - - {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.6.17', test_filter: --include-ignored} - - {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.7.22', test_filter: --include-ignored} - - {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.8.24', test_filter: --include-ignored} - - {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.9.30', test_filter: --include-ignored} - - {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.10.12', test_filter: --include-ignored} - - {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.11.33', test_filter: --include-ignored} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pipenv:: --ignored', pipenv: '2023.12.1'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pipenv:: --ignored', pipenv: '2024.4.1'} - - {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pipenv:: --ignored', pipenv: '2025.1.3'} - - {os: ubuntu-latest, suite: e2e_nuget_dotnet_build, dotnet: '7'} - - {os: ubuntu-latest, suite: e2e_nuget_dotnet_build, dotnet: '8'} - - {os: ubuntu-latest, suite: e2e_nuget_dotnet_build, dotnet: '9'} + - {os: 'ubuntu-latest', suite: 'e2e_redirect_gem_build e2e_vendor_gem_build', ruby: '3.3', bundler: '2.7.2', ci_group: 'e2e_redirect_gem_build e2e_vendor_gem_build-3.3-2.7.2-1'} + - {os: 'ubuntu-latest', uv: '0.2.37', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_uv_build","uv":"0.2.37"},{"os":"ubuntu-latest","suite":"e2e_vendor_pypi_build","uv":"0.2.37","test_filter":"--include-ignored"}]', ci_group: 'uv-0.2.37-2'} + - {os: 'ubuntu-latest', uv: '0.3.5', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_uv_build","uv":"0.3.5"},{"os":"ubuntu-latest","suite":"e2e_vendor_pypi_build","uv":"0.3.5","test_filter":"--include-ignored"}]', ci_group: 'uv-0.3.5-3'} + - {os: 'ubuntu-latest', uv: '0.4.30', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_uv_build","uv":"0.4.30"},{"os":"ubuntu-latest","suite":"e2e_vendor_pypi_build","uv":"0.4.30","test_filter":"--include-ignored"}]', ci_group: 'uv-0.4.30-4'} + - {os: 'ubuntu-latest', uv: '0.5.3', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_uv_build","uv":"0.5.3"},{"os":"ubuntu-latest","suite":"e2e_vendor_pypi_build","uv":"0.5.3","test_filter":"--include-ignored"}]', ci_group: 'uv-0.5.3-5'} + - {os: 'ubuntu-latest', uv: '0.5.6', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_uv_build","uv":"0.5.6"},{"os":"ubuntu-latest","suite":"e2e_vendor_pypi_build","uv":"0.5.6","test_filter":"--include-ignored"}]', ci_group: 'uv-0.5.6-6'} + - {os: 'ubuntu-latest', uv: '0.6.17', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_uv_build","uv":"0.6.17"},{"os":"ubuntu-latest","suite":"e2e_vendor_pypi_build","uv":"0.6.17","test_filter":"--include-ignored"}]', ci_group: 'uv-0.6.17-7'} + - {os: 'ubuntu-latest', uv: '0.7.22', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_uv_build","uv":"0.7.22"},{"os":"ubuntu-latest","suite":"e2e_vendor_pypi_build","uv":"0.7.22","test_filter":"--include-ignored"}]', ci_group: 'uv-0.7.22-8'} + - {os: 'ubuntu-latest', uv: '0.8.24', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_uv_build","uv":"0.8.24"},{"os":"ubuntu-latest","suite":"e2e_vendor_pypi_build","uv":"0.8.24","test_filter":"--include-ignored"}]', ci_group: 'uv-0.8.24-9'} + - {os: 'ubuntu-latest', uv: '0.9.30', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_uv_build","uv":"0.9.30"},{"os":"ubuntu-latest","suite":"e2e_vendor_pypi_build","uv":"0.9.30","test_filter":"--include-ignored"}]', ci_group: 'uv-0.9.30-10'} + - {os: 'ubuntu-latest', uv: '0.10.12', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_uv_build","uv":"0.10.12"},{"os":"ubuntu-latest","suite":"e2e_vendor_pypi_build","uv":"0.10.12","test_filter":"--include-ignored"}]', ci_group: 'uv-0.10.12-11'} + - {os: 'ubuntu-latest', uv: '0.11.33', suite: 'e2e_redirect_uv_build e2e_vendor_pypi_build', cases: '[{"os":"ubuntu-latest","suite":"e2e_redirect_uv_build","uv":"0.11.33"},{"os":"ubuntu-latest","suite":"e2e_vendor_pypi_build","uv":"0.11.33","test_filter":"--include-ignored"}]', ci_group: 'uv-0.11.33-12'} + - {os: 'ubuntu-latest', suite: 'e2e_vex_build', test_filter: 'pipenv:: --ignored', pipenv: '2023.12.1', ci_group: 'python-installers-2023.12.1-13'} + - {os: 'ubuntu-latest', suite: 'e2e_vex_build', test_filter: 'pipenv:: --ignored', pipenv: '2024.4.1', ci_group: 'python-installers-2024.4.1-14'} + - {os: 'ubuntu-latest', suite: 'e2e_vex_build', test_filter: 'pipenv:: --ignored', pipenv: '2025.1.3', ci_group: 'python-installers-2025.1.3-15'} + - {os: 'ubuntu-latest', suite: 'e2e_nuget_dotnet_build', dotnet: '7', ci_group: 'jvm-dotnet-7-16'} + - {os: 'ubuntu-latest', suite: 'e2e_nuget_dotnet_build', dotnet: '8', ci_group: 'jvm-dotnet-8-17'} + - {os: 'ubuntu-latest', suite: 'e2e_nuget_dotnet_build', dotnet: '9', ci_group: 'jvm-dotnet-9-18'} runs-on: ${{ matrix.os }} timeout-minutes: 40 # What .cargo/config.toml's [env] gives processes cargo launches; these @@ -1993,7 +1665,7 @@ jobs: # 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 + if: github.event.pull_request.draft != true && needs.clippy.outputs.reuse != 'true' needs: clippy runs-on: ubuntu-latest timeout-minutes: 40 @@ -2036,7 +1708,7 @@ jobs: # 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 + if: github.event.pull_request.draft != true && needs.clippy.outputs.reuse != 'true' needs: clippy strategy: fail-fast: false @@ -2079,7 +1751,7 @@ 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' + if: github.event_name != 'pull_request' && needs.clippy.outputs.reuse != 'true' name: yarn-berry ${{ matrix.yarn }} (${{ matrix.os }}) needs: clippy strategy: @@ -2118,6 +1790,7 @@ jobs: # toolchain x lock cross off pull_request. Each leg also runs # e2e_safety_cargo_build (its headline test honours the knobs). cargo-vex-matrix: + if: needs.e2e-build.outputs.reuse != 'true' 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. @@ -2199,6 +1872,7 @@ jobs: exit "$status" cargo-vex-matrix-windows: + if: needs.e2e-build-windows.outputs.reuse != 'true' name: cargo ${{ matrix.toolchain }} lock-v${{ matrix.lock || 'own' }} (${{ matrix.os }}) needs: [e2e-build-windows] runs-on: ${{ matrix.os }} @@ -2214,7 +1888,7 @@ jobs: # The macOS rows run on main, the merge queue and nightly, not per PR push. cargo-vex-matrix-macos: - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && needs.e2e-build-macos.outputs.reuse != 'true' name: cargo ${{ matrix.toolchain }} lock-v${{ matrix.lock || 'own' }} (${{ matrix.os }}) needs: [e2e-build-macos] runs-on: ${{ matrix.os }} @@ -2288,6 +1962,7 @@ jobs: - name: Install Rust run: rustup show - name: Pull the old cargos under test + if: needs.clippy.outputs.reuse != 'true' run: | docker pull rust:1.41-slim docker pull rust:1.56-slim @@ -2296,7 +1971,12 @@ jobs: with: 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' + 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' shell: bash env: SOCKET_PATCH_CARGO_E2E_REQUIRED: '1' @@ -2340,7 +2020,7 @@ jobs: # ---------------------------------------------------------------------- hosted-e2e: name: hosted-e2e # may be a required check; do not rename - if: github.event.pull_request.draft != true + if: github.event.pull_request.draft != true && needs.clippy.outputs.reuse != 'true' needs: clippy runs-on: ubuntu-latest permissions: @@ -2546,7 +2226,7 @@ jobs: 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-build-windows, e2e-build-macos, e2e, e2e-windows, e2e-full, e2e-docker, yarn-classic-matrix, yarn-berry-e2e, yarn-berry-full, cargo-vex-matrix, cargo-vex-matrix-windows, cargo-vex-matrix-full, cargo-old-toolchains, hosted-e2e, e2e-macos, yarn-berry-e2e-macos, cargo-vex-matrix-macos] + needs: [clippy, node-addon, lint-ecosystems, release-readiness, test, test-release, coverage, docker-base, coverage-docker, coverage-merge, dispatch-tests, e2e-build, e2e-build-windows, e2e-build-macos, e2e, e2e-gradle-mid, e2e-windows, e2e-full, e2e-docker, yarn-classic-matrix, yarn-berry-e2e, yarn-berry-full, cargo-vex-matrix, cargo-vex-matrix-windows, cargo-vex-matrix-full, cargo-old-toolchains, hosted-e2e, e2e-macos, yarn-berry-e2e-macos, cargo-vex-matrix-macos] runs-on: ubuntu-latest timeout-minutes: 5 steps: diff --git a/.github/workflows/gradle-compatibility.yml b/.github/workflows/gradle-compatibility.yml index b8196642c..038e42dc7 100644 --- a/.github/workflows/gradle-compatibility.yml +++ b/.github/workflows/gradle-compatibility.yml @@ -39,10 +39,12 @@ name: Gradle patch compatibility # and sha256, hash-dir naming, refresh / RO-cache / transform canaries). # # On pull_request the ubuntu `cells` are skipped: ci.yml's `e2e` PR tier runs -# the same suites, filters, Gradle lines and JDKs on ubuntu on every PR and -# in the merge queue. The ubuntu `extras` run on a PR only when it touches +# the same suites and JDKs on ubuntu (boundary lines on PRs, all four lines +# in the merge queue). The ubuntu `extras` run on a PR only when it touches # Gradle code (`changes` below); every other PR gets them from the nightly -# (#1177). Windows cells run on every PR that matches `paths:`. +# (#1177). Matching PRs run Windows agent/vendor on all four Gradle lines +# and hosted on the oldest/newest lines. The middle hosted lines run nightly +# and on workflow_dispatch (#1300). on: pull_request: @@ -233,8 +235,11 @@ jobs: exclude: # macOS legs run on push to main (and nightly where scheduled), not per PR push. - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} - # ci.yml's `e2e` runs these ubuntu cells on every PR (#1177). + # ci.yml covers ubuntu: boundary lines on PRs, middle lines in the queue. - os: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || '' }} + # Boundary lines retain every hosted test on Windows on PRs. + - {os: "${{ github.event_name == 'pull_request' && 'windows-latest' || '' }}", gradle: '7.6.6', mode: hosted} + - {os: "${{ github.event_name == 'pull_request' && 'windows-latest' || '' }}", gradle: '8.14.3', mode: hosted} runs-on: ${{ matrix.os }} timeout-minutes: 60 steps: &cell-steps diff --git a/scripts/ci-e2e-groups.py b/scripts/ci-e2e-groups.py new file mode 100644 index 000000000..cd256ffbe --- /dev/null +++ b/scripts/ci-e2e-groups.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Pack compatible short e2e rows while preserving their original selections. + +Run with --write after editing ci.yml's rows. The cases stored in each bin +are the complete original rows, so regrouping is lossless and idempotent. +Long Gradle/sbt legs stay separate. Estimates are deliberately conservative +against #1172's measured short-leg times; bins target at most six minutes +of tests, below the merge queue's long-running jobs. +""" + +import argparse +import importlib.util +import json +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = ROOT / ".github/workflows/ci.yml" +FAMILIES = ("e2e", "e2e-windows", "e2e-macos", "e2e-full", "e2e-gradle-mid") +CASE_KEYS = {"suite", "test_filter", "npm_required", "vlt_store_linker", "allow_empty"} +BUDGET_SECONDS = 360 +ESTIMATES = { + "e2e_safety_pnpm": 30, "e2e_redirect_npm_build": 90, "e2e_redirect_rush_sim": 30, + "e2e_redirect_pnpm_build": 90, "e2e_redirect_bun_build": 30, "e2e_vendor_bun_build": 30, + "mode_migration_bun": 30, "e2e_bun_lockb": 180, "e2e_redirect_vlt_build": 45, + "e2e_vendor_vlt_build": 45, "mode_migration_vlt": 45, "e2e_safety_vlt": 45, "e2e_vlt": 45, + "e2e_redirect_uv_build": 30, "e2e_vendor_pypi_build": 30, "e2e_vex_build": 60, + "e2e_redirect_maven_build": 45, "e2e_vendor_maven_build": 60, "e2e_vendor_jvm_build": 60, + "e2e_nuget_dotnet_build": 45, +} + + +def reader(): + spec = importlib.util.spec_from_file_location("ci_rows", ROOT / "scripts/tests/test_ci_vlt_rows.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def family(row): + if row.get("gradle") or row.get("sbt"): + return None + if row.get("vlt") or "e2e_redirect_pnpm_build" in row["suite"].split(): + return "node24" + if row.get("bun") or row["suite"] in ("e2e_safety_pnpm", "e2e_redirect_npm_build", "e2e_redirect_rush_sim"): + return "node20" + if row.get("uv"): + return "uv" + # Pip/pipenv use setup-uv's current release; the other Python tools + # bootstrap through pinned uv 0.11.19. Do not overwrite either PATH. + if row.get("pipenv") or row.get("pip"): + return "python-installers" + if any(row.get(key) for key in ("poetry", "pdm", "hatch")): + return "python" + if row.get("maven") or row.get("dotnet"): + return "jvm-dotnet" + return None + + +def estimate(row): + return sum(ESTIMATES.get(suite, BUDGET_SECONDS) for suite in row["suite"].split()) + + +def settings(row): + return {key: value for key, value in row.items() if key not in CASE_KEYS} + + +def pack(rows): + bins = [] + for row in rows: + kind, config = family(row), settings(row) + for bucket in bins: + if (kind is not None and bucket["family"] == kind + and bucket["seconds"] + estimate(row) <= BUDGET_SECONDS + and all(bucket["settings"].get(k, v) == v for k, v in config.items())): + break + else: + bucket = {"family": kind, "settings": {}, "rows": [], "seconds": 0} + bins.append(bucket) + bucket["rows"].append(row) + bucket["settings"].update(config) + bucket["seconds"] += estimate(row) + result = [] + for index, bucket in enumerate(bins): + cases = bucket["rows"] + if len(cases) == 1: + group = dict(cases[0]) + else: + group = dict(bucket["settings"]) + group["suite"] = " ".join(dict.fromkeys(s for row in cases for s in row["suite"].split())) + group["cases"] = json.dumps(cases, separators=(",", ":")) + versions = [v for k, v in bucket["settings"].items() if k not in ("os", "jvm_tool", "java")] + group["ci_group"] = "-".join([bucket["family"] or cases[0].get("gradle") or cases[0]["suite"], + *versions, str(index + 1)]) + result.append(group) + return result + + +def render(group): + def scalar(value): + return "'" + value.replace("'", "''") + "'" + return " - {" + ", ".join(f"{k}: {scalar(v)}" for k, v in group.items()) + "}" + + +def regroup(text): + rows_reader = reader() + jobs = rows_reader.jobs(text) + for name in FAMILIES: + rows = rows_reader.matrix_include(jobs[name]) + # Generated labels describe bins, never affect a member's setup. + rows = [{k: v for k, v in row.items() if k != "ci_group"} for row in rows] + groups = pack(rows) + pattern = rf"(\n {re.escape(name)}:\n.*? include:\n).*?(?= runs-on:)" + text, count = re.subn(pattern, lambda m: m[1] + "\n".join(map(render, groups)) + "\n", + text, count=1, flags=re.S) + if count != 1: + raise ValueError(f"Missing matrix for {name}") + print(f"{name}: {len(rows)} selections -> {len(groups)} jobs") + return text + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--write", action="store_true") + args = parser.parse_args() + text = regroup(WORKFLOW.read_text(encoding="utf-8")) + if args.write: + WORKFLOW.write_text(text, encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/scripts/ci-e2e-run.py b/scripts/ci-e2e-run.py new file mode 100644 index 000000000..c84df6be7 --- /dev/null +++ b/scripts/ci-e2e-run.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Run a grouped CI row without losing any member's filters or environment. + +Each original row keeps its own required-tool guards, versions and vlt leg +checker. A failed or empty suite is reported, and later members still run. +""" + +import json +import os +import re +import shlex +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +TOOLS = { + "bun": "BUN", "uv": "UV", "poetry": "POETRY", "pdm": "PDM", "hatch": "HATCH", + "bundler": "BUNDLER", "composer": "COMPOSER", "gradle": "GRADLE", "dotnet": "DOTNET", + "deno": "DENO", "sbt": "SBT", "vlt": "VLT", +} + + +def members(row): + cases = json.loads(row["cases"]) if row.get("cases") else [row] + if not cases or any(not case.get("suite", "").strip() for case in cases): + raise ValueError("An e2e group must contain nonempty suite selections") + return cases + + +def environment(row, suite, base, root): + env = dict(base) + for key, tool in TOOLS.items(): + version = str(row.get(key) or "") + env[f"SOCKET_PATCH_{tool}_E2E_REQUIRED"] = "1" if version else "" + env[f"SOCKET_PATCH_{tool}_E2E_VERSION"] = version + for key in ("pipenv", "pip"): + versions = str(row.get(key) or "") + env[f"SOCKET_PATCH_{key.upper()}_E2E_REQUIRED"] = "1" if versions else "" + env[f"SOCKET_PATCH_{key.upper()}_E2E_VERSIONS"] = versions + env["SOCKET_PATCH_NPM_E2E_REQUIRED"] = str(row.get("npm_required") or "") + env["SOCKET_PATCH_CARGO_E2E_REQUIRED"] = "1" if suite == "e2e_safety_cargo_build" else "" + for key in ("REQUIRED", "EXTENDED", "PRODUCTION"): + env[f"SOCKET_PATCH_BUN_LOCKB_{key}"] = "1" if suite == "e2e_bun_lockb" else "" + env["SOCKET_PATCH_BUN_LOCKB_VERSION"] = str(row.get("bun") or "") if suite == "e2e_bun_lockb" else "" + maven = row.get("jvm_tool") == "maven" or ( + row.get("jvm_tool") == "gradle" and "gradle_vendor_" in row.get("test_filter", "")) + env["SOCKET_PATCH_MAVEN_E2E_REQUIRED"] = "1" if maven else "" + env["SOCKET_PATCH_MAVEN_E2E_VERSION"] = str(row.get("maven") or "3.9.16") if maven else "" + env["SOCKET_PATCH_GRADLE_E2E_PROBE_DIR"] = str(root / "target/gradle-probe") if row.get("gradle") else "" + # An unset linker is distinct from an explicitly pinned hardlink cell. + env.pop("SOCKET_PATCH_VLT_E2E_STORE_LINKER", None) + if row.get("vlt_store_linker"): + env["SOCKET_PATCH_VLT_E2E_STORE_LINKER"] = str(row["vlt_store_linker"]) + if not row.get("vlt_upgrade"): + env.pop("SOCKET_PATCH_VLT_E2E_UPGRADE_JS", None) + env.pop("SOCKET_PATCH_VLT_E2E_UPGRADE_VERSION", None) + env["CARGO_MANIFEST_DIR"] = str(root / "crates/socket-patch-cli") + return env + + +def run_binary(args, env, cwd, log): + with log.open("w", encoding="utf-8") as output: + with subprocess.Popen(args, env=env, cwd=cwd, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace") as process: + for line in process.stdout: + print(line, end="", flush=True) + output.write(line) + return process.wait() + + +def run_cases(row, root=ROOT, base=None): + base = os.environ if base is None else base + cli = root / "crates/socket-patch-cli" + suffix = ".exe" if sys.platform == "win32" else "" + status = 0 + for index, case in enumerate(members(row)): + for suite in case["suite"].split(): + if not re.fullmatch(r"[A-Za-z0-9_]+", suite): + raise ValueError(f"Invalid test suite: {suite}") + if case.get("allow_empty") == "true" and not any( + path.is_file() for path in (cli / f"tests/{suite}.rs", cli / f"tests/{suite}/main.rs")): + print(f"::notice::{suite} has not landed yet; skipped (allow_empty)") + continue + filters = shlex.split(case.get("test_filter") or "--ignored") + log = root / f"target/e2e-{index}-{suite}.log" + env = environment(case, suite, base, root) + print(f"::group::{suite} {case.get('test_filter', '--ignored')}", flush=True) + try: + failed = run_binary([str(root / f"target/e2e-bin/{suite}{suffix}"), *filters], + env, cli, log) != 0 + passed = re.search(r"^test result: .*? (\d+) passed;", log.read_text(encoding="utf-8"), re.M) + if not passed or int(passed[1]) == 0: + print(f"::error::{suite} ran no tests; check its filters.") + failed = True + if case.get("vlt"): + checker = subprocess.run([sys.executable, str(root / "scripts/check-vlt-legs.py"), + "--binary", suite, "--manifest", + str(cli / "tests/vlt-leg-manifest.json"), str(log)], cwd=root) + failed |= checker.returncode != 0 + if failed: + print(f"::error::{suite} failed (row {index}, filters: {' '.join(filters)}).") + status = 1 + except OSError as error: + print(f"::error::{suite} could not run: {error}") + status = 1 + finally: + print("::endgroup::", flush=True) + return status + + +if __name__ == "__main__": + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + sys.exit(run_cases(json.loads(os.environ["E2E_ROW_JSON"]))) diff --git a/scripts/ci-lcov-summary.py b/scripts/ci-lcov-summary.py new file mode 100644 index 000000000..6455af76c --- /dev/null +++ b/scripts/ci-lcov-summary.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Render the existing LCOV export without another llvm-profdata/llvm-cov pass. + +LCOV carries line, function and branch totals, not LLVM region totals. +The raw LCOV artifact remains the source of truth for the merged report. +""" + +import argparse +from pathlib import Path + +METRICS = (("Lines", "LH", "LF"), ("Functions", "FNH", "FNF"), ("Branches", "BRH", "BRF")) + + +def read_lcov(text): + files = {} + name, counts = None, {} + for line in text.splitlines(): + key, _, value = line.partition(":") + if key == "SF": + if name is not None: + raise ValueError("LCOV record missing end_of_record") + name, counts = value, {} + elif key in {key for _, hit, found in METRICS for key in (hit, found)}: + counts[key] = int(value) + elif line == "end_of_record": + if not name or name in files: + raise ValueError("LCOV source missing or duplicated") + if "LF" not in counts or "LH" not in counts: + raise ValueError(f"LCOV line totals missing: {name}") + for _, hit, found in METRICS: + if not 0 <= counts.get(hit, 0) <= counts.get(found, 0): + raise ValueError(f"Invalid LCOV totals: {name}") + files[name] = counts + name = None + if name is not None or not files: + raise ValueError("LCOV export empty or truncated") + return files + + +def summary(files, root): + def metric(counts, hit, found): + total, covered = counts.get(found, 0), counts.get(hit, 0) + return f"{covered}/{total} ({100 * covered / total:.2f}%)" if total else "0/0 (-)" + + def label(name): + try: + return str(Path(name).relative_to(root)) + except ValueError: + return name + + rows = [["File", *(name for name, _, _ in METRICS)]] + for name, counts in sorted(files.items()): + rows.append([label(name), *(metric(counts, hit, found) for _, hit, found in METRICS)]) + totals = {key: sum(counts.get(key, 0) for counts in files.values()) + for _, hit, found in METRICS for key in (hit, found)} + rows.append(["TOTAL", *(metric(totals, hit, found) for _, hit, found in METRICS)]) + widths = [max(len(row[i]) for row in rows) for i in range(4)] + return "\n".join(" ".join(value.ljust(widths[i]) if i == 0 else value.rjust(widths[i]) + for i, value in enumerate(row)).rstrip() for row in rows) + "\n" + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("lcov", type=Path) + parser.add_argument("--root", type=Path, default=Path.cwd()) + args = parser.parse_args() + print(summary(read_lcov(args.lcov.read_text(encoding="utf-8")), args.root), end="") + + +if __name__ == "__main__": + main() diff --git a/scripts/ci-reuse-merge-group.py b/scripts/ci-reuse-merge-group.py new file mode 100644 index 000000000..317995f0e --- /dev/null +++ b/scripts/ci-reuse-merge-group.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Reuse CI only after the merge queue passed this exact main commit. + +One bounded, workflow-scoped API read; any missing or unreadable evidence +falls back to running CI. PRs, nightlies and manual runs never reuse results. +""" + +import json +import os +import subprocess +from pathlib import Path +from urllib.parse import urlencode + + +def successful_run(payload, repository, sha): + for run in payload["workflow_runs"]: + if (run.get("event") == "merge_group" + and run.get("head_sha") == sha + and run.get("status") == "completed" + and run.get("conclusion") == "success" + and run.get("path") == ".github/workflows/ci.yml" + and run.get("head_repository", {}).get("full_name") == repository + and run.get("head_branch", "").startswith("gh-readonly-queue/main/")): + return run["id"] + return None + + +def reusable_run(env): + if env.get("GITHUB_EVENT_NAME") != "push" or env.get("GITHUB_REF") != "refs/heads/main": + return None + try: + repository, sha = env["GITHUB_REPOSITORY"], env["GITHUB_SHA"] + query = urlencode({"event": "merge_group", "head_sha": sha, "per_page": 100}) + endpoint = f"repos/{repository}/actions/workflows/ci.yml/runs?{query}" + result = subprocess.run(["gh", "api", endpoint], check=True, capture_output=True, + text=True, timeout=15) + return successful_run(json.loads(result.stdout), repository, sha) + except (OSError, subprocess.SubprocessError, ValueError, KeyError, TypeError, AttributeError) as error: + # Do not print response bodies or stderr, which may contain credentials. + print(f"::notice::Could not verify merge-queue CI ({type(error).__name__}); running all checks.") + return None + + +def main(): + run = reusable_run(os.environ) + reuse = run is not None + with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="utf-8") as output: + output.write(f"reuse={'true' if reuse else 'false'}\n") + if reuse: + print(f"Merge-queue CI run {run} passed this SHA; retaining cache builds and the full tier.") + else: + print("No verified merge-queue CI result for this push; running all checks.") + + +if __name__ == "__main__": + main() diff --git a/scripts/ci-test-durations.json b/scripts/ci-test-durations.json new file mode 100644 index 000000000..61e0ab69c --- /dev/null +++ b/scripts/ci-test-durations.json @@ -0,0 +1,253 @@ +{ + "source_run": 37951515328, + "source_jobs": [ + 113892063480, + 113892063546 + ], + "unit_seconds": 185.45, + "compile_seconds": 2.0, + "default_seconds": 1.0, + "targets": { + "api_retry_e2e": 2.01, + "api_timeout_e2e": 4.29, + "apply": 10.48, + "binary_fetch_error_classification_e2e": 0.01, + "blob_fetcher_edges_e2e": 2.06, + "child_deadline_guard": 0.25, + "cli": 7.69, + "cli_apply_silent": 0.1, + "cli_argv_non_utf8": 0.0, + "cli_config_fallback": 0.18, + "cli_get_silent": 0.06, + "cli_global_args": 0.36, + "cli_parse_apply": 0.05, + "cli_parse_get": 0.1, + "cli_parse_list": 0.44, + "cli_parse_main": 0.12, + "cli_parse_remove": 0.07, + "cli_parse_repair": 0.06, + "cli_parse_rollback": 0.03, + "cli_parse_scan": 0.4, + "cli_parse_vendor": 0.06, + "cli_parse_vex": 1.06, + "cli_path_flags_validated": 0.52, + "cli_remove_silent": 0.28, + "cli_scan_silent": 0.56, + "cli_sigpipe": 0.0, + "command_module_layering": 0.11, + "contract_gradle_codes": 0.93, + "coverage_fix_apply_silent_mute_exit": 4.05, + "coverage_fix_rollback_ecosystem_scoped_hosted": 0.09, + "coverage_fix_scan_hosted_dryrun_vendored": 1.61, + "coverage_fix_vendor_silent_mute_exit": 7.4, + "covgap_api_blob_fetcher": 4.04, + "covgap_commands_get": 1.81, + "covgap_commands_rollback": 1.86, + "covgap_commands_scan_hosted": 1.46, + "covgap_commands_scan_mod": 1.81, + "covgap_commands_vendor": 3.75, + "covgap_commands_vex": 0.2, + "covgap_crawlers_composer_crawler": 0.01, + "covgap_crawlers_npm_crawler": 0.04, + "covgap_patch_apply": 0.06, + "covgap_update_state": 0.01, + "covgap_utils_socket_cli_config": 0.08, + "crawl_fd_limit_e2e": 0.0, + "crawler_cargo_e2e": 0.06, + "crawler_composer_e2e": 0.11, + "crawler_deno_e2e": 0.04, + "crawler_go_e2e": 0.04, + "crawler_gradle_e2e": 0.17, + "crawler_maven_e2e": 0.07, + "crawler_monorepo_gaps": 0.02, + "crawler_npm_e2e": 1.2, + "crawler_nuget_e2e": 0.07, + "crawler_python_e2e": 1.29, + "crawler_ruby_e2e": 0.85, + "crawlers_empty_paths_e2e": 0.03, + "diff_created_file_e2e": 0.15, + "diff_e2e": 0.01, + "docker_e2e_cargo": 0.0, + "docker_e2e_composer": 0.0, + "docker_e2e_deno": 0.0, + "docker_e2e_gem": 0.0, + "docker_e2e_golang": 0.0, + "docker_e2e_maven": 0.0, + "docker_e2e_npm": 0.0, + "docker_e2e_nuget": 0.0, + "docker_e2e_pypi": 0.0, + "docker_e2e_sbt": 0.0, + "docker_e2e_vendor_composer": 0.0, + "docker_e2e_vendor_gem": 0.0, + "docker_e2e_vendor_maven": 0.0, + "docker_e2e_vendor_nuget": 0.0, + "docker_e2e_vendor_pypi_pm": 0.0, + "e2e_bun_lockb": 1.02, + "e2e_cargo": 0.12, + "e2e_composer": 0.11, + "e2e_composer_version_identity": 0.25, + "e2e_embedded_vex": 0.92, + "e2e_gem": 0.09, + "e2e_golang": 0.12, + "e2e_golang_build": 0.0, + "e2e_golang_hosted_build": 0.0, + "e2e_golang_hosted_state": 0.0, + "e2e_golang_workspace_build": 0.0, + "e2e_gradle_agent_build": 0.36, + "e2e_gradle_discovery_build": 0.46, + "e2e_hosted_production": 0.07, + "e2e_maven": 0.12, + "e2e_npm": 0.01, + "e2e_nuget": 0.12, + "e2e_nuget_dotnet_build": 0.0, + "e2e_pypi": 0.01, + "e2e_pypi_multi_copy": 0.18, + "e2e_redirect_bun_build": 0.04, + "e2e_redirect_cargo_build": 2.95, + "e2e_redirect_cargo_shapes": 7.46, + "e2e_redirect_composer_build": 0.0, + "e2e_redirect_gem_stale_install": 0.01, + "e2e_redirect_gradle_build": 0.28, + "e2e_redirect_maven_build": 0.0, + "e2e_redirect_npm_build": 0.01, + "e2e_redirect_pnpm_build": 0.15, + "e2e_redirect_rush_sim": 0.01, + "e2e_redirect_uv_build": 0.0, + "e2e_redirect_vlt_build": 0.05, + "e2e_redirect_yarn_berry_build": 23.13, + "e2e_redirect_yarn_classic_build": 1.04, + "e2e_safety_cargo_build": 0.13, + "e2e_safety_cow": 0.0, + "e2e_safety_internals": 0.01, + "e2e_safety_lock": 1.08, + "e2e_safety_pnpm": 0.01, + "e2e_safety_vlt": 0.04, + "e2e_safety_yarn_pnp": 0.34, + "e2e_sbt": 1.17, + "e2e_sbt_build": 0.02, + "e2e_sbt_hosted": 0.4, + "e2e_sbt_vendor": 2.83, + "e2e_sbt_vendor_build": 0.03, + "e2e_scala_cli_vendor": 0.13, + "e2e_scan": 0.01, + "e2e_socket_yml_policy": 3.98, + "e2e_vendor_bun_build": 0.03, + "e2e_vendor_cargo_build": 10.85, + "e2e_vendor_composer_build": 0.06, + "e2e_vendor_composer_crlf": 0.31, + "e2e_vendor_gem_build": 0.03, + "e2e_vendor_golang_build": 0.0, + "e2e_vendor_gradle_build": 0.32, + "e2e_vendor_jvm_build": 0.3, + "e2e_vendor_maven_build": 0.04, + "e2e_vendor_npm_build": 15.21, + "e2e_vendor_pnpm_build": 0.5, + "e2e_vendor_pypi_build": 0.0, + "e2e_vendor_vlt_build": 0.05, + "e2e_vendor_yarn_berry_build": 14.67, + "e2e_vendor_yarn_classic_build": 0.03, + "e2e_vendor_yarn_classic_dev_flow": 0.03, + "e2e_vendored_production": 0.07, + "e2e_vex": 1.94, + "e2e_vex_build": 0.0, + "e2e_vex_lockfile": 118.65, + "e2e_vex_redirect": 3.28, + "e2e_vex_vendor": 2.72, + "e2e_vlt": 0.76, + "e2e_yarn4_pnpm_linker_build": 13.72, + "e2e_yarn4_workspaces_build": 9.39, + "e2e_yarn_legacy_cachekey_refusal_build": 3.67, + "ecosystem_dispatch_e2e": 0.48, + "fuzzy_match_e2e": 0.0, + "get": 8.01, + "global_probe_spawn_e2e": 0.24, + "global_scope_project_state": 0.88, + "gradle_agent_cli": 2.27, + "help_text_hygiene": 0.05, + "hosted_inventory": 0.06, + "hosted_memory_engine": 1.47, + "hosted_memory_parity": 1.77, + "hosted_memory_rollout": 0.93, + "hosted_superseding_pypi": 0.26, + "in_process_agent_reapply": 0.51, + "in_process_alternate_installers": 0.19, + "in_process_cargo_apply": 5.85, + "in_process_edge_cases": 0.18, + "in_process_gem_apply": 0.02, + "in_process_gem_multi_platform": 0.65, + "in_process_get": 3.45, + "in_process_get_corrupt_manifest": 0.01, + "in_process_get_hosted_ecosystems": 1.26, + "in_process_get_manifest_path": 0.21, + "in_process_get_modes": 1.29, + "in_process_get_update_count": 0.04, + "in_process_get_uuid_fallback": 0.03, + "in_process_pypi_apply": 25.35, + "in_process_pypi_multi_release": 32.83, + "in_process_python_envs": 1.03, + "in_process_redirect": 11.08, + "in_process_redirect_pdm": 2.46, + "in_process_redirect_pipenv": 2.45, + "in_process_redirect_pnpm": 4.63, + "in_process_redirect_poetry": 1.97, + "in_process_remote_ecosystems_apply": 9.9, + "in_process_remove_repair_lifecycle": 0.8, + "in_process_rollback_all_ecosystems": 0.3, + "in_process_rollback_hosted": 4.88, + "in_process_rollback_vendored": 1.02, + "in_process_scan": 2.94, + "in_process_target_ambiguity": 0.14, + "in_process_vendor": 25.4, + "in_process_vendor_bun_takeover": 3.72, + "in_process_vendor_npm_v1_takeover": 0.57, + "in_process_vendor_pnpm_parent_child": 0.67, + "in_process_vendor_pnpm_takeover": 1.62, + "in_process_vendor_pypi_takeover": 0.34, + "json_error_shape": 0.21, + "maven_sidecar_cli": 0.26, + "mode_migration_bun": 0.04, + "mode_migration_cargo": 3.9, + "mode_migration_npm": 0.03, + "mode_migration_pypi": 6.14, + "mode_migration_vlt": 0.04, + "output_helpers_e2e": 0.0, + "package_e2e": 0.02, + "pnpm_hosted": 0.09, + "poetry_hosted": 0.11, + "policy_pypi_names": 0.26, + "proxy_batch_e2e": 0.47, + "redirect_golden": 0.68, + "redirect_sbt_golden": 0.35, + "remedy_commands_parse": 0.56, + "remove": 2.05, + "remove_rollback_api_overrides": 0.07, + "repair": 10.03, + "rollback": 4.66, + "rollback_new_file_e2e": 0.01, + "scan": 9.08, + "scan_api_retry_e2e": 2.42, + "scan_pnpm_relocated_store_cwd_e2e": 0.07, + "scan_requirements_lock_only": 2.68, + "scan_rollout_e2e": 6.6, + "scan_vendor_e2e": 3.11, + "scan_vendor_requirements_unwired": 0.33, + "self_update_e2e": 7.68, + "self_update_failures_e2e": 8.18, + "spawn_env_hygiene": 0.39, + "telemetry_helpers_e2e": 0.01, + "update": 16.45, + "upstream_restore_golden": 8.1, + "uv_hosted": 0.01, + "vendor": 7.77, + "vendor_crash_safety_e2e": 0.67, + "vendor_eject": 0.35, + "vendor_eject_bun_lockb": 2.2, + "vendor_eject_fresh_checkout": 0.63, + "vendor_group_commit_e2e": 10.18, + "vendor_jvm_cli": 6.72, + "vendor_ledger_schema_e2e": 3.13, + "vendor_partial_staging_e2e": 0.56, + "vex_terminal_output": 0.18, + "vlt_locks": 16.39 + } +} diff --git a/scripts/ci-test-shard.py b/scripts/ci-test-shard.py index 05336ce9b..28b7e02f0 100644 --- a/scripts/ci-test-shard.py +++ b/scripts/ci-test-shard.py @@ -10,6 +10,10 @@ share to balance its unit tests. Every target lands in exactly one shard, so the union of the shards is the old single `cargo test --workspace` run. +Windows debug CI sets CI_TEST_TIMINGS to the checked-in runtime sample. +Those shards balance measured test time plus linking work, with shard 1's +unit tests reserved up front. Other runners retain the count-based split. + Extra arguments after `SHARD COUNT` go to every `cargo test` invocation. Each invocation runs with `--no-fail-fast`; the exit status is non-zero if any of them failed. @@ -19,6 +23,8 @@ """ import json +import os +from pathlib import Path import subprocess import sys @@ -34,11 +40,23 @@ def integration_targets(metadata): for t in p["targets"] if "test" in t["kind"]}) -def partition(names, count): +def partition(names, count, timings=None): """`count` lists covering `names` exactly once, in order, with the first list weighted by FIRST_SHARD_WEIGHT.""" if count < 1: raise ValueError("count must be >= 1") + if timings is not None: + # Longest processing time first, reserving shard 1's unit-test cost. + # Link overhead keeps a pile of fast tests from becoming a slow build. + def cost(name): + return timings["compile_seconds"] + timings["targets"].get(name, timings["default_seconds"]) + shards = [[] for _ in range(count)] + load = [timings["unit_seconds"]] + [0.0] * (count - 1) + for name in sorted(names, key=lambda n: (-cost(n), n)): + i = min(range(count), key=lambda k: (load[k], k)) + shards[i].append(name) + load[i] += cost(name) + return [sorted(shard) for shard in shards] weights = [FIRST_SHARD_WEIGHT if count > 1 else 1.0] + [1.0] * (count - 1) shards = [[] for _ in range(count)] load = [0.0] * count @@ -50,12 +68,12 @@ def partition(names, count): return shards -def invocations(shard, count, names, extra=()): +def invocations(shard, count, names, extra=(), timings=None): """The `cargo test` argument lists shard `shard` (1-based) runs.""" if not 1 <= shard <= count: raise ValueError(f"shard {shard} not in 1..{count}") base = ["cargo", "test", "--workspace", "--no-fail-fast", *extra] - mine = partition(names, count)[shard - 1] + mine = partition(names, count, timings)[shard - 1] runs = [] if shard == 1: runs.append(base + ["--lib", "--bins"] + [a for n in mine for a in ("--test", n)]) @@ -74,10 +92,12 @@ def main(argv): ["cargo", "metadata", "--no-deps", "--format-version", "1"], check=True, capture_output=True, text=True).stdout) names = integration_targets(metadata) + timing_path = os.environ.get("CI_TEST_TIMINGS") + timings = json.loads(Path(timing_path).read_text(encoding="utf-8")) if timing_path else None print(f"ci-test-shard: shard {shard}/{count}: " - f"{len(partition(names, count)[shard - 1])} of {len(names)} integration targets", flush=True) + f"{len(partition(names, count, timings)[shard - 1])} of {len(names)} integration targets", flush=True) status = 0 - for args in invocations(shard, count, names, argv[2:]): + for args in invocations(shard, count, names, argv[2:], timings): print("+ " + " ".join(args), flush=True) if subprocess.run(args).returncode != 0: status = 1 diff --git a/scripts/tests/test_ci_e2e_groups.py b/scripts/tests/test_ci_e2e_groups.py new file mode 100644 index 000000000..a42f8d5ef --- /dev/null +++ b/scripts/tests/test_ci_e2e_groups.py @@ -0,0 +1,139 @@ +"""Grouped jobs must keep each member's tool pins, filters and failure verdict.""" + +import importlib.util +import json +import subprocess +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).parents[2] + + +def load(name): + spec = importlib.util.spec_from_file_location(name, ROOT / "scripts" / f"{name}.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +groups = load("ci-e2e-groups") +runner = load("ci-e2e-run") + + +class Groups(unittest.TestCase): + def test_regrouping_preserves_all_selections_and_is_idempotent(self): + text = (ROOT / ".github/workflows/ci.yml").read_text() + reader = groups.reader() + jobs = reader.jobs(text) + updated = groups.regroup(text) + updated_jobs = reader.jobs(updated) + for job in groups.FAMILIES: + key = lambda row: json.dumps(row, sort_keys=True) + self.assertCountEqual(list(map(key, reader.matrix_include(jobs[job]))), + list(map(key, reader.matrix_include(updated_jobs[job])))) + self.assertEqual(groups.regroup(updated), updated) + for job, limit in (("e2e", 60), ("e2e-macos", 6), ("e2e-windows", 6), ("e2e-full", 20)): + self.assertLessEqual(len(reader.matrix_include(jobs[job], expand=False)), limit) + + def test_conflicting_versions_and_long_gradle_legs_do_not_share_jobs(self): + rows = [ + {"os": "ubuntu-latest", "suite": "e2e_redirect_uv_build", "uv": version} + for version in ("0.1.45", "0.12.17") + ] + [{"os": "ubuntu-latest", "suite": "e2e_redirect_gradle_build", "gradle": "9.8.0"}] * 2 + self.assertEqual(len(groups.pack(rows)), 4) + + def test_all_bins_respect_their_member_setup_and_runtime_budget(self): + reader = groups.reader() + jobs = reader.jobs((ROOT / ".github/workflows/ci.yml").read_text()) + for job in groups.FAMILIES: + for row in reader.matrix_include(jobs[job], expand=False): + cases = runner.members(row) + if len(cases) == 1: + continue + self.assertLessEqual(sum(groups.estimate(c) for c in cases), groups.BUDGET_SECONDS) + for case in cases: + for key, value in groups.settings(case).items(): + self.assertEqual(row[key], value) + + +class Runner(unittest.TestCase): + def test_empty_groups_cannot_pass_without_running_anything(self): + for row in ({"cases": "[]"}, {"suite": ""}, {"cases": '[{"suite":" "}]'}): + with self.subTest(row=row), self.assertRaises(ValueError): + runner.members(row) + + def test_numeric_yaml_versions_become_process_environment_strings(self): + env = runner.environment({"dotnet": 8, "npm_required": 1}, "suite", {}, ROOT) + self.assertEqual(env["SOCKET_PATCH_DOTNET_E2E_VERSION"], "8") + self.assertEqual(env["SOCKET_PATCH_NPM_E2E_REQUIRED"], "1") + + def test_each_case_has_its_own_guards_versions_and_linker(self): + base = {"SOCKET_PATCH_BUN_E2E_REQUIRED": "1", "SOCKET_PATCH_VLT_E2E_STORE_LINKER": "hardlink", + "SOCKET_PATCH_VLT_E2E_JS": "/pinned/vlt.js", "SOCKET_PATCH_VLT_E2E_UPGRADE_JS": "/upgrade/vlt.js"} + plain = runner.environment({"suite": "e2e_redirect_npm_build", "npm_required": "1"}, + "e2e_redirect_npm_build", base, ROOT) + self.assertEqual(plain["SOCKET_PATCH_NPM_E2E_REQUIRED"], "1") + self.assertEqual(plain["SOCKET_PATCH_BUN_E2E_REQUIRED"], "") + self.assertNotIn("SOCKET_PATCH_VLT_E2E_STORE_LINKER", plain) + self.assertNotIn("SOCKET_PATCH_VLT_E2E_UPGRADE_JS", plain) + self.assertEqual(base["SOCKET_PATCH_VLT_E2E_STORE_LINKER"], "hardlink") + pinned = runner.environment({"vlt": "1.2.0", "vlt_store_linker": "hardlink"}, "e2e_safety_vlt", base, ROOT) + self.assertEqual(pinned["SOCKET_PATCH_VLT_E2E_REQUIRED"], "1") + self.assertEqual(pinned["SOCKET_PATCH_VLT_E2E_VERSION"], "1.2.0") + self.assertEqual(pinned["SOCKET_PATCH_VLT_E2E_STORE_LINKER"], "hardlink") + self.assertEqual(pinned["SOCKET_PATCH_VLT_E2E_JS"], "/pinned/vlt.js") + + def test_maven_guard_follows_the_case_and_bun_lockb_only_its_suite(self): + cases = [({"jvm_tool": "maven", "maven": "3.6.3"}, "1", "3.6.3"), + ({"jvm_tool": "gradle", "test_filter": "--ignored gradle_vendor_"}, "1", "3.9.16"), + ({"jvm_tool": "gradle", "test_filter": "--ignored gradle_multi_project"}, "", ""), + ({"dotnet": "8"}, "", "")] + for case, required, version in cases: + env = runner.environment(case, "test_suite", {}, ROOT) + self.assertEqual(env["SOCKET_PATCH_MAVEN_E2E_REQUIRED"], required) + self.assertEqual(env["SOCKET_PATCH_MAVEN_E2E_VERSION"], version) + for suite in ("e2e_bun_lockb", "e2e_redirect_bun_build"): + env = runner.environment({"bun": "1.1.45"}, suite, {}, ROOT) + self.assertEqual(env["SOCKET_PATCH_BUN_LOCKB_VERSION"], "1.1.45" if suite == "e2e_bun_lockb" else "") + + def test_failure_empty_selection_and_vlt_proof_do_not_hide_later_cases(self): + cases = [{"suite": "broken", "test_filter": "--ignored first"}, + {"suite": "empty", "test_filter": "--exact missing"}, + {"suite": "e2e_vlt", "vlt": "1.2.0", "test_filter": "--include-ignored vlt_pinned_matrix"}, + {"suite": "last", "test_filter": "--ignored final"}] + calls = [] + + def execute(args, env, cwd, log): + calls.append(args) + name = Path(args[0]).stem + count = 0 if name == "empty" else 1 + log.write_text(f"test result: ok. {count} passed; 0 failed; 0 ignored; finished in 0.00s\n") + return 1 if name == "broken" else 0 + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "target").mkdir() + with patch.object(runner, "run_binary", side_effect=execute), patch.object( + runner.subprocess, "run", return_value=subprocess.CompletedProcess([], 1)) as checker: + self.assertEqual(runner.run_cases({"cases": json.dumps(cases)}, root, {}), 1) + self.assertEqual(len(calls), 4) + self.assertEqual(calls[-1][1:], ["--ignored", "final"]) + self.assertEqual(checker.call_count, 1) + self.assertIn("--binary", checker.call_args.args[0]) + self.assertIn("e2e_vlt", checker.call_args.args[0]) + + def test_a_missing_binary_fails_but_other_members_still_run(self): + cases = {"cases": json.dumps([{"suite": "missing"}, {"suite": "present"}])} + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "target").mkdir() + (root / "target/e2e-1-present.log").write_text("test result: ok. 1 passed; 0 failed;\n") + with patch.object(runner, "run_binary", side_effect=[FileNotFoundError(), 0]) as execute: + self.assertEqual(runner.run_cases(cases, root, {}), 1) + self.assertEqual(execute.call_count, 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_ci_e2e_tiers.py b/scripts/tests/test_ci_e2e_tiers.py index c995384c0..0183e7597 100644 --- a/scripts/tests/test_ci_e2e_tiers.py +++ b/scripts/tests/test_ci_e2e_tiers.py @@ -126,14 +126,17 @@ def test_bundle_reads_cargo_json(self): # HostedShards checks every hosted test runs in exactly one of them). HOSTED_SHARD_1 = ["gradle_hosted_3", "gradle_hosted_4", "gradle_hosted_5"] HOSTED_SHARD_2 = ["gradle_hosted_" + c for c in "bcdeflmnop"] +VENDOR_HOSTED = ["gradle_hosted_config_cache_second_row", "gradle_hosted_fallback_snippet_compiles_kotlin", + "gradle_hosted_vendored_takeover_and_eject"] AGENT_HOSTED = ( ("e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build", " ".join(["--ignored", "gradle_agent_", *HOSTED_SHARD_1])), - ("e2e_redirect_gradle_build", " ".join(["--ignored", *HOSTED_SHARD_2])), + ("e2e_redirect_gradle_build", " ".join(["--ignored", *HOSTED_SHARD_2] + [w for p in VENDOR_HOSTED[:2] for w in ("--skip", p)])), ("e2e_redirect_gradle_build", - " ".join(["--ignored", "gradle_hosted_"] + [w for p in HOSTED_SHARD_1 + HOSTED_SHARD_2 for w in ("--skip", p)])), + " ".join(["--ignored", "gradle_hosted_"] + [w for p in HOSTED_SHARD_1 + HOSTED_SHARD_2 + VENDOR_HOSTED for w in ("--skip", p)])), ) -VENDOR = ("e2e_vendor_gradle_build e2e_vendor_jvm_build", "--ignored gradle_vendor_ gradle_multi_project") +VENDOR = ("e2e_vendor_gradle_build e2e_vendor_jvm_build e2e_redirect_gradle_build", + " ".join(["--ignored", "gradle_vendor_", "gradle_multi_project", *VENDOR_HOSTED])) def matrix_axes(job_lines): @@ -210,10 +213,8 @@ def test_steps_key_on_jvm_tool_and_maven_only_where_seeded(self): steps["Setup Java (JDK not on the runner image)"]) self.assertIn("if: steps.jvm.outputs.maven == 'true'", steps["Install Maven ${{ matrix.maven || '3.9.16' }}"]) run = steps["Run e2e tests"] - self.assertIn("SOCKET_PATCH_MAVEN_E2E_REQUIRED: ${{ steps.jvm.outputs.maven == 'true' && '1' || '' }}", run) - self.assertIn("SOCKET_PATCH_MAVEN_E2E_VERSION: ${{ steps.jvm.outputs.maven == 'true' && " - "(matrix.maven || '3.9.16') || '' }}", run) - self.assertNotIn("matrix.gradle != '') && '1'", run) + self.assertIn("scripts/ci-e2e-run.py", run) + self.assertIn("E2E_ROW_JSON: ${{ toJSON(matrix) }}", run) def test_maven_seeding_follows_the_filter(self): def needs_maven(row): @@ -225,7 +226,7 @@ def needs_maven(row): gradle = [r for r in rows("e2e") if r.get("jvm_tool") == "gradle"] self.assertEqual(sum(needs_maven(r) for r in gradle), 5, "the vendor legs + the windows multi-project leg") for row in gradle: - self.assertEqual(needs_maven(row), "gradle_hosted_" not in row["test_filter"], row) + self.assertEqual(needs_maven(row), "gradle_vendor_" in row["test_filter"] or "gradle_multi_project" in row["test_filter"], row) def test_compat_grid_expands_to_36_cells_plus_extras(self): compat = rows_mod.jobs(GRADLE_COMPAT.read_text(encoding="utf-8")) @@ -251,6 +252,28 @@ def test_compat_grid_expands_to_36_cells_plus_extras(self): self.assertEqual(set(GRADLE_LINES), {r["gradle"] for r in rows("e2e") if r.get("jvm_tool") == "gradle"}, "both tiers run the same Gradle lines") + def test_compat_pr_keeps_windows_boundaries_and_all_agent_vendor_cells(self): + compat = rows_mod.jobs(GRADLE_COMPAT.read_text(encoding="utf-8")) + cells = expand(compat["cells"]) + exclude_block = "\n".join(compat["cells"]).split(" exclude:", 1)[1] + excludes = rows_mod.matrix_include((" include:" + exclude_block).splitlines()) + for event in ("pull_request", "schedule", "workflow_dispatch"): + resolved = [] + for row in excludes: + row = dict(row) + match = re.fullmatch(r"\$\{\{ github.event_name == 'pull_request' && '([^']+)' \|\| '' \}\}", row["os"]) + self.assertIsNotNone(match, row) + row["os"] = match[1] if event == "pull_request" else "" + resolved.append(row) + remaining = [c for c in cells if not any(all(c[k] == v for k, v in r.items()) for r in resolved)] + with self.subTest(event=event): + if event == "pull_request": + want = {("windows-latest", g, m) for g in GRADLE_LINES for m in ("agent", "vendor")} + want |= {("windows-latest", g, "hosted") for g in ("6.9.4", "9.8.0")} + self.assertEqual({(c["os"], c["gradle"], c["mode"]) for c in remaining}, want) + else: + self.assertEqual(remaining, cells, "nightly and dispatch keep the full grid") + def test_compat_workflow_builds_its_own_binaries(self): text = GRADLE_COMPAT.read_text(encoding="utf-8") compat = rows_mod.jobs(text) diff --git a/scripts/tests/test_ci_gradle_prefixes.py b/scripts/tests/test_ci_gradle_prefixes.py index b4dc6e45f..fd1f7e869 100644 --- a/scripts/tests/test_ci_gradle_prefixes.py +++ b/scripts/tests/test_ci_gradle_prefixes.py @@ -217,7 +217,7 @@ def test_every_hosted_test_runs_in_exactly_one_leg_per_line(self): def test_catch_all_skips_exactly_the_other_legs_words(self): for line, filters in self.rows_by_line().items(): with self.subTest(gradle=line): - catch_all = [f for f in filters if "--skip" in f] + catch_all = [f for f in filters if "gradle_hosted_" in f] self.assertEqual(len(catch_all), 1) skips = {w for a, w in zip(catch_all[0], catch_all[0][1:]) if a == "--skip"} named = {w for f in filters if f is not catch_all[0] diff --git a/scripts/tests/test_ci_lcov_summary.py b/scripts/tests/test_ci_lcov_summary.py new file mode 100644 index 000000000..041885bbd --- /dev/null +++ b/scripts/tests/test_ci_lcov_summary.py @@ -0,0 +1,52 @@ +"""The cheap coverage summary counts the exported data, including empty metrics.""" + +import importlib.util +import unittest +from pathlib import Path + +ROOT = Path(__file__).parents[2] +spec = importlib.util.spec_from_file_location("lcov_summary", ROOT / "scripts/ci-lcov-summary.py") +lcov = importlib.util.module_from_spec(spec) +spec.loader.exec_module(lcov) + + +class Summary(unittest.TestCase): + sample = """TN: +SF:/work/src/one.rs +FN:2,one +FNDA:1,one +FNF:2 +FNH:1 +DA:2,1 +DA:3,0 +LF:2 +LH:1 +BRF:2 +BRH:1 +end_of_record +SF:/work/src/two.rs +LF:10 +LH:9 +FNF:1 +FNH:1 +end_of_record +""" + + def test_totals_are_weighted_by_counts_not_file_percentages(self): + text = lcov.summary(lcov.read_lcov(self.sample), Path("/work")) + self.assertIn("src/one.rs", text) + total = text.splitlines()[-1] + self.assertIn("10/12 (83.33%)", total) + self.assertIn("2/3 (66.67%)", total) + self.assertIn("1/2 (50.00%)", total) + self.assertIn("0/0 (-)", text) + + def test_truncated_duplicate_empty_and_invalid_exports_fail(self): + for text in ("", "SF:file.rs\nLF:1\nLH:1\n", self.sample + self.sample, + "SF:file.rs\nend_of_record\n", self.sample.replace("LH:1\n", "LH:3\n")): + with self.subTest(text=text), self.assertRaises(ValueError): + lcov.read_lcov(text) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_ci_reuse_merge_group.py b/scripts/tests/test_ci_reuse_merge_group.py new file mode 100644 index 000000000..ffa058a77 --- /dev/null +++ b/scripts/tests/test_ci_reuse_merge_group.py @@ -0,0 +1,75 @@ +"""A reused CI verdict must belong to this workflow, repository and exact SHA.""" + +import importlib.util +import json +import subprocess +import unittest +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).parents[2] +spec = importlib.util.spec_from_file_location("reuse", ROOT / "scripts/ci-reuse-merge-group.py") +reuse = importlib.util.module_from_spec(spec) +spec.loader.exec_module(reuse) + + +class Reuse(unittest.TestCase): + env = {"GITHUB_EVENT_NAME": "push", "GITHUB_REF": "refs/heads/main", + "GITHUB_REPOSITORY": "SocketDev/socket-patch", "GITHUB_SHA": "a" * 40} + queue_run = {"id": 123, "event": "merge_group", "head_sha": "a" * 40, + "status": "completed", "conclusion": "success", "path": ".github/workflows/ci.yml", + "head_repository": {"full_name": "SocketDev/socket-patch"}, + "head_branch": "gh-readonly-queue/main/pr-1-abc"} + + def result(self, runs): + return subprocess.CompletedProcess([], 0, stdout=json.dumps({"workflow_runs": runs})) + + def test_only_the_same_successful_merge_queue_workflow_is_reused(self): + with patch.object(reuse.subprocess, "run", return_value=self.result([self.queue_run])) as call: + self.assertEqual(reuse.reusable_run(self.env), 123) + endpoint = call.call_args.args[0][-1] + self.assertIn("/actions/workflows/ci.yml/runs?", endpoint) + self.assertIn("event=merge_group", endpoint) + self.assertIn("head_sha=" + self.env["GITHUB_SHA"], endpoint) + self.assertLessEqual(call.call_args.kwargs["timeout"], 15) + + def test_unrelated_or_incomplete_results_cannot_skip_tests(self): + mismatches = [ + {"head_sha": "b" * 40}, {"event": "pull_request"}, + {"path": ".github/workflows/other.yml"}, {"status": "in_progress"}, + {"conclusion": "failure"}, {"conclusion": "cancelled"}, + {"conclusion": "skipped"}, {"conclusion": None}, + {"head_repository": {"full_name": "fork/socket-patch"}}, + {"head_branch": "gh-readonly-queue/release/pr-1-abc"}, + ] + for change in mismatches: + with self.subTest(change=change), patch.object( + reuse.subprocess, "run", return_value=self.result([dict(self.queue_run, **change)])): + self.assertIsNone(reuse.reusable_run(self.env)) + + def test_direct_push_with_no_queue_run_keeps_all_checks(self): + with patch.object(reuse.subprocess, "run", return_value=self.result([])): + self.assertIsNone(reuse.reusable_run(self.env)) + + def test_non_main_pushes_and_other_events_never_query_or_skip(self): + changes = [{"GITHUB_EVENT_NAME": e} for e in + ("pull_request", "merge_group", "schedule", "workflow_dispatch")] + changes.append({"GITHUB_REF": "refs/heads/feature"}) + for change in changes: + with self.subTest(change=change), patch.object(reuse.subprocess, "run") as call: + self.assertIsNone(reuse.reusable_run(dict(self.env, **change))) + call.assert_not_called() + + def test_api_failure_timeout_and_bad_payload_keep_all_checks(self): + for error in (FileNotFoundError(), subprocess.CalledProcessError(1, "gh"), + subprocess.TimeoutExpired("gh", 15)): + with self.subTest(error=error), patch.object(reuse.subprocess, "run", side_effect=error): + self.assertIsNone(reuse.reusable_run(self.env)) + for text in ("not json", "{}", '{"workflow_runs": null}', '{"workflow_runs": [null]}'): + with self.subTest(text=text), patch.object(reuse.subprocess, "run", return_value= + subprocess.CompletedProcess([], 0, stdout=text)): + self.assertIsNone(reuse.reusable_run(self.env)) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_ci_scheduling.py b/scripts/tests/test_ci_scheduling.py index 1a7b83e67..f9599506d 100644 --- a/scripts/tests/test_ci_scheduling.py +++ b/scripts/tests/test_ci_scheduling.py @@ -75,7 +75,45 @@ def test_os_builds_use_the_same_artifact_contract(self): for family in ("e2e", "cargo-vex-matrix"): self.assertIn("pattern: e2e-bin-${{ matrix.os }}*", "\n".join(JOBS[family])) for job in ("e2e-build-macos", "e2e-macos", "cargo-vex-matrix-macos", "yarn-berry-e2e-macos"): - self.assertIn(" if: github.event_name != 'pull_request'", JOBS[job]) + self.assertTrue(any(line.startswith(" if: github.event_name != 'pull_request'") + for line in JOBS[job])) + + def test_reused_push_keeps_cache_writers_and_full_tier(self): + for job in ("clippy", "node-addon", "test", "test-release", "coverage", "e2e-build", + "e2e-build-windows", "e2e-build-macos", "cargo-old-toolchains", + "e2e-full", "cargo-vex-matrix-full", "yarn-berry-full"): + with self.subTest(job=job): + condition = next((line for line in JOBS[job] if line.startswith(" if:")), "") + self.assertNotIn("outputs.reuse", condition) + for job in ("docker-base", "yarn-classic-matrix", "yarn-berry-e2e", + "yarn-berry-e2e-macos", "hosted-e2e"): + self.assertIn("needs.clippy.outputs.reuse != 'true'", "\n".join(JOBS[job])) + for family in ("e2e", "cargo-vex-matrix"): + for suffix in ("", "-windows", "-macos"): + self.assertIn(f"needs.e2e-build{suffix}.outputs.reuse != 'true'", + "\n".join(JOBS[family + suffix])) + for job in ("test", "coverage", "cargo-old-toolchains"): + 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("--no-run", warm[0]) + self.assertIn("actions: read", "\n".join(JOBS["clippy"])) + self.assertIn("steps.merge-queue.outputs.reuse", "\n".join(JOBS["clippy"])) + self.assertNotIn("cargo build --workspace", "\n".join(JOBS["test"])) + addon = reader.step(JOBS["test"], "Build Node addon") + self.assertIn("if: matrix.shard == 1", addon) + self.assertIn("cargo build --locked -p socket-patch-node", addon) + + def test_gradle_boundaries_run_on_prs_and_middle_lines_gate_the_queue(self): + pr = [r for r in reader.matrix_include(JOBS["e2e"]) if r.get("gradle")] + middle = reader.matrix_include(JOBS["e2e-gradle-mid"]) + self.assertEqual({r["gradle"] for r in pr}, {"6.9.4", "9.8.0"}) + self.assertEqual({r["gradle"] for r in middle}, {"7.6.6", "8.14.3"}) + self.assertEqual(len(pr), 8) + self.assertEqual(len(middle), 8) + self.assertIn("e2e-gradle-mid", dependencies("ci-ok")) + self.assertIn(" if: github.event_name != 'pull_request' && needs.e2e-build.outputs.reuse != 'true'", + JOBS["e2e-gradle-mid"]) def test_row_reader_preserves_both_os_siblings(self): jobs = reader.jobs("""jobs: diff --git a/scripts/tests/test_ci_test_shard.py b/scripts/tests/test_ci_test_shard.py index bb2d446e0..aba66fbb6 100644 --- a/scripts/tests/test_ci_test_shard.py +++ b/scripts/tests/test_ci_test_shard.py @@ -88,6 +88,20 @@ def test_integration_targets_reads_workspace_test_kinds(self): } self.assertEqual(shard.integration_targets(metadata), ["e2e_x", "zz"]) + def test_timed_shards_cover_new_targets_and_balance_the_recorded_work(self): + timings = json.loads((ROOT / "scripts/ci-test-durations.json").read_text()) + names = sorted(timings["targets"]) + ["a_new_test_target"] + partitions = shard.partition(names, 2, timings) + self.assertCountEqual([name for part in partitions for name in part], names) + loads = [timings["unit_seconds"], 0.0] + for i, part in enumerate(partitions): + loads[i] += sum(timings["compile_seconds"] + timings["targets"].get(n, timings["default_seconds"]) + for n in part) + self.assertLess(abs(loads[0] - loads[1]), 5) + actual = [args for k in (1, 2) for args in shard.invocations(k, 2, names, timings=timings)] + self.assertCountEqual(selected(actual), names) + self.assertEqual(sum("--doc" in args for args in actual), 1) + def test_the_checkout_has_integration_targets(self): try: out = subprocess.run(["cargo", "metadata", "--no-deps", "--format-version", "1"], @@ -140,7 +154,7 @@ def test_three_release_shards_run_the_same_tests_as_cargo_workspace(self): ' let n = std::hint::black_box(u8::MAX);\n' ' assert_eq!(n + 1, 0);\n}\n', "one/src/main.rs": 'fn main() {}\n#[test] fn binary_unit() {}\n', - "one/tests/shared.rs": '#[test] fn first_shared() {}\n' + "one/tests/shared.rs": '#[test] fn first_shared() { assert!(std::path::Path::new(env!("CARGO_BIN_EXE_one")).is_file()); }\n' '#[test] #[ignore] fn ignored_case() {}\n', "one/tests/tail.rs": '#[test] fn tail_case() {}\n', "two/Cargo.toml": '[package]\nname="two"\nversion="0.1.0"\nedition="2021"\n' @@ -152,7 +166,7 @@ def test_three_release_shards_run_the_same_tests_as_cargo_workspace(self): path = root / name path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") - env = dict(os.environ, CARGO_TARGET_DIR=str(root / "target")) + env = dict(os.environ, CARGO_TARGET_DIR=str(root / "target"), CARGO_PROFILE_DEV_DEBUG="line-tables-only") def run(args): result = subprocess.run(args, cwd=root, env=env, capture_output=True, text=True) diff --git a/scripts/tests/test_ci_vlt_rows.py b/scripts/tests/test_ci_vlt_rows.py index 62d325099..aab0daa72 100644 --- a/scripts/tests/test_ci_vlt_rows.py +++ b/scripts/tests/test_ci_vlt_rows.py @@ -4,6 +4,7 @@ subset they use (no PyYAML on the runners).""" import importlib.util +import json import re import unittest from pathlib import Path @@ -48,7 +49,7 @@ def indent(line): def scalar(text): text = text.strip() if len(text) >= 2 and text[0] == text[-1] and text[0] in "'\"": - return text[1:-1] + return json.loads(text) if text[0] == '"' else text[1:-1].replace("''", "'") return text @@ -99,7 +100,7 @@ def jobs(text): return found -def matrix_include(job_lines): +def matrix_include(job_lines, expand=True): """The `strategy.matrix.include` rows of a job (flow or block style).""" lines = [strip_comment(l) for l in job_lines] at = next(i for i, l in enumerate(lines) if l.strip() == "include:") @@ -123,13 +124,16 @@ def matrix_include(job_lines): elif current is not None and indent(line) > item_indent: key, _, value = stripped.partition(":") current[key.strip()] = scalar(value) + if expand: + return [{k: v for k, v in case.items() if k != "ci_group"} + for row in rows for case in (json.loads(row["cases"]) if row.get("cases") else [row])] return rows def job_rows(jobs_by_id, job): """All rows of a job family, including its independent OS siblings.""" rows = matrix_include(jobs_by_id[job]) - for os_name in ("windows", "macos"): + for os_name in ("windows", "macos", "gradle-mid"): sibling = f"{job}-{os_name}" if sibling in jobs_by_id: rows += matrix_include(jobs_by_id[sibling]) @@ -221,15 +225,13 @@ def test_the_steps_install_vlt_and_check_the_legs(self): self.assertIn(exported, setup) node = step(self.ci["e2e"], "Setup Node.js 24 (vlt legs)") self.assertIn("node-version: '24.21.0'", node) - run = step(self.ci["e2e"], "Run vlt e2e tests") - self.assertIn("if: matrix.vlt != ''", run) - self.assertIn("SOCKET_PATCH_VLT_E2E_REQUIRED: ${{ matrix.vlt != '' && '1' || '' }}", run) - self.assertIn("scripts/check-vlt-legs.py", run) - self.assertIn('--binary "$VLT_SUITE"', run, - "a directly run binary prints no cargo `Running` line to name it") - self.assertIn("vlt-leg-manifest.json", run) - other = step(self.ci["e2e"], "Run e2e tests") - self.assertIn("if: matrix.vlt == ''", other) + run = step(self.ci["e2e"], "Run e2e tests") + self.assertIn("E2E_ROW_JSON: ${{ toJSON(matrix) }}", run) + self.assertIn("scripts/ci-e2e-run.py", run) + runner = (ROOT / "scripts/ci-e2e-run.py").read_text() + self.assertIn("scripts/check-vlt-legs.py", runner) + self.assertIn('"--binary", suite', runner) + self.assertIn("vlt-leg-manifest.json", runner) def test_hosted_e2e_proves_vlt_against_production(self): hosted = self.ci["hosted-e2e"] diff --git a/tests/docker/Dockerfile.sbt b/tests/docker/Dockerfile.sbt index 247a5f16e..99509ea52 100644 --- a/tests/docker/Dockerfile.sbt +++ b/tests/docker/Dockerfile.sbt @@ -98,6 +98,8 @@ RUN set -eu \ # the image's Central cache from memory, which more lines would bloat past # its 2g container. ARG SBT_WARM_VERSIONS="0.13.18 1.2.8 1.13.0 2.0.9" +# CI's blocking slice uses only two sbt lines; nightly/compat keep all tools. +ARG SBT_WARM_TOOLS=1 RUN set -eu \ && for v in ${SBT_WARM_VERSIONS}; do \ d="/tmp/warm-$v"; mkdir -p "$d/project"; \ @@ -110,7 +112,8 @@ RUN set -eu \ sbt -batch -no-colors -Dsbt.server.autostart=false $extra update); \ rm -rf "$d"; \ done \ - && d=/tmp/warm-mill && mkdir -p "$d/foo/src" && cd "$d" \ + && if [ "$SBT_WARM_TOOLS" = 1 ]; then \ + d=/tmp/warm-mill && mkdir -p "$d/foo/src" && cd "$d" \ && printf '%s\n' '//| mill-version: '"${MILL_1_VERSION}" 'package build' 'import mill.*, scalalib.*' \ 'object foo extends ScalaModule {' ' def scalaVersion = "2.13.16"' \ ' def mvnDeps = Seq(mvn"org.apache.commons:commons-lang3:3.11")' '}' > build.mill \ @@ -120,4 +123,5 @@ RUN set -eu \ && printf '%s\n' '//> using scala 3.3.6' '//> using dep org.apache.commons:commons-lang3:3.11' > project.scala \ && echo '@main def m() = println("warm")' > Main.scala \ && scala-cli compile . --server=false > /dev/null \ - && cd / && rm -rf "$d" + && cd / && rm -rf "$d"; \ + fi From d4efcbd7332d65ece25caf5f3899a7c7b1a1da89 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Fri, 9 Oct 2026 14:09:53 -0400 Subject: [PATCH 2/7] ci: pass each vlt row environment to its manifest checker --- scripts/ci-e2e-run.py | 3 ++- scripts/tests/test_ci_e2e_groups.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/ci-e2e-run.py b/scripts/ci-e2e-run.py index c84df6be7..f193cec45 100644 --- a/scripts/ci-e2e-run.py +++ b/scripts/ci-e2e-run.py @@ -96,7 +96,8 @@ def run_cases(row, root=ROOT, base=None): if case.get("vlt"): checker = subprocess.run([sys.executable, str(root / "scripts/check-vlt-legs.py"), "--binary", suite, "--manifest", - str(cli / "tests/vlt-leg-manifest.json"), str(log)], cwd=root) + str(cli / "tests/vlt-leg-manifest.json"), str(log)], + cwd=root, env=env) failed |= checker.returncode != 0 if failed: print(f"::error::{suite} failed (row {index}, filters: {' '.join(filters)}).") diff --git a/scripts/tests/test_ci_e2e_groups.py b/scripts/tests/test_ci_e2e_groups.py index a42f8d5ef..339342c8f 100644 --- a/scripts/tests/test_ci_e2e_groups.py +++ b/scripts/tests/test_ci_e2e_groups.py @@ -101,7 +101,8 @@ def test_maven_guard_follows_the_case_and_bun_lockb_only_its_suite(self): def test_failure_empty_selection_and_vlt_proof_do_not_hide_later_cases(self): cases = [{"suite": "broken", "test_filter": "--ignored first"}, {"suite": "empty", "test_filter": "--exact missing"}, - {"suite": "e2e_vlt", "vlt": "1.2.0", "test_filter": "--include-ignored vlt_pinned_matrix"}, + {"suite": "e2e_safety_vlt", "vlt": "1.2.0", "vlt_store_linker": "hardlink", + "test_filter": "--include-ignored vlt_pinned_matrix"}, {"suite": "last", "test_filter": "--ignored final"}] calls = [] @@ -122,7 +123,9 @@ def execute(args, env, cwd, log): self.assertEqual(calls[-1][1:], ["--ignored", "final"]) self.assertEqual(checker.call_count, 1) self.assertIn("--binary", checker.call_args.args[0]) - self.assertIn("e2e_vlt", checker.call_args.args[0]) + self.assertIn("e2e_safety_vlt", checker.call_args.args[0]) + self.assertEqual(checker.call_args.kwargs["env"]["SOCKET_PATCH_VLT_E2E_STORE_LINKER"], + "hardlink") def test_a_missing_binary_fails_but_other_members_still_run(self): cases = {"cases": json.dumps([{"suite": "missing"}, {"suite": "present"}])} From 7164d6b2308b91f5d11a011c4b358cb8bb71feae Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Fri, 9 Oct 2026 14:13:16 -0400 Subject: [PATCH 3/7] ci: release cancelled PR runs without waiting for ci-ok --- .github/workflows/ci.yml | 5 ++++- scripts/tests/test_ci_scheduling.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a8f8fb2a..4a617aba6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2225,7 +2225,10 @@ jobs: # 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() + # A superseded PR must release its concurrency slot without waiting for + # an aggregate runner. Keep an unconditional verdict in the merge queue + # and on main: a cancelled dependency there must never pass the gate. + if: ${{ always() && (github.event_name != 'pull_request' || !cancelled()) }} needs: [clippy, node-addon, lint-ecosystems, release-readiness, test, test-release, coverage, docker-base, coverage-docker, coverage-merge, dispatch-tests, e2e-build, e2e-build-windows, e2e-build-macos, e2e, e2e-gradle-mid, e2e-windows, e2e-full, e2e-docker, yarn-classic-matrix, yarn-berry-e2e, yarn-berry-full, cargo-vex-matrix, cargo-vex-matrix-windows, cargo-vex-matrix-full, cargo-old-toolchains, hosted-e2e, e2e-macos, yarn-berry-e2e-macos, cargo-vex-matrix-macos] runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/scripts/tests/test_ci_scheduling.py b/scripts/tests/test_ci_scheduling.py index f9599506d..0511acd02 100644 --- a/scripts/tests/test_ci_scheduling.py +++ b/scripts/tests/test_ci_scheduling.py @@ -34,7 +34,10 @@ def test_required_verdict_includes_every_job(self): # A successful aggregate must never hide a failed OS builder/consumer # introduced when a matrix is split into independently scheduled jobs. self.assertEqual(dependencies("ci-ok"), set(JOBS) - {"ci-ok"}) - self.assertIn(" if: always()", JOBS["ci-ok"]) + # Superseded PRs release their workflow concurrency slot. Main and + # merge_group still report a failed verdict for cancelled dependencies. + self.assertIn(" if: ${{ always() && (github.event_name != 'pull_request' || !cancelled()) }}", + JOBS["ci-ok"]) self.assertIn('if v["result"] not in ("success", "skipped")', "\n".join(JOBS["ci-ok"])) for job in JOBS: ancestors(job) # All dependencies exist and the graph is acyclic. From bb74536e284085b0559feaef74d9e601702e343b Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Fri, 9 Oct 2026 14:20:24 -0400 Subject: [PATCH 4/7] test: preserve required CI gate contracts after cancellation fix --- scripts/tests/test_ci_test_shard.py | 2 +- scripts/tests/test_merge_queue_fail_fast.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/tests/test_ci_test_shard.py b/scripts/tests/test_ci_test_shard.py index aba66fbb6..8865cecce 100644 --- a/scripts/tests/test_ci_test_shard.py +++ b/scripts/tests/test_ci_test_shard.py @@ -131,7 +131,7 @@ def test_all_release_shards_are_required_and_use_the_matrix_size(self): verdict = workflow.split("\n ci-ok:\n")[1] needs = re.search(r"^ needs: \[(.*)\]$", verdict, re.M)[1].split(", ") self.assertIn("test-release", needs) - self.assertIn("if: always()", verdict) + self.assertIn("if: ${{ always() && (github.event_name != 'pull_request' || !cancelled()) }}", verdict) self.assertIn('if v["result"] not in ("success", "skipped")', verdict) diff --git a/scripts/tests/test_merge_queue_fail_fast.py b/scripts/tests/test_merge_queue_fail_fast.py index afc5590e6..d6d807e45 100644 --- a/scripts/tests/test_merge_queue_fail_fast.py +++ b/scripts/tests/test_merge_queue_fail_fast.py @@ -47,9 +47,11 @@ def test_no_job_tolerates_failure(self): # job, and this script would then cancel a run that could still land. self.assertIsNone(re.search(r"^\s*continue-on-error:", self.text, re.M)) - def test_gate_runs_on_a_cancelled_run(self): + def test_gate_runs_on_a_cancelled_merge_group_run(self): gate = self.text[self.text.index("\n ci-ok:"):] - self.assertRegex(gate, r"\n if: always\(\)\n") + # Only PR cancellations can skip the verdict. The merge-queue + # watcher must still turn failed/cancelled dependencies into failure. + self.assertIn("\n if: ${{ always() && (github.event_name != 'pull_request' || !cancelled()) }}\n", gate) if __name__ == "__main__": From c0438a6a0394995b8c4bf2403113720e59600ac3 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Fri, 9 Oct 2026 14:59:34 -0400 Subject: [PATCH 5/7] ci: build platform addons in parallel with test shards --- .github/workflows/ci.yml | 27 ++++++++++++++++----------- scripts/tests/test_ci_scheduling.py | 18 +++++++++++++++--- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a617aba6..e0765be67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,12 +105,20 @@ jobs: # linking hundreds of executables or waiting for the E2E fan-out. 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. + # Cargo's tests do not link the addon's test=false cdylib. Build it once + # per OS alongside the test shards; serializing it ahead of shard 1 left + # that shard waiting on a separate production-profile compilation. + # Linux also exercises the JS loader and engine/provider boundary. node-addon: if: github.event.pull_request.draft != true needs: clippy - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + exclude: + - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} + runs-on: ${{ matrix.os }} timeout-minutes: 30 steps: - name: Checkout @@ -124,7 +132,10 @@ jobs: - name: Cache cargo uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: - shared-key: dev-ubuntu-latest + # The addon uses production features; the test shards enable dev + # features. Keep both cache writers instead of racing to save a + # partial dependency set under one key. + shared-key: addon-${{ matrix.os }} save-if: ${{ github.ref == 'refs/heads/main' }} - name: Setup Node.js @@ -138,7 +149,7 @@ jobs: run: node crates/socket-patch-node/npm/scripts/build-addon.mjs - name: Smoke-test addon - if: needs.clippy.outputs.reuse != 'true' + if: matrix.os == 'ubuntu-latest' && needs.clippy.outputs.reuse != 'true' run: node --test crates/socket-patch-node/npm/test/smoke.mjs # Check the standalone installer, release scripts, and native installer @@ -320,12 +331,6 @@ jobs: shared-key: dev-${{ matrix.os }} save-if: ${{ github.ref == 'refs/heads/main' }} - # cargo test builds the CLI/bench executables itself. The addon is a - # cdylib with test=false, so retain its platform link check once per OS. - - name: Build Node addon - if: matrix.shard == 1 - run: cargo build --locked -p socket-patch-node - - name: Warm the test cache after merge-queue validation if: needs.clippy.outputs.reuse == 'true' run: cargo test --locked --workspace --no-run diff --git a/scripts/tests/test_ci_scheduling.py b/scripts/tests/test_ci_scheduling.py index 0511acd02..fc1d49aa9 100644 --- a/scripts/tests/test_ci_scheduling.py +++ b/scripts/tests/test_ci_scheduling.py @@ -103,9 +103,21 @@ def test_reused_push_keeps_cache_writers_and_full_tier(self): self.assertIn("actions: read", "\n".join(JOBS["clippy"])) self.assertIn("steps.merge-queue.outputs.reuse", "\n".join(JOBS["clippy"])) self.assertNotIn("cargo build --workspace", "\n".join(JOBS["test"])) - addon = reader.step(JOBS["test"], "Build Node addon") - self.assertIn("if: matrix.shard == 1", addon) - self.assertIn("cargo build --locked -p socket-patch-node", addon) + self.assertNotIn("socket-patch-node", "\n".join(JOBS["test"])) + + def test_addon_platform_links_run_in_parallel_with_test_shards(self): + addon = "\n".join(JOBS["node-addon"]) + self.assertEqual(dependencies("node-addon"), {"clippy"}) + self.assertEqual(dependencies("test"), {"clippy"}) + self.assertIn("os: [ubuntu-latest, macos-latest, windows-latest]", addon) + self.assertIn("github.event_name == 'pull_request' && 'macos-latest' || ''", addon) + self.assertIn("shared-key: addon-${{ matrix.os }}", addon) + build = reader.step(JOBS["node-addon"], "Build addon") + self.assertIn("SOCKET_PATCH_NODE_CARGO_PROFILE: dev", build) + self.assertIn("node crates/socket-patch-node/npm/scripts/build-addon.mjs", build) + self.assertNotIn("if:", build) + smoke = reader.step(JOBS["node-addon"], "Smoke-test addon") + self.assertIn("matrix.os == 'ubuntu-latest'", smoke) def test_gradle_boundaries_run_on_prs_and_middle_lines_gate_the_queue(self): pr = [r for r in reader.matrix_include(JOBS["e2e"]) if r.get("gradle")] From 662d711143514dbd5e2ff668a17d050e22a9467d Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Fri, 9 Oct 2026 15:10:45 -0400 Subject: [PATCH 6/7] ci: overlap independent Gradle agent suites --- .github/workflows/ci.yml | 11 ++-- scripts/ci-e2e-groups.py | 2 +- scripts/ci-e2e-run.py | 89 +++++++++++++++++------------ scripts/tests/test_ci_e2e_groups.py | 37 ++++++++++++ scripts/tests/test_ci_e2e_tiers.py | 4 +- 5 files changed, 101 insertions(+), 42 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0765be67..b6db59321 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1017,7 +1017,8 @@ jobs: # Short cases share setup and artifact downloads. `cases` preserves each # original suite/filter/toolchain row; ci-e2e-run.py isolates its environment - # and checks every result. Repack with scripts/ci-e2e-groups.py --write. + # and checks every result. The Gradle agent leg overlaps its three suites + # with isolated Gradle homes. Repack with scripts/ci-e2e-groups.py --write. e2e: name: e2e (${{ matrix.os }}, ${{ matrix.ci_group }}) if: needs.e2e-build.outputs.reuse != 'true' @@ -1071,11 +1072,11 @@ jobs: - {os: 'ubuntu-latest', suite: 'e2e_redirect_maven_build', jvm_tool: 'maven', maven: '3.9.3', ci_group: 'jvm-dotnet-3.9.3-42'} - {os: 'ubuntu-latest', suite: 'e2e_redirect_maven_build', jvm_tool: 'maven', maven: '3.9.4', ci_group: 'jvm-dotnet-3.9.4-43'} - {os: 'ubuntu-latest', suite: 'e2e_vendor_jvm_build', jvm_tool: 'maven', maven: '3.9.2', test_filter: '--ignored maven_reactor', ci_group: 'jvm-dotnet-3.9.2-44'} - - {os: 'ubuntu-latest', suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5', ci_group: '6.9.4-6.9.4-45'} + - {os: 'ubuntu-latest', suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', parallel_suites: 'true', jvm_tool: 'gradle', gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5', ci_group: '6.9.4-6.9.4-45'} - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin', ci_group: '6.9.4-6.9.4-46'} - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin --skip gradle_hosted_vendored_takeover_and_eject', ci_group: '6.9.4-6.9.4-47'} - {os: 'ubuntu-latest', suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_vendor_ gradle_multi_project gradle_hosted_config_cache_second_row gradle_hosted_fallback_snippet_compiles_kotlin gradle_hosted_vendored_takeover_and_eject', ci_group: '6.9.4-6.9.4-48'} - - {os: 'ubuntu-latest', suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5', ci_group: '9.8.0-9.8.0-49'} + - {os: 'ubuntu-latest', suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', parallel_suites: 'true', jvm_tool: 'gradle', gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5', ci_group: '9.8.0-9.8.0-49'} - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin', ci_group: '9.8.0-9.8.0-50'} - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin --skip gradle_hosted_vendored_takeover_and_eject', ci_group: '9.8.0-9.8.0-51'} - {os: 'ubuntu-latest', suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_vendor_ gradle_multi_project gradle_hosted_config_cache_second_row gradle_hosted_fallback_snippet_compiles_kotlin gradle_hosted_vendored_takeover_and_eject', ci_group: '9.8.0-9.8.0-52'} @@ -1465,11 +1466,11 @@ jobs: fail-fast: false matrix: include: - - {os: 'ubuntu-latest', suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5', ci_group: '7.6.6-7.6.6-1'} + - {os: 'ubuntu-latest', suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', parallel_suites: 'true', jvm_tool: 'gradle', gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5', ci_group: '7.6.6-7.6.6-1'} - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin', ci_group: '7.6.6-7.6.6-2'} - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin --skip gradle_hosted_vendored_takeover_and_eject', ci_group: '7.6.6-7.6.6-3'} - {os: 'ubuntu-latest', suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_vendor_ gradle_multi_project gradle_hosted_config_cache_second_row gradle_hosted_fallback_snippet_compiles_kotlin gradle_hosted_vendored_takeover_and_eject', ci_group: '7.6.6-7.6.6-4'} - - {os: 'ubuntu-latest', suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5', ci_group: '8.14.3-8.14.3-5'} + - {os: 'ubuntu-latest', suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', parallel_suites: 'true', jvm_tool: 'gradle', gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5', ci_group: '8.14.3-8.14.3-5'} - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin', ci_group: '8.14.3-8.14.3-6'} - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin --skip gradle_hosted_vendored_takeover_and_eject', ci_group: '8.14.3-8.14.3-7'} - {os: 'ubuntu-latest', suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_vendor_ gradle_multi_project gradle_hosted_config_cache_second_row gradle_hosted_fallback_snippet_compiles_kotlin gradle_hosted_vendored_takeover_and_eject', ci_group: '8.14.3-8.14.3-8'} diff --git a/scripts/ci-e2e-groups.py b/scripts/ci-e2e-groups.py index cd256ffbe..d4a6ad158 100644 --- a/scripts/ci-e2e-groups.py +++ b/scripts/ci-e2e-groups.py @@ -17,7 +17,7 @@ ROOT = Path(__file__).resolve().parents[1] WORKFLOW = ROOT / ".github/workflows/ci.yml" FAMILIES = ("e2e", "e2e-windows", "e2e-macos", "e2e-full", "e2e-gradle-mid") -CASE_KEYS = {"suite", "test_filter", "npm_required", "vlt_store_linker", "allow_empty"} +CASE_KEYS = {"suite", "test_filter", "npm_required", "vlt_store_linker", "allow_empty", "parallel_suites"} BUDGET_SECONDS = 360 ESTIMATES = { "e2e_safety_pnpm": 30, "e2e_redirect_npm_build": 90, "e2e_redirect_rush_sim": 30, diff --git a/scripts/ci-e2e-run.py b/scripts/ci-e2e-run.py index f193cec45..f222e2f08 100644 --- a/scripts/ci-e2e-run.py +++ b/scripts/ci-e2e-run.py @@ -11,6 +11,7 @@ import shlex import subprocess import sys +from concurrent.futures import ThreadPoolExecutor from pathlib import Path ROOT = Path(__file__).resolve().parents[1] @@ -64,49 +65,67 @@ def run_binary(args, env, cwd, log): with subprocess.Popen(args, env=env, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace") as process: for line in process.stdout: - print(line, end="", flush=True) + # Parallel Gradle suites keep raw, separate logs while the + # Actions stream always identifies the emitting binary. + print(f"[{Path(args[0]).stem}] {line}", end="", flush=True) output.write(line) return process.wait() -def run_cases(row, root=ROOT, base=None): - base = os.environ if base is None else base +def run_suite(case, index, suite, root, base, grouped=True): cli = root / "crates/socket-patch-cli" suffix = ".exe" if sys.platform == "win32" else "" + if not re.fullmatch(r"[A-Za-z0-9_]+", suite): + raise ValueError(f"Invalid test suite: {suite}") + if case.get("allow_empty") == "true" and not any( + path.is_file() for path in (cli / f"tests/{suite}.rs", cli / f"tests/{suite}/main.rs")): + print(f"::notice::{suite} has not landed yet; skipped (allow_empty)") + return 0 + filters = shlex.split(case.get("test_filter") or "--ignored") + log = root / f"target/e2e-{index}-{suite}.log" + env = environment(case, suite, base, root) + marker = "::group::" if grouped else "Starting " + print(f"{marker}{suite} {case.get('test_filter', '--ignored')}", flush=True) + try: + failed = run_binary([str(root / f"target/e2e-bin/{suite}{suffix}"), *filters], + env, cli, log) != 0 + passed = re.search(r"^test result: .*? (\d+) passed;", log.read_text(encoding="utf-8"), re.M) + if not passed or int(passed[1]) == 0: + print(f"::error::{suite} ran no tests; check its filters.") + failed = True + if case.get("vlt"): + checker = subprocess.run([sys.executable, str(root / "scripts/check-vlt-legs.py"), + "--binary", suite, "--manifest", + str(cli / "tests/vlt-leg-manifest.json"), str(log)], + cwd=root, env=env) + failed |= checker.returncode != 0 + if failed: + print(f"::error::{suite} failed (row {index}, filters: {' '.join(filters)}).") + return int(failed) + except OSError as error: + print(f"::error::{suite} could not run: {error}") + return 1 + finally: + if grouped: + print("::endgroup::", flush=True) + + +def run_cases(row, root=ROOT, base=None): + base = os.environ if base is None else base status = 0 for index, case in enumerate(members(row)): - for suite in case["suite"].split(): - if not re.fullmatch(r"[A-Za-z0-9_]+", suite): - raise ValueError(f"Invalid test suite: {suite}") - if case.get("allow_empty") == "true" and not any( - path.is_file() for path in (cli / f"tests/{suite}.rs", cli / f"tests/{suite}/main.rs")): - print(f"::notice::{suite} has not landed yet; skipped (allow_empty)") - continue - filters = shlex.split(case.get("test_filter") or "--ignored") - log = root / f"target/e2e-{index}-{suite}.log" - env = environment(case, suite, base, root) - print(f"::group::{suite} {case.get('test_filter', '--ignored')}", flush=True) - try: - failed = run_binary([str(root / f"target/e2e-bin/{suite}{suffix}"), *filters], - env, cli, log) != 0 - passed = re.search(r"^test result: .*? (\d+) passed;", log.read_text(encoding="utf-8"), re.M) - if not passed or int(passed[1]) == 0: - print(f"::error::{suite} ran no tests; check its filters.") - failed = True - if case.get("vlt"): - checker = subprocess.run([sys.executable, str(root / "scripts/check-vlt-legs.py"), - "--binary", suite, "--manifest", - str(cli / "tests/vlt-leg-manifest.json"), str(log)], - cwd=root, env=env) - failed |= checker.returncode != 0 - if failed: - print(f"::error::{suite} failed (row {index}, filters: {' '.join(filters)}).") - status = 1 - except OSError as error: - print(f"::error::{suite} could not run: {error}") - status = 1 - finally: - print("::endgroup::", flush=True) + suites = case["suite"].split() + if case.get("parallel_suites") == "true": + # Opt in only the Gradle agent/discovery/hosted-3/4/5 leg. + # Cases in packed short-job bins remain sequential. + with ThreadPoolExecutor(max_workers=min(3, len(suites))) as pool: + futures = [pool.submit(run_suite, case, index, suite, root, base, False) + for suite in suites] + for future in futures: + status |= future.result() + else: + for suite in suites: + status |= run_suite(case, index, suite, root, base) return status diff --git a/scripts/tests/test_ci_e2e_groups.py b/scripts/tests/test_ci_e2e_groups.py index 339342c8f..4523eb4d1 100644 --- a/scripts/tests/test_ci_e2e_groups.py +++ b/scripts/tests/test_ci_e2e_groups.py @@ -4,6 +4,7 @@ import json import subprocess import tempfile +import threading import unittest from pathlib import Path from unittest.mock import patch @@ -57,6 +58,17 @@ def test_all_bins_respect_their_member_setup_and_runtime_budget(self): for key, value in groups.settings(case).items(): self.assertEqual(row[key], value) + def test_only_the_four_gradle_agent_legs_opt_into_parallel_suites(self): + reader = groups.reader() + jobs = reader.jobs((ROOT / ".github/workflows/ci.yml").read_text()) + parallel = [case for job in groups.FAMILIES for case in reader.matrix_include(jobs[job]) + if case.get("parallel_suites") == "true"] + self.assertEqual({case["gradle"] for case in parallel}, {"6.9.4", "7.6.6", "8.14.3", "9.8.0"}) + self.assertEqual(len(parallel), 4) + for case in parallel: + self.assertEqual(case["suite"].split(), ["e2e_gradle_discovery_build", "e2e_gradle_agent_build", + "e2e_redirect_gradle_build"]) + class Runner(unittest.TestCase): def test_empty_groups_cannot_pass_without_running_anything(self): @@ -137,6 +149,31 @@ def test_a_missing_binary_fails_but_other_members_still_run(self): self.assertEqual(runner.run_cases(cases, root, {}), 1) self.assertEqual(execute.call_count, 2) + def test_parallel_suites_overlap_and_preserve_all_failure_verdicts(self): + barrier = threading.Barrier(3, timeout=5) + logs = [] + + def execute(args, env, cwd, log): + # A serial implementation cannot reach this barrier three times. + barrier.wait() + name = Path(args[0]).stem + logs.append(log) + count = 0 if name == "empty" else 1 + log.write_text(f"test result: ok. {count} passed; 0 failed;\n") + return 1 if name == "broken" else 0 + + for suites, expected in (("broken passing last", 1), ("empty passing last", 1), + ("first passing last", 0)): + with self.subTest(suites=suites), tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "target").mkdir() + logs.clear() + with patch.object(runner, "run_binary", side_effect=execute) as run: + result = runner.run_cases({"suite": suites, "parallel_suites": "true"}, root, {}) + self.assertEqual(result, expected) + self.assertEqual(run.call_count, 3) + self.assertEqual(len(set(logs)), 3) + if __name__ == "__main__": unittest.main() diff --git a/scripts/tests/test_ci_e2e_tiers.py b/scripts/tests/test_ci_e2e_tiers.py index 0183e7597..cf92b0d31 100644 --- a/scripts/tests/test_ci_e2e_tiers.py +++ b/scripts/tests/test_ci_e2e_tiers.py @@ -175,6 +175,8 @@ def test_pr_rows_are_the_lean_table(self): for suite, test_filter in (*AGENT_HOSTED, VENDOR): row = {"os": "ubuntu-latest", "suite": suite, "jvm_tool": "gradle", "gradle": line, "java": java, "test_filter": test_filter} + if "e2e_gradle_agent_build" in suite.split(): + row["parallel_suites"] = "true" # The allowance lasts only while a suite of the row is unlanded. if not all(bundle.landed(s) for s in suite.split()): row["allow_empty"] = "true" @@ -182,7 +184,7 @@ def test_pr_rows_are_the_lean_table(self): want.append({"os": "windows-latest", "suite": "e2e_vendor_jvm_build", "jvm_tool": "gradle", "gradle": "8.14.3", "java": "17", "test_filter": "--ignored gradle_multi_project"}) self.assertEqual(len(gradle), 17) - self.assertEqual(sorted(map(str, gradle)), sorted(map(str, want))) + self.assertCountEqual(gradle, want) self.assertFalse([r for r in rows("e2e-full") if "gradle" in r or "jvm_tool" in r]) def test_jvm_tool_marks_every_jvm_row(self): From dc59e4b68cfeb3461eba019c5dee2466d155db7b Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Fri, 9 Oct 2026 16:32:29 -0400 Subject: [PATCH 7/7] Run this branch's Linux jobs on Depot runners (#1362) Applies #1362's runs-on mapping to this branch's ci.yml and compatibility workflows: Linux jobs map to depot-ubuntu-24.04-4 / depot-ubuntu-22.04-4 unless the repository variable DISABLE_DEPOT_RUNNERS=true. The two PRs touch the same workflow files, so carrying the mapping here lets them land back to back without a conflict eviction. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/bun-compatibility.yml | 6 +- .github/workflows/ci.yml | 62 ++++++++++---------- .github/workflows/composer-compatibility.yml | 6 +- .github/workflows/go-compatibility.yml | 4 +- .github/workflows/gradle-compatibility.yml | 8 +-- .github/workflows/npm-compatibility.yml | 4 +- .github/workflows/pdm-compatibility.yml | 8 +-- .github/workflows/pipenv-compatibility.yml | 4 +- .github/workflows/pnpm-compatibility.yml | 4 +- .github/workflows/poetry-compatibility.yml | 4 +- .github/workflows/sbt-compatibility.yml | 10 ++-- .github/workflows/vlt-compatibility.yml | 20 +++---- 12 files changed, 70 insertions(+), 70 deletions(-) diff --git a/.github/workflows/bun-compatibility.yml b/.github/workflows/bun-compatibility.yml index 7d04cd593..a16bb4a9c 100644 --- a/.github/workflows/bun-compatibility.yml +++ b/.github/workflows/bun-compatibility.yml @@ -116,7 +116,7 @@ jobs: exclude: # macOS legs run on push to main (and nightly where scheduled), not per PR push. - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} - runs-on: ${{ matrix.os }} + 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: 30 steps: - name: Checkout @@ -174,7 +174,7 @@ jobs: - {os: windows-latest, bun: '1.0.36'} # macOS legs run on push to main (and nightly where scheduled), not per PR push. - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} - runs-on: ${{ matrix.os }} + 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: 30 steps: - name: Checkout @@ -374,7 +374,7 @@ jobs: exclude: # macOS legs run on push to main (and nightly where scheduled), not per PR push. - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} - runs-on: ${{ matrix.os }} + 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: 30 steps: - name: Checkout diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fc2f13ec..47ba372cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: # failure immediately. Expensive jobs also depend on this preflight. clippy: if: github.event.pull_request.draft != true - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 20 permissions: contents: read @@ -119,7 +119,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] exclude: - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} - runs-on: ${{ matrix.os }} + 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: 30 steps: - name: Checkout @@ -158,7 +158,7 @@ jobs: # test harnesses. lint-ecosystems: if: github.event.pull_request.draft != true - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 10 steps: - name: Checkout @@ -250,7 +250,7 @@ jobs: # tag doesn't already exist. release-readiness: if: github.event.pull_request.draft != true - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -307,7 +307,7 @@ jobs: - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} # One compile-only writer per OS is enough for an already tested SHA. - shard: ${{ needs.clippy.outputs.reuse == 'true' && 2 || 0 }} - runs-on: ${{ matrix.os }} + 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: VEXCTL_VERSION: v0.3.0 @@ -474,7 +474,7 @@ jobs: # queue already skips this job because each constituent PR ran it. if: github.event.pull_request.draft != true && github.event_name != 'merge_group' needs: clippy - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} strategy: fail-fast: false matrix: @@ -526,7 +526,7 @@ jobs: # numbers are report-only so contributors get visibility without flaky # CI when coverage shifts naturally with test edits. A failing TEST # fails the job: this is the Linux leg of `test`. - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 35 permissions: contents: read @@ -646,7 +646,7 @@ jobs: docker-base: if: github.event.pull_request.draft != true && needs.clippy.outputs.reuse != 'true' needs: clippy - runs-on: ubuntu-22.04 + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-22.04' || 'depot-ubuntu-22.04-4' }} timeout-minutes: 30 permissions: contents: read @@ -702,7 +702,7 @@ jobs: # container ships fails to load. ubuntu-22.04's older glibc is # the highest base that's forward-compatible with debian:12. needs: docker-base - runs-on: ubuntu-22.04 + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-22.04' || 'depot-ubuntu-22.04-4' }} # sbt's image bakes three JDKs and warm sbt / Mill / scala-cli caches. timeout-minutes: ${{ matrix.ecosystem == 'sbt' && 45 || 30 }} permissions: @@ -832,7 +832,7 @@ jobs: # single lcov.info. lcov(1) handles the union — same files are # summed line-by-line so a line covered by ANY test counts. needs: [coverage, coverage-docker] - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 15 permissions: contents: read @@ -895,7 +895,7 @@ jobs: dispatch-tests: if: github.event.pull_request.draft != true - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 10 steps: - name: Checkout @@ -939,7 +939,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - runs-on: ${{ matrix.os }} + 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: 45 env: &e2e-build-env CARGO_PROFILE_DEV_DEBUG: '0' @@ -1002,7 +1002,7 @@ jobs: fail-fast: false matrix: os: [windows-latest] - runs-on: ${{ matrix.os }} + 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: 60 env: *e2e-build-env steps: *e2e-build-steps @@ -1016,7 +1016,7 @@ jobs: fail-fast: false matrix: os: [macos-latest] - runs-on: ${{ matrix.os }} + 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: 45 env: *e2e-build-env steps: *e2e-build-steps @@ -1090,7 +1090,7 @@ jobs: - {os: 'ubuntu-latest', suite: 'e2e_sbt_vendor_build', jvm_tool: 'sbt', sbt: '1.13.0', test_filter: '--ignored --test-threads=1', ci_group: 'e2e_sbt_vendor_build-1.13.0-54'} - {os: 'ubuntu-latest', suite: 'e2e_vex_build', test_filter: 'deno:: --ignored', deno: '1.46.3', ci_group: 'e2e_vex_build-1.46.3-55'} - {os: 'ubuntu-latest', suite: 'e2e_vex_build', test_filter: 'deno:: --ignored', deno: '2.9.7', ci_group: 'e2e_vex_build-2.9.7-56'} - runs-on: ${{ matrix.os }} + 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 }} # The real-toolchain capstones loop several releases per leg (pip, # pipenv) or bootstrap a tool from PyPI before the suite (poetry, pdm, # hatch), hence more than the 25 minutes the older legs needed. @@ -1480,7 +1480,7 @@ jobs: - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin', ci_group: '8.14.3-8.14.3-6'} - {os: 'ubuntu-latest', suite: 'e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p --skip gradle_hosted_config_cache_second_row --skip gradle_hosted_fallback_snippet_compiles_kotlin --skip gradle_hosted_vendored_takeover_and_eject', ci_group: '8.14.3-8.14.3-7'} - {os: 'ubuntu-latest', suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build e2e_redirect_gradle_build', jvm_tool: 'gradle', gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_vendor_ gradle_multi_project gradle_hosted_config_cache_second_row gradle_hosted_fallback_snippet_compiles_kotlin gradle_hosted_vendored_takeover_and_eject', ci_group: '8.14.3-8.14.3-8'} - runs-on: ${{ matrix.os }} + 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 env: *e2e-env steps: *e2e-steps @@ -1498,7 +1498,7 @@ jobs: - {os: 'windows-latest', vlt: '1.0.0-rc.14', suite: 'e2e_vendor_vlt_build mode_migration_vlt e2e_vlt', cases: '[{"os":"windows-latest","suite":"e2e_vendor_vlt_build","vlt":"1.0.0-rc.14","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"windows-latest","suite":"mode_migration_vlt","vlt":"1.0.0-rc.14","test_filter":"--include-ignored vlt_pinned_matrix"},{"os":"windows-latest","suite":"e2e_vlt","vlt":"1.0.0-rc.14","test_filter":"--include-ignored vlt_pinned_matrix"}]', ci_group: 'node24-1.0.0-rc.14-3'} - {os: 'windows-latest', suite: 'e2e_vendor_jvm_build', jvm_tool: 'maven', maven: '3.9.16', test_filter: '--ignored maven_reactor', ci_group: 'jvm-dotnet-3.9.16-4'} - {os: 'windows-latest', suite: 'e2e_vendor_jvm_build', jvm_tool: 'gradle', gradle: '8.14.3', java: '17', test_filter: '--ignored gradle_multi_project', ci_group: '8.14.3-8.14.3-5'} - runs-on: ${{ matrix.os }} + 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 env: *e2e-env steps: *e2e-steps @@ -1520,7 +1520,7 @@ jobs: - {os: 'macos-latest', poetry: '2.4.3', pdm: '2.29.2', hatch: '1.18.1', suite: 'e2e_vex_build', cases: '[{"os":"macos-latest","suite":"e2e_vex_build","test_filter":"poetry:: --ignored","poetry":"2.4.3"},{"os":"macos-latest","suite":"e2e_vex_build","test_filter":"pdm:: --ignored","pdm":"2.29.2"},{"os":"macos-latest","suite":"e2e_vex_build","test_filter":"hatch:: --ignored","hatch":"1.18.1"}]', ci_group: 'python-2.4.3-2.29.2-1.18.1-4'} - {os: 'macos-latest', pipenv: '2022.12.19 2026.8.0', pip: '22 26', suite: 'e2e_vex_build', cases: '[{"os":"macos-latest","suite":"e2e_vex_build","test_filter":"pipenv:: --ignored","pipenv":"2022.12.19 2026.8.0"},{"os":"macos-latest","suite":"e2e_vex_build","test_filter":"pip:: --ignored","pip":"22 26"}]', ci_group: 'python-installers-2022.12.19 2026.8.0-22 26-5'} - {os: 'macos-latest', jvm_tool: 'maven', maven: '3.9.16', dotnet: '8', suite: 'e2e_redirect_maven_build e2e_vendor_maven_build e2e_vendor_jvm_build e2e_nuget_dotnet_build', cases: '[{"os":"macos-latest","suite":"e2e_redirect_maven_build","jvm_tool":"maven","maven":"3.9.16"},{"os":"macos-latest","suite":"e2e_vendor_maven_build","jvm_tool":"maven","maven":"3.9.16"},{"os":"macos-latest","suite":"e2e_vendor_jvm_build","jvm_tool":"maven","maven":"3.9.16","test_filter":"--ignored maven_reactor"},{"os":"macos-latest","suite":"e2e_nuget_dotnet_build","dotnet":"8"}]', ci_group: 'jvm-dotnet-3.9.16-8-6'} - runs-on: ${{ matrix.os }} + 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 }} # The real-toolchain capstones loop several releases per leg (pip, # pipenv) or bootstrap a tool from PyPI before the suite (poetry, pdm, # hatch), hence more than the 25 minutes the older legs needed. @@ -1561,7 +1561,7 @@ jobs: - {os: 'ubuntu-latest', suite: 'e2e_nuget_dotnet_build', dotnet: '7', ci_group: 'jvm-dotnet-7-16'} - {os: 'ubuntu-latest', suite: 'e2e_nuget_dotnet_build', dotnet: '8', ci_group: 'jvm-dotnet-8-17'} - {os: 'ubuntu-latest', suite: 'e2e_nuget_dotnet_build', dotnet: '9', ci_group: 'jvm-dotnet-9-18'} - runs-on: ${{ matrix.os }} + 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 # legs launch the test binaries themselves. @@ -1593,7 +1593,7 @@ jobs: # pull_request runs of its PR into main) run it too. if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.head_ref == 'release/v5-prerelease' needs: docker-base - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: ${{ matrix.ecosystem == 'sbt' && 45 || 35 }} permissions: contents: read @@ -1679,7 +1679,7 @@ jobs: # Only wait for the clippy preflight. if: github.event.pull_request.draft != true && needs.clippy.outputs.reuse != 'true' needs: clippy - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 40 strategy: fail-fast: false @@ -1733,7 +1733,7 @@ jobs: - {os: ubuntu-latest, yarn: '4.1.0'} - {os: ubuntu-latest, yarn: '4.18.0'} - {os: windows-latest, yarn: '4.12.0'} - runs-on: ${{ matrix.os }} + 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: 30 steps: &yarn-berry-steps - name: Checkout @@ -1775,7 +1775,7 @@ jobs: # of the spread (4.6.0, 4.12.0 on ubuntu) is yarn-berry-full. include: - {os: macos-latest, yarn: '4.12.0'} - runs-on: ${{ matrix.os }} + 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: 30 steps: *yarn-berry-steps @@ -1790,7 +1790,7 @@ jobs: include: - {os: ubuntu-latest, yarn: '4.6.0'} - {os: ubuntu-latest, yarn: '4.12.0'} - runs-on: ${{ matrix.os }} + 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: 30 steps: *yarn-berry-steps @@ -1808,7 +1808,7 @@ jobs: # e2e-build only (its binaries; only the matching OS build is needed); # see yarn-classic-matrix on test/coverage. needs: [e2e-build] - runs-on: ${{ matrix.os }} + 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 # legs launch the test binaries themselves. @@ -1889,7 +1889,7 @@ jobs: if: needs.e2e-build-windows.outputs.reuse != 'true' name: cargo ${{ matrix.toolchain }} lock-v${{ matrix.lock || 'own' }} (${{ matrix.os }}) needs: [e2e-build-windows] - runs-on: ${{ matrix.os }} + 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 env: *e2e-env strategy: @@ -1905,7 +1905,7 @@ jobs: if: github.event_name != 'pull_request' && needs.e2e-build-macos.outputs.reuse != 'true' name: cargo ${{ matrix.toolchain }} lock-v${{ matrix.lock || 'own' }} (${{ matrix.os }}) needs: [e2e-build-macos] - runs-on: ${{ matrix.os }} + 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 # legs launch the test binaries themselves. @@ -1926,7 +1926,7 @@ jobs: # 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: [e2e-build] - runs-on: ${{ matrix.os }} + 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 # legs launch the test binaries themselves. @@ -1966,7 +1966,7 @@ jobs: # Only wait for the clippy preflight. if: github.event.pull_request.draft != true needs: clippy - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 30 steps: - name: Checkout @@ -2036,7 +2036,7 @@ jobs: name: hosted-e2e # may be a required check; do not rename if: github.event.pull_request.draft != true && needs.clippy.outputs.reuse != 'true' needs: clippy - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} permissions: contents: read timeout-minutes: 30 @@ -2244,7 +2244,7 @@ jobs: # and on main: a cancelled dependency there must never pass the gate. if: ${{ always() && (github.event_name != 'pull_request' || !cancelled()) }} needs: [clippy, node-addon, lint-ecosystems, release-readiness, test, test-release, coverage, docker-base, coverage-docker, coverage-merge, dispatch-tests, e2e-build, e2e-build-windows, e2e-build-macos, e2e, e2e-gradle-mid, e2e-windows, e2e-full, e2e-docker, yarn-classic-matrix, yarn-berry-e2e, yarn-berry-full, cargo-vex-matrix, cargo-vex-matrix-windows, cargo-vex-matrix-full, cargo-old-toolchains, hosted-e2e, e2e-macos, yarn-berry-e2e-macos, cargo-vex-matrix-macos] - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 5 steps: - name: Check every needed job diff --git a/.github/workflows/composer-compatibility.yml b/.github/workflows/composer-compatibility.yml index 62a8d7fec..feb82a6e0 100644 --- a/.github/workflows/composer-compatibility.yml +++ b/.github/workflows/composer-compatibility.yml @@ -130,7 +130,7 @@ jobs: - {os: windows-latest, composer: '2.2.30', php: '8.3', sha256: 8c2b4478b64f8f7cdf1574838fdb0033b29049ca821dad452db7a3dcfcdbffc2} - {os: windows-latest, composer: '2.9.8', php: '8.4', sha256: 59b2c50e10cafa0d8efc19ede9a326d782f096c674a26baf98cf042ce23de890} - {os: windows-latest, composer: '2.10.3', php: '8.5', sha256: 7a2d379d5b8ffdaa028580ef26494c36d2feef4b178d3dd1473a4dbc5e17c8d6} - runs-on: ${{ matrix.os }} + 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: 60 steps: &native-steps - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -216,7 +216,7 @@ jobs: # all (#1210); 2.40.0 resolves the tap's formula aliases first. # Ubuntu and Windows keep the 2.10.3 / 8.5 cell. - {os: macos-latest, composer: '2.10.3', php: '8.4', sha256: 7a2d379d5b8ffdaa028580ef26494c36d2feef4b178d3dd1473a4dbc5e17c8d6} - runs-on: ${{ matrix.os }} + 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: 60 steps: *native-steps @@ -226,7 +226,7 @@ jobs: # Composer 1, so 1.10.28 is covered by the native legs only. name: docker composer ${{ matrix.composer }} if: github.event.pull_request.draft != true - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 35 strategy: fail-fast: false diff --git a/.github/workflows/go-compatibility.yml b/.github/workflows/go-compatibility.yml index 3066d36bd..d7f3333e2 100644 --- a/.github/workflows/go-compatibility.yml +++ b/.github/workflows/go-compatibility.yml @@ -78,7 +78,7 @@ jobs: matrix: os: [ubuntu-latest] go: ['1.18.10', '1.21.13', '1.24.13', '1.26.3'] - runs-on: ${{ matrix.os }} + 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: 45 steps: &go-steps - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -118,6 +118,6 @@ jobs: # macOS-latest dyld refuses binaries without LC_UUID (Go < 1.24 # linkers; see ci.yml's vexctl step). go: ['1.24.13', '1.26.3'] - runs-on: ${{ matrix.os }} + 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: 45 steps: *go-steps diff --git a/.github/workflows/gradle-compatibility.yml b/.github/workflows/gradle-compatibility.yml index 038e42dc7..d668a680a 100644 --- a/.github/workflows/gradle-compatibility.yml +++ b/.github/workflows/gradle-compatibility.yml @@ -112,7 +112,7 @@ jobs: # Whether this run needs the ubuntu `extras`: always off pull_request, and # on a PR only when it touches Gradle code. if: github.event.pull_request.draft != true - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 5 outputs: gradle_core: ${{ steps.diff.outputs.gradle_core }} @@ -160,7 +160,7 @@ jobs: - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} # On a PR only the ubuntu `extras` use the ubuntu build. - os: ${{ github.event_name == 'pull_request' && needs.changes.outputs.gradle_core != 'true' && 'ubuntu-latest' || '' }} - runs-on: ${{ matrix.os }} + 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: 60 env: CARGO_PROFILE_DEV_DEBUG: '0' @@ -240,7 +240,7 @@ jobs: # Boundary lines retain every hosted test on Windows on PRs. - {os: "${{ github.event_name == 'pull_request' && 'windows-latest' || '' }}", gradle: '7.6.6', mode: hosted} - {os: "${{ github.event_name == 'pull_request' && 'windows-latest' || '' }}", gradle: '8.14.3', mode: hosted} - runs-on: ${{ matrix.os }} + 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: 60 steps: &cell-steps - name: Checkout @@ -428,7 +428,7 @@ jobs: # Only the suite that owns those tests: e2e_vendor_jvm_build has none, # and every landed suite a cell runs must run a test. - {os: ubuntu-latest, gradle: '8.14.3', java: '21', mode: vendor, label: real-central, real_central: '1', suites: 'e2e_vendor_gradle_build', test_filter: 'gradle_vendor_511 gradle_vendor_487'} - runs-on: ${{ matrix.os }} + 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: 60 continue-on-error: ${{ matrix.record_only == 'true' }} steps: *cell-steps diff --git a/.github/workflows/npm-compatibility.yml b/.github/workflows/npm-compatibility.yml index 9ef2ae22c..f7ad21e3d 100644 --- a/.github/workflows/npm-compatibility.yml +++ b/.github/workflows/npm-compatibility.yml @@ -74,7 +74,7 @@ concurrency: jobs: build: if: github.event.pull_request.draft != true - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 25 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -103,7 +103,7 @@ jobs: install-proof: needs: build - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 25 strategy: fail-fast: false diff --git a/.github/workflows/pdm-compatibility.yml b/.github/workflows/pdm-compatibility.yml index 05619435d..9ab38d3b9 100644 --- a/.github/workflows/pdm-compatibility.yml +++ b/.github/workflows/pdm-compatibility.yml @@ -85,7 +85,7 @@ jobs: exclude: # macOS legs run on push to main (and nightly where scheduled), not per PR push. - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} - runs-on: ${{ matrix.os }} + 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: 30 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -128,7 +128,7 @@ jobs: # every Windows cell skipped; backtest-pdm.py now fails such a cell. os: [ubuntu-latest] pdm: ['0.12.3', '1.15.5', '2.0.3', '2.1.5', '2.3.4', '2.6.1', '2.7.4', '2.8.2', '2.9.3', '2.10.4', '2.11.2', '2.17.3', '2.20.1', '2.22.4', '2.25.9', '2.29.2'] - runs-on: ${{ matrix.os }} + 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: 45 steps: &native-steps - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -204,7 +204,7 @@ jobs: # The ends of the range and the 2.8 lock-format boundary. os: [macos-latest] pdm: ['0.12.3', '2.8.2', '2.29.2'] - runs-on: ${{ matrix.os }} + 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: 45 steps: *native-steps @@ -225,7 +225,7 @@ jobs: - {os: macos-latest, pdm: '2.29.2'} # macOS legs run on push to main (and nightly where scheduled), not per PR push. - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} - runs-on: ${{ matrix.os }} + 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: 30 steps: # The binaries resolve fixtures through the build job's checkout path, diff --git a/.github/workflows/pipenv-compatibility.yml b/.github/workflows/pipenv-compatibility.yml index 27e07c721..cea9c0e5e 100644 --- a/.github/workflows/pipenv-compatibility.yml +++ b/.github/workflows/pipenv-compatibility.yml @@ -86,7 +86,7 @@ jobs: - os: ubuntu-latest versions: 2022.12.19 2026.8.0 shapes: crlf marker-excluded extras category - runs-on: ${{ matrix.os }} + 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: 60 steps: &matrix-steps - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -153,6 +153,6 @@ jobs: - os: macos-latest versions: 2018.11.26 2022.12.19 2023.12.1 2026.8.0 shapes: direct - runs-on: ${{ matrix.os }} + 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: 60 steps: *matrix-steps diff --git a/.github/workflows/pnpm-compatibility.yml b/.github/workflows/pnpm-compatibility.yml index 271117272..06bec0175 100644 --- a/.github/workflows/pnpm-compatibility.yml +++ b/.github/workflows/pnpm-compatibility.yml @@ -60,7 +60,7 @@ concurrency: jobs: build: if: github.event.pull_request.draft != true - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 20 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -97,7 +97,7 @@ jobs: # failed the whole run. Every version still runs; a failure names it. name: install-proof (node ${{ matrix.node }}) needs: build - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 30 strategy: fail-fast: false diff --git a/.github/workflows/poetry-compatibility.yml b/.github/workflows/poetry-compatibility.yml index 00be6232b..e1f40c991 100644 --- a/.github/workflows/poetry-compatibility.yml +++ b/.github/workflows/poetry-compatibility.yml @@ -69,7 +69,7 @@ jobs: exclude: # macOS legs run on push to main (and nightly where scheduled), not per PR push. - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} - runs-on: ${{ matrix.os }} + 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: 30 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -97,7 +97,7 @@ jobs: exclude: # macOS legs run on push to main (and nightly where scheduled), not per PR push. - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} - runs-on: ${{ matrix.os }} + 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: 45 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/sbt-compatibility.yml b/.github/workflows/sbt-compatibility.yml index 26cc6c768..80726c17a 100644 --- a/.github/workflows/sbt-compatibility.yml +++ b/.github/workflows/sbt-compatibility.yml @@ -115,7 +115,7 @@ jobs: if: github.event.pull_request.draft != true # Builds the sbt image once (base image first: Dockerfile.sbt is # `FROM socket-patch-test-base:latest`) and hands it to every leg. - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 45 steps: - name: Checkout @@ -168,7 +168,7 @@ jobs: # rust container. Absolute paths are kept (`tar -P`): # the test binaries carry their compile-time paths, and every leg checks # out to the same workspace path. - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 40 steps: - name: Checkout @@ -194,7 +194,7 @@ jobs: docker: name: sbt ${{ matrix.sbt }} / jdk ${{ matrix.jdk }} / ${{ matrix.group }} needs: [image, linux-bins] - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 45 strategy: fail-fast: false @@ -257,7 +257,7 @@ jobs: # vendored (the versions the image installs). name: ${{ matrix.group }} ${{ matrix.version }} needs: [image, linux-bins] - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 45 strategy: fail-fast: false @@ -308,7 +308,7 @@ jobs: # The warm-seed step boots sbt once so the tests share its caches. name: sbt 1.13.0 ${{ matrix.suite }} / ${{ matrix.os }} if: github.event.pull_request.draft != true - runs-on: ${{ matrix.os }} + 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: 60 strategy: fail-fast: false diff --git a/.github/workflows/vlt-compatibility.yml b/.github/workflows/vlt-compatibility.yml index fcf9aa5ec..473d8f8f3 100644 --- a/.github/workflows/vlt-compatibility.yml +++ b/.github/workflows/vlt-compatibility.yml @@ -131,7 +131,7 @@ env: jobs: matrix-coverage: if: github.event.pull_request.draft != true - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 5 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -146,7 +146,7 @@ jobs: # matrix-coverage above still checks it, the rest is skipped. changes: if: github.event.pull_request.draft != true - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 5 outputs: matrix: ${{ steps.gate.outputs.matrix }} @@ -182,7 +182,7 @@ jobs: exclude: # macOS legs run on push to main (and nightly where scheduled), not per PR push. - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} - runs-on: ${{ matrix.os }} + 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 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -291,7 +291,7 @@ jobs: - {os: windows-latest, vlt: '1.2.0', linker: auto, suites: e2e_safety_vlt} - {os: windows-latest, vlt: '1.2.0', linker: hardlink, suites: e2e_safety_vlt} - {os: ubuntu-latest, vlt: '1.2.0', linker: hardlink, cache_root: /dev/shm/vlt-e2e-cache, suites: e2e_safety_vlt} - runs-on: ${{ matrix.os }} + 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: 45 # The capstones resolve fixtures through the build job's checkout path, # which is the same on every runner of one OS. @@ -405,14 +405,14 @@ jobs: - {os: macos-latest, vlt: '1.2.0'} - {os: macos-latest, vlt: '1.2.0', linker: auto, suites: e2e_safety_vlt} - {os: macos-latest, vlt: '1.2.0', linker: hardlink, suites: e2e_safety_vlt} - runs-on: ${{ matrix.os }} + 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: 45 steps: *install-proof-steps plan: needs: changes if: needs.changes.outputs.matrix == 'true' - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 5 outputs: native: ${{ steps.plan.outputs.native }} @@ -444,7 +444,7 @@ jobs: # Each job runs its cells against the public patch service. max-parallel: 6 matrix: ${{ fromJSON(needs.plan.outputs.native) }} - runs-on: ${{ matrix.os }} + 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: 45 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -508,7 +508,7 @@ jobs: lock-diff: needs: [changes, native] if: ${{ !cancelled() && needs.changes.outputs.matrix == 'true' }} - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 10 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -555,7 +555,7 @@ jobs: exclude: # macOS legs run on push to main (and nightly where scheduled), not per PR push. - os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }} - runs-on: ${{ matrix.os }} + 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: 45 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -624,7 +624,7 @@ jobs: # Advisory until the socket-patch release that adds vlt support (with the # forward-compatible ledger handling) is the latest published one. continue-on-error: true - runs-on: ubuntu-latest + runs-on: ${{ vars.DISABLE_DEPOT_RUNNERS == 'true' && 'ubuntu-latest' || 'depot-ubuntu-24.04-4' }} timeout-minutes: 20 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2