|
40 | 40 | permissions: |
41 | 41 | contents: read |
42 | 42 |
|
| 43 | +# Retrigger CI after dir-fsync / oauth deadline follow-ups (tip 34a1ac46). |
43 | 44 | concurrency: |
44 | 45 | group: cross-platform-ci-${{ github.ref }} |
45 | 46 | cancel-in-progress: true |
46 | 47 |
|
47 | 48 | jobs: |
| 49 | + # Which Windows runner this run is allowed to use. |
| 50 | + # |
| 51 | + # READ THIS BEFORE TREATING IT AS A SECURITY BOUNDARY: it is not one. |
| 52 | + # |
| 53 | + # On `pull_request` this workflow is loaded from the PR head, so the `case` |
| 54 | + # below is owned by the proposed patch exactly like an `if:` guard would be. |
| 55 | + # A hostile PR can delete the branch and hardcode the self-hosted labels into |
| 56 | + # `$GITHUB_OUTPUT`, and `runs-on` will honour it. That this job runs on |
| 57 | + # `ubuntu-latest` changes nothing — the untrusted part is its OUTPUT, not its |
| 58 | + # host. `.github/workflows/ci.yml` is in this workflow's `pull_request.paths`, |
| 59 | + # so such an edit triggers its own run. |
| 60 | + # |
| 61 | + # What actually keeps untrusted code off a self-hosted runner lives OUTSIDE |
| 62 | + # this file, where a PR cannot reach it: the fork-PR approval policy |
| 63 | + # (`all_external_contributors`) and the judgement of whoever clicks approve. |
| 64 | + # Runner groups would be the other lever, but they are an organisation |
| 65 | + # feature and this repository is user-owned, so the approval policy is the |
| 66 | + # only one available here. GitHub's own guidance is to avoid self-hosted |
| 67 | + # runners on public repositories for this reason. |
| 68 | + # |
| 69 | + # So read the routing below as a COST control that keeps honest pull requests |
| 70 | + # on GitHub-hosted runners, not as a guarantee about hostile ones. |
| 71 | + # |
| 72 | + # `push` on dev/main/preview requires the push permission, and |
| 73 | + # `workflow_dispatch` requires write access, so both carry a trusted author. |
| 74 | + # A trusted author is not audited code: merging a contributor PR into `dev` |
| 75 | + # fires `push`, and its dependencies and postinstall hooks then run here. |
| 76 | + select-windows-runner: |
| 77 | + name: select windows runner |
| 78 | + runs-on: ubuntu-latest |
| 79 | + timeout-minutes: 2 |
| 80 | + outputs: |
| 81 | + runner: ${{ steps.pick.outputs.runner }} |
| 82 | + label: ${{ steps.pick.outputs.label }} |
| 83 | + steps: |
| 84 | + - name: Pick runner |
| 85 | + id: pick |
| 86 | + env: |
| 87 | + # Read through env rather than interpolating directly into the script: |
| 88 | + # `github.event_name` is a fixed vocabulary, but keeping the habit means |
| 89 | + # no future edit here can grow a script-injection sink. |
| 90 | + EVENT_NAME: ${{ github.event_name }} |
| 91 | + USE_SELF_HOSTED: ${{ vars.OCX_SELF_HOSTED_WINDOWS }} |
| 92 | + shell: bash |
| 93 | + run: | |
| 94 | + set -euo pipefail |
| 95 | + trusted=no |
| 96 | + case "$EVENT_NAME" in |
| 97 | + push|workflow_dispatch) trusted=yes ;; |
| 98 | + esac |
| 99 | +
|
| 100 | + # Repository variable OCX_SELF_HOSTED_WINDOWS is an OPERATIONAL switch, |
| 101 | + # not a security control: a PR that rewrites this script ignores it for |
| 102 | + # the same reason it ignores the event check above. Its job is to keep CI |
| 103 | + # working when the box is off or busy. Anything other than `1` — |
| 104 | + # including unset, the state before a runner exists — falls back to |
| 105 | + # windows-latest. |
| 106 | + if [ "$trusted" = "yes" ] && [ "${USE_SELF_HOSTED:-}" = "1" ]; then |
| 107 | + echo 'runner=["self-hosted","Windows","X64","ocx-home"]' >> "$GITHUB_OUTPUT" |
| 108 | + echo 'label=self-hosted (ocx-home)' >> "$GITHUB_OUTPUT" |
| 109 | + else |
| 110 | + echo 'runner="windows-latest"' >> "$GITHUB_OUTPUT" |
| 111 | + echo 'label=windows-latest' >> "$GITHUB_OUTPUT" |
| 112 | + fi |
| 113 | +
|
48 | 114 | test: |
49 | | - name: ${{ matrix.os }} |
50 | | - runs-on: ${{ matrix.os }} |
| 115 | + name: ${{ matrix.name }} |
| 116 | + needs: select-windows-runner |
| 117 | + runs-on: ${{ matrix.runner }} |
51 | 118 | # Windows dominates this matrix: on run 30459554635 the same suite took |
52 | 119 | # ubuntu 4.6min / macos 5.6min / windows 11.8min. Against the previous |
53 | 120 | # 12-minute ceiling that left ~12s of headroom, so runner variance decided |
54 | 121 | # the result rather than the code under review — #711's rerun finished at |
55 | 122 | # 11.8min and passed while #653's was killed at 12.0min (issue #717). |
56 | 123 | # A cancelled job renders as `fail` in `gh pr checks`, so that flakiness |
57 | | - # reads as a broken PR. 20 minutes keeps a green Windows run green with |
58 | | - # real margin; it is not a licence for the suite to grow into it. If |
59 | | - # Windows approaches this ceiling too, fix the 2.5x platform gap instead |
60 | | - # of raising the number again. |
61 | | - timeout-minutes: 20 |
| 124 | + # reads as a broken PR. After the 2026-08-01 state-store admission merge, |
| 125 | + # Windows under `bun test --isolate` on #827 hit the 20-minute kill while |
| 126 | + # still green mid-suite (~19m of tests). 30 minutes is the margin for that |
| 127 | + # tip — not a licence to absorb hung tests (see oauth mutation waitMs |
| 128 | + # unref fix). Shrink the suite / close the platform gap rather than raising |
| 129 | + # this again for ordinary variance. |
| 130 | + timeout-minutes: 30 |
62 | 131 | strategy: |
63 | 132 | fail-fast: false |
64 | 133 | matrix: |
65 | | - os: [ubuntu-latest, windows-latest, macos-latest] |
| 134 | + # `name` is spelled out rather than derived from `runner`: a runner given |
| 135 | + # as a label array renders as "self-hosted Windows X64 ocx-home", so the |
| 136 | + # check name would change with the routing and break any branch |
| 137 | + # protection rule that names it. Keeping `windows` fixed means the |
| 138 | + # required check stays the same whichever machine served it. |
| 139 | + include: |
| 140 | + - name: ubuntu |
| 141 | + runner: ubuntu-latest |
| 142 | + - name: windows |
| 143 | + runner: ${{ fromJSON(needs.select-windows-runner.outputs.runner) }} |
| 144 | + - name: macos |
| 145 | + runner: macos-latest |
66 | 146 | steps: |
| 147 | + - name: Show selected runner |
| 148 | + if: matrix.name == 'windows' |
| 149 | + shell: bash |
| 150 | + run: echo "windows leg on ${{ needs.select-windows-runner.outputs.label }}" |
| 151 | + |
| 152 | + # A self-hosted runner keeps its working directory between jobs. Without an |
| 153 | + # explicit wipe, a file deleted in the commit under test survives on disk |
| 154 | + # and the suite passes against a tree that no longer exists in git. |
| 155 | + # `--ephemeral` registration de-registers the runner after each job but does |
| 156 | + # not clean the workspace, so this step is what makes the checkout honest. |
| 157 | + - name: Clean workspace (self-hosted only) |
| 158 | + if: runner.environment == 'self-hosted' |
| 159 | + shell: bash |
| 160 | + run: git clean -xffd . || true |
| 161 | + |
67 | 162 | - name: Checkout |
68 | 163 | uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 |
69 | 164 |
|
@@ -113,6 +208,10 @@ jobs: |
113 | 208 | strategy: |
114 | 209 | fail-fast: false |
115 | 210 | matrix: |
| 211 | + # Deliberately NOT routed to the self-hosted box. This job runs |
| 212 | + # `npm install -g`, which writes into the machine's global prefix and |
| 213 | + # would leave an `ocx` on a maintainer's personal PATH. It is an |
| 214 | + # 8-minute job, so there is nothing to win by moving it. |
116 | 215 | os: [ubuntu-latest, windows-latest, macos-latest] |
117 | 216 | steps: |
118 | 217 | - name: Checkout |
|
0 commit comments