From 6a520e27b1d14c3df1e682241869e82e67b514ef Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Fri, 31 Jul 2026 04:14:55 +0200 Subject: [PATCH 1/2] #236: pin the Node version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .node-version (22.15.1) becomes the single authoritative Node version: CI/CD select Node only via node-version-file, every Dockerfile pins node:22.15.1-alpine, and the engines floor in package.json states the same version (open-ended upwards so a newer local Node keeps working — reproducibility rests on images and CI). An early CI step fails on any drift between those places; update procedure in operations.md (Update strategy). Precondition for the reproducibility claim in #219. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ --- .gitea/workflows/cd.yml | 2 +- .gitea/workflows/ci.yml | 35 +++++++++++++++++++++++++++++--- .node-version | 1 + apps/api/Dockerfile | 4 ++-- apps/backup/Dockerfile | 4 ++-- apps/collab/Dockerfile | 4 ++-- apps/web/Dockerfile | 2 +- docs/architecture/operations.md | 10 +++++++++ docs/vs-nfd/20-massnahmenplan.md | 2 +- package.json | 2 +- 10 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 .node-version diff --git a/.gitea/workflows/cd.yml b/.gitea/workflows/cd.yml index 3f7401d..a899e2f 100644 --- a/.gitea/workflows/cd.yml +++ b/.gitea/workflows/cd.yml @@ -86,7 +86,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 22 + node-version-file: .node-version cache: pnpm - name: Install dependencies diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index a881ee5..e744794 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -58,13 +58,42 @@ jobs: exit 1 fi + # One authoritative Node version (issue #236): `.node-version` is the + # pin; every Dockerfile image tag and the engines floor must match it + # exactly, and workflows select Node only through node-version-file. + # Raising Node = update .node-version, every `FROM node:` tag and the + # engines floor in ONE commit (procedure: docs/architecture/operations.md + # §Update strategy). The bracketed grep pattern keeps this step from + # matching its own source (same trick as the secret fence above). + - name: Node version pin is consistent + run: | + set -euo pipefail + ver="$(cat .node-version)" + echo "pinned Node version: $ver" + bad=0 + for f in apps/*/Dockerfile; do + if grep '^FROM node:' "$f" | grep -v "node:${ver}-alpine"; then + echo "$f pins a different Node image than node:${ver}-alpine" + bad=1 + fi + done + if grep -rn "node-version[:] " .gitea/workflows; then + echo "workflows must use node-version-file, not a literal version" + bad=1 + fi + if ! grep -q "\"node\": \">=${ver}\"" package.json; then + echo "package.json engines floor does not match ${ver}" + bad=1 + fi + exit "$bad" + - name: Set up pnpm uses: pnpm/action-setup@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 22 + node-version-file: .node-version cache: pnpm - name: Install dependencies @@ -119,7 +148,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 22 + node-version-file: .node-version cache: pnpm - name: Install dependencies @@ -594,7 +623,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 22 + node-version-file: .node-version cache: pnpm - name: Install dependencies diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..8320a6d --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22.15.1 diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index ace9431..f8dfe32 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -1,7 +1,7 @@ # Build context is the repository root (workspace build): # docker build -f apps/api/Dockerfile . -FROM node:22.15-alpine AS build +FROM node:22.15.1-alpine AS build WORKDIR /repo RUN npm install -g pnpm@11 COPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json ./ @@ -23,7 +23,7 @@ RUN pnpm install --frozen-lockfile --filter @dorfteich/api... \ && cp -r apps/api/dist /out/dist \ && cp -r /repo/fonts /out/fonts -FROM node:22.15-alpine +FROM node:22.15.1-alpine ARG APP_VERSION=0.0.0-dev # Default the data dirs to the writable, node-owned locations created below, so # the image works out of the box even where compose does not set them; compose diff --git a/apps/backup/Dockerfile b/apps/backup/Dockerfile index df287e6..b2a5aa4 100644 --- a/apps/backup/Dockerfile +++ b/apps/backup/Dockerfile @@ -1,7 +1,7 @@ # Build context is the repository root (workspace build): # docker build -f apps/backup/Dockerfile . -FROM node:22.15-alpine AS build +FROM node:22.15.1-alpine AS build WORKDIR /repo RUN npm install -g pnpm@11 COPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json ./ @@ -14,7 +14,7 @@ RUN pnpm install --frozen-lockfile --filter @dorfteich/backup... \ && pnpm --filter @dorfteich/backup deploy --prod --legacy /out \ && cp -r apps/backup/dist /out/dist -FROM node:22.15-alpine +FROM node:22.15.1-alpine ARG APP_VERSION=0.0.0-dev ENV NODE_ENV=production APP_VERSION=${APP_VERSION} \ # Baked-in volume paths (self-sufficient without compose env, like the diff --git a/apps/collab/Dockerfile b/apps/collab/Dockerfile index 84285fe..78a1f44 100644 --- a/apps/collab/Dockerfile +++ b/apps/collab/Dockerfile @@ -1,7 +1,7 @@ # Build context is the repository root (workspace build): # docker build -f apps/collab/Dockerfile . -FROM node:22.15-alpine AS build +FROM node:22.15.1-alpine AS build WORKDIR /repo RUN npm install -g pnpm@11 COPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json ./ @@ -14,7 +14,7 @@ RUN pnpm install --frozen-lockfile --filter @dorfteich/collab... \ && pnpm --filter @dorfteich/collab deploy --prod --legacy /out \ && cp -r apps/collab/dist /out/dist -FROM node:22.15-alpine +FROM node:22.15.1-alpine ARG APP_VERSION=0.0.0-dev ENV NODE_ENV=production APP_VERSION=${APP_VERSION} WORKDIR /app diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 7574ebe..e91f977 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -1,7 +1,7 @@ # Build context is the repository root (workspace build): # docker build -f apps/web/Dockerfile . -FROM node:22.15-alpine AS build +FROM node:22.15.1-alpine AS build ARG APP_VERSION=0.0.0-dev WORKDIR /repo RUN npm install -g pnpm@11 diff --git a/docs/architecture/operations.md b/docs/architecture/operations.md index ba6cb82..fa78c7c 100644 --- a/docs/architecture/operations.md +++ b/docs/architecture/operations.md @@ -169,6 +169,16 @@ not a copy of the purged page. - **Base image / dependency hygiene**: monthly dependency-update story (renovate-style batch PR); security advisories for pinned images tracked in the release checklist. +- **Toolchain pin (issue #236)**: `.node-version` is the single + authoritative Node version. CI/CD select Node exclusively via + `node-version-file`, every Dockerfile pins `node:-alpine`, and + the `engines.node` floor in `package.json` states the same version + (open-ended upwards — a newer local Node keeps working; reproducibility + rests on the images and CI, not the laptop). An early CI step fails on + any drift between those places. Raising Node (e.g. for a security fix): + update `.node-version`, all four Dockerfiles and the engines floor in + ONE commit and let CI confirm. pnpm is pinned the same way via + `packageManager`. ## Capacity & limits (initial values, instance-tunable) diff --git a/docs/vs-nfd/20-massnahmenplan.md b/docs/vs-nfd/20-massnahmenplan.md index c21b831..82fe633 100644 --- a/docs/vs-nfd/20-massnahmenplan.md +++ b/docs/vs-nfd/20-massnahmenplan.md @@ -255,7 +255,7 @@ fehlten — als Issues angelegt: · 1 AT · #234 (M24, I-23) - [x] `page_links.target_slug`-Residuum nach Purge entscheiden · 0,5 AT · #235 (M24, I-24) -- [ ] Node-Version pinnen — Voraussetzung für #219 · 0,5 AT · #236 (M25, I-26) +- [x] Node-Version pinnen — Voraussetzung für #219 · 0,5 AT · #236 (M25, I-26) Ohne eigenes Issue: IndexedDB-Kopie auf Endgeräten (I-25) — als Akzeptanzkriterium in #226 (Abgrenzungserklärung) und #231 diff --git a/package.json b/package.json index 3e42aad..c85b0b4 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "Dorfteich — an open-source wiki system with real-time collaboration", "license": "MIT", "engines": { - "node": ">=22" + "node": ">=22.15.1" }, "packageManager": "pnpm@11.9.0", "scripts": { -- 2.45.2 From 93261775342c9bb6e063257f43aa916959f9a6da Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Fri, 31 Jul 2026 04:16:07 +0200 Subject: [PATCH 2/2] #236: also pin the node helper images in workflows release.yml and drill.yml ran throwaway `docker run node:22.15-alpine` helpers outside the pin; the drift check now also fails on any node:-alpine reference in .gitea/workflows. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ --- .gitea/workflows/ci.yml | 4 ++++ .gitea/workflows/drill.yml | 2 +- .gitea/workflows/release.yml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index e744794..09406f0 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -81,6 +81,10 @@ jobs: echo "workflows must use node-version-file, not a literal version" bad=1 fi + if grep -rnE 'node:[0-9][^ ]*-alpine' .gitea/workflows | grep -v "node:${ver}-alpine"; then + echo "a workflow references a different node image than node:${ver}-alpine" + bad=1 + fi if ! grep -q "\"node\": \">=${ver}\"" package.json; then echo "package.json engines floor does not match ${ver}" bad=1 diff --git a/.gitea/workflows/drill.yml b/.gitea/workflows/drill.yml index 9278d43..bc8272e 100644 --- a/.gitea/workflows/drill.yml +++ b/.gitea/workflows/drill.yml @@ -55,7 +55,7 @@ jobs: } > comment.md # JSON-encode via a node container — the runner image guarantees # only git/curl/docker, not python or node. - docker run --rm -i node:22.15-alpine node -e \ + docker run --rm -i node:22.15.1-alpine node -e \ 'const fs=require("fs");process.stdout.write(JSON.stringify({body:fs.readFileSync(0,"utf8")}))' \ < comment.md > comment.json curl -sf -X POST \ diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 3aec54d..0f32860 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -54,7 +54,7 @@ jobs: echo '_No database migrations in this release._' fi } > notes.md - TAG=$TAG docker run --rm -i -e TAG node:22.15-alpine node -e \ + TAG=$TAG docker run --rm -i -e TAG node:22.15.1-alpine node -e \ 'const fs=require("fs");const body=fs.readFileSync(0,"utf8");process.stdout.write(JSON.stringify({tag_name:process.env.TAG,name:process.env.TAG,body}))' \ < notes.md > release.json curl -sf -X POST \ -- 2.45.2