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-rad3 omits E2E_MERGE_QUEUE, which all four sibling lanes set — so it silently runs the cheap PR canary during merge-queue.
e2e-gpu-nightly is the only GPU lane with no preflight step at all.
- The per-PR
e2e-gpu-strix-windows lane has no E2E_SERVE_TIMEOUT_SECS while its nightly twin sets "300".
e2e-gpu-nightly-rad3 drops 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's E2E tests and E2E consolidated report are 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.rs tests above.
- Windows vs Linux is a hard job split (PowerShell 5.1 vs bash throughout).
nightly.yml is 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.md fails CI.
- The four drift instances listed above are resolved or deliberately documented.
Problem
The self-hosted e2e lane definitions in
.github/workflows/e2e-selfhosted.ymland.github/workflows/nightly.ymlare 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.GPU preflight (bounded wait…)bashUpload E2E report(differs only inname:)Reclaim GPU from stray E2E processesThe 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 mainRun E2E testsscript only escapes exact matching because of path substitutions — by sequence similarity it is 97.1% identical betweene2e-gpuande2e-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.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:orstrategy:, 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.ymlalready proves thename: Analyze (${{ matrix.language }})pattern here), butmatrixis not in scope atjobs.<id>.if, so the per-laneinputs.platform == '…'gating would have to move into a step gate. That means waking a scale-to-zero runner just to no-op. It also needsstrategy.fail-fast: falseor 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 bridgeci.ymlande2e-selfhosted.yml. That split exists so a job queued on an offline self-hosted runner cannot stall a required check, and it is enforced byci_yml_schedules_no_self_hosted_jobandworkflows_use_distinct_concurrency_groupsinxtask/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.workflow_contract.rstests above.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_platformsinxtask/src/workflow_contract.rscomparesdocs/ci-hardware-testing.mdagainst hardcoded expectations, never againste2e-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
docs/ci-hardware-testing.mdfails CI.