fix: make the runtime work on Firefox (V8 stack shim + dev Service Wo… #189
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 | |
| # Three tiers, cheapest first (mirrors the retired GitLab CI): | |
| # | |
| # gate - offline, Wasm-free guards (toolchain subsystem + llhttp Wasm HTTP | |
| # parser). Runs on every push/PR; seconds; catches silent regressions | |
| # such as esbuild-wasm drift or a broken native->wasm alias table. | |
| # verify - the hermetic offline suite (scripts/verify-node.mjs). Needs the Wasm | |
| # VFS/codec/crypto built for Node; runs on PRs + the default branch. | |
| # pm-gate - the package-manager North Star (real npm/yarn/pnpm/corepack in-VM). | |
| # Network + the Wasm VFS/codec/crypto, so it's manual/scheduled like | |
| # the spikes tier — but it FAILS the run, unlike spikes-net. | |
| # spikes - the network tier: boots real templates and installs from the live | |
| # npm registry in-VM. Slow + registry-dependent, so it's manual/scheduled. | |
| on: | |
| push: | |
| branches: [master] | |
| pull_request: | |
| schedule: | |
| # Nightly network spikes. | |
| - cron: "0 3 * * *" | |
| workflow_dispatch: | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # --- fast gate: offline, no Wasm, every push/PR ---------------------------- | |
| toolchain-gate: | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: 22 | |
| - run: node scripts/run-spikes.mjs --offline | |
| # The notebook's view, rendered under jsdom. It is `net` only because it | |
| # installs React, a DOM and a bundler off the registry — it runs no | |
| # container, needs no Wasm and touches no network of its own, so it belongs | |
| # in the PR gate and not in spikes-net, which is schedule-only and | |
| # `continue-on-error: true` and therefore gates nothing. That distinction is | |
| # not academic here: the notebook shipped with every cell editor dead, and | |
| # this is the only job that would have gone red for it. | |
| - name: the notebook view mounts | |
| run: node scripts/run-spikes.mjs --net notebook-view | |
| # The static probes. Both are Wasm-free and take under a second, and | |
| # neither was wired into any job before — probe:node-registry shipped as | |
| # the guard against unresolvable builtin ids and had never actually run in | |
| # CI, which is the same "written but never executed" gap the spike table | |
| # had. | |
| - run: npm run probe:node-registry | |
| - run: npm run probe:http-egress | |
| # The browser egress header policy. Pure logic on purpose: Node has no CORS | |
| # and the headless fetchers forward every header, so no spike can see a | |
| # regression here — a stripped Authorization header shows up as a request | |
| # that quietly succeeds anonymously. | |
| - run: npm run probe:egress-headers | |
| - run: npm run probe:terminal-feedback | |
| # The cookie jar's RFC 6265 subset. Pure logic on purpose: a path rule that | |
| # is slightly too generous hands one route's session to another, and neither | |
| # that nor an over-eager expiry shows up as an error anywhere. | |
| - run: npm run probe:cookie-jar | |
| # --- the same gate, on the OLDEST Node package.json admits ----------------- | |
| # Every other job pins `node-version: 22`, and `.nvmrc` says `22`. Both resolve | |
| # to the newest 22.x, so before this job existed NO MACHINE ANYWHERE ran the | |
| # regime `engines.node` promises to support — not CI, not a contributor using | |
| # nvm. That is not a hypothetical gap: `npm run dev` shipped broken on | |
| # 22.0-22.17 twice, behind three rounds of green gates, because Node only | |
| # strips TypeScript types unprompted from 22.18 and every runner had it. | |
| # | |
| # Pinned to the exact floor rather than a range, so that raising `engines.node` | |
| # without raising this line leaves an untested version admitted, and lowering | |
| # it without lowering this line is a promise nothing checks. The two are meant | |
| # to be edited together; scripts/spike-install-latency.mjs asserts they match. | |
| # | |
| # Cheap by construction: it is the Wasm-free offline tier, the same one | |
| # `toolchain-gate` runs, so it costs a second checkout and about a minute. | |
| # Without the crates, run-spikes skips the `needsWasm` spikes — which is what | |
| # keeps this job honest about Node versions rather than about OpenSSL: the | |
| # `constants` spike compares the host's bundled OpenSSL and zlib against the | |
| # VM's and legitimately differs on any Node but the newest. | |
| toolchain-floor: | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| # Keep in step with `engines.node`. | |
| node-version: "22.15.0" | |
| # Named separately from the tier below so that the failure this job exists | |
| # to catch reads as itself. Thirteen scripts import the studio's templates | |
| # out of a .ts file and two of them run on every `npm run dev`; when that | |
| # broke, the generators failed soft and the only visible symptom was a dev | |
| # server that was quietly slow. | |
| - name: the studio templates must import on the floor, not only on latest | |
| run: | | |
| node --input-type=module -e " | |
| const { loadShippedTemplates } = await import('./scripts/lib/shipped-templates.mjs'); | |
| const t = await loadShippedTemplates(); | |
| if (!Array.isArray(t) || t.length === 0) throw new Error('loaded no templates'); | |
| console.log('loaded ' + t.length + ' templates on Node ' + process.versions.node); | |
| " | |
| - run: node scripts/run-spikes.mjs --offline | |
| # The corpus gate that installs each shipped lock in the VM lives in | |
| # `template-gate`, which is schedule/dispatch only because it needs the Rust | |
| # toolchain and an hour. That leaves merges unguarded against the change most | |
| # likely to poison a lock: an edit to templates.ts, to the generator, or to | |
| # the kernel's alias table. RESOLVING all of them needs none of that — no | |
| # Wasm, no VM, ~35s — and resolution is where both guards run, so a template | |
| # that starts pulling in an aliased native is refused here rather than on the | |
| # deploy that ships it. It is not the in-VM gate and does not replace it; it | |
| # is the part of that gate which is cheap enough to block a merge. | |
| # | |
| # `--strict`, because producing them IS this job's subject. The cost is that a | |
| # registry outage reddens a PR — the same trade `template-gate` already makes | |
| # for the same command, against the alternative of hearing it from a nightly. | |
| lock-resolution-gate: | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: 22 | |
| - name: every shipped lockfile must still resolve, and pass both guards | |
| run: npm run vendor:locks -- --strict | |
| # SDK packaging + public-API contract guard (offline, no Wasm, no browser). | |
| # Also regression-checks the shipped preview sw.js branding + examples/basic. | |
| sdk-smoke: | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: 22 | |
| - run: npm run smoke | |
| # --- offline hermetic suite (needs the Rust->Wasm build) ------------------- | |
| verify: | |
| if: github.event_name == 'push' || github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: 22 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32-unknown-unknown, wasm32-wasip1 | |
| - uses: jetli/wasm-pack-action@v0.4.0 | |
| with: | |
| version: v0.13.1 | |
| - name: Cache cargo | |
| uses: actions/cache@v6 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| packages/*/target | |
| key: cargo-${{ runner.os }}-${{ hashFiles('packages/*/Cargo.lock') }} | |
| - name: Build Wasm crates for Node | |
| run: npm run build:vfs:node && npm run build:codec:node && npm run build:crypto:node && npm run build:wasi-demo | |
| # These are offline but need the Rust/Wasm VFS, so they can't run in the | |
| # Wasm-free toolchain-gate (run-spikes.mjs would silently skip them); run them | |
| # here, after the crates are built above. The `bun` filter is a substring | |
| # match, so it also re-runs the Wasm-free bun-offline spike — cheap, and it | |
| # means a Bun change is never merged on a green run that skipped Bun. | |
| # | |
| # EVERY `needsWasm` offline spike belongs in this list. One that is registered | |
| # but missing here runs nowhere at all: skipped in toolchain-gate for want of | |
| # the crates, and never selected in the only job that has them. | |
| - name: offline spikes that need the Wasm VFS | |
| run: node scripts/run-spikes.mjs --offline dep-cache depcache-shipped seed-swc bun http-binary-body http-response-bytes cookie-session fs-cp fs-errors large-fs-payloads worker-pool node-cli repl stack-traces readline watch child-stdin fs-metadata crypto-jwk zlib-brotli port-liveness constants diag-liveness fatal-errors net-close-order net-blocklist | |
| - run: npm run verify | |
| # --- package-manager gate: the North Star, gated on its own ---------------- | |
| # Running the real npm/yarn/pnpm/corepack CLIs in-VM is the headline capability, | |
| # so it gets a job that can actually go red. It is deliberately NOT part of | |
| # spikes-net: that job is `continue-on-error: true` (registry flakiness across | |
| # 40+ template installs would make it noise), which means a broken package | |
| # manager would show up as a green run there. | |
| # | |
| # Not on push/PR either: each PM spike vendors a real CLI and runs a real | |
| # install from the registry, so the eight of them cost far more than the PR | |
| # budget allows. Making this PR-blocking needs the vendor assets cached or | |
| # committed first — see scripts/run-spikes.mjs VENDORS. | |
| pm-gate: | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: 22 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32-unknown-unknown | |
| - uses: jetli/wasm-pack-action@v0.4.0 | |
| with: | |
| version: v0.13.1 | |
| - name: Cache cargo | |
| uses: actions/cache@v6 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| packages/*/target | |
| key: cargo-${{ runner.os }}-${{ hashFiles('packages/*/Cargo.lock') }} | |
| # All three crates, not just the VFS. A package manager is a crypto and | |
| # compression workload before it is a filesystem one: npm verifies every | |
| # tarball against its sha512 integrity, and only md5/sha1/sha256 have a JS | |
| # fallback, so without the crypto crate `npm install` dies on the first | |
| # dependency with FETCH_ERROR "digest 'sha512' needs the wasm codec". The | |
| # codec crate is just as load-bearing — a registry tarball is a .tgz, and | |
| # without zlib the extract fails with "zlib wasm codec is not available". | |
| - name: Build the Node Wasm crates | |
| run: npm run build:vfs:node && npm run build:codec:node && npm run build:crypto:node | |
| # Provisions its own scratch dirs + studio delivery assets (see VENDORS); | |
| # a provisioning failure is reported as a FAIL, not skipped. | |
| - run: npm run spikes:pm | |
| # --- framework-template gate: the shipped templates, on a job that can go red - | |
| # Same argument as pm-gate, and it has now been paid for. `vite: ^8.0.0` floated | |
| # onto rolldown 1.2.2, which dropped @rolldown/binding-wasm32-wasi from its | |
| # optionalDependencies — so nothing installed the wasm binding any more, and the | |
| # dev server in every Vite 8 template stopped starting. FOUR spikes went red and | |
| # said so for as long as it took anyone to look, which was never: spikes-net is | |
| # `continue-on-error: true`, so its run is green whatever happens inside it. | |
| # | |
| # These six are separable from that tolerance on their own merits. They are fast | |
| # (11-16s each, versus the 40+ installs that made the tier noisy), they install one | |
| # small tree apiece, and a failure here is not registry weather — it is the | |
| # product's headline surface not starting. Registry drift is exactly what it is | |
| # supposed to catch, so tolerating it defeats the job. | |
| # | |
| # Scheduled/dispatch rather than per-push for the reason pm-gate gives: the shared | |
| # harness vendors a real npm CLI, and that provisioning is not cached yet. | |
| template-gate: | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: 22 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32-unknown-unknown | |
| - uses: jetli/wasm-pack-action@v0.4.0 | |
| with: | |
| version: v0.13.1 | |
| - name: Cache cargo | |
| uses: actions/cache@v6 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| packages/*/target | |
| key: cargo-${{ runner.os }}-${{ hashFiles('packages/*/Cargo.lock') }} | |
| # All three crates, for the reason pm-gate spells out: a real install verifies | |
| # sha512 integrity and extracts .tgz tarballs. | |
| - name: Build the Node Wasm crates | |
| run: npm run build:vfs:node && npm run build:codec:node && npm run build:crypto:node | |
| # `react` was missing from this list until the template most users start from | |
| # turned out to be the one nothing ran. Each of these now boots its server the | |
| # way the studio does — through the manifest's own dev command — rather than by | |
| # calling the vite bin directly, which is the shortcut that let a broken shipped | |
| # path sit behind six green spikes. | |
| # | |
| # Anchored (`name$`) because the filters are a substring match by default, and | |
| # a bare `lit` also selects sqlite and pglite: this step was running two in-VM | |
| # database spikes it does not name, on a job whose failures are supposed to mean | |
| # "a shipped template stopped booting". Nine ran where seven were asked for. | |
| # There is deliberately no `vendor:locks` step here. It used to be one, and | |
| # a step is the wrong place for it: `spikes-net` runs the same lock-reading | |
| # spikes with no such step, so `vendor/locks/` was empty there and | |
| # `template-locks` died in 0.2s on the first missing file while the eight | |
| # template spikes silently installed from RANGES — two jobs exercising | |
| # different code paths for the same template, which is the one thing a | |
| # second job must not do. Provisioning now belongs to the spikes that need | |
| # it (`VENDORS.locks` in scripts/run-spikes.mjs), so every job that selects | |
| # one gets the locks, and a job added tomorrow cannot forget. | |
| # Every id in COVERAGE, installed in the VM from the lock the runner | |
| # provisions for it. This is the gate that made COVERAGE forty templates | |
| # instead of one: a spike per template would have been ~50 near-identical | |
| # files, so this is one spike driven over the list. It installs and checks | |
| # the tree is usable rather than booting a server — the step below is what | |
| # boots servers, for the eight frameworks it names. | |
| - name: every shipped lockfile must install into a usable tree | |
| run: node scripts/run-spikes.mjs --net template-locks$ | |
| - name: framework templates must start their dev server | |
| run: node scripts/run-spikes.mjs --net react$ preact$ lit$ solid$ vue$ svelte$ qwik$ ember$ | |
| # The two the step above used to pick up by accident, kept blocking on purpose. | |
| # Anchoring `lit` would otherwise have moved them to spikes-net alone, which is | |
| # `continue-on-error: true` — so a broken in-VM database would have stopped being | |
| # able to turn the workflow red. They earn a step here on the same terms as the | |
| # templates: a WASM SQL engine running in the browser is a headline capability, | |
| # and at ~10s for the pair they are cheaper than anything else in this job. Their | |
| # own step, because a failure here means something different from a template that | |
| # will not boot — which was the whole problem with them arriving via `lit`. | |
| - name: the in-VM databases must work | |
| run: node scripts/run-spikes.mjs --net sqlite$ pglite$ | |
| # Nest's dev command is a watch supervisor rather than a dev server, so its gate | |
| # is that a SAVE reaches the browser: boot, edit, recompile, restart, new | |
| # response. Separate step because a failure here means something different. | |
| - name: the Nest template must survive a save | |
| run: node scripts/run-spikes.mjs --net nest-watch | |
| # Its own step rather than an extra filter above, because it proves something | |
| # different: that an interactive scaffolder can be ANSWERED, not that a template | |
| # serves. Needs the vendored npm, hence the net tier. | |
| - name: an interactive scaffolder must be answerable | |
| run: node scripts/run-spikes.mjs --net scaffolder | |
| # --- network tier: real template installs from the live registry ----------- | |
| # Manual (workflow_dispatch) or scheduled: slow and depends on | |
| # registry.npmjs.org being reachable. | |
| spikes-net: | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 120 | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: 22 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32-unknown-unknown | |
| - uses: jetli/wasm-pack-action@v0.4.0 | |
| with: | |
| version: v0.13.1 | |
| - name: Cache cargo | |
| uses: actions/cache@v6 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| packages/*/target | |
| key: cargo-${{ runner.os }}-${{ hashFiles('packages/*/Cargo.lock') }} | |
| - name: Build Wasm crates for Node | |
| run: npm run build:vfs:node && npm run build:codec:node && npm run build:crypto:node | |
| - run: npm run spikes:net |