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": {