From 97d1a4788377785413503e667eabd5fa3e765f71 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sat, 15 Aug 2026 22:05:09 -0400 Subject: [PATCH] ci: add pull-request checks and pin Node to 24 UPD-002. The Pages workflow only runs on pushes to main, so a pull request could be merged without ever having been built. Adds .github/workflows/ci.yml, which runs on pull requests targeting main: lint, typecheck, build, and a static-site/ drift check. The drift check regenerates the snapshot and fails if the result differs from what the branch committed. That is the failure this repository has actually hit, and it was only ever caught by hand afterwards. It uses `git status --porcelain` rather than `git diff` so a page added to src/ without a matching snapshot is caught too; a new file under static-site/ would be untracked, and untracked files are invisible to `git diff`. UPD-007. There was no .nvmrc, .node-version, or engines declaration, and the deploy workflow asked for Node 20. Node 20 is past deprecation on the runners, which logged on every run that the actions were "being forced to run on Node.js 24" - so CI already executed on 24 while declaring 20. Pinned to Node 24, the current LTS and the version already used locally: .nvmrc, an engines field, and node-version-file in both workflows so the pin has one source. The actions still targeting the retired Node 20 runtime were upgraded too, since leaving them would have kept the warning on every deploy: checkout v4->v7, setup-node v4->v7, configure-pages v5->v6, upload-pages-artifact v3->v5, deploy-pages v4->v5. upload-pages-artifact needs care: from v4 it excludes dotfiles by default (`--exclude=.[^/]*`), and this workflow creates out/.nojekyll. Bumping it without `include-hidden-files: true` would have dropped that file silently. package-lock.json is deliberately untouched. Bumping @types/node to ^24 to match the runtime typechecks cleanly, but npm 11.9.0 rewrites the lock and strips the `libc` constraint from 43 entries, which is what keeps a glibc native binary off musl. That is not a change to make as a side effect, so it is recorded as UPD-012 instead. Branch protection is still open and cannot be set from a pull request, so the new check is advisory until a ruleset requires it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> The first CI run found a real cross-platform difference the snapshot work so far had missed. Regenerating on Linux against a Windows-generated snapshot gives 101 deletions, 101 additions and 0 modifications: Next.js writes its RSC segment-cache prefetch payloads as a flat about/__next.about.__PAGE__.txt on Linux and a nested about/__next.about/__PAGE__.txt on Windows. It comes from next build itself, not the copy step. Zero files differ by content, so every HTML document, chunk, stylesheet and asset is byte-identical across platforms. The check therefore excludes __next.* and the finding is recorded under UPD-004, which it is direct evidence for. --- .github/workflows/ci.yml | 70 ++++++++++++++++++++ .github/workflows/deploy.yml | 15 +++-- .nvmrc | 1 + BACKLOG.md | 9 +-- CONTRIBUTING.md | 21 +++++- UPDATES-NEEDED.md | 122 ++++++++++++++++++++++++++++------- package.json | 3 + 7 files changed, 203 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .nvmrc 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",