All checks were successful
PR checks / checks (pull_request) Successful in 9m36s
PLAN.md §13 phase 10, with four decisions of record — D47-D50, taking the count
to fifty. Three were straightforward; the CSP turned into the phase's real work,
because the thing meant to be a configuration flag was broken in a dependency and
broken silently.
D47 — search reaches the marketing pages, and the header gets a box.
Base.astro marks its <main> as a Pagefind body, so all ten join the index the
docs already query, and Search.astro opens it in a <dialog>. Nothing is fetched
until the dialog is opened (the bundle is 120 kB and these pages otherwise ship
almost no JavaScript). Pagefind titles a result from the first <h1>, and these
pages have editorial ones — "The app for a deployment you already use" — so the
index is given the page's short name instead. applyBrand.mjs now re-indexes after
a rewrite, closing a note phase 2 left for this phase.
D48 — the CSP is a real response header, sent by the container. Not a <meta>,
which ignores frame-ancestors, and not advice for someone's reverse proxy, which
puts the strictest promise in §6 outside what this repo tests. Three things
fought it, all the same shape — correct build, broken page, no error:
* Astro does not hash <script is:inline>, and Starlight ships six per docs
page, so the first build with CSP on had a strict header and a dead theme
switcher. The hashes are now generated into src/config/cspHashes.mjs and
checkCsp.mjs verifies every inline block against its own page's policy.
* Expressive Code writes ~3,700 inline style ATTRIBUTES, which cannot be
hashed, hence style-src-attr 'unsafe-inline' — scoped to that directive, so
script-src is untouched.
* @astrojs/node matched a request to a policy with pathname.includes(), a
substring test: /modules/ was served /docs/modules/building-a-module's
policy and rendered with its own stylesheet refused. scripts/serve.mjs keeps
the same _headers.json and matches by equality; test/headers.test.mjs starts
the server and reads the responses, because nothing that reads dist/ can see
this.
D49 — robots.txt allows everything and names the sitemap (there was no way to
find it: no robots.txt, and D9 rules out a search console). D50 — Organization
and SoftwareApplication, no ratings and no docs-wide Article markup.
checkA11y.mjs is the eleventh check: seven structural rules over all fifty pages,
verified by breaking each in turn. The walk at 390/768/1280 found no overflow
anywhere, the CSP violations above, a 17x17 consent checkbox (WCAG 2.2 SC 2.5.8
wants 24), and a skip link that moved the scroll but not the focus.
npm run verify is green: fourteen steps, both test suites, all eleven checks.
Co-Authored-By: Claude <noreply@anthropic.com>
203 lines
10 KiB
YAML
203 lines
10 KiB
YAML
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 <h1> 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, <html lang>, one <main> 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 <script is:inline>,
|
|
# and Starlight ships six of them per documentation page, so the first build with CSP
|
|
# enabled had a strict, correct header and a dead theme switcher — a failure with no
|
|
# symptom except a console message. A Starlight upgrade can reintroduce it at any
|
|
# time, which is why this runs on every PR rather than once.
|
|
#
|
|
# After the build, because it reads dist/. No token and no network.
|
|
run: npm run check:csp
|