Skip to content

ci(e2e): deduplicate the self-hosted e2e lanes into a composite action #294

Description

@rominf

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:

  1. ci.yml's E2E tests and E2E consolidated report are required checks and must stay distinct.
  2. Self-hosted lanes must live in a separate workflow file for concurrency isolation — enforced by the two workflow_contract.rs tests above.
  3. Windows vs Linux is a hard job split (PowerShell 5.1 vs bash throughout).
  4. 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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions