# Gate every pull request into `main`. This repo is documentation plus a template # module, so the checks are about whether the documentation is still TRUE rather # than whether software works. # # ── What each job is really asking ─────────────────────────────────────────── # # • `prose` — the documentation, checked as far as documentation can be. Every # relative link resolves, and no link pins a reader to a commit snapshot of a # document that moves. Nothing is fetched: this project's Gitea is self-hosted, # so an HTTP check would fail on a runner without credentials and teach # everyone to ignore red. What breaks in practice is a relative path after a # file moves, and that is answerable offline. # # It also holds `template/README.md`'s rename checklist against the template # tree, in both directions — an unlisted file that still carries the # placeholder, and a listed file that no longer does, are both failures. That # checklist is the only instruction a reader has for the first thing they do # with the template, and it is prose, so it rots the way prose does. The two # checks in `scripts/` have their own unit tests, run in the same job. # # • `template` — the interesting one, and the anti-rot mechanism of the whole # repo (MODULE_SYSTEM.md §2.11.1 d2). It clones CORE at the ref pinned in # `ci/core-ref.json` and asks three things: # # 1. does `template/module.json`'s `coreApi` still EQUAL that core's # `MODULE_API_VERSION`? Equality, not "satisfies" — a range check would # stay green across a contract bump, and green would then mean "the # template still loads" when we need it to mean "someone has re-read the # book since the contract changed". This failing is the system working. # 2. does the template still build? A kit whose examples do not compile is # worse than no kit, because the reader trusts it first. # 3. do the template's own boundary guards still pass? They are the same # checks a real module ships (MODULE_API.md §5.1, §3.6), and the template # is what teaches a newcomer that they exist. # # ── The guard, and why the template job can report green with no template ──── # # Slice 0 is this scaffold; the template lands in slice 1. Rather than leave the # repo ungated in between, or land a workflow that red-Xes every docs PR until # there is something to build, the template steps are conditional on # `template/module.json` existing. Before it lands the job prints why it did # nothing; the moment the file appears the job arms itself with no edit here. # Same guard Module-uo#1 used through its own planning phase. # # Enforcement (one-time, in the Gitea UI): # Repository Settings → Branches → Branch Protection (rule for `main`) # • Enable Status Check # • Status check patterns: PR Checks / * # Gitea only lists a context in its dropdown after it has reported once, so let # this run on one PR first. The glob keeps matching as jobs are added. # # Runner: the shared self-hosted `ubuntu-latest` runner. Node only — no database, # no Docker socket. name: PR Checks on: pull_request: branches: [main] concurrency: group: pr-checks-${{ github.ref }} cancel-in-progress: true env: NPM_CONFIG_FETCH_RETRIES: 5 NPM_CONFIG_FETCH_RETRY_MINTIMEOUT: 20000 NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT: 120000 jobs: prose: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 # No dependencies on purpose — every step in this job has to run on a clone # with nothing installed, which is also how a reader will run them. - name: Check every link in the book run: node scripts/checkLinks.js - name: Check the rename checklist against the template run: node scripts/checkRenameSites.js # The checks, checked. A check that has never been shown to fail is a check # nobody knows the state of — and this one gates the instructions for the # first thing a reader does. - name: Test the checks themselves run: node --test scripts/checkRenameSites.test.js template: runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - name: Is there a template yet? id: guard run: | if [ -f template/module.json ]; then echo "present=true" >> "$GITHUB_OUTPUT" else echo "present=false" >> "$GITHUB_OUTPUT" echo "No template/module.json — the template lands in Phase 5 slice 1." echo "The steps below are skipped until it does; see this file's header." fi # Anonymous HTTPS, and a full clone rather than a shallow one: the pin is a # commit sha, and `--depth 1` can only fetch a branch tip. - name: Clone core at the pinned ref if: steps.guard.outputs.present == 'true' run: | REPO=$(node -p "require('./ci/core-ref.json').repo") REF=$(node -p "require('./ci/core-ref.json').ref") echo "core: $REPO @ $REF" git clone --quiet "$REPO" .core git -C .core checkout --quiet "$REF" - name: Is the kit still written against this core? (MODULE_SYSTEM.md §2.11.1 d2) if: steps.guard.outputs.present == 'true' run: node scripts/checkCoreApi.js --core .core - name: Install the template's deps if: steps.guard.outputs.present == 'true' run: | npm ci --prefix template/server npm ci --prefix template/client - name: Check the template's module boundary (MODULE_API.md §5.1) if: steps.guard.outputs.present == 'true' run: npm run check:imports --prefix template/server # The build comes before the externals check because that check reads the # BUILT chunk: whether `import { useState } from 'react'` became core's React # or a bare specifier no browser can resolve is decided by vite.config.js, and # is invisible in source. - name: Build the template's client chunk if: steps.guard.outputs.present == 'true' run: npm run build --prefix template/client - name: Check the built chunk's externals (MODULE_API.md §3.6) if: steps.guard.outputs.present == 'true' run: npm run check:externals --prefix template/client - name: Run the template's tests if: steps.guard.outputs.present == 'true' run: npm test --prefix template/server # After the build, and that ordering is the point: two of the client tests # read the BUILT chunk and SKIP when there is none. Run before the build, # this job would report green while asking nothing about the artifact that # ships — which is exactly how the first real module's two artifact tests sat # green and inert. - name: Run the template's client tests if: steps.guard.outputs.present == 'true' run: npm test --prefix template/client # The committed OpenAPI fragment, regenerated and compared. Core merges that # file verbatim into its own spec, so a stale one documents a URL surface the # module does not serve — and nothing at runtime will ever say so. - name: Check the template's OpenAPI fragment is current (MODULE_API.md §2.8) if: steps.guard.outputs.present == 'true' run: npm run check:swagger --prefix template/server