-
Notifications
You must be signed in to change notification settings - Fork 1
710 lines (611 loc) · 30.4 KB
/
Copy pathci.yml
File metadata and controls
710 lines (611 loc) · 30.4 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
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# `labeled` and `unlabeled` are here because a label decides a required
# result: applying `ci-main-red-fix` has to recompute `green`, and removing
# it has to recompute it back. Listing types replaces the default set, so
# the three defaults are spelled out beside them.
types: [opened, reopened, synchronize, labeled, unlabeled]
jobs:
# An ordinary pull request may claim `green` only once CI has completed
# successfully for `main`'s exact current head.
#
# Main Health reports a red `main`; reporting is not gating. Without this job a
# pull request proves itself against a base nothing proved, and the next `main`
# failure cannot be told apart from the one already open. Exactness is the
# whole claim — a successful run for the previous head is the state a red
# `main` is in one commit after it broke — so the decision reuses Main
# Health's own authoritative-run rules rather than re-deciding main health.
#
# Only `contents: read` and `actions: read`: this job decides a required
# check, so it holds no permission to write an issue, a pull request, or the
# repository. That is also why it checks out the pull request rather than
# `main` — nothing it runs is privileged, and the gate has to be changeable by
# the pull request that changes it.
#
# A `main` push skips it. The push is the run that decides main health, so a
# job waiting on its own workflow would never resolve.
main-green:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
# The gate waits for main's verdict rather than failing when one has not
# arrived. Main's own CI takes roughly half of this, which leaves one broad
# margin and stays finite while main keeps advancing. The waiter gives up
# inside this bound and says what it was waiting on; reaching the bound
# itself means the pull request obtained no proof — not that main failed.
timeout-minutes: 60
permissions:
actions: read # list main's CI runs
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
with:
deno-version: v2.9.5
- name: CI has proven main's exact current head
env:
GH_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
PULL_REQUEST_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
run: deno run --frozen --allow-env --allow-run=gh scripts/main-green.ts
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- run: pnpm install
- name: Lint + Format
run: pnpm lint
# The corpus is partitioned by measured per-file weight, so the shards finish
# together rather than one carrying the slow files. `fail-fast: false` is what
# makes a failing shard report its own failure instead of cancelling its
# siblings and hiding theirs.
#
# Everything above `Test` repeats in every shard. That is deliberate: it sits
# outside the window this is trying to shorten, and sharing it would introduce
# an artifact-and-dependency boundary between jobs that #279 spent its length
# removing.
test-deno:
name: test-deno (${{ matrix.shard }}/15)
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
with:
deno-version: v2.9.5
# `deno task test` covers `scripts/tests/**`, where the runtime drivers
# spawn a literal `bun`. The runner image ships Node but not bun.
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.0
- name: Typecheck
run: deno task check
- name: Publish workflow is generated from manifests
run: |
deno task gen:publish-workflow
git diff --exit-code .github/workflows/publish-packages.yml \
|| { echo "::error::publish-packages.yml is out of date — run 'deno task gen:publish-workflow' and commit the result"; exit 1; }
# The npm build packages every workspace dependency of the CLI, and
# `@executablemd/web` carries a generated browser bundle that is not
# committed. Without this the CLI's npm artifact cannot be built at all.
# Preparing is its own step: a build installs nothing (AGENTS.md).
- name: Install dependencies
run: deno task deps
- name: Build the browser bundle
run: deno task build:web
- name: Test
run: deno run --allow-all --frozen scripts/runtime-tests.ts deno ${{ matrix.shard }}/15
# The same command the release publishes with, minus --dry-run. Catches slow
# types and unresolvable specifiers before they reach a tag.
jsr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
with:
deno-version: v2.9.5
- name: Install dependencies
run: deno task deps
# With the generated bundle absent the negated `publish.exclude` glob
# matches nothing and the dry run quietly checks a package the release
# would never upload. Building first makes this validate the real shape.
- name: Build the browser bundle
run: deno task build:web
- name: The workspace is publishable to JSR
run: deno task check:jsr
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
with:
deno-version: v2.9.5
# `README.md#Build` prepares the checkout before it compiles, and
# preparation owns both dependency layouts — so this job needs the Node
# and pnpm halves as well as Deno's.
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
# Preparing the checkout is the installation owner's job, not a side
# effect of the first `deno task xmd` to run: under
# `nodeModulesDir: "auto"` that task would initialize `node_modules`
# itself, before any README block executed. So this job prepares first,
# exactly as the README tells a contributor to on a fresh clone.
#
# The Build target then prepares again, because preparation is its
# preamble and every target inherits it. The repeat is deliberate: what
# the document does is not conditional on who ran it.
- name: Prepare the checkout
run: deno task setup
# The README documents how a contributor prepares a checkout and builds
# the binary, and this is that document doing it. Everything below
# consumes the `dist/xmd` it produced, so a README whose Build target
# stopped working stops this job.
- name: Prepare and build through the README
run: deno task xmd run README.md#Build
- name: Smoke test the compiled binary
run: |
./dist/xmd test smoke-test/README.md \
--include smoke-test \
--include packages/core/components \
--raw
- name: Smoke test attached-service ping-pong with the compiled binary
run: |
./dist/xmd test smoke-test/attached-service-ping-pong.test.md \
--include smoke-test \
--include packages/core/components \
--raw
# Foreground execution is about what a caller sees and what status they
# get, and only the binary shows both surviving `deno compile`.
- name: Smoke test foreground output and fail-fast with the compiled binary
run: deno run --allow-all --frozen scripts/smoke-foreground.ts
# The script installs a second copy of core beside a repository component.
# The declaration must cross into the bundled engine so the failure prints
# and execution continues.
- name: Smoke test metadata from a separately loaded core
run: deno run --allow-all --frozen scripts/smoke-loaded-copy.ts
# The same two-copies shape, asserted rather than smoked: which form a
# component runs when the binary minted the invocation and a file on the
# search path supplied the implementation. It lives here because its
# subject is `dist/xmd`, and this is the job that builds one.
- name: Form dispatch across the compiled binary's loaded copies
run: deno test --allow-all --frozen scripts/tests/component-form-dispatch.test.ts
# `<Plan>` is packaged Markdown rather than a module, so it is exactly the
# kind of thing `deno compile` ships only because an `--include` names it.
# Asking the binary which Component source it carries is what catches a forgotten
# include here rather than at a person's first `xmd plan`.
- name: The <Plan> Component the compiled binary carries
run: deno test --allow-all --frozen scripts/tests/plan-component-compiled.test.ts
# Each package's `components.md` is a packaged asset on the same footing,
# and the long-form documentation is the only thing that reports whether
# one shipped — a binary missing them still lists every component and
# prints every component's registry metadata. `release.yml` runs this
# against its own Linux x64 binary before attesting; here it runs against
# the one the README's Build target produced, so a dropped include fails a
# pull request rather than a release.
- name: Smoke test the packaged component documentation
run: deno run --allow-all --frozen scripts/smoke-documentation.ts
# The ordinary repository provider is assembled at a runtime-named
# entrypoint and holds managed checkouts with a kernel-released advisory
# lock, so only the binary shows both surviving `deno compile`. The script
# runs two of them at once against a managed root of its own.
- name: Smoke test repository composition with the compiled binary
run: deno run --allow-all --frozen scripts/smoke-run-composition.ts
# `<Fetch>` resolves from core's registry, requests through the contextual
# Fetch adapter, and detaches the response before binding it. All three
# live in the module graph, so only the binary shows they survived
# `deno compile`. The script owns the server the request reaches.
- name: Smoke test a retained HTTP read with the compiled binary
run: deno run --allow-all --frozen scripts/smoke-fetch.ts
# A built-in resolves from the module graph rather than a search path.
# Only the compiled binary proves it survives `deno compile`. The
# directory target discovers every colocated document beneath core's
# source at once — the built-in components under `components/`, and the
# structural directives, which resolve no file at all.
- name: Smoke test the built-ins with no search path
run: ./dist/xmd test packages/core/src --raw
# An inline root document exercises the compiled module graph the same way
# a file does, and only the binary proves the graph survived `deno compile`.
- name: Smoke test an inline document
run: |
set -eu
test "$(./dist/xmd -e '# Hello' --raw)" = "# Hello"
# Components resolve from the current directory, not from the root's
# identity, so a search path still finds them.
./dist/xmd -e '<Badge />' --include smoke-test --raw | grep -q '✓ verified'
# So does an ordinary relative filesystem read.
./dist/xmd -e '<File path="smoke-test/Badge.md" />' --raw | grep -q 'verified'
# A stray <Else> is a positioned diagnostic nothing recovers, so the
# run reports it and exits 1. The identity is what is being checked.
stray="${RUNNER_TEMP:-/tmp}/stray.err"
if ./dist/xmd -e '<Else>orphan</Else>' --raw 2>"$stray"; then
echo "expected a stray <Else> to end the run" >&2
exit 1
fi
grep -q '(<eval>:1:1)' "$stray"
# Nothing is written to run a document that was never a file.
before=$(ls -A)
./dist/xmd -e '# Hello' --raw > /dev/null
test "$(ls -A)" = "$before"
# Secret detection is on by default and `--no-secret-detection` is the
# only way off, so both directions belong to the binary rather than only
# to the source runner. The credential is assembled here, so no
# usable-looking literal is committed.
- name: Smoke test the secret-detection opt-out
run: |
set -eu
canary="ghp_$(printf 'abcdefghijklmnopqrstuvwxyz0123456789')"
document="# Smoke
token $canary
"
# Default-on: the run fails, and the credential reaches no output.
if ./dist/xmd -e "$document" --raw > /tmp/on.out 2> /tmp/on.err; then
echo "::error::the compiled binary persisted a credential by default"
exit 1
fi
grep -q 'secret detection rejected content' /tmp/on.err
! grep -q "$canary" /tmp/on.out
# Opted out: the run succeeds, renders the document, and says so once.
./dist/xmd -e "$document" --raw --no-secret-detection \
> /tmp/off.out 2> /tmp/off.err
grep -q "$canary" /tmp/off.out
test "$(grep -cx 'WARNING: secret detection is disabled; credentials may be persisted.' /tmp/off.err)" = "1"
# The value form is refused rather than read as enabled.
if ./dist/xmd -e "$document" --raw --secret-detection=false 2> /tmp/bad.err; then
echo "::error::the compiled binary accepted --secret-detection=false"
exit 1
fi
grep -q 'does not take a value' /tmp/bad.err
# The guide documents this command and its output; running it keeps the
# value-root contract executable rather than described.
- name: Smoke test a value root's JSON result
run: |
test "$(./dist/xmd run smoke-test/value-root.md)" = \
'{"passed":true,"summary":"no findings"}'
# Reading a document's own outline and projecting it happen inside the
# engine, so only the compiled binary proves the catalog and the
# projection survived `deno compile`.
- name: Smoke test document targets
run: |
set -eu
printf '%s\n%s\n' \
'smoke-test/document-targets.md#Alpha' \
'smoke-test/document-targets.md#Beta' > /tmp/targets.expected
./dist/xmd run smoke-test/document-targets.md --help > /tmp/help.out
grep -o 'smoke-test/document-targets\.md#[A-Za-z]*' /tmp/help.out > /tmp/targets.out
diff /tmp/targets.expected /tmp/targets.out
grep -q 'ALPHA_DESCRIBED' /tmp/help.out
./dist/xmd run 'smoke-test/document-targets.md#Alpha' --raw > /tmp/targeted.out
grep -q 'ALPHA_RAN' /tmp/targeted.out
! grep -q 'BETA_RAN' /tmp/targeted.out
# `<WebForm>` is registered by the CLI, so the compiled binary must know it.
# The document fails in preflight, before a listener or a browser, which is
# what makes this runnable on a headless runner: a binary missing the
# registration reports an unresolved component instead, and one that served
# before checking would hang.
- name: Smoke test WebForm registration and preflight
run: |
./dist/xmd run smoke-test/web-form-preflight.md 2>&1 \
| grep -q '<WebForm> schema must be a JSON object'
# The preflight smoke above stops before assets, so it cannot tell a binary
# that embedded the browser bundle from one that did not — and a bundle-less
# compile succeeds silently. This serves a real form and reads the client
# script back over HTTP. Headless is fine: the opener fails and that is a
# warning by design, so the URL is still printed and the form still serves.
- name: Smoke test the compiled binary serving a real form
run: |
set -eu
./dist/xmd run smoke-test/web-form-live.md > /tmp/web-form-live.log 2>&1 &
xmd_pid=$!
trap 'kill "$xmd_pid" 2>/dev/null || true' EXIT
url=""
for _ in $(seq 1 40); do
url=$(grep -oE 'http://127\.0\.0\.1:[0-9]+/f/[A-Za-z0-9_-]+/' /tmp/web-form-live.log | head -1 || true)
[ -n "$url" ] && break
sleep 0.5
done
if [ -z "$url" ]; then
echo "::error::the compiled binary never printed a form URL"
cat /tmp/web-form-live.log
exit 1
fi
curl -fsS "$url" | grep -q '<div id="root"></div>'
bytes=$(curl -fsS "${url}client.js" | wc -c | tr -d ' ')
# The real bundle is ~600 KB of React and RJSF; a placeholder or an
# empty asset would be orders of magnitude smaller.
if [ "$bytes" -lt 100000 ]; then
echo "::error::client.js was $bytes bytes — the binary did not embed the browser bundle"
exit 1
fi
echo "served a real client bundle: $bytes bytes"
# The themed stylesheet is built the same way and by the same task, so
# an embedded font face is the cheapest proof the binary is serving it
# rather than the vendored default.
curl -fsS "${url}theme.css" | grep -q 'font/woff2;base64'
echo "served the themed stylesheet with embedded fonts"
# Terminating with the form still open is the interruption path.
kill "$xmd_pid"
wait "$xmd_pid" 2>/dev/null || true
# Covers the compiled binary relaunching itself as `xmd test-agent`,
# which the source-mode worker command never exercises.
- name: Smoke test the compiled binary as a test agent
run: ./dist/xmd test smoke-test/test-agent/README.md --raw
- name: Smoke test xmd run through ACPX
run: |
./dist/xmd test smoke-test/agent/README.md \
--include smoke-test/agent/components \
--raw
# The host document-filesystem contract, on every target a release ships.
#
# `test-deno`, `test-node`, and `test-bun` run the whole corpus on Linux x64
# and prove the contract holds there. What they cannot prove is that it holds
# on the other four triples: the host adapter's containment is path arithmetic
# plus `realpath`, and both are the platform's — Windows has drive letters,
# UNC paths, junctions, and reparse points that POSIX does not, and the two
# macOS rows resolve `/var` through a symlink that a naive comparison reads as
# an escape.
#
# So this row is focused rather than exhaustive: one suite, plus a compiled
# probe, on each of the five. The compiled probe is the second half of the
# claim — the shipped artifact is a binary, and the adapter reaches
# `node:path`, `node:fs`, and `node:os` through whatever `deno compile` put in
# its graph.
filesystem-contract:
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15
target: aarch64-apple-darwin
- runner: macos-15-intel
target: x86_64-apple-darwin
- runner: ubuntu-24.04
target: x86_64-unknown-linux-gnu
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
- runner: windows-2025
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.runner }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
with:
deno-version: v2.9.5
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.0
# `deno install` rather than `deno task deps`: the task also caches the
# graphs a browser build and a release compile walk, and it reaches them
# by spawning a child — which does not survive the Windows runner's path
# handling. Nothing here builds the bundle or compiles the CLI, so the
# plain frozen install is the whole preparation this job needs.
- name: Install dependencies
run: deno install --frozen
# The compile below runs under `--node-modules-dir=none`, which resolves
# npm packages from the Deno cache rather than from `node_modules`. This
# caches the probe's graph in that mode without touching the layout the
# step above just created.
- name: Cache the probe's graph for a compile
run: >
deno install --entrypoint --node-modules-dir=none --frozen
scripts/files-contract-probe.ts
# Deno first, and the compile with it: `pnpm install` and `bun install`
# each rewrite `node_modules` into their own layout, so a Deno step after
# one of them resolves through links the other pruned (#279).
- name: Host contract under Deno
run: deno test --allow-all --frozen packages/runtime/tests/host-files.test.ts
- name: Host contract as a compiled binary
run: |
set -eu
deno compile --node-modules-dir=none --cached-only --frozen --allow-all \
--output dist/files-contract-probe scripts/files-contract-probe.ts
if [ -f dist/files-contract-probe.exe ]; then
./dist/files-contract-probe.exe
else
./dist/files-contract-probe
fi
- name: Install the Node layout
run: pnpm install
- name: Host contract under Node
run: pnpm exec tsx --tsconfig tsconfig.node.json --test packages/runtime/tests/host-files.test.ts
- name: Install the Bun layout
run: bun install
- name: Host contract under Bun
run: bun test --timeout=300000 packages/runtime/tests/host-files.test.ts
# The same chain `deno task verify:clean` runs locally. It is the regression
# for #279's ownership claim: a build that installs anything moves the
# prepared-state fingerprint, prunes pnpm's links, and fails the resolution
# probe.
#
# It proves ownership and non-interference, and nothing else. Correctness
# belongs to the jobs `green` already requires: the sharded `test-deno`,
# `test-node` and `test-bun` matrices, which also carry the Deno and Node
# typechecks; `lint`, `jsr` and `site`; and the documentation check inside
# `smoke`. This job used to re-run the complete runtime corpora for load,
# which cost ~1,420s of its ~31 minutes and proved nothing about ownership
# that the direct probe does not (#546).
composability:
runs-on: ubuntu-latest
# `main` only. What this job uniquely proves — a clean checkout prepares,
# builds stay cache-pure and offline, and the real producer cannot corrupt
# or replace the state Deno, Node and Bun resolve through while it runs — is
# worth a post-merge run rather than a place on every pull request's
# critical path (#279).
#
# A `ci-main-red-fix` pull request is the exception, and the reason the
# exception is safe: it is excused the main-health lookup because the base it
# would prove is the broken one, so it proves itself the way `main` is
# proven instead. `green` requires this job to succeed there.
if: >-
github.event_name == 'push'
|| contains(github.event.pull_request.labels.*.name, 'ci-main-red-fix')
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
# The interference proof runs a Bun consumer beside the Deno and Node
# ones; without the runtime it fails at spawn, in zero seconds, with an
# empty spool.
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.0
- uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
with:
deno-version: v2.9.5
# Caches the harness's own graph, so it can run --cached-only below.
- name: Prepare this checkout
run: deno task deps
# Clones itself, prepares that clone against a scratch DENO_DIR, then
# fingerprints node_modules, the cache's dependency content, and the lock
# around every build phase — each run offline — and finishes with the
# concurrent interference proof and one comparison of what the repository
# owns.
- name: The chain holds from a clean checkout
run: deno task verify:clean
site:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
with:
deno-version: v2.9.5
- name: Install workspace deps
run: deno install
- name: Check (fmt + lint + typecheck)
run: deno task check
working-directory: site
- name: Production build (clean runner)
run: deno task build
working-directory: site
test-node:
name: test-node (${{ matrix.shard }}/10)
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
# `tsc` resolves the literal dynamic import of the generated browser
# bundle, which `deno check` leaves alone, so the typecheck needs the file
# to exist. The specifier stays literal on purpose: `deno compile` follows
# it to embed the bundle in the binary, and an opaque one would ship a
# binary that cannot serve a form.
#
# Before `pnpm install`, not after: `deno task build:web` rewrites
# node_modules into Deno's layout, which strips the packages pnpm placed
# there and fails the typecheck on two dozen unrelated modules. Installing
# afterwards restores pnpm's layout, and the bundle is outside
# node_modules so it survives.
- uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
with:
deno-version: v2.9.5
- name: Install dependencies
run: deno task deps
- name: Build the browser bundle
run: deno task build:web
- run: pnpm install
- name: Typecheck
run: pnpm exec tsc --project tsconfig.node.json --noEmit
- name: Test
run: pnpm test:node ${{ matrix.shard }}/10
test-bun:
name: test-bun (${{ matrix.shard }}/5)
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.0
- run: bun install
- name: Test
run: bun run test:bun ${{ matrix.shard }}/5
# test:bun never loads packages/cli/src/bun.ts, so it cannot tell whether
# the Bun entrypoint's API.Env providers work. This document drives a
# <TestAgent> scenario, which makes the parent relaunch bun.ts as
# `test-agent` through the command it builds — the only check that
# exercises that relaunch.
- name: Bun entrypoint smoke
run: bun run packages/cli/src/bun.ts test smoke-test/test-agent/README.md --raw
# The one required check. It installs nothing and uses no action, so the
# result it reports is the jobs' and never the infrastructure's.
#
# "Succeeded or skipped" was too weak once two jobs became conditional: it
# read a `main-green` that never ran, and a `composability` that never ran, as
# satisfied. So the requirement is per event instead. `main-green` runs on a
# pull request and may be skipped on a push; `composability` runs on a push and
# on a `ci-main-red-fix` pull request, and may be skipped on an ordinary one.
# Every other job must succeed outright — an unexpected skip is an unproven
# job, which is exactly what this check exists to catch.
green:
needs:
[
lint,
main-green,
test-deno,
jsr,
smoke,
filesystem-contract,
composability,
site,
test-node,
test-bun,
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Every CI job produced the result this event requires
env:
RESULTS: ${{ toJSON(needs) }}
EVENT: ${{ github.event_name }}
REPAIR: ${{ contains(github.event.pull_request.labels.*.name, 'ci-main-red-fix') }}
run: |
set -euo pipefail
echo "$RESULTS" | jq -r 'to_entries[] | "\(.value.result)\t\(.key)"' | sort
echo "event=$EVENT repair=$REPAIR"
unproven=$(echo "$RESULTS" | jq -r --arg event "$EVENT" --arg repair "$REPAIR" '
def required($job):
if $job == "main-green" then
(if $event == "push" then ["success", "skipped"] else ["success"] end)
elif $job == "composability" then
(if $event == "push" or $repair == "true"
then ["success"]
else ["success", "skipped"] end)
else ["success"]
end;
to_entries[]
| . as $entry
| select((required($entry.key) | index($entry.value.result)) == null)
| "\($entry.key)=\($entry.value.result)"')
if [ -n "$unproven" ]; then
echo "::error::CI is not green: $(echo "$unproven" | tr '\n' ' ')"
exit 1
fi