Rendered from docs/process/decisions/0016-ci-runner-is-an-opt-in-that-only-a-push-or-a-merge-group-reads-and-an-unset-value-falls-back-to-ubuntu-latest.md in the Headwater corpus. Every document on this half of the site is typed by the taxonomy the descriptor names: corpus.json.

CI_RUNNER is an opt-in that only a push or a merge group reads, and an unset value falls back to ubuntu-latest

Context

Before this record, the reasons lived in comments of .github/workflows/ci.yml on 2026-09-27. The comments were on the runs-on of the engine job (lines 213 to 222) and at the end of the header (lines 73 to 77). The runs-on expressions are at lines 223 and 493. Two triggers, one verdict holds the argument.

The variable lives in the repository settings and not in the tree. So a reader of the tree cannot see its value, and a document that states the value goes stale when a person changes it.

Decision

An opt-in. runs-on reads vars.CI_RUNNER only when the event is a push and the router did not report overflow (HW-PD-0018). Since HW-PD-0020, a merge_group run reads it in the same way as a push. When the variable is unset, the expression falls back to ["ubuntu-latest"]. The self-hosted labels reach a job only when the variable names them, with gh variable set CI_RUNNER --body '["self-hosted", "headwater"]'.

A pull request never reads it. A pull_request run takes the last branch of the expression, ubuntu-latest, whatever the variable holds.

Read the live value. Run gh variable list before you state which runner a push uses.

Consequences

gh variable delete CI_RUNNER sends every run to a hosted runner, with no commit. A statement that the self-hosted labels are the default is wrong.

A pull request that is CONFLICTING against a moved main gets no check suite on either runner. From outside, that looks the same as a dead runner. So read mergeable before you read a missing check suite as a runner fault.