-
Notifications
You must be signed in to change notification settings - Fork 514
245 lines (219 loc) · 9.13 KB
/
Copy pathci.yml
File metadata and controls
245 lines (219 loc) · 9.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
name: Cross-platform CI
on:
pull_request:
branches: [main, dev]
paths:
- "src/**"
- "bin/**"
- "tests/**"
- "scripts/**"
- "gui/**"
- ".gitattributes"
- ".npmignore"
- "package.json"
- "bun.lock"
- "tsconfig.json"
- ".github/workflows/ci.yml"
- ".github/workflows/release.yml"
- ".github/workflows/enforce-pr-target.yml"
- ".github/workflows/stale-needs-info.yml"
push:
branches: [main, preview, dev]
paths:
- "src/**"
- "bin/**"
- "tests/**"
- "scripts/**"
- "gui/**"
- ".gitattributes"
- ".npmignore"
- "package.json"
- "bun.lock"
- "tsconfig.json"
- ".github/workflows/ci.yml"
- ".github/workflows/release.yml"
- ".github/workflows/enforce-pr-target.yml"
- ".github/workflows/stale-needs-info.yml"
workflow_dispatch:
permissions:
contents: read
# Retrigger CI after dir-fsync / oauth deadline follow-ups (tip 34a1ac46).
concurrency:
group: cross-platform-ci-${{ github.ref }}
cancel-in-progress: true
jobs:
# Which Windows runner this run is allowed to use.
#
# READ THIS BEFORE TREATING IT AS A SECURITY BOUNDARY: it is not one.
#
# On `pull_request` this workflow is loaded from the PR head, so the `case`
# below is owned by the proposed patch exactly like an `if:` guard would be.
# A hostile PR can delete the branch and hardcode the self-hosted labels into
# `$GITHUB_OUTPUT`, and `runs-on` will honour it. That this job runs on
# `ubuntu-latest` changes nothing — the untrusted part is its OUTPUT, not its
# host. `.github/workflows/ci.yml` is in this workflow's `pull_request.paths`,
# so such an edit triggers its own run.
#
# What actually keeps untrusted code off a self-hosted runner lives OUTSIDE
# this file, where a PR cannot reach it: the fork-PR approval policy
# (`all_external_contributors`) and the judgement of whoever clicks approve.
# Runner groups would be the other lever, but they are an organisation
# feature and this repository is user-owned, so the approval policy is the
# only one available here. GitHub's own guidance is to avoid self-hosted
# runners on public repositories for this reason.
#
# So read the routing below as a COST control that keeps honest pull requests
# on GitHub-hosted runners, not as a guarantee about hostile ones.
#
# `push` on dev/main/preview requires the push permission, and
# `workflow_dispatch` requires write access, so both carry a trusted author.
# A trusted author is not audited code: merging a contributor PR into `dev`
# fires `push`, and its dependencies and postinstall hooks then run here.
select-windows-runner:
name: select windows runner
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
runner: ${{ steps.pick.outputs.runner }}
label: ${{ steps.pick.outputs.label }}
steps:
- name: Pick runner
id: pick
env:
# Read through env rather than interpolating directly into the script:
# `github.event_name` is a fixed vocabulary, but keeping the habit means
# no future edit here can grow a script-injection sink.
EVENT_NAME: ${{ github.event_name }}
USE_SELF_HOSTED: ${{ vars.OCX_SELF_HOSTED_WINDOWS }}
shell: bash
run: |
set -euo pipefail
trusted=no
case "$EVENT_NAME" in
push|workflow_dispatch) trusted=yes ;;
esac
# Repository variable OCX_SELF_HOSTED_WINDOWS is an OPERATIONAL switch,
# not a security control: a PR that rewrites this script ignores it for
# the same reason it ignores the event check above. Its job is to keep CI
# working when the box is off or busy. Anything other than `1` —
# including unset, the state before a runner exists — falls back to
# windows-latest.
if [ "$trusted" = "yes" ] && [ "${USE_SELF_HOSTED:-}" = "1" ]; then
echo 'runner=["self-hosted","Windows","X64","ocx-home"]' >> "$GITHUB_OUTPUT"
echo 'label=self-hosted (ocx-home)' >> "$GITHUB_OUTPUT"
else
echo 'runner="windows-latest"' >> "$GITHUB_OUTPUT"
echo 'label=windows-latest' >> "$GITHUB_OUTPUT"
fi
test:
name: ${{ matrix.name }}
needs: select-windows-runner
runs-on: ${{ matrix.runner }}
# Windows dominates this matrix: on run 30459554635 the same suite took
# ubuntu 4.6min / macos 5.6min / windows 11.8min. Against the previous
# 12-minute ceiling that left ~12s of headroom, so runner variance decided
# the result rather than the code under review — #711's rerun finished at
# 11.8min and passed while #653's was killed at 12.0min (issue #717).
# A cancelled job renders as `fail` in `gh pr checks`, so that flakiness
# reads as a broken PR. After the 2026-08-01 state-store admission merge,
# Windows under `bun test --isolate` on #827 hit the 20-minute kill while
# still green mid-suite (~19m of tests). 30 minutes is the margin for that
# tip — not a licence to absorb hung tests (see oauth mutation waitMs
# unref fix). Shrink the suite / close the platform gap rather than raising
# this again for ordinary variance.
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
# `name` is spelled out rather than derived from `runner`: a runner given
# as a label array renders as "self-hosted Windows X64 ocx-home", so the
# check name would change with the routing and break any branch
# protection rule that names it. Keeping `windows` fixed means the
# required check stays the same whichever machine served it.
include:
- name: ubuntu
runner: ubuntu-latest
- name: windows
runner: ${{ fromJSON(needs.select-windows-runner.outputs.runner) }}
- name: macos
runner: macos-latest
steps:
- name: Show selected runner
if: matrix.name == 'windows'
shell: bash
run: echo "windows leg on ${{ needs.select-windows-runner.outputs.label }}"
# A self-hosted runner keeps its working directory between jobs. Without an
# explicit wipe, a file deleted in the commit under test survives on disk
# and the suite passes against a tree that no longer exists in git.
# `--ephemeral` registration de-registers the runner after each job but does
# not clean the workspace, so this step is what makes the checkout honest.
- name: Clean workspace (self-hosted only)
if: runner.environment == 'self-hosted'
shell: bash
run: git clean -xffd . || true
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd gui
bun install --frozen-lockfile
- name: Typecheck
run: bun x tsc --noEmit
- name: Test
run: bun test --isolate tests
- name: GUI tests
run: cd gui && bun test tests
- name: Privacy scan
run: bun run privacy:scan
- name: Check release helper syntax
run: bun build scripts/release.ts --target=bun --outdir=.tmp/ci-release-script-check
- name: GUI lint
run: |
cd gui
bun run lint
- name: GUI build
run: |
cd gui
bun run build
- name: CLI help smoke
run: bun run src/cli/index.ts help
npm-global-smoke:
name: npm-global ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 8
strategy:
fail-fast: false
matrix:
# Deliberately NOT routed to the self-hosted box. This job runs
# `npm install -g`, which writes into the machine's global prefix and
# would leave an `ocx` on a maintainer's personal PATH. It is an
# 8-minute job, so there is nothing to win by moving it.
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# Deliberately NO setup-bun: prove `npm install -g` works without a
# separately-installed Bun. The launcher uses the bundled `bun` dependency.
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
- name: Install package dependencies
run: npm install
- name: Build package assets
run: npm run build:gui
- name: Pack
shell: bash
run: npm pack --json > pack.json
- name: Verify packed GUI asset
run: node -e "const p=require('./pack.json')[0]; if (!p.files.some(f => f.path === 'gui/dist/index.html')) { console.error('missing gui/dist/index.html in npm pack'); process.exit(1); }"
- name: Install globally (downloads bundled bun)
shell: bash
run: npm install -g ./bitkyc08-opencodex-*.tgz
- name: ocx help via bundled bun
run: ocx help