name: PR checks # Gitea Actions caution, learned elsewhere in this org: never leave an empty # template expression anywhere in a `run:` script, not even inside a comment. # The runner silently SKIPS the whole step without failing the job, and the # problem is invisible in the workflow list. on: pull_request: branches: [main] push: branches: [main] jobs: checks: runs-on: ubuntu-latest steps: - name: Check out uses: actions/checkout@v4 - name: Set up Node uses: actions/setup-node@v4 with: node-version: '22' cache: npm - name: Install run: npm ci - name: Design tokens # PLAN.md §7 — no colour literal outside src/styles/tokens.css. run: npm run check:tokens - name: Branding pipeline # PLAN.md §7 — brand-default is complete, every /brand/* URL the source asks for # resolves against the route's own allowlist, and every brand string the boot # rewrite replaces is distinctive enough to replace blindly. run: npm run check:brand - name: Play Data Safety declaration # PLAN.md §9 / D33 — PLAY_DATA_SAFETY.md is generated from the same # src/data/collection.mjs rows that /privacy section 2 renders, so the published # policy and the answers given to Google cannot drift apart. This re-runs the # generator and fails if the committed copy differs. # # It runs before the build because it needs neither one: it is the cheapest check # here and the one whose failure is easiest to act on. run: npm run check:datasafety - name: Types run: npm run check - name: Unit tests # PLAN.md §8 — the beta signup's decision path: honeypot, form token, timing, rate # limit, cap, validation, duplicate, removal. # # The first thing in this repository that the other checks cannot see. They all read # the built output, and none of this appears there: a honeypot that has stopped # working produces a build that is identical in every way to one where it works. # # The test file is NAMED rather than the directory passed. `node --test test/` fails # on Node 22 with MODULE_NOT_FOUND — directory mode is not portable across the # versions this org runs, and this workflow pins 22 while developers are on 24, so # the shorter form would pass locally and break only here. run: npm test - name: Sidebar # PLAN.md §12, phase 8. src/config/sidebar.mjs holds two trees — the one Starlight # renders and the one §10 planned — and they must agree on groups, labels and # ORDER. Order because the order of "Getting started" IS the installation path. # # While pages were being written the planned tree was a checklist; now that every # page exists it is a hand-maintained second copy, and it had already drifted # unnoticed (phase 7 added Content under D37 and never updated it). Nothing caught # that because nothing read it. # # No token, no network, no build — so it runs early and fails fast. run: npm run check:sidebar - name: Screenshots # PLAN.md §12, phase 9 (D45). src/data/screens.mjs is the one list of what the site # shows of itself: every entry must have a file, at the size the markup declares, and # every file must have an entry. The size half is the one that repays the check — # a re-capture taken at the wrong viewport looks perfectly fine on its own and only # reveals itself as a page that reflows while it decodes. # # No browser and no game server: the capture tool is an authoring script whose output # is committed, exactly like the brand assets, so CI only reads what it produced. run: npm run check:screens - name: Production build run: npm run build - name: Links # PLAN.md §12 — every internal link resolves, and every outbound link into a # RunicGateway repository points at a branch path rather than a commit permalink. # # It runs AFTER the build, and that ordering is the design rather than a # convenience: it reads the built HTML, so links assembled from data files and # template literals are checked as the strings they actually become. A source scan # would see an expression and skip most of what phase 4 added. # # No network: the outbound rule is about the shape of a URL, and a build that # fails because some other host is slow is a check people learn to ignore. run: npm run check:links - name: Platform facts # PLAN.md §12 — every version, protocol number and bundle tag is re-read from # its authority over the Gitea API and must agree with src/data/platform.json. # # This needs a token that can read the OTHER repositories in the org: link, # servuo-plugins, website and installer. The automatic per-run token is scoped # to this repository alone and 404s on all four, so the job uses the org-level # REGISTRY_TOKEN, which already exists and already carries the right scope. # # The secret is named for the registry; the script reads GITEA_TOKEN. Mapping it # here rather than renaming either side keeps the script's interface honest — it # wants a Gitea token, not this org's particular secret. # # It runs last, and it is the only step that touches the network, so a Gitea # outage cannot mask a real failure in the build. env: GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }} run: npm run check:facts - name: Quickstart against website main # PLAN.md §12, phase 7 (D35). /docs/getting-started/install-the-site/ prints a # Compose file and an environment file the reader copies without leaving the page, # which is the one place this site knowingly keeps a copy of another repo's file. # # So the copy is checked in BOTH directions: every value it states must match # website's own docker-compose.yml and .env.example on main, and every service and # variable THEY have must be either included or listed as deliberately omitted with # a reason. A new variable upstream turns this repo red until someone decides # whether a first install needs it — the same intent as the facts check above. # # Same token, and for the same reason: it reads another repository in the org. env: GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }} run: npm run check:quickstart - name: Reference enumerations against their sources # PLAN.md §12, phase 8. The Reference section names things — every environment # variable, config key, installer command, visibility rung and canonical document. # §1 forbids re-specifying a contract, and this is what makes writing the NAMES # down safe anyway: each list is a SET comparison against the repository that owns # it, in both directions. # # The second direction is the one that earns its keep. A reference page does not # usually rot by describing something that vanished — it rots by quietly not # mentioning the three things added since it was written. # # Descriptions are deliberately NOT checked; nothing here can know whether a # one-line summary is still true, so it does not pretend to. # # Same token, and for the same reason: it reads five other repositories in the org. env: GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }} run: npm run check:reference - name: The headers the server actually sends # PLAN.md §6 / D48, phase 10. Every other check reads dist/; this one starts # scripts/serve.mjs and reads the responses, because the defect it exists for # happened after the build was already correct. @astrojs/node matched a request to a # policy with a SUBSTRING test, so /modules/ was served the policy built for # /docs/modules/building-a-module — every file on disk right, the bytes on the wire # wrong, and the page rendered with its own stylesheet refused. # # It needs the build, so it cannot live in the "Unit tests" step above. run: npm run test:served - name: Accessibility # PLAN.md §13, phase 10. Seven structural rules over every built page: one

and # no skipped heading level, an alt attribute on every image, a label on every form # control, an accessible name on every link and button, , one
with # a skip link that reaches it, and no positive tabindex. # # Structural on purpose. A static check cannot measure contrast on a rendered page # or find a focus trap, and a check that pretended to would be trusted for things it # cannot see. What it does catch is the class of defect that is invisible to a # sighted author and permanent once shipped — and it covers Starlight's forty pages # too, so a dependency upgrade that loses a label is a red build rather than a # discovery. # # After the build, because it reads dist/client. No token and no network. run: npm run check:a11y - name: Content-Security-Policy # PLAN.md §6 / D48. The policy is a real response header — the Node adapter's # staticHeaders writes dist/_headers.json and the standalone server sends it — so # frame-ancestors applies and the operator's proxy needs no CSP config. # # The check that matters is the second one: every inline script and style must be # covered by a hash in ITS OWN page's policy. Astro does not hash