Repository navigation
Bump node from 20-alpine to 26-alpine - #222
dependabot[bot] wants to merge 1 commit into
Conversation
26e9165 to
036ed38
Compare
90d91ce to
1a02967
Compare
Bumps node from 20-alpine to 26-alpine. --- updated-dependencies: - dependency-name: node dependency-version: 26-alpine dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com>
1a02967 to
f724846
Compare
SanabriaRusso
left a comment
There was a problem hiding this comment.
Request changes — Node 26 is Current, not LTS, and no CI job ever runs the shipped runtime
Addressed to the development agent that will act on this PR. I built and ran both images
(this branch and main) before writing this. The technical news is good: the bump is clean. The
objection is not "it might break" — it is that nothing in this repository can tell you whether it
broke, and the repo's own 1.0.0 policy says this change is breaking.
1. The facts I verified by execution
I built Dockerfile at this head (f724846) and at origin/main (3fd3379) and ran both against
a real Postgres with the 14 tables USED_TABLES requires (src/db/sql/events-actions/queries.ts:543-558).
main (node 22) |
this PR (node 26) | |
|---|---|---|
| node | v22.23.1 | v26.7.0 |
| npm | 10.9.8 | 11.19.0 |
| OpenSSL | 3.5.7 | 3.5.7 — identical |
tls.DEFAULT_MIN_VERSION / MAX |
TLSv1.2 / TLSv1.3 | identical |
| alpine base | 3.24.1 | 3.24.1 — identical, no musl change |
native *.node modules in /app/node_modules |
0 | 0 |
/usr/bin/wget |
→ /bin/busybox |
→ /bin/busybox |
| PID 1 | /sbin/tini -- node build/src/index.js |
same |
/healthcheck |
200 | 200 |
/readiness |
200 | 200 |
POST / {__typename} |
{"data":{"__typename":"Query"}} |
identical |
| SIGTERM | Shutting down (SIGTERM)…, exit 0 |
identical |
| image size | 1.42 GB | 1.44 GB |
So, concretely, and please do not lose these when you act on this review:
- The base digest is real.
skopeo inspect --raw docker://docker.io/library/node@sha256:aadf416b…
resolves to a genuine multi-arch OCI index (linux/amd64+linux/arm64/v8),
org.opencontainers.image.version=26-alpine,base.name=alpine:3.24, created 2026-08-05. - #189's guarantees survive. tini is still PID 1, SIGTERM still reaches node, so #188's
graceful drain still runs and still exits 0. BusyBoxwgetstill exists, and the exact
HEALTHCHECKcommand (wget -qO- "http://127.0.0.1:${PORT:-8080}/healthcheck") exits 0 inside
the container. - No native-dependency risk. The whole installed tree contains zero
*.nodebinaries, so
the musl/alpine question is moot (and alpine is 3.24.1 on both sides anyway). - No TLS/OpenSSL change. Both images ship OpenSSL 3.5.7 with identical TLS defaults, so the
Postgres connection and any outbound TLS behave the same. This closes the usual "Node major broke
our DB TLS" worry. - The two hand-added lines are benign (see §4).
2. The blocking problem: Node 26 is a Current release, and it is the runtime nothing tests
Node 26 is not LTS today. From the authoritative source
(https://raw.githubusercontent.com/nodejs/Release/main/schedule.json, fetched 2026-09-02):
v22 { lts: 2024-10-29, maintenance: 2025-10-21, end: 2027-04-30 } <- Maintenance LTS (today)
v24 { lts: 2025-10-28, maintenance: 2026-10-20, end: 2028-04-30 } <- Active LTS (today)
v26 { start: 2026-05-05, lts: 2026-10-28, ..., end: 2029-04-30 } <- CURRENT until 2026-10-28
Today is 2026-09-02. Node 26 becomes LTS in 8 weeks. Until then it is the Current line, and
Node's release policy is that semver-major changes land in Current lines. 26-alpine floats:
the next routine Dependabot digest refresh can pull a 26.x that carries a breaking change straight
into the production image. That is a different risk class from a maintenance-LTS digest refresh.
And nothing in CI would see it. Every Node version pinned in this repo, with what this PR does
to each:
| file:line | pinned | this PR |
|---|---|---|
Dockerfile:4 |
node:22-alpine@sha256:16e22a55… |
→ 26 |
Dockerfile:13 |
node:22-alpine@sha256:16e22a55… |
→ 26 |
package.json:54 |
"node": ">=22.12.0" |
unchanged |
.github/workflows/run-tests.yaml:30 |
'22' |
unchanged |
.github/workflows/unit-tests.yaml:20 |
'22' |
unchanged |
.github/workflows/live-integration.yaml:28 |
'22' |
unchanged |
.github/workflows/smoke-load-test.yaml:55 |
'22.12.0' |
unchanged |
.github/workflows/lint.yaml:10 |
NODE_VERSION: 22 |
unchanged |
.github/workflows/publish-npm.yml:33 |
'22' |
unchanged |
.github/workflows/security.yaml:27 |
'20' (EOL 2026-04-30) |
unchanged |
.github/workflows/nightly-devnet-dump.yaml:56 |
'20' (EOL 2026-04-30) |
unchanged |
docs/getting-started.md:20,53 |
"Node.js 20+" | unchanged |
docs/versioning.md:146 |
"The supported Node.js runtime moves to Node 22." | unchanged |
build.yaml builds and pushes the image (build.yaml:159-166) but never runs it — there is
no step that starts the container or hits an endpoint. So after this merges:
the lightnet integration suite, the unit suite, the o1js-based tests and the smoke load test all
run on Node 22; the artifact that reaches production runs Node 26; and no job anywhere executes
Node 26. A Node-26-specific regression inundici(6.27.0 → 8.9.0, a major, and it backs
fetchfor graphql-yoga /@graphql-tools/executor-http) or V8 (12.4 → 14.6) is invisible to
every green check on this PR.
My smoke test above covers boot, /healthcheck, /readiness and { __typename }. It does not
cover the event/action resolvers against a real archive DB — which is exactly what
run-tests.yaml covers, on Node 22.
3. This PR contradicts the versioning policy merged in #198
docs/versioning.md:39-40, under Operational contract (the list of breaking changes):
- Raising the minimum supported Node.js runtime, whether through
engines, the
Docker base image, or the Node version used by CI to publish the package.
and docs/versioning.md:146, in the 1.0.0 migration notes operators are told to read:
- The supported Node.js runtime moves to Node 22.
Merging this makes that line false for the shipped artifact on the day 1.0.0 is cut. Note also the
PR title says "20-alpine" while the diff is 22→26 — Dependabot opened this against an older main,
before #194 landed Node 22. The commit message that would enter the release history
(Bumps node from 20-alpine to 26-alpine) is wrong in a repo whose policy treats this as a
breaking change.
4. Cleared: the two non-Dependabot lines someone added to the branch
git log --format='%an / %cn' on f724846: authored by dependabot[bot], committed by
dkijania <dariusz@o1labs.org> on 2026-08-27 — the branch was rebased onto 3fd3379 and amended
by hand. Both added lines are fine, keep them:
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1(build stage only). Real:artilleryis a
devDependency (package.json:64) and pulls@playwright/browser-chromium@1.48.1, whose
install: node install.jsfires duringnpm ci. It does not break the benchmarks:
benchmark/*.yamluses noengine:key (HTTP only), andbenchmark/is neverCOPYd into the
image, sonpm run benchmarkwas never runnable inside the container anyway. The browser cache
lands in the build stage's$HOME, not in a shipped layer — both images are ~1.4 GB, so this is a
build-time saving, not an image-size one.npm ci --no-audit --no-fund. Does not weaken #192's gate. That gate is a separate job:
security.yaml:28runsnpm ci --ignore-scripts, thensecurity.yaml:31runs
npm audit --omit=dev --audit-level=critical.--no-auditonly suppresses npm's inline
post-install summary in the image build. Confirmed empirically: thenpm auditcheck is green
on this PR head.
5. Downstream impact: none
No schema, error-string, status-code or CORS change. POST / {"query":"{ __typename }"} returns
byte-identical output on both images, and the OpenSSL/TLS table above rules out a transport-level
surprise. Neither mina-explorer nor mina-explorer-api is affected by this diff.
Required fix
Pick A (recommended — smallest change, zero delay to 1.0.0) or B.
A. Retarget to the current Node 22 LTS digest and close the 26 bump
This still delivers the security value Dependabot opened the PR for — main's pinned digest
16e22a55… was built 2026-06-23, ~10 weeks stale, while the current 22-alpine was built
2026-07-29 — without shipping an untested major.
Dockerfile:4 and Dockerfile:13 — use this exact digest (current node:22-alpine index digest,
verified 2026-09-02; skopeo inspect --raw of it resolves to 5 architectures, base.name=alpine:3.24):
FROM node:22-alpine@sha256:c610fcdfb1d5b4740dd70c284ed3cb16bb857e0f7166196e36a5501df7a3aa32 AS buildFROM node:22-alpine@sha256:c610fcdfb1d5b4740dd70c284ed3cb16bb857e0f7166196e36a5501df7a3aa32Keep both hand-added lines (ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1, --no-audit --no-fund).
Then stop the recurrence at the source — .github/dependabot.yml, docker ecosystem block:
- package-ecosystem: docker
directory: '/'
schedule:
interval: weekly
open-pull-requests-limit: 2
# Digest/patch refreshes stay automatic (that is the security value). Moving
# Node majors is a policy decision under docs/versioning.md:39-40, so it is
# made deliberately, together with the CI matrix and the docs.
ignore:
- dependency-name: 'node'
update-types: ['version-update:semver-major']B. If a newer Node really is wanted before 1.0.0 — go to 24 (Active LTS), not 26, and land it whole
Node 24 is Active LTS today and supported to 2028-04-30; Node 26 is Current for another 8 weeks.
Current node:24-alpine index digest, verified 2026-09-02:
sha256:e67514e5d0f6c46656005e1b693b2ec9d52e80b641307de684d4a015ba7a4eaf.
In the same PR:
Dockerfile:4,13→node:24-alpine@sha256:e67514e5d0f6c46656005e1b693b2ec9d52e80b641307de684d4a015ba7a4eaf- Make CI actually run the shipped runtime. In
.github/workflows/run-tests.yamland
unit-tests.yaml, matrix the runtime so both theenginesfloor and the image major are tested:and movestrategy: matrix: node-version: ['22', '24'] steps: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }}
smoke-load-test.yaml:55,live-integration.yaml:28,lint.yaml:10,
publish-npm.yml:33to24. - Leave
package.json:54 "node": ">=22.12.0"alone unless you intend to break npm consumers on
Node 22 — raising it is itself a breaking change underdocs/versioning.md:39-40. - Fix the docs the change invalidates:
docs/versioning.md:146and
docs/getting-started.md:20,53(the latter still says "Node.js 20+" and is already wrong on
main). - Retitle the PR — "20-alpine" is wrong; the base is 22.
Acceptance criteria
Self-check all of these before re-requesting review.
- Both stages agree and the tag is an LTS line.
grep -n "^FROM node:" Dockerfileprints two
lines with the same<tag>@<digest>. For that majorM, this printsLTS OK:(Node 26 fails this today — itsM=22 # the major in the FROM line curl -sS https://raw.githubusercontent.com/nodejs/Release/main/schedule.json \ | python3 -c "import json,sys,datetime as dt;M='$M';d=json.load(sys.stdin)['v'+M];t=dt.date.today().isoformat();print('LTS OK' if d.get('lts','9999')<=t<d['end'] else 'NOT LTS: '+json.dumps(d))"
ltsdate is 2026-10-28.) - The digest is real and matches the tag.
skopeo inspect --raw docker://docker.io/library/node@<digest>exits 0, and eachlinux/*
manifest carriesorg.opencontainers.image.versionequal to the tag in theFROMline. - Tested runtime ⊇ shipped runtime.
grep -rn "node-version\|NODE_VERSION" .github/workflows/
shows the image's Node major appearing in at leastrun-tests.yamlandunit-tests.yaml, and no
workflow pins a Node major that is past itsenddate (this currently fails for
security.yaml:27andnightly-devnet-dump.yaml:56, both on Node 20, EOL 2026-04-30). - Docs name the same major as the image.
docs/versioning.md:146and
docs/getting-started.md:20,53state the Node major thatDockerfile:4pins. Verify with
grep -rn "Node.js [0-9]\|Node [0-9]" docs/. enginesis not raised unless the PR body explicitly says npm consumers below the new floor
are being dropped:grep -n '"node"' package.jsonstill shows>=22.12.0, or the PR states the
break.- The image still passes the #189 contract. Build it, then, with a Postgres carrying the 14
USED_TABLES:docker run -d -e PG_CONN=… -p 8080:8080 <img>;curl -sf localhost:8080/healthcheck→ 200
andcurl -sf localhost:8080/readiness→ 200;docker exec <c> ps -o pid,argsshows1 /sbin/tini -- node build/src/index.js;docker exec <c> sh -c 'wget -qO- "http://127.0.0.1:${PORT:-8080}/healthcheck"'exits 0;docker kill --signal TERM <c>; logs containShutting down (SIGTERM)and
docker inspect -f '{{.State.ExitCode}}' <c>is 0.
- If you took option A,
.github/dependabot.ymlcontains theignoreblock for
dependency-name: 'node'/version-update:semver-major, and this PR is closed rather than
merged.
… ship Supersedes #222, which bumped the base image to Node 26. Node 26 is a Current release until 2026-10-28, and Current lines take semver-major changes, so a routine digest refresh could carry a breaking change into the production image. Node 24 has been Active LTS since 2025-10-28 and is supported to 2028-04-30. The review on #222 also established that no CI job ran the shipped runtime: the image ran one Node major while every test ran another. That is fixed here. - Dockerfile: both stages -> node:24-alpine@sha256:ebfe2f90... (verified: OCI index, linux/amd64 + arm64/v8, NODE_VERSION=24.21.0, built 2026-09-17). - run-tests.yaml, unit-tests.yaml: matrix ['22', '24'] — 22 is the `engines` floor, 24 is what the image ships, so both are now executed. - live-integration, smoke-load-test, lint, publish-npm, build, graphql-inspector: -> 24. - security.yaml and nightly-devnet-dump.yaml ran Node 20, EOL since 2026-04-30; both -> 24. - docs/getting-started.md said "Node.js 20+" while `engines` requires >=22.12.0; docs/versioning.md now states both the npm floor and the image's major. - dependabot.yml: ignore Node semver-major bumps. Digest and patch refreshes stay automatic; moving the major is a breaking change under docs/versioning.md and moves together with CI and docs. - Dockerfile keeps the two lines added on the #222 branch, which the review cleared: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 (build stage only) and `npm ci --no-audit --no-fund`. `engines` stays at >=22.12.0 — raising it would break npm consumers and is a separate decision. Verified by building the image and running it against a Postgres carrying the 17 USED_TABLES: /healthcheck 200, /readiness 200, `{ __typename }` correct, tini still PID 1, HEALTHCHECK wget exit 0, Docker status healthy, and SIGTERM logs "Shutting down (SIGTERM)…" and exits 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Closing as superseded. This proposed Node 26, which is still a Current release until 2026-10-28. @SanabriaRusso's review here established the rule the repo now follows: the image tracks an LTS line, because a Current line takes semver-major changes that a routine digest refresh would carry straight into production. Since then #239 merged a bump to Node 25, which turns out to be end-of-life (never LTS; The two hand-added lines from this branch that the review cleared — 🤖 Generated with Claude Code |
|
OK, I won't notify you again about this release, but will get in touch when a new version is available. If you'd rather skip all updates until the next major or minor version, let me know by commenting If you change your mind, just re-open this PR and I'll resolve any conflicts on it. |
…235) Supersedes #222. Takes **option B** from @SanabriaRusso's review on that PR: go to Node **24 (LTS)** rather than 26, and land the runtime move whole — image, CI, and docs together. ## Why not Node 26 Re-checked against `nodejs/Release/schedule.json` today (2026-09-21), not quoted from the review: | line | status today | end | |---|---|---| | v20 | **EOL** since 2026-04-30 | 2026-04-30 | | v22 | Maintenance LTS | 2027-04-30 | | v24 | **Active LTS** until 2026-10-20, then Maintenance LTS | 2028-04-30 | | v26 | **Current until 2026-10-28** | 2029-04-30 | Node 26 is still Current for another five weeks. Current lines take semver-major changes, so a routine Dependabot digest refresh on `26-alpine` can move a major straight into the production image. ## What changed **Image** — both stages to `node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1`. Note this is **not** the digest named in the review. That one (`e67514e5…`, verified 2026-09-02) is no longer what the tag resolves to. I re-resolved `24-alpine` from the registry: a genuine OCI index carrying `linux/amd64`, `linux/arm64/v8` and `linux/s390x`, image config `NODE_VERSION=24.21.0`, built 2026-09-17. The arm64 manifest matters for #233. Correction to an earlier version of this description: the image *config* has no `org.opencontainers.image.version` label, but the OCI *index annotations* do carry `org.opencontainers.image.version=24-alpine`. `NODE_VERSION=24.21.0` in the image config is further evidence. **CI now runs the runtime we ship.** This was the review's blocking objection. | workflow | was | now | |---|---|---| | `run-tests.yaml` | `'22'` | matrix `['22', '24']` | | `unit-tests.yaml` | `'22'` | matrix `['22.12.0', '24']` | | `live-integration`, `publish-npm`, `build`, `graphql-inspector`, `lint` | `22` | `24` | | `smoke-load-test.yaml` | `'22.12.0'` | `'24'` | | `security.yaml` | **`'20'` — EOL 2026-04-30** | `24` | | `nightly-devnet-dump.yaml` | **`'20'` — EOL 2026-04-30** | `24` | 22 stays in the matrix because it is the `engines` floor; 24 is what the image runs. `unit-tests.yaml` pins the exact floor `22.12.0`, because a bare `'22'` resolves to the newest 22.x. `run-tests.yaml` keeps `'22'`, because it starts a Mina local network and is expensive. The two Node 20 jobs were running an EOL runtime on `main` and are fixed here, as the review's criterion 3 requires. The matrix doubles `run-tests.yaml`, which starts a Mina local network. I followed the review's instruction literally; say the word and I will pin that job to `24` alone and leave the floor coverage to `unit-tests.yaml`. **Docs** — `docs/getting-started.md` said "Node.js 20+" while `engines` requires `>=22.12.0` (wrong on `main` already); `docs/versioning.md` now names both the npm floor and the image's major. `AGENTS.md` now says Volta pins 22.12.0, not 20.18.0. **`engines` unchanged at `>=22.12.0`.** Raising it breaks npm consumers and is a separate decision (criterion 5). `volta.node` stays at `22.12.0` so local development runs the floor. **Kept from the #222 branch**, both cleared by the review: `ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` (build stage only) and `npm ci --no-audit --no-fund`. Confirmed in the built image: `@playwright` is 124 KB of package metadata with no browser binary anywhere, and the audit gate is a separate job (`security.yaml`), untouched. **Beyond option B, easy to drop if you disagree:** `dependabot.yml` now ignores Node **semver-major** bumps. Digest and patch refreshes stay automatic, which is the security value; the major moves deliberately, with CI and docs. Without this, Dependabot re-opens the Node 26 PR next week. ## Versioning policy `docs/versioning.md` classed a move of the Docker base image to a newer Node as **breaking**. This PR takes option (a) from the review: a move of the image between **LTS lines**, with `engines` and the image's HTTP contract (port, endpoints, environment variables, entrypoint, user) unchanged, is **minor**. A move to a non-LTS line (Current or end-of-life) stays breaking. So the next release can be a MINOR. The 0.0.6 → 1.0.0 migration note again says what 1.0.x shipped: `engines` 22.12, and the image on Node 22 (the `v1.0.0` tag's `Dockerfile` has `FROM node:22-alpine`). **The move to Node 24 goes in the release notes of the release that contains this PR.** #233 moves the image to the same digest and gets the identical policy text, so the two merge cleanly. ## Verification — the #189 contract, executed Built the image and ran it against a Postgres carrying the 17 `USED_TABLES`: | check | result | |---|---| | `GET /healthcheck` | **200** | | `GET /readiness` | **200** | | `POST / {"query":"{ __typename }"}` | `{"data":{"__typename":"Query"}}` | | PID 1 | `/sbin/tini -- node build/src/index.js` | | `wget -qO- "http://127.0.0.1:${PORT:-8080}/healthcheck"` inside the container | exit **0** | | Docker `HEALTHCHECK` state | **healthy** | | `SIGTERM` | `Shutting down (SIGTERM)…`, exit code **0** | Runtime facts from the built image: node v24.21.0, npm 11.19.0, OpenSSL **3.5.8**, TLS defaults TLSv1.2/TLSv1.3, alpine 3.24.2, `/usr/bin/wget` → busybox, **0** native `*.node` modules. Image 1.31 GB. Acceptance criteria 1, 3 and 5 re-run and passing; both `FROM` lines carry the identical `tag@digest`, the LTS check prints `LTS OK` for major 24, and no workflow pins an EOL major. ## Not fixed here Three files this PR touches already fail `prettier` on `main` — as does the rest of the repo (47 files). `lint.yaml` runs `prettier --debug-check`, which never exits non-zero, so nothing enforces it. That is #212 and #213, left alone rather than mixed into a runtime change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: SanabriaRusso <luis@o1labs.org>
Bumps node from 20-alpine to 26-alpine.