#236: pin the Node version
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 5m40s
CI / Build container images (pull_request) Successful in 4m15s
CI / Auth e2e pack (pull_request) Successful in 8m33s
CI / Import/export fidelity gate (pull_request) Successful in 59s

.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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
This commit is contained in:
Claude Fable 5 2026-07-31 04:14:55 +02:00
parent 9a43a2f6bb
commit 6a520e27b1
10 changed files with 53 additions and 13 deletions

View File

@ -86,7 +86,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 22 node-version-file: .node-version
cache: pnpm cache: pnpm
- name: Install dependencies - name: Install dependencies

View File

@ -58,13 +58,42 @@ jobs:
exit 1 exit 1
fi 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 - name: Set up pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 22 node-version-file: .node-version
cache: pnpm cache: pnpm
- name: Install dependencies - name: Install dependencies
@ -119,7 +148,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 22 node-version-file: .node-version
cache: pnpm cache: pnpm
- name: Install dependencies - name: Install dependencies
@ -594,7 +623,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 22 node-version-file: .node-version
cache: pnpm cache: pnpm
- name: Install dependencies - name: Install dependencies

1
.node-version Normal file
View File

@ -0,0 +1 @@
22.15.1

View File

@ -1,7 +1,7 @@
# Build context is the repository root (workspace build): # Build context is the repository root (workspace build):
# docker build -f apps/api/Dockerfile . # docker build -f apps/api/Dockerfile .
FROM node:22.15-alpine AS build FROM node:22.15.1-alpine AS build
WORKDIR /repo WORKDIR /repo
RUN npm install -g pnpm@11 RUN npm install -g pnpm@11
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json ./ 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 apps/api/dist /out/dist \
&& cp -r /repo/fonts /out/fonts && cp -r /repo/fonts /out/fonts
FROM node:22.15-alpine FROM node:22.15.1-alpine
ARG APP_VERSION=0.0.0-dev ARG APP_VERSION=0.0.0-dev
# Default the data dirs to the writable, node-owned locations created below, so # 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 # the image works out of the box even where compose does not set them; compose

View File

@ -1,7 +1,7 @@
# Build context is the repository root (workspace build): # Build context is the repository root (workspace build):
# docker build -f apps/backup/Dockerfile . # docker build -f apps/backup/Dockerfile .
FROM node:22.15-alpine AS build FROM node:22.15.1-alpine AS build
WORKDIR /repo WORKDIR /repo
RUN npm install -g pnpm@11 RUN npm install -g pnpm@11
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json ./ 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 \ && pnpm --filter @dorfteich/backup deploy --prod --legacy /out \
&& cp -r apps/backup/dist /out/dist && 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 ARG APP_VERSION=0.0.0-dev
ENV NODE_ENV=production APP_VERSION=${APP_VERSION} \ ENV NODE_ENV=production APP_VERSION=${APP_VERSION} \
# Baked-in volume paths (self-sufficient without compose env, like the # Baked-in volume paths (self-sufficient without compose env, like the

View File

@ -1,7 +1,7 @@
# Build context is the repository root (workspace build): # Build context is the repository root (workspace build):
# docker build -f apps/collab/Dockerfile . # docker build -f apps/collab/Dockerfile .
FROM node:22.15-alpine AS build FROM node:22.15.1-alpine AS build
WORKDIR /repo WORKDIR /repo
RUN npm install -g pnpm@11 RUN npm install -g pnpm@11
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json ./ 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 \ && pnpm --filter @dorfteich/collab deploy --prod --legacy /out \
&& cp -r apps/collab/dist /out/dist && 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 ARG APP_VERSION=0.0.0-dev
ENV NODE_ENV=production APP_VERSION=${APP_VERSION} ENV NODE_ENV=production APP_VERSION=${APP_VERSION}
WORKDIR /app WORKDIR /app

View File

@ -1,7 +1,7 @@
# Build context is the repository root (workspace build): # Build context is the repository root (workspace build):
# docker build -f apps/web/Dockerfile . # 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 ARG APP_VERSION=0.0.0-dev
WORKDIR /repo WORKDIR /repo
RUN npm install -g pnpm@11 RUN npm install -g pnpm@11

View File

@ -169,6 +169,16 @@ not a copy of the purged page.
- **Base image / dependency hygiene**: monthly dependency-update story - **Base image / dependency hygiene**: monthly dependency-update story
(renovate-style batch PR); security advisories for pinned images tracked (renovate-style batch PR); security advisories for pinned images tracked
in the release checklist. 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:<version>-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) ## Capacity & limits (initial values, instance-tunable)

View File

@ -255,7 +255,7 @@ fehlten — als Issues angelegt:
· 1 AT · #234 (M24, I-23) · 1 AT · #234 (M24, I-23)
- [x] `page_links.target_slug`-Residuum nach Purge entscheiden - [x] `page_links.target_slug`-Residuum nach Purge entscheiden
· 0,5 AT · #235 (M24, I-24) · 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 Ohne eigenes Issue: IndexedDB-Kopie auf Endgeräten (I-25) — als
Akzeptanzkriterium in #226 (Abgrenzungserklärung) und #231 Akzeptanzkriterium in #226 (Abgrenzungserklärung) und #231

View File

@ -5,7 +5,7 @@
"description": "Dorfteich — an open-source wiki system with real-time collaboration", "description": "Dorfteich — an open-source wiki system with real-time collaboration",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=22" "node": ">=22.15.1"
}, },
"packageManager": "pnpm@11.9.0", "packageManager": "pnpm@11.9.0",
"scripts": { "scripts": {