Harden review infrastructure with contextual APIs #899
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] | |
| jobs: | |
| 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 | |
| test-deno: | |
| 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.3.14 | |
| - 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 task test | |
| # 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 | |
| - name: Install dependencies | |
| run: deno task deps | |
| - name: Build the xmd binary | |
| run: deno task build | |
| - name: Smoke test the compiled binary | |
| run: | | |
| ./dist/xmd test smoke-test/README.md \ | |
| --component-dir smoke-test \ | |
| --component-dir 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 \ | |
| --component-dir smoke-test \ | |
| --component-dir packages/core/components \ | |
| --raw | |
| # 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 | |
| # 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 />' --component-dir 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 the root collects, so this | |
| # renders it and exits 0. The identity is what is being checked. | |
| ./dist/xmd -e '<Else>orphan</Else>' --raw | grep -q '(<eval>:1:1)' | |
| # 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"}' | |
| # `<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 \ | |
| --component-dir smoke-test/agent/components \ | |
| --raw | |
| # 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 duplicates work other jobs do — that is the cost of asserting the | |
| # whole chain end to end rather than each link separately. | |
| composability: | |
| runs-on: ubuntu-latest | |
| # `main` only. The battery this job runs spends ~1,420s on the Deno, Node, | |
| # and Bun suites — which `test-deno`, `test-node`, and `test-bun` already | |
| # run in parallel, on their own runners. Compressing them onto one four-core | |
| # runner made the workflow's critical path 434s → 649s to prove a property | |
| # about installation that most changes cannot break. What this job uniquely | |
| # proves — a clean checkout prepares, builds stay cache-pure and offline, | |
| # and concurrent checks do not dirty what another reads — is worth a | |
| # post-merge run, not a place on every pull request's critical path (#279). | |
| if: github.event_name == 'push' | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: "22" | |
| - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 | |
| # The battery this job runs includes `bun run test:bun`; without the | |
| # runtime that command fails at spawn, in zero seconds, with an empty | |
| # spool. | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: 1.3.14 | |
| - 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. | |
| - 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: | |
| 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 | |
| test-bun: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: 1.3.14 | |
| - run: bun install | |
| - name: Test | |
| run: bun run test:bun | |
| # 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 | |
| green: | |
| needs: [lint, test-deno, jsr, smoke, composability, site, test-node, test-bun] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Every CI job succeeded | |
| env: | |
| RESULTS: ${{ toJSON(needs) }} | |
| run: | | |
| set -euo pipefail | |
| echo "$RESULTS" | jq -r 'to_entries[] | "\(.value.result)\t\(.key)"' | sort | |
| unproven=$(echo "$RESULTS" | jq -r ' | |
| to_entries[] | |
| | select(.value.result != "success" and .value.result != "skipped") | |
| | .key') | |
| if [ -n "$unproven" ]; then | |
| echo "::error::CI is not green: $(echo "$unproven" | tr '\n' ' ')" | |
| exit 1 | |
| fi |