# 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 ─────────────────────────────────────────── # # • `links` — 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. # # • `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: links: 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 — this has to run on a clone with nothing # installed, which is also how a reader will run it. - name: Check every link in the book run: node scripts/checkLinks.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