Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
15 changes: 9 additions & 6 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,4 +47,4 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
9 changes: 5 additions & 4 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<https://ethical-tech-colab.github.io/website/>; the apex domain returned
nothing when tested on 2026-07-23. Could be local network rather than DNS,
Expand Down
21 changes: 18 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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;
Expand Down
122 changes: 97 additions & 25 deletions UPDATES-NEEDED.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "website",
"version": "0.1.0",
"private": true,
"engines": {
"node": ">=24"
},
"scripts": {
"dev": "next dev",
"build": "next build",
Expand Down