@@ -45,9 +45,62 @@ concurrency:
4545 cancel-in-progress : true
4646
4747jobs :
48+ # Which Windows runner this run is allowed to use.
49+ #
50+ # The self-hosted Windows box is a maintainer's PERSONAL machine on a home
51+ # network, reachable over Tailscale. Anything that runs on it runs as a local
52+ # user with that user's filesystem and LAN in reach, so the only question that
53+ # matters is whether the commit is trusted BEFORE a runner ever sees it.
54+ #
55+ # `push` on dev/main/preview is trusted: reaching those branches requires the
56+ # push permission, which only the maintainers in MAINTAINERS.md hold.
57+ # `pull_request` is NOT trusted, and no author check can make it so — the
58+ # workflow file itself comes from the PR head, so any `if:` guard written here
59+ # is deleted by the same patch it is meant to stop. Fork PRs therefore stay on
60+ # GitHub-hosted runners, unconditionally.
61+ #
62+ # `workflow_dispatch` is the maintainer escape hatch: dispatch requires write
63+ # access, so a maintainer can deliberately aim a branch at the home box.
64+ select-windows-runner :
65+ name : select windows runner
66+ runs-on : ubuntu-latest
67+ timeout-minutes : 2
68+ outputs :
69+ runner : ${{ steps.pick.outputs.runner }}
70+ label : ${{ steps.pick.outputs.label }}
71+ steps :
72+ - name : Pick runner
73+ id : pick
74+ env :
75+ # Read through env rather than interpolating directly into the script:
76+ # `github.event_name` is a fixed vocabulary, but keeping the habit means
77+ # no future edit here can grow a script-injection sink.
78+ EVENT_NAME : ${{ github.event_name }}
79+ USE_SELF_HOSTED : ${{ vars.OCX_SELF_HOSTED_WINDOWS }}
80+ shell : bash
81+ run : |
82+ set -euo pipefail
83+ trusted=no
84+ case "$EVENT_NAME" in
85+ push|workflow_dispatch) trusted=yes ;;
86+ esac
87+
88+ # Repository variable OCX_SELF_HOSTED_WINDOWS acts as a kill switch. When
89+ # it is anything other than `1` — including unset, which is the state
90+ # before the runner exists — every job falls back to windows-latest and
91+ # CI keeps working with the box powered off.
92+ if [ "$trusted" = "yes" ] && [ "${USE_SELF_HOSTED:-}" = "1" ]; then
93+ echo 'runner=["self-hosted","Windows","X64","ocx-home"]' >> "$GITHUB_OUTPUT"
94+ echo 'label=self-hosted (ocx-home)' >> "$GITHUB_OUTPUT"
95+ else
96+ echo 'runner="windows-latest"' >> "$GITHUB_OUTPUT"
97+ echo 'label=windows-latest' >> "$GITHUB_OUTPUT"
98+ fi
99+
48100 test :
49- name : ${{ matrix.os }}
50- runs-on : ${{ matrix.os }}
101+ name : ${{ matrix.name }}
102+ needs : select-windows-runner
103+ runs-on : ${{ matrix.runner }}
51104 # Windows dominates this matrix: on run 30459554635 the same suite took
52105 # ubuntu 4.6min / macos 5.6min / windows 11.8min. Against the previous
53106 # 12-minute ceiling that left ~12s of headroom, so runner variance decided
@@ -64,8 +117,34 @@ jobs:
64117 strategy :
65118 fail-fast : false
66119 matrix :
67- os : [ubuntu-latest, windows-latest, macos-latest]
120+ # `name` is spelled out rather than derived from `runner`: a runner given
121+ # as a label array renders as "self-hosted Windows X64 ocx-home", so the
122+ # check name would change with the routing and break any branch
123+ # protection rule that names it. Keeping `windows` fixed means the
124+ # required check stays the same whichever machine served it.
125+ include :
126+ - name : ubuntu
127+ runner : ubuntu-latest
128+ - name : windows
129+ runner : ${{ fromJSON(needs.select-windows-runner.outputs.runner) }}
130+ - name : macos
131+ runner : macos-latest
68132 steps :
133+ - name : Show selected runner
134+ if : matrix.name == 'windows'
135+ shell : bash
136+ run : echo "windows leg on ${{ needs.select-windows-runner.outputs.label }}"
137+
138+ # A self-hosted runner keeps its working directory between jobs. Without an
139+ # explicit wipe, a file deleted in the commit under test survives on disk
140+ # and the suite passes against a tree that no longer exists in git.
141+ # `--ephemeral` registration de-registers the runner after each job but does
142+ # not clean the workspace, so this step is what makes the checkout honest.
143+ - name : Clean workspace (self-hosted only)
144+ if : runner.environment == 'self-hosted'
145+ shell : bash
146+ run : git clean -xffd . || true
147+
69148 - name : Checkout
70149 uses : actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
71150
@@ -115,6 +194,10 @@ jobs:
115194 strategy :
116195 fail-fast : false
117196 matrix :
197+ # Deliberately NOT routed to the self-hosted box. This job runs
198+ # `npm install -g`, which writes into the machine's global prefix and
199+ # would leave an `ocx` on a maintainer's personal PATH. It is an
200+ # 8-minute job, so there is nothing to win by moving it.
118201 os : [ubuntu-latest, windows-latest, macos-latest]
119202 steps :
120203 - name : Checkout
0 commit comments