diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..3f89edb7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,70 @@ +name: CI + +# Non-deploying checks for pull requests. The Pages workflow only runs on +# pushes to main, so without this a pull request could be merged without ever +# having been built. +on: + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +# A new push to the same pull request supersedes the run in progress. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + name: Lint, typecheck, build, snapshot + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npx eslint + + - name: Typecheck + run: npx tsc --noEmit + + - name: Build static export + run: npm run build + + # static-site/ is a tracked snapshot of the export. Regenerating it on an + # unchanged checkout must produce no diff, so any diff here means the + # snapshot no longer describes src/. + - name: Regenerate the static snapshot + run: npm run sync:static + + - name: Check the snapshot is in sync + run: | + # --porcelain rather than `git diff`, so a page added to src/ without + # regenerating the snapshot is caught too: its new file under + # static-site/ would be untracked, and untracked files are invisible + # to `git diff`. + # + # __next.* files are excluded. Next.js writes its RSC segment-cache + # prefetch payloads with a platform-dependent path shape — Linux emits + # a flat `about/__next.about.__PAGE__.txt`, Windows emits a nested + # `about/__next.about/__PAGE__.txt`. Same content, different layout, so + # a snapshot generated on one platform can never match a rebuild on the + # other. Everything else (HTML, JS, CSS, assets) is byte-identical + # across both. Tracked as UPD-004 in UPDATES-NEEDED.md. + drift="$(git status --porcelain -- static-site | grep -v '/__next\.' || true)" + if [ -z "$drift" ]; then + echo "static-site/ matches src/." + exit 0 + fi + echo "::error::static-site/ is out of sync with src/. Run 'npm run sync:static' and commit the result as a separate commit (see CONTRIBUTING.md section 4)." + echo "$drift" + exit 1 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index dfe238fa..1b4b857a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -20,21 +20,24 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: - node-version: 20 + node-version-file: .nvmrc cache: npm - - uses: actions/configure-pages@v5 + - uses: actions/configure-pages@v6 - name: Install dependencies run: npm ci - name: Build static site run: npm run build - name: Disable Jekyll (serve _next assets) run: touch out/.nojekyll - - uses: actions/upload-pages-artifact@v3 + - uses: actions/upload-pages-artifact@v5 with: path: out + # .nojekyll above is a dotfile, and this action excludes dotfiles by + # default from v4 onward. Without this it would be dropped silently. + include-hidden-files: true deploy: needs: build @@ -44,4 +47,4 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..a45fd52c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/BACKLOG.md b/BACKLOG.md index e0ab5e49..3b6c703a 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -316,11 +316,12 @@ All twenty ETC Pages sites now serve the purple ETC mark as `etc-icon.svg` + carrying into the write-ups if they are not there yet: the web pipeline still runs the non-canonical deduction scorer, and the catalogue's confidence scores are researched by hand rather than computed. -- [ ] **The `gh` token has no `workflow` scope.** Any push touching - `.github/workflows/*` in an ETC repo is rejected outright. It forced a +- [x] **The `gh` token has no `workflow` scope.** Any push touching + `.github/workflows/*` in an ETC repo was rejected outright. It forced a redesign in `ercf` (the icon href is relative so no build-time rewrite is - needed, which is the better fix anyway) but it will block the next - workflow change. Re-authorise with `workflow` scope when convenient. + needed, which is the better fix anyway). Resolved: the active token now + carries `workflow`, which unblocked pull-request CI (`UPD-002`) and the + Node pin (`UPD-007`). - [ ] **ethicaltechlab.org did not resolve.** The live site answered on ; the apex domain returned nothing when tested on 2026-07-23. Could be local network rather than DNS, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 034a4270..055fcbc9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,6 +25,14 @@ git status --short npm ci ``` +The supported Node version is pinned in `.nvmrc` and declared in +`package.json`. Select it before installing, so your build matches CI: + +```powershell +nvm use +node --version +``` + Use one branch and one pull request per focused change. Before editing: 1. Check [BACKLOG.md](BACKLOG.md) and the open GitHub issues. @@ -147,9 +155,9 @@ Run the smallest targeted lint command that covers the files you changed: npx eslint src\path\to\changed-file.tsx ``` -The repository's full lint baseline currently has known legacy failures. Do not -introduce a new warning or error, and do not hide a new failure behind the -baseline. See [UPDATES-NEEDED.md](UPDATES-NEEDED.md). +The repository's lint baseline is clean: `npm run lint` prints nothing and exits +0. Pull-request CI runs it, so a new warning or error will fail the build. Fix +the finding rather than suppressing it. Always run the production build: @@ -239,6 +247,13 @@ Push the branch without force: git push -u origin HEAD ``` +Opening the pull request starts `.github/workflows/ci.yml`, which lints, +typechecks, builds the static export, and regenerates `static-site/` to confirm +the committed snapshot still matches `src/`. That last check is the one most +often tripped: if it fails, run `npm run sync:static` and commit the result as +its own commit. CI is not a substitute for the local checks above — it runs on +Linux and will not catch what you did not look at. + The pull request should state: - what changed and why; diff --git a/UPDATES-NEEDED.md b/UPDATES-NEEDED.md index 5c50402a..d26ea648 100644 --- a/UPDATES-NEEDED.md +++ b/UPDATES-NEEDED.md @@ -44,25 +44,29 @@ linting of generated output. ### UPD-002 - Add pull-request CI and protect `main` -**Priority:** High +**Priority:** High — **CI added; branch protection still open.** + +The Pages workflow runs only on pushes to `main`, so an invalid pull request +received no automated build before merge. -The repository has no branch protection or rulesets. The Pages workflow runs -only on pushes to `main`, so an invalid pull request receives no automated -build before merge. +`.github/workflows/ci.yml` now runs on every pull request targeting `main` and +on manual dispatch. It lints, typechecks, builds the static export, and then +regenerates `static-site/` and fails if the result differs from what the branch +committed — the drift this repository has actually been bitten by, now caught +before merge rather than after. -**Blocked by:** the `gh` token in use has no `workflow` scope, so any push -touching `.github/workflows/*` is rejected outright (recorded in `BACKLOG.md` -section 8). Adding pull-request CI means adding a file under -`.github/workflows/`, so re-authorising that token is a prerequisite of this -item — and therefore of every control that depends on a required build check. +The earlier blocker is resolved: the `gh` token in use now carries the +`workflow` scope, so pushes touching `.github/workflows/*` succeed. -**Proposed update:** Re-authorise the `gh` token with `workflow` scope, add -non-deploying pull-request CI, then require its build status and pull-request -approval through a GitHub ruleset. Follow +**Still open:** the repository has no branch protection or rulesets, so the new +check is advisory. Requiring the `Lint, typecheck, build, snapshot` status and +a pull-request approval through a GitHub ruleset needs an admin action that +cannot be made from a pull request. Follow [SITE-CONTROL-RECOMMENDATIONS.md](SITE-CONTROL-RECOMMENDATIONS.md). -**Acceptance:** A test pull request cannot merge until its required build -passes and its review requirement is met. +**Acceptance:** partially met — a pull request is now built automatically. Not +met until a test pull request *cannot merge* while that build is failing. + ### UPD-003 - Add ownership and review templates @@ -80,18 +84,49 @@ and new pull requests consistently capture validation and provenance. ### UPD-004 - Make static snapshot synchronization cross-platform -**Priority:** Medium +**Priority:** Medium — **raised: the two platforms are confirmed to disagree.** `npm run sync:static` calls a Bash script. This creates avoidable setup differences for Windows maintainers and AI clients. +**Measured difference.** Pull-request CI regenerates the snapshot on Linux and +compares it against the tracked copy, which was generated on Windows. The +result is precise: **101 files deleted, 101 added, 0 modified.** Every one is a +Next.js RSC segment-cache prefetch payload, and the difference is path shape +only: + +| Platform | Path | +|---|---| +| Linux | `static-site/about/__next.about.__PAGE__.txt` (flat file) | +| Windows | `static-site/about/__next.about/__PAGE__.txt` (nested directory) | + +This comes from `next build` itself, not from the copy step: the Windows `out/` +directory already has the nested shape. Because zero files differ by content, +every HTML document, JavaScript chunk, stylesheet, and asset is byte-identical +across the two platforms — the build-ID pin and `.gitattributes` work. The +remaining difference is layout alone. + +Two consequences: + +1. The drift check in `.github/workflows/ci.yml` excludes `__next.*` files. It + cannot compare them until this is resolved. +2. The tracked snapshot does not exactly mirror what is deployed. GitHub Pages + serves a Linux build, so production has the flat shape while the snapshot in + the repository has the nested one. The impact is limited to prefetch payload + locations, but it does contradict the snapshot's stated purpose of being a + 1:1 mirror of the export. + **Proposed update:** Replace or wrap the script with a Node-based equivalent that preserves the current build, clean copy, and `GENERATED.md` behavior. Review destructive file operations carefully and test path handling on Windows -and Linux. +and Linux. Decide separately whether to normalize the segment-cache paths, +regenerate the snapshot on Linux in CI, or accept the divergence and keep the +exclusion. **Acceptance:** The same npm command produces an equivalent snapshot on Windows -and Linux from a clean checkout. +and Linux from a clean checkout, and the CI drift check no longer needs an +exclusion. + ### UPD-005 - Decide the long-term `static-site/` policy @@ -160,17 +195,54 @@ risk decisions for each remaining production finding. ### UPD-007 - Pin the local Node version -**Priority:** Medium +**Priority:** Medium — **done.** + +The repository had no `.nvmrc`, `.node-version`, or `package.json` `engines` +declaration, and the deploy workflow asked for Node 20. + +Node 20 is now past deprecation on GitHub Actions runners: every run logged +`Node.js 20 is deprecated. The following actions target Node.js 20 but are +being forced to run on Node.js 24`. So CI was already executing on Node 24 +while declaring Node 20 — the two had silently diverged. + +Pinned to Node 24, the current LTS line and the version already in use locally: + +- `.nvmrc` contains `24`; +- `package.json` declares `"engines": { "node": ">=24" }`; +- both workflows read `node-version-file: .nvmrc`, so the pin has a single + source. + +The actions targeting the retired Node 20 runtime were upgraded at the same +time, since leaving them would have kept the deprecation warning on every +deploy: `checkout@v4→v7`, `setup-node@v4→v7`, `configure-pages@v5→v6`, +`upload-pages-artifact@v3→v5`, `deploy-pages@v4→v5`. + +**Acceptance:** met — a contributor can run `nvm use` before `npm ci`, and +local and CI builds are on the same major version by construction. + +**Deliberately left out:** `@types/node` still resolves to 20.19.43 while the +runtime is 24. Bumping it to `^24` typechecks cleanly, but running `npm install` +under npm 11.9.0 rewrites `package-lock.json` and strips the `libc` constraint +from 43 entries (43 removed, 0 added). Those constraints are what stop a glibc +native binary being installed on musl, so weakening them as a side effect of a +types bump is a poor trade inside a CI pull request. See UPD-012. + +### UPD-012 - Decide how the lockfile is regenerated + +**Priority:** Low -GitHub Actions uses Node 20, but the repository has no `.nvmrc`, -`.node-version`, or `package.json` `engines` declaration. +`npm install` under npm 11.9.0 removes the `libc` field from 43 entries in +`package-lock.json` that were written by an older npm. The change is purely an +npm-version artifact — it appears whether or not any dependency actually +changed — but it silently relaxes platform constraints for native binaries, +and it makes every dependency pull request look larger than it is. -**Proposed update:** Select a repository-supported Node 20 release and expose it -through the local version mechanism the maintainers actually use. Keep it -aligned with deployment. +**Proposed update:** Agree one npm version for lockfile regeneration, note it +next to the Node pin in `.nvmrc`, and re-add the missing `libc` entries in a +single dedicated commit rather than as a side effect of an unrelated change. -**Acceptance:** A new contributor can select the supported Node version before -running `npm ci`, and local and CI builds use the same major version. +**Acceptance:** A dependency pull request shows only the packages it actually +changed. ### UPD-008 - Add focused smoke checks diff --git a/package.json b/package.json index 52e3f405..9bc48040 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,9 @@ "name": "website", "version": "0.1.0", "private": true, + "engines": { + "node": ">=24" + }, "scripts": { "dev": "next dev", "build": "next build",