ci(e2e): deduplicate the self-hosted e2e lanes into a composite action
@volen-silo is already working on this.
Since Aug 27, 2026.
- Dominant language
- Rust
- Stars
- 40
- Forks
- 9
- Avg merge
- 4d 20h
- Merged PRs (30d)
- 59
Description
Problem
The self-hosted e2e lane definitions in .github/workflows/e2e-selfhosted.yml and .github/workflows/nightly.yml are largely copy-paste. Measured across the 10 lanes in those two files, 62% of step-body lines (502 of 806) sit inside a block that appears verbatim in at least one other lane.
| Repeated block | Copies | Lines |
|---|---|---|
GPU preflight (bounded wait…) bash |
5 | 130 |
| GPU preflight — advisory + PowerShell variants | 2 + 2 | 128 |
Upload E2E report (differs only in name:) |
10 | 60 |
Reclaim GPU from stray E2E processes |
8 | 66 |
| WSL bootstrap / toolchain / checkout | various | 106 |
The five copies of the GPU preflight script differ in exactly one integer (MIN_FREE_GIB, 16 vs 8); the other ~26 lines are byte-identical. The main Run E2E tests script only escapes exact matching because of path substitutions — by sequence similarity it is 97.1% identical between e2e-gpu and e2e-gpu-rad3, and 97.6% between their nightly equivalents.
The cost is drift, and it is already visible. Examples found while reviewing #278:
e2e-gpu-rad3omitsE2E_MERGE_QUEUE, which all four sibling lanes set — so it silently runs the cheap PR canary during merge-queue.e2e-gpu-nightlyis the only GPU lane with no preflight step at all.- The per-PR
e2e-gpu-strix-windowslane has noE2E_SERVE_TIMEOUT_SECSwhile its nightly twin sets"300". e2e-gpu-nightly-rad3drops the pre-warm diagnostics its own comment says it mirrors.
Each of these is a one-line divergence in a block that is otherwise identical five ways over. There is no mechanism that would catch any of them.
Proposed approach
Extract the repeated blocks into a composite action (e.g. .github/actions/e2e-gpu-lane-linux/action.yml) parameterised on the axes that genuinely vary: min-free-gib, preflight strict-vs-advisory, shared uv cache dir, scenario name filter, artifact name.
A composite action cannot touch name:, if:, needs:, runs-on:, continue-on-error: or strategy:, so it is provably invisible to branch protection — job headers stay byte-identical. Eight Linux call sites (4 lanes × 2 files) collapse onto one definition; an individual lane goes from ~139 lines to ~25. Windows needs a second PowerShell-only action or stays inline.
Alternatives considered
Matrix — viable for check names (codeql.yml already proves the name: Analyze (${{ matrix.language }}) pattern here), but matrix is not in scope at jobs.<id>.if, so the per-lane inputs.platform == '…' gating would have to move into a step gate. That means waking a scale-to-zero runner just to no-op. It also needs strategy.fail-fast: false or one flaky self-hosted machine cancels the rest, and the PowerShell lane can't join. Reasonable as a second step, not the first.
workflow_call — should not be used to bridge ci.yml and e2e-selfhosted.yml. That split exists so a job queued on an offline self-hosted runner cannot stall a required check, and it is enforced by ci_yml_schedules_no_self_hosted_job and workflows_use_distinct_concurrency_groups in xtask/src/workflow_contract.rs. It also mangles check names to <caller> / <called>.
Non-goal: collapsing to one or two jobs
Four constraints set the floor, none of them stylistic:
ci.yml'sE2E testsandE2E consolidated reportare required checks and must stay distinct.- Self-hosted lanes must live in a separate workflow file for concurrency isolation — enforced by the two
workflow_contract.rstests above. - Windows vs Linux is a hard job split (PowerShell 5.1 vs bash throughout).
nightly.ymlis a separate trigger and report tier.
Realistic floor is 8 jobs from the current 14, and only if the if:-to-step-gate refactor is accepted. Keeping all 14 jobs and applying composite actions alone removes ~500 duplicated lines at zero structural risk, which is the better trade and should be done first.
Also in scope: make the docs guard actually guard
hardware_testing_docs_cover_all_four_self_hosted_platforms in xtask/src/workflow_contract.rs compares docs/ci-hardware-testing.md against hardcoded expectations, never against e2e-selfhosted.yml / nightly.yml. It therefore cannot detect a newly added lane — it only fails when someone tries to fix the docs to match reality, which is backwards. Adding a lane in #278 left the doc table, the artifact list, two "the four self-hosted jobs" sentences and the dispatch-options list stale, with the test green throughout.
Derive the expected rows from the actual job list (runs_on_values() and friends already exist in that file) so adding a lane fails the build until the docs follow.
Acceptance
- Repeated preflight / reclaim / build-and-run / upload blocks exist in one place each.
- Every existing job name and required-check context is unchanged.
- Adding a new lane without updating
docs/ci-hardware-testing.mdfails CI. - The four drift instances listed above are resolved or deliberately documented.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.