Repository navigation
451 lines (403 loc) · 22.1 KB
/
Copy pathci.yml
File metadata and controls
451 lines (403 loc) · 22.1 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
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
build-and-test:
name: Build & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# No setup-node here, unlike every other job: the self-host installer
# step below runs its own `pnpm install`, which provisions pnpm's pinned
# runtime (devEngines.runtime) whatever is on PATH. So this job takes its
# one Node.js from that install and puts it on PATH for the steps that
# call `node` directly.
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Put the pinned Node.js on PATH
run: |
set -euo pipefail
dir="$(pnpm exec node -p 'require("node:path").dirname(process.execPath)')"
pin="v$("$dir/node" -p 'require("./package.json").devEngines.runtime.version')"
actual="$("$dir/node" --version)"
[ "$actual" = "$pin" ] || { echo "::error::pnpm exec node is $actual, not the pinned $pin"; exit 1; }
echo "$dir" >> "$GITHUB_PATH"
# The disclosed dependency snapshots are a `FAIL IF` in docs/specs/security-supply-chain.md
# (Disclosure), but until this step existed the only thing
# that ever ran the generator was the nightly security audit — strictly
# after the fact, and only if it reached that check. Two prod-dependency
# bumps shipped undisclosed that way (`ws` via vscode-ext, `hono` via a
# Renovate lockfile-only bump). Running it here makes forgetting to
# regenerate a failed PR instead of a finding filed the next morning.
#
# The generator needs `node_modules` to match the lockfile — it resolves
# every dependency by walking real directories and throws rather than
# under-reporting — which is exactly what `--frozen-lockfile` above
# guarantees.
- name: Dependency disclosure is current
run: |
node website/scripts/generate-deps.js
if ! git diff --quiet -- website/src/data/; then
echo "::error::The disclosed dependency snapshots are stale. Run \`node website/scripts/generate-deps.js\` and commit the result."
git --no-pager diff -- website/src/data/
exit 1
fi
# zsh is not on the ubuntu runner image, and it is the default shell on
# macOS — our primary platform. Without it, `standalone/sidecar`'s
# shell-integration suite silently covered only bash, which is half of
# what stands behind an emit-side security fix (docs/specs/terminal-state.md's OSC 633
# rules; the emitters are the boundary, since the parser cannot defend
# against a terminator that arrives inside a directory name). The suite
# names the shells it covered on every run, so a future image change that
# drops one is visible rather than silent.
# `update` first: the runner image's apt lists are baked at image build, so
# once the archive rotates zsh's version the cached Packages entry 404s.
# `timeout` because a degraded Azure mirror dribbles bytes rather than
# failing — see the fuller treatment in Standalone Smoketest below, which
# also rotates mirrors. That much machinery is not worth it for one
# package; the retry here covers the common case without turning a
# `Build & Test` red for a reason unrelated to the diff.
# 13 minutes, not 6: the budget has to outlast the schedule it wraps, or
# Actions kills the step mid-attempt and the ::error:: below — the line
# that says what the failure costs — never prints. Worst case is
# 3 x (120 update + 120 install) + 2 x 15 sleep = 750s, and the last sleep
# is guarded to keep that `2 x` true: unguarded it is 3 x 15 = 765s, which
# halves the headroom for pure dead time before a step that is already
# failing.
- name: Install zsh (shell-integration tests)
timeout-minutes: 13
run: |
for i in 1 2 3; do
if sudo timeout 120 apt-get update -q \
&& sudo timeout 120 apt-get install -y -q --no-install-recommends zsh; then
exit 0
fi
echo "::warning::apt attempt $i for zsh failed or timed out"
if [ "$i" -lt 3 ]; then sleep 15; fi
done
echo "::error::could not install zsh; the shell-integration suite would silently cover only bash"
exit 1
# Library tests have their own runner. test:other finishes the checkout
# isolation check before overlapping audit and package tests.
- name: Test
timeout-minutes: 20
run: pnpm test:other
- name: Build
run: pnpm build
# The Linux installer is the only one CI can execute, and most of what it
# does is shared with its two siblings: the release build, the staging,
# the self-contained runtime copy, the candidate health probe on an
# ephemeral port, the `current` switch, the prune, and the generated
# `manage`. Test mode stops short of systemd and Serve, and the injected
# origin is what lets it run on a machine with no tailnet.
- name: Self-host installer (Linux, test mode)
env:
DORMOUSE_INSTALL_TEST: '1'
DORMOUSE_INSTALL_ORIGIN: https://ci.example.ts.net
run: |
set -euo pipefail
root="$(mktemp -d)/dormouse-relay"
offer="$root/run/enroll-offer.json"
# Prints the token, because it must rotate on every run before the
# first Host enrollment — and this job is the only executable check.
#
# The shape check is the server's own `isEnrollmentOffer`, imported
# rather than restated: a CI copy of the token regex and the origin
# rule is free to drift from the validator the redemption path
# actually runs, and then agrees with itself while the server rejects
# what the installer writes. No build step is needed for it: the
# `Test` step above builds remote-lib-common, as does the
# `pnpm --filter relay build` inside every install below.
# The single-quoted script is JavaScript, so its `${…}` is not shell's.
# shellcheck disable=SC2016
read_offer() {
node --input-type=module -e '
import { readFileSync, statSync } from "node:fs";
import { isEnrollmentOffer } from "./remote-lib-common/dist/index.js";
const [path, wantOrigin] = process.argv.slice(1);
const mode = statSync(path).mode & 0o777;
if (mode !== 0o600) throw new Error(`${path} is mode ${mode.toString(8)}, expected 600`);
const offer = JSON.parse(readFileSync(path, "utf8"));
if (!isEnrollmentOffer(offer)) throw new Error(`not an enrollment offer: ${JSON.stringify(offer)}`);
// Beyond the structural guard, and nothing else checks it: the
// server hard-rejects a mintedAt it cannot parse, which the shape
// guard — a length bound — accepts.
if (Number.isNaN(Date.parse(offer.mintedAt))) throw new Error(`mintedAt is ${offer.mintedAt}`);
if (offer.origin !== wantOrigin) throw new Error(`origin is ${offer.origin}`);
process.stdout.write(offer.token);
' "$offer" "$DORMOUSE_INSTALL_ORIGIN"
}
# Twice: the first is a first install, the second exercises the update
# path — previous pointer, prune, and preserving config/relay.env.
DORMOUSE_INSTALL_ROOT="$root" ./deploy/local/install-linux.sh --yes
before="$(sha256sum "$root/config/relay.env" | cut -d' ' -f1)"
first_token="$(read_offer)"
DORMOUSE_INSTALL_ROOT="$root" ./deploy/local/install-linux.sh --yes
after="$(sha256sum "$root/config/relay.env" | cut -d' ' -f1)"
second_token="$(read_offer)"
[ "$before" = "$after" ] || { echo "::error::relay.env was not preserved across an update"; exit 1; }
[ "$first_token" != "$second_token" ] || { echo "::error::the enrollment offer was not re-minted on the update"; exit 1; }
# burrows.json existence, not its current row count, is the durable
# "first Burrow happened" marker. Even an empty hand-edited file keeps
# a later installer run from reopening bootstrap.
printf '[]\n' > "$root/state/burrows.json"
chmod 0600 "$root/state/burrows.json"
DORMOUSE_INSTALL_ROOT="$root" ./deploy/local/install-linux.sh --yes
[ ! -e "$offer" ] || { echo "::error::the enrollment offer survived after burrows.json existed"; exit 1; }
[ -L "$root/previous" ] || { echo "::error::no previous release retained after an update"; exit 1; }
bash -n "$root/bin/manage"
bash -n "$root/bin/run-relay"
library-test:
name: Library Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: package.json
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
# --no-runtime: setup-node above already put the pinned Node.js on PATH,
# so pnpm does not download the same devEngines.runtime from nodejs.org.
- name: Install dependencies
run: pnpm install --frozen-lockfile --no-runtime
# browser-launch-env.test.ts skips its real POSIX shell cases without zsh.
- name: Install zsh (browser shell tests)
timeout-minutes: 13
run: |
for i in 1 2 3; do
if sudo timeout 120 apt-get update -q \
&& sudo timeout 120 apt-get install -y -q --no-install-recommends zsh; then
exit 0
fi
echo "::warning::apt attempt $i for zsh failed or timed out"
if [ "$i" -lt 3 ]; then sleep 15; fi
done
echo "::error::could not install zsh; the browser shell tests would silently skip zsh"
exit 1
# Includes the library's pretest builds and typecheck on a fresh checkout.
# The library suite runs with Vitest's default worker settings.
- name: Test library
timeout-minutes: 20
run: pnpm test:lib
# The workflows themselves: actionlint for syntax, expressions, and the
# shell in every `run:` block; zizmor for GitHub Actions security
# (`.github/zizmor.yml` names each accepted exception). The repository's own
# workflow rules — docs/specs/security-ci.md's `FAIL IF` lines — are
# `scripts/workflow-lint.mjs`, which runs in `test:checks` above. Both tools
# sit outside the lockfile, so each is pinned exactly and verified by hash
# (docs/specs/security-supply-chain.md -> "Cooldown and alerts").
workflow-lint:
name: Workflow Lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install actionlint
env:
ACTIONLINT_VERSION: 1.7.12
ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8
run: |
set -euo pipefail
tarball="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
curl -fsSL -o "$RUNNER_TEMP/$tarball" "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/$tarball"
echo "$ACTIONLINT_SHA256 $RUNNER_TEMP/$tarball" | sha256sum -c -
mkdir -p "$RUNNER_TEMP/actionlint"
tar -xzf "$RUNNER_TEMP/$tarball" -C "$RUNNER_TEMP/actionlint" actionlint
# Uses the runner's preinstalled shellcheck for `run:` blocks.
- name: actionlint
run: '"$RUNNER_TEMP/actionlint/actionlint" -color'
- name: Install zizmor
run: |
set -euo pipefail
python3 -m venv "$RUNNER_TEMP/zizmor"
"$RUNNER_TEMP/zizmor/bin/pip" install --require-hashes --only-binary :all: --no-deps -r .github/zizmor-requirements.txt
# Online, so impostor commits and known-vulnerable pins are checked too;
# the token reads public repositories only.
- name: zizmor
env:
GH_TOKEN: ${{ github.token }}
run: '"$RUNNER_TEMP/zizmor/bin/zizmor" --format github .github/workflows'
webview-smoketest:
name: Webview Boot Smoketest
runs-on: ubuntu-latest
# Its own job, in parallel with Build & Test: this is the only check that
# needs a browser, and pinning that cost to one job keeps `pnpm test` free of
# it. It is also the only check that runs the shipped bundle rather than
# inspecting it — the VS Code webview's CSP is enforced by Chromium, so a
# policy that blocks the app is invisible to every string-level test we have
# (docs/specs/vscode.rationale.md -> "CSP policy").
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: package.json
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
# Frozen for the same supply-chain reason as every other job here;
# --no-runtime as in Library Tests.
- name: Install dependencies
run: pnpm install --frozen-lockfile --no-runtime
# `playwright-core` ships no browser, deliberately — a unit-test run should
# not drag one down. Fetch it here, where it is actually used.
- name: Install Chromium
run: pnpm --filter dormouse exec playwright-core install --with-deps chromium
# The smoketest loads what the build emits, so the build has to precede
# it. `build:frontend` alone — this job never touches the extension host
# bundle — and it reads every workspace package from source, so a clean
# checkout needs no other build first.
- name: Build the webview frontend
run: pnpm --filter dormouse build:frontend
- name: Smoketest
run: pnpm --filter dormouse test:smoke
standalone-smoketest:
name: Standalone Smoketest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# `cargo check` runs standalone/src-tauri/build.rs, which fails the build
# unless the Node.js on PATH exactly matches package.json's
# devEngines.runtime.version pin. setup-node's node-version-file reads that
# exact pin from package.json (volta.node → devEngines.runtime →
# engines.node), so the smoketest can't drift off the pin the way a bare
# `node-version: 24` would when the runner image bumps its 24.x.
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: package.json
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
# rustup ships on every GitHub-hosted runner, so no third-party action
# installs the toolchain: dtolnay/rust-toolchain force-pushes the branch
# its SHA pin points into on every upstream change, which turns the pin
# into an impostor commit for zizmor.
- name: Install Rust stable
shell: bash
run: rustup toolchain install stable --profile minimal --no-self-update && rustup default stable
- name: Install system dependencies
# apt mirrors are flaky: a healthy run finishes in <1min, but a degraded
# Azure mirror can dribble bytes for 15-80min, and retrying the SAME mirror
# doesn't help. Wrap each apt call in `timeout` (turns "slow" into a failed
# attempt) and rotate the mirror host between attempts so a sustained
# single-mirror problem is escaped. Azure first (fast, same datacenter),
# archive.ubuntu.com as fallback. --no-install-recommends trims the download.
# The runner image's ubuntu.sources points at mirror+file:/etc/apt/apt-mirrors.txt,
# which lists the hosts, so that file is the one the rotation has to rewrite.
#
# 16 minutes, not 10: the budget has to outlast the schedule it wraps, or
# Actions kills the step mid-attempt and the ::error:: below — the line
# that says the mirrors were rotated and still failed — never prints,
# leaving a bare timeout in exactly the sustained-outage case the mirror
# rotation exists for. Worst case is
# 3 x (120 update + 180 install) + 3 x 15 sleep = 945s (the loop
# sleeps after the third failed attempt too, before the ::error::).
timeout-minutes: 16
run: |
mirrors=(azure.archive.ubuntu.com archive.ubuntu.com)
src_files=(/etc/apt/apt-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list)
for i in 0 1 2; do
host=${mirrors[$(( i % ${#mirrors[@]} ))]}
for f in "${src_files[@]}"; do
[ -f "$f" ] && sudo sed -i -E "s|https?://[a-z.]*archive\.ubuntu\.com/ubuntu|http://$host/ubuntu|g" "$f"
done
echo "::group::apt attempt $((i + 1)) via $host"
if sudo timeout 120 apt-get update -q && \
sudo timeout 180 apt-get install -y -q --no-install-recommends \
libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev; then
echo "::endgroup::"; exit 0
fi
echo "::endgroup::"
echo "::warning::apt via $host failed or timed out; rotating mirror, retrying in 15s"
sleep 15
done
echo "::error::apt failed after 3 attempts across mirrors"
exit 1
# Frozen for the same supply-chain reason as every other job here;
# --no-runtime as in Library Tests, which also leaves build.rs the
# setup-node binary on PATH that it checks against the pin.
- name: Install npm dependencies
run: pnpm install --frozen-lockfile --no-runtime
working-directory: standalone
- name: TypeScript check
run: npx tsc --noEmit
working-directory: standalone
# `cargo test` rather than `cargo check`: the crate carries unit tests
# (path/session/sidecar resolution in lib.rs) that no job ran, so they
# compiled at most as `cargo check` and never executed. Building the test
# harness also links, which `check` does not.
- name: Cargo test
run: cargo test
working-directory: standalone/src-tauri
standalone-platform-check:
# The job above runs on Linux only, so it compiles none of the crate's
# `#[cfg(windows)]` / `#[cfg(target_os = "macos")]` code — two whole modules
# (clipboard_win.rs, pe_subsystem.rs), the platform arms throughout lib.rs
# and build.rs, and the unit tests gated with them. Without this job their
# first compile is the Windows / macOS leg of release.yml, which runs only on
# a `v*` tag: a typo in Windows-only Rust surfaces as a failed release rather
# than as a failed PR. Deliberately a separate job from the smoketest above,
# so that job keeps its registered check name.
name: Standalone Platform Check (${{ matrix.platform }})
strategy:
# Each platform's compile is independent; a Windows failure must not hide
# what macOS would have said.
fail-fast: false
matrix:
platform: [windows-latest, macos-latest]
runs-on: ${{ matrix.platform }}
# A hung compile on a premium-billed runner must not ride the 6-hour job
# default. Cold-cache runs land at ~2min (macOS) and ~5min (Windows).
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Same pin-driven setup as the smoketest above: build.rs fails unless the
# Node.js on PATH matches package.json's devEngines.runtime.version.
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: package.json
# Same direct rustup install as the smoketest above.
- name: Install Rust stable
shell: bash
run: rustup toolchain install stable --profile minimal --no-self-update && rustup default stable
# Windows and macOS runners bill at a multiple of Linux, so the dependency
# compile is cached rather than paid on every PR.
- name: Rust cache
uses: swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
workspaces: standalone/src-tauri
# The full TypeScript check stays on Linux. This job covers native Rust,
# the dor launchers, and actual shell execution for browser initialization
# and for the paths a drop types into cmd and PowerShell.
- name: Cargo test
run: cargo test
working-directory: standalone/src-tauri
# `dor.cmd` is the one launcher only Windows can run. The test needs Node
# alone — no install, no build — so it rides this job rather than a
# Windows runner of its own.
- name: dor launcher test
run: node --test dor/test/launcher.test.mjs
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
# These small suites need no native addon builds; --no-runtime as in
# Library Tests.
- name: Install shell test dependencies
run: pnpm install --frozen-lockfile --ignore-scripts --no-runtime
- name: Windows browser host and shell tests
run: pnpm --dir lib exec vitest run src/host/browser-launch-env.test.ts src/host/agent-browser-host.test.ts src/lib/shell-escape.test.ts
# node-pty ships the Windows addon and ConPTY DLL as prebuilds, so the
# install above suffices. The Linux suite cannot exercise native close.
- name: Windows PTY shutdown tests
if: runner.os == 'Windows'
run: pnpm exec node --test --test-force-exit standalone/sidecar/graceful-kill-windows.test.js standalone/sidecar/pty-host-wiring.test.js