diff --git a/.gitea/workflows/pr-checks.yml b/.gitea/workflows/pr-checks.yml new file mode 100644 index 0000000..c8cb328 --- /dev/null +++ b/.gitea/workflows/pr-checks.yml @@ -0,0 +1,127 @@ +# Gate every pull request into `main` on a fast, DB-free check suite, so a broken +# build or a failing test can't reach the branch that gets released. +# +# Mirrors RunicGateway/website's pr-checks.yml — this module is two npm packages +# shaped like that repo's `server/` and `client/`, and it is loaded into that +# repo's process, so it is checked the same way with the same Node version. +# +# ── Package guard ──────────────────────────────────────────────────────────── +# This repo is in the planning phase and has no module code yet: the API contract +# is settled in Phase 1 and Phase 3 is what extracts the UO half of `website/` +# into this repo (docs/website/MODULE_SYSTEM.md §2.7). Rather than leave the repo +# ungated until then — or land a workflow that red-Xes every governance/docs PR — +# each half's gates are conditional on its package.json existing. Before the code +# lands, the job reports green with a notice saying so. The moment a package.json +# appears the gates arm themselves; nothing here has to change. +# +# The same trick guards the client build, which is the higher-risk half: it must +# build with Vite in library mode with react/react-dom/react-router-dom EXTERNAL, +# because there is exactly one React instance in the page and core owns it. A +# module that bundles its own React loads and then breaks hooks at runtime, which +# is precisely the kind of failure worth catching before merge. +# +# Not here yet, deliberately, because there is nothing for them to act on until +# Phase 3: a release workflow (the `module-uo-.tar.gz` artifact and its +# sha256 manifest), the zero-internal-imports check, and the module's own frozen +# route manifest. Each lands with the code it checks. +# +# Enforcement (one-time, in the Gitea UI): +# Repository Settings → Branches → Branch Protection (rule for `main`) +# • Enable Status Check +# • Status check patterns: PR Checks / * +# Note: Gitea only lists a context in its dropdown after it has reported once, +# so let this workflow run on one PR first. The `PR Checks / *` glob matches +# without needing the dropdown, and keeps matching as jobs are added. +# +# Runner: the shared self-hosted `ubuntu-latest` runner. These jobs need only +# Node — no Docker socket, no database. + +name: PR Checks + +on: + pull_request: + branches: [main] + +# A newer push to the same PR cancels the in-flight run. +concurrency: + group: pr-checks-${{ github.ref }} + cancel-in-progress: true + +jobs: + server-tests: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + + - name: Detect whether the server half exists yet + id: detect + run: | + set -euo pipefail + if [ -f server/package.json ]; then + echo "pkg=true" >> "$GITHUB_OUTPUT" + echo "==> server/package.json found - running the server gates." + else + echo "pkg=false" >> "$GITHUB_OUTPUT" + echo "==> No server/package.json yet (planning phase)." + echo " Skipping install/test. These gates arm themselves as soon" + echo " as Phase 3 lands the server half - see MODULE_SYSTEM.md." + fi + + - uses: actions/setup-node@v4 + if: ${{ steps.detect.outputs.pkg == 'true' }} + with: + node-version: 20 + cache: npm + cache-dependency-path: server/package-lock.json + + # `npm ci` rather than `npm install`: it also proves the lockfile is in + # sync with package.json instead of silently updating it. + - name: Install server deps + if: ${{ steps.detect.outputs.pkg == 'true' }} + run: npm ci --prefix server + + - name: Run server tests + if: ${{ steps.detect.outputs.pkg == 'true' }} + run: npm test --prefix server + + client-build: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + + - name: Detect whether the client half exists yet + id: detect + run: | + set -euo pipefail + if [ -f client/package.json ]; then + echo "pkg=true" >> "$GITHUB_OUTPUT" + echo "==> client/package.json found - running the client gates." + else + echo "pkg=false" >> "$GITHUB_OUTPUT" + echo "==> No client/package.json yet (planning phase)." + echo " Skipping install/test/build. These gates arm themselves as" + echo " soon as Phase 3 lands the client half." + fi + + - uses: actions/setup-node@v4 + if: ${{ steps.detect.outputs.pkg == 'true' }} + with: + node-version: 20 + cache: npm + cache-dependency-path: client/package-lock.json + + - name: Install client deps + if: ${{ steps.detect.outputs.pkg == 'true' }} + run: npm ci --prefix client + + - name: Run client tests + if: ${{ steps.detect.outputs.pkg == 'true' }} + run: npm test --prefix client + + # Building the ESM chunk in CI is not just a check: it is how the chunk + # that ships in the release is produced, since an operator never builds. + - name: Build the client chunk + if: ${{ steps.detect.outputs.pkg == 'true' }} + run: npm run build --prefix client