CI explained
Français · Type: reference + troubleshooting · Audience: contributor · Context: engine repository · Status: ready · Applies from: 4.5.0
What GitHub Actions runs on the engine repository, why a job ran or said "not needed by the plan", and what to do when
one is red. The workflows are in .github/workflows/; the plan that decides what a
pull request runs is tools/ci-plan.ts. Which test suite runs in which job is
Which tests run when.
The workflows
| Workflow | Starts on | Does | Gates |
|---|---|---|---|
ci | a pull request; a push to main; a v* tag; by hand (pages-only) | the three tiers below, then Pages on main | the pull request (pr-gate); a release (the tag's run) |
candidate | by hand on main for a SHA (gh workflow run candidate --ref main -f sha=<full sha>); a pull request labelled full-ci (information only) | every heavy family on one exact SHA, then candidate-gate, which writes and attests candidate-manifest.json | a tag must name one green run of it (Releasing) |
release | the completion of ci on a v* tag | the mutation sets, release-check:ci, the release made from the candidate's files, ship verify; then docs-publish (4.5.0) | publication |
nightly | every day at 03:17 UTC; by hand | npm names, the heavy suite, the connectors' fuzz (a minute each), the abstraction corpus (500 seeds, four shards), every mutation set from scratch, the Bridge's loads on SQLite and Postgres, the proof matrix, cross-runtime with 500 Remix seeds, the worker in a container | nothing directly; a red night is a bug to fix before the next release |
prove | every Monday at 04:00 UTC; by hand (game, rooms) | prove:game, the committed playtests (--strict), and the bench on a 100-room generated game (uploaded as bench) | never a gate |
mutation | by hand: gh workflow run mutation --ref main -f set=<set> -f ref=<branch or SHA> [-f file=<path>] | one mutation set, measured on a runner, its report kept as an artifact | nothing |
qualify | a pull request or a push to main touching ops/, scripts/ops-qualify*, scripts/pack.mjs, bridge/, package.json or the lockfile; by hand | npm run ops:qualify: the Bridge's qualification profile on Docker | the profile does not rot between candidates |
docs-publish | called by release after ship verify; by hand (revision, rollback, promote, preview) | the wiki and the publication record (4.5.0) | publication of the documentation |
The three tiers of ci
feature since 4.1.9
The fast tier, on every change. plan reads the diff; check and coverage run beside it:
check: on a pull request,changes -- --check(the fragment, Conventions) andfield:carry -- --check-range(no carried pass changes its trust root); thenquality(format, lint, knip, both TypeScript configurations, content lint),docs:truth, ffmpeg,build:game(the sample game's gates, the bundle,verify:dist, the spoiler check, the asset audit),weight -- --release,quality:baseline -- --check --dist,prove:game,audit:depswhenpackage.jsonor the lockfile moved, then the two-entry build (game and Studio demo) that the e2e rows play, kept as thedistartifact; onmainit also assembles Pages'/docs/from the released documentation archives.coverage: the unit suite once, under V8 coverage (test:coverage), then the coverage ratchet: a warning on a pull request, a failure onmainand on a tag (The coverage ratchet).
The second tier, what the change can break. Each job needs plan (most also check) and reads its gate in the
plan: it runs its real work, or one step that says "not needed by the plan" and succeeds.
| Job | Gate | Runs |
|---|---|---|
node-24 | node24 | the unit suite on Node 24 |
windows | windows | the doctor, the type checks, the unit suite without the files that assume POSIX, build:game, the production server answering |
e2e (six rows) | e2e | the production build played by touch: Chromium full, WebKit generic, Chromium and WebKit at the keyboard, Chromium in French, Chromium with the Canvas painter; with weight, music, visual, perf, Studio, accessibility and PWA steps on some rows |
pwa-firefox | pwaFirefox | the PWA scenario in Firefox (install, update, interrupted update, reinstall, offline) |
reference (Chromium, WebKit), reference-fr, heroes | reference | the reference chapter built, proved, played at the keyboard, against its visual references, at 30 fps or more with the CPU slowed 4×; reference-fr plays it in French and with the generic verbs; heroes builds, proves and plays the heroes example |
second-game | secondGame | a game made by npm run new-game, through every gate the sample passes |
fresh-install | freshInstall | the engine packed, a game created outside the repository, installed, built and played |
external-consumer (Ubuntu, macOS, Windows) | freshInstall | the archives installed outside the repository on three systems |
upgrade | upgrade | a game of the previous release moved to this engine, its old save played to the end |
reality-xcheck, connectors, bridge-postgres, reality (Chromium, WebKit) | reality | Biscuit's samples in JavaScript and Rust, the connectors against a real Bridge with their fuzz, the Bridge on Postgres 16, the signals game end to end |
studio-hosted | studioHosted | the Studio Server on Postgres and S3 (SeaweedFS) with a real Keycloak, then the hosted Studio in Chromium and Firefox |
cross-runtime | none (every change) | the same canonical text, speedrun proofs and Remix worlds in Node, Chromium, WebKit and Firefox |
The third tier, off the pull request path: the mutation sets on a pull request labelled full-ci, the nightly,
the candidate and the release.
pr-gate reads every job's result: each must be success; the mutation jobs may be skipped where they do not
run. It is the one check that sums a pull request up.
pages, on main only, after a green pr-gate: deploys the game, the Studio demo and /docs/ to GitHub Pages.
The plan (npm run ci:plan)
feature since 4.1.9
# in the engine repository, on your branch
npm run ci:plan # against origin/main
npm run ci:plan -- --base=<ref>
npm run ci:plan -- --files=src/engine/dom/room.ts,games/demo/rooms/garden.ts
It prints, on stderr, one line per changed file with the rule that matched and the gates it opened, then the plan;
on stdout, one line of JSON ({"auditDeps":…,"node24":…,…,"all":…,"docs":…,"docsPages":[…],"pagesOnly":…}), which
the plan job writes to its output. The first rule that matches a path wins, and a change runs the union of what its
files ask:
| A change to | Opens |
|---|---|
.github/workflows/ci.yml, tools/ci-plan.ts, package.json, the lockfile, a shared configuration (tsconfig*.json, vite.config.ts, vitest*.ts, biome.json, knip.json, requirements.txt, .nvmrc, .gitattributes), the app shell, tools/vite/ | everything |
docs/, changes/, wiki/, any .md, LICENSE*, .claude/, .cursor/, clevercloud/, another .github/ file | the fast tier only |
| the Reality runtime | reality, e2e, node24, windows |
| the painters, the music director | e2e, reference, node24, windows |
the PWA and the storage (boot.ts, dom/update, offline, save-store, storage) | e2e, pwaFirefox, upgrade, node24, windows |
any other file of src/engine/ (the core) | everything |
src/studio/ | e2e, studioHosted, node24, windows |
bridge/ | reality, freshInstall, node24, windows (and studioHosted for its SQL layer) |
games/demo/ | e2e, reference, node24, windows |
games/reference/, games/heroes/ | reference, node24 |
games/_template/, cli/, scripts/{pack,fresh-install,external-consumer,new-game}.mjs | secondGame, freshInstall, upgrade, node24, windows |
studio-server/ | the authoring gates and studioHosted |
other tools/ and scripts/ | the authoring gates (node24, windows, secondGame, freshInstall, upgrade, reference) |
tests/ | node24, windows |
| a path no rule knows | everything |
A git error runs everything too: the plan never decides that it does not need to be checked. docs is true when
the change touches the documentation, its generators, or a file a wiki page rests on (a sources glob of
wiki/manifest.json); docsPages names those pages, to reread. It opens no job: the fast tier runs the doc tests on
every change.
Everything on a pull request: the full-ci label
On main, on a tag and on a pull request labelled full-ci, the plan is --all: every gate opens. The label also
starts the mutation jobs of ci and a candidate run for information. Add the label, then push (the label applies to
the next run).
pages-only runs
Applies from 4.5.0 · feature since 4.5.0
docs-publish ends by gh workflow run ci.yml --ref main -f pages-only=true. The plan is then --pages-only: no
gate opens, and check, coverage and cross-runtime skip everything but the Pages build, /docs/ and the upload.
A dispatched run cancels no running run: it waits. GitHub keeps one pending run per concurrency group whatever
cancel-in-progress says, so a dispatch queued while a push's run on main is pending replaces it (that commit is
then deployed by the dispatch without its own checks), and a later push replaces a pending dispatch.
Limit ·
pages-fail-closedIf/docs/cannot be assembled from the released archives (a network failure, an archive refused), thecheckjob fails onmainand Pages is not deployed: never a site without/docs/nor with an unchecked one. A rerun of the job is the recovery.
A job failed on my pull request
- Read the plan's summary in the
planjob (or runnpm run ci:planlocally): it says why each job ran.
Reproduce locally, in the engine repository, with the job's own command:
Red job Run locally checkthe failing step: npm run quality,npm run docs:truth,npm run build:game,npm run quality:baseline -- --check --dist(afternpm run build:web),npm run prove:game,npm run changes -- --checkcoveragenpm run test:coverage(a test, or a floor ofvite.config.tsnot met)node-24npm run test:nodeunder Node 24e2e,reference,second-gamenpm run build:game,npm run preview -- --port 5173 &, then the row'snpm run e2e -- http://127.0.0.1:5173/ --game demo --prod …(E2E_BROWSER=webkitfor a WebKit row)fresh-install,external-consumer,upgradenpm run fresh-install,npm run external-consumer,npm run upgrade-check -- --from=previousmutation (<set>)npm run test:mutation:core -- --set=<set>(Mutation testing)A failed e2e row uploads its screenshots as an artifact: read them first.
- Check the known flakes below before rerunning. A rerun of a red job that is not a known flake hides a bug.
- Fix, push, read
pr-gateagain.
Known flakes and their handling
| Symptom | Where | What is done | Since |
|---|---|---|---|
WebKit's Context is stopped (or Cache API operation failed: Context is stopped) after every check passed | e2e:pwa and e2e on WebKit, during the teardown | Reported as a warning ("reported, not counted"), only on WebKit, only in that exact form and only after every check passed; any other browser error still fails. Seen on #111, #118 (13 Oct 2026) and #139 (18 Oct 2026) | 4.1.20 (e2e:pwa), 4.2.1 (e2e) |
The Firefox pass of e2e:studio-hosted timed out at step 6d (Prepare assets) | studio-hosted | A real bug (a late status poll overwrote the panel), fixed in 4.3.0 (#210). If it comes back, it is a regression, not a flake | fixed in 4.3.0 |
apt-get update hangs for many minutes on the runner's Ubuntu mirror | every job that installs ffmpeg | scripts/ci-ffmpeg.sh: apt bounded to four minutes an attempt, then a static ffmpeg from GitHub's CDN (BtbN/FFmpeg-Builds); release.yml carries the same logic inline. Install steps have 20 minutes. Seen on 7 Oct 2026 | 4.1.15 |
toomanyrequests pulling an image from Docker Hub | bridge-postgres, mutation, the nightly and the candidate's services | Images pulled from mirror.gcr.io at the same digest. Seen on two jobs on 9 Oct 2026 | 4.1.18 |
npm ci or pip install fails on a network error | any job | Every install step is tried three times, 15 s apart | — |
| WebKit cannot navigate offline under Playwright | e2e:pwa WebKit, reality WebKit | --allow-skip: that step reports "skipped", never counted as a proof; a person checks Safari offline before each release (Field passes) | 3.1.1 |
Limit ·
windows-not-requiredThewindowsjob runs the unit suite without the files that assume POSIX; it is not a required check of the ruleset until the maintainer adds it.
Related identifiers
npm run ci:plan, thefull-cilabel,pr-gate, thepages-onlyinput.- Which tests run when, Before a pull request, Releasing, Mutation testing.