🔖 Release 0.13.0 #1874
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }}/13) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] | |
| 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 }}/13 | |
| # 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 }}/7) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4, 5, 6, 7] | |
| 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 }}/7 | |
| test-bun: | |
| name: test-bun (${{ matrix.shard }}/4) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4] | |
| 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 }}/4 | |
| # 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 |