Skip to content

build(docker): move the image to Node 24 LTS, and test what we ship - #235

Merged
dkijania merged 7 commits into
mainfrom
chore/node-24-lts
Oct 7, 2026
Merged

dkijania merged 7 commits into
mainfrom
chore/node-24-lts

Conversation

@dkijania

@dkijania dkijania commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

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

SanabriaRusso
SanabriaRusso previously approved these changes Sep 21, 2026
@dkijania

Copy link
Copy Markdown
Contributor Author

Pushed one commit since your approval, 4724afd, which is why the review was dismissed. Sorry for the churn — the change is CI-only and does not alter what runs.

What was wrong: adding the matrix renamed the checks. Run-Tests became Run-Tests (22) / Run-Tests (24), and unit-tests likewise. The ruleset requires the unsuffixed names, so those two contexts simply stopped existing on this branch and GitHub waited forever on a status that could never arrive — which showed up as a test run that never finished, with every job already green.

The fix: each workflow keeps its matrix under *-matrix, plus a small aggregate job carrying the required name and gated on needs.<matrix>.result. Both Node versions still run; the ruleset gets its context back.

Renaming the required contexts to the suffixed names instead would break every branch without a matrix, #237 among them, so I did not take that route.

Now reporting 12 checks, all green, including Run-Tests and unit-tests. Re-approval please when you have a moment.

🤖 Generated with Claude Code

SanabriaRusso
SanabriaRusso previously approved these changes Sep 23, 2026
dkijania and others added 2 commits September 24, 2026 10:59
… 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>
Adding a matrix renamed the checks: "Run-Tests" became "Run-Tests (22)" and
"Run-Tests (24)", and "unit-tests" likewise. The branch ruleset requires the
unsuffixed names, so those two contexts stopped existing on this PR — GitHub
then waits forever on a status that can never arrive, which reads in the UI as
a test run that never finishes, and leaves the PR unmergeable with every job
green.

Each workflow now has an aggregate job carrying the required name, gated on the
matrix result, so both Node versions still run and the ruleset still has the
context it asks for. Renaming the required contexts instead would break every
branch without a matrix, such as #237.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dkijania

Copy link
Copy Markdown
Contributor Author

Rebased onto main after #239 merged. Force-push, so the approval is dismissed again — sorry, but this PR was DIRTY and could not have merged either way.

#239 bumped the base image to node:25-alpine, and Node 25 is end-of-life. Verified rather than inferred:

  • main's pin sha256:bdf2cca6… resolves to an image whose config declares NODE_VERSION=25.9.0, built 2026-04-15.
  • nodejs/Release gives v25 lts: false, end: 2026-06-01. It is an odd-numbered line that was never LTS and stopped receiving patches on 2026-06-01, almost four months ago.

So the published container currently ships an unsupported runtime, and the base image itself is five months old. That is the failure mode the #222 review set out the LTS policy to prevent.

This PR is the fix, unchanged in substance: both stages on node:24-alpine@sha256:ebfe2f90…, Active LTS until 2028-04-30. I re-resolved the tag today and the digest has not moved since I pinned it on 2026-09-21, so the pin is current.

It also stops the recurrence. The dependabot.yml block in this PR ignores Node semver-major bumps, so digest and patch refreshes stay automatic while a major move stays a deliberate decision. Had it been in place, #239 would not have been opened.

Conflict resolution was only the two FROM lines — Node 25 replaced by Node 24. Nothing else changed: the diff against the new main touches no action pin bumped by #240, and the CI matrix, gate jobs and docs are exactly as reviewed. Both commits are signed.

🤖 Generated with Claude Code

@SanabriaRusso SanabriaRusso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes: docs/policy only. The runtime move itself is good.

Thanks for the rebase and the aggregate gate jobs. I re-verified the substance:

  • Merge with main (#162) is clean locally. The effective image is node:24-alpine@sha256:ebfe2f90… in both stages. That deliberately replaces #239's node:25-alpine (v25 ended 2026-06-01 and was never LTS). Agreed.
  • The pin is still what 24-alpine resolves to today (OCI index: amd64, arm64/v8, s390x). One small correction to the PR body: org.opencontainers.image.version=24-alpine is present, in the index annotations.
  • The runtime contract is unchanged versus main and versus the current release image (ghcr…:latest, Node 22.23.1): ENTRYPOINT (tini), CMD, USER nodeuser, WORKDIR /app, EXPOSE 8080 and HEALTHCHECK are byte-identical. PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD is set only in the build stage.
  • npm consumers are unaffected: engines (>=22.12.0) and volta are unchanged, and the bin shebang is intact.
  • CI: unit-tests and Run-Tests show 0 failures on both 22.23.x and 24.2x, and the aggregates report matrix result: success. Locally, on the merged tree, npm ci, build, lint and test:unit all pass.
  • The GCP-auth ordering rule is unaffected. build.yaml only changes the setup-node value, which already runs before Authenticate to Google Cloud. No step is added or moved.
  • No API-client impact. mina-explorer and mina-explorer-api reach the API over HTTPS with blocks/networkState and match on errors[] wording. Neither runs this image or depends on its Node version.
  • The dependabot semver-major ignore for node prevents another #239 while keeping digest and patch refreshes.

Blocking: under this repo's own policy, this change is breaking

Problem. docs/versioning.md:39-40 lists "Raising the minimum supported Node.js runtime, whether through engines, the Docker base image, or the Node version used by CI…" as a breaking change. The last release (1.0.x) ships Node 22, and the next one will ship 24. As written, that forces a MAJOR, and a base-image runtime cannot be put behind a flag.

Separately, docs/versioning.md:166-167 now says "the container image ships Node 24" inside the 0.0.6 → 1.0.0 migration notes. That is false for the 1.0.x artifact.

Proposed fix. Pick one and state it in the PR body.

(a) Recommended: treat the image's Node major as an implementation detail as long as it stays on LTS and the HTTP contract is unchanged. Replace L39-40 with:

- Raising the minimum supported Node.js runtime for npm consumers, through
  `engines` or the Node version used by CI to publish the package.
- Moving the container image to a Node line that is not LTS (Current or
  end-of-life). Moving it between LTS lines, with `engines` and the image's
  HTTP contract (port, endpoints, environment variables, entrypoint, user)
  unchanged, is **minor**.

Then restore the 1.0.0 migration note to what 1.0.0 actually shipped:

- The supported Node.js runtime moves to Node 22.12 (`engines`); the 1.0.x
  container image runs Node 22.

The move to Node 24 then goes in the release notes of the release that contains this PR.

(b) Keep the policy as it is, revert L166-167 as above, and state in the PR body that the next release must be a MAJOR.

Either way this also governs #233, which moves the image to the same node:24-alpine digest. Whichever of the two lands first carries the policy change.

Acceptance criteria

  • Either grep -n "Docker base image" docs/versioning.md returns nothing (option a), or the PR body states that the next release is a MAJOR (option b).
  • grep -n "container image" docs/versioning.md no longer attributes Node 24 to the 0.0.6 → 1.0.0 migration.
  • Whatever the migration note says about 1.0.x matches git show v1.0.0:Dockerfile | grep '^FROM'.

Testing: docs only. Run npx prettier --check docs/versioning.md and the greps above.

Also required: update the branch

mergeStateStatus is BEHIND, because #162 landed after the rebase. The merge is clean locally. Please click "Update branch" so CI runs the merged tree on Node 24; #162's code has only ever run on Node 22 in CI.

Non-blocking (follow-ups welcome)

  1. "Test what we ship" tests the Node major, not the artifact. build.yaml builds and pushes the image but never runs it. smoke-load-test.yaml's paths: filter excludes Dockerfile, so a Dependabot digest refresh gets no runtime check at all. A follow-up could add a job that runs the image against a throwaway Postgres: docker run -d -e PG_CONN=… -p 8080:8080 <img>, then curl -sf :8080/healthcheck, curl -sf :8080/readiness, and POST / {"query":"{ __typename }"}.
  2. The engines floor is no longer tested exactly. smoke-load-test.yaml was the only job pinned to 22.12.0, and the matrix value '22' resolves to 22.23.x. Consider ['22.12.0', '24'] in unit-tests.yaml; it is cheap and needs no Lightnet. The aggregate job keeps the required check name.
  3. AGENTS.md:63 is stale: it still says Volta 20.18.0 and "Node ≥ 20". Suggested text:
    -- Node version is pinned by Volta to **20.18.0** (`package.json#volta.node`). The package targets Node ≥ 20; `--env-file` flag and modern ESM behavior assumed.
    +- Local Node is pinned by Volta to **22.12.0** (`package.json#volta.node`), the `engines` floor (`>=22.12.0`) for npm consumers. The Docker image ships Node **24** (LTS); CI runs unit and integration tests on both 22 and 24. `--env-file` flag and modern ESM behavior assumed.
  4. "Active LTS" will go stale. v24 enters Maintenance LTS on 2026-10-20; "LTS line, supported to 2028-04-30" won't go out of date.
  5. Coordination with #233. It pins the same digest but replaces PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 / --no-audit --no-fund with npm ci [--omit=dev] --ignore-scripts in a three-stage layout. Whichever PR lands second should resolve that deliberately: keep #233's three stages. With --ignore-scripts the Playwright ENV is redundant.

Comment thread docs/versioning.md Outdated
- Browser deployments must set `CORS_ORIGIN` deliberately.
- Rate limiting is enabled and depends on the correct `TRUST_PROXY` hop count.
- The supported Node.js runtime moves to Node 22.
- The supported Node.js runtime moves to Node 22.12 for npm consumers

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sits in the 0.0.6 → 1.0.0 migration notes, but 1.0.x shipped node:22-alpine. Please keep the note historically accurate and put the image move in the next release's notes. The policy line at L39-40 also still classifies this change as breaking; see the review body.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 94a1e3c. The migration note now says what 1.0.x shipped: engines 22.12, and the 1.0.x image runs Node 22. This agrees with FROM node:22-alpine in the v1.0.0 tag. We took option (a) for L39-40, with your text word for word. An image move between LTS lines is now minor. The move to Node 24 goes in the notes of the next release.

Comment thread .github/workflows/unit-tests.yaml Outdated
strategy:
fail-fast: false
matrix:
# 22 is the `engines` floor, 24 is what the container image ships.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: in CI '22' resolves to 22.23.x, not the 22.12.0 engines floor. smoke-load-test was the only job pinned to 22.12.0. Consider ['22.12.0', '24'] here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 94a1e3c. The matrix is now ['22.12.0', '24'], so it tests the exact engines floor. The aggregate unit-tests job is not changed, so the required check name continues to report. run-tests.yaml keeps '22', because it starts a Mina local network and costs more.

Comment thread Dockerfile Outdated
# Pinned by digest for reproducible, tamper-evident builds; Dependabot's docker
# ecosystem keeps it current. Bump both stages together.
FROM node:25-alpine@sha256:bdf2cca6fe3dabd014ea60163eca3f0f7015fbd5c7ee1b0e9ccb4ced6eb02ef4 AS build
# Node 24 is the Active LTS line (LTS since 2025-10-28, supported to 2028-04-30).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: v24 moves to Maintenance LTS on 2026-10-20. "LTS line, supported to 2028-04-30" won't go stale.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 94a1e3c. The comment now says "an LTS line (LTS since 2025-10-28, supported to 2028-04-30)". I also removed "Active LTS" from the PR title and body.

dkijania and others added 2 commits October 2, 2026 22:08
… floor

Review on #235:
- docs/versioning.md: a move of the container image between Node LTS
  lines, with engines and the HTTP contract unchanged, is minor. A move to
  a non-LTS line stays breaking. The 1.0.0 migration note again says what
  1.0.x shipped: engines 22.12, image on Node 22.
- unit-tests.yaml: test the exact engines floor 22.12.0, not the newest 22.x.
- Dockerfile: "an LTS line" instead of "the Active LTS line", which goes
  stale on 2026-10-20.
- AGENTS.md: Volta pin is 22.12.0, not 20.18.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dkijania dkijania changed the title build(docker): move the image to Node 24 Active LTS, and test what we ship build(docker): move the image to Node 24 LTS, and test what we ship Oct 2, 2026
@dkijania

dkijania commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. All blocking points are fixed in two commits:

Blocking: versioning policy

We took option (a).

  • docs/versioning.md L39-40 now has your text, word for word. An image move between LTS lines is minor if engines and the HTTP contract do not change. A move to a non-LTS line stays breaking.
  • The 0.0.6 → 1.0.0 migration note now says: "engines 22.12; the 1.0.x container image runs Node 22".
  • The PR body records this decision. It also says that the Node 24 move goes in the release notes of the next release.

#233 will get the same text, word for word, so the two PRs merge cleanly.

Acceptance greps:

  • grep -n "Docker base image" docs/versioning.md gives no result.
  • grep -n "container image" docs/versioning.md gives only the new policy line and "the 1.0.x container image runs Node 22".
  • The v1.0.0 tag's Dockerfile has FROM node:22-alpine@sha256:16e22a55… in both stages. This agrees with the migration note.

prettier --check docs/versioning.md reports some lines, but all of them were already there on main: an *emphasis* on L72 and the WIF table. The lines that I changed are clean.

Non-blocking

  1. Run the shipped image in CI. We agree. I recommend a separate follow-up PR, so that this runtime change stays small. I can open an issue for it if you want one.
  2. Test the exact engines floor. Done. unit-tests.yaml now runs ['22.12.0', '24']. The aggregate unit-tests check keeps its name.
  3. AGENTS.md:63. Done, with your text. I checked the values first: volta.node is 22.12.0 and engines is >=22.12.0.
  4. "Active LTS" will go stale. Done in the Dockerfile comment, the PR title and the PR body.
  5. Coordination with ci: publish the container for linux/arm64 as well as linux/amd64 #233. Noted. The PR that merges second will keep ci: publish the container for linux/arm64 as well as linux/amd64 #233's three-stage layout. With --ignore-scripts, the PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD ENV is redundant, so we will remove it at that time.

The PR body also has a correction about the annotation: the OCI index does carry org.opencontainers.image.version=24-alpine.

🤖 Generated with Claude Code

SanabriaRusso
SanabriaRusso previously approved these changes Oct 7, 2026

@SanabriaRusso SanabriaRusso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Every change requested on 2026-10-02 is addressed on 94a1e3c, and the branch is up to date with main (behind 0, includes #162/#237/#239).

  • Policy and docs: docs/versioning.md now treats an LTS-to-LTS image move, with engines and the HTTP contract unchanged, as minor. The 1.0.0 migration note matches v1.0.0:Dockerfile (Node 22). unit-tests covers the exact floor 22.12.0 and 24, and AGENTS.md and the Dockerfile wording are fixed.
  • Against main: this replaces #239's node:25-alpine, which is EOL and was never LTS, with node:24-alpine@sha256:ebfe2f90…. Measured against the released 1.0.x image (Node 22) it is a minor LTS-to-LTS move, and the dependabot semver-major ignore stops another #239.
  • Image, built from the head and run against the fixture DB: Node v24.21.0 LTS. /healthcheck and /readiness return 200, { __typename } and networkState respond correctly, tini is PID 1, it runs as nodeuser on 8080, the in-container wget healthcheck exits 0, and SIGTERM logs the shutdown and exits 0. The runtime stage differs from main only in FROM. The env, ports and entrypoint are unchanged, so there is no impact on HTTP consumers (mina-explorer, mina-explorer-api) or npm engines.
  • CI: all 12 checks are green on both Node legs, and the aggregate jobs keep the required check names.

Nit (non-blocking, inline): the comment in .github/dependabot.yml contradicts the new policy. Running the built image in CI (point 1 of the last review) is still a good follow-up.

Comment thread .github/dependabot.yml Outdated
dkijania and others added 2 commits October 7, 2026 10:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The Node 24 publishing workflow conflicts with the newly documented breaking-change policy and stated minor-release classification.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Moves the Docker runtime to pinned Node 24 LTS and aligns CI, documentation, and dependency automation.

Changes:

  • Updates Docker build/runtime stages to Node 24.
  • Tests Node 22 compatibility and Node 24 runtime behavior in CI.
  • Documents runtime requirements and LTS upgrade policy.
File Description
Dockerfile Moves both stages to pinned Node 24 and optimizes installation.
AGENTS.md Documents Node versions used locally, in CI, and in Docker.
docs/​getting-started.md Corrects the npm runtime prerequisite.
docs/​versioning.md Defines versioning rules for Node runtime changes.
.github/​dependabot.yml Prevents automated Node major upgrades.
.github/​workflows/​build.yaml Builds and publishes using Node 24.
.github/​workflows/​graphql-inspector.yaml Runs schema checks on Node 24.
.github/​workflows/​lint.yaml Runs linting on Node 24.
.github/​workflows/​live-integration.yaml Runs live integration checks on Node 24.
.github/​workflows/​nightly-devnet-dump.yaml Moves nightly dump tests off Node 20.
.github/​workflows/​publish-npm.yml Publishes npm releases using Node 24.
.github/​workflows/​run-tests.yaml Adds Node 22/24 integration-test matrix and aggregate gate.
.github/​workflows/​security.yaml Runs npm security checks on Node 24.
.github/​workflows/​smoke-load-test.yaml Runs smoke load tests on Node 24.
.github/​workflows/​unit-tests.yaml Tests the exact engine floor and Node 24.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: '22'
node-version: '24'
Comment thread Dockerfile
Comment on lines +15 to +16
# --no-audit only suppresses npm's inline post-install summary; the audit gate is
# a separate job (.github/workflows/security.yaml).
@dkijania
dkijania merged commit 80e6293 into main Oct 7, 2026
13 checks passed
@dkijania
dkijania deleted the chore/node-24-lts branch October 7, 2026 09:37
dkijania added a commit that referenced this pull request Oct 7, 2026
Resolve the Dockerfile conflict with #235 as the review asked: keep this
branch's three stages and the shared node:24-alpine digest. From main, keep
--no-audit --no-fund on both npm ci lines and the LTS rationale. Drop
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD, because --ignore-scripts already stops the
browser download.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dkijania added a commit that referenced this pull request Oct 7, 2026
## Why

`ghcr.io/o1-labs/archive-node-api` ships **one architecture** now. Its
manifest index lists only:

```
linux amd64
unknown unknown   <- build provenance attestation, not an architecture
```

`docker/build-push-action` had no `platforms:`, so it built only for the
`ubuntu-latest` runner. On Apple Silicon, this causes emulation and a
platform-mismatch warning, or `no matching manifest for linux/arm64`
when emulation is off.

## What changed

### Workflow (`.github/workflows/build.yaml`)

- `platforms: linux/amd64,linux/arm64` on the build step.
- **Set up QEMU** registers the binfmt handlers, so that the amd64
runner can execute aarch64 build steps.
- The step runs a `--privileged` container. Thus it comes **before**
"Authenticate to Google Cloud", as the ORDERING RULE at the top of the
job requires.
- The binfmt image is pinned by digest:
`docker.io/tonistiigi/binfmt:qemu-v10.2.3@sha256:400a4873…`. The
action's default is the mutable `:latest` tag. Dependabot does not bump
a `with: image:` input, so a QEMU update is a manual change.
- `cache-image: false`. The action's cache entry is keyed by tag, not
digest, and adds ~32 MB per PR (#241).
- `Move cache` uses `if [ -d … ]; then mv …; fi`. A build that wrote no
new cache passes, and a real `mv` failure fails the step.

### `Dockerfile`: three stages, `node:24-alpine`

A single stage ran `npm ci` under QEMU for the arm64 leg (752 s against
112 s native) and shipped the dev tree in the runtime image. Now:

1. `deps` (target platform): `npm ci --omit=dev --ignore-scripts`.
Production dependencies only. None of them has an install script.
2. `build` (`--platform=$BUILDPLATFORM`): `npm ci --ignore-scripts` and
`tsc`. The compiled output does not depend on the architecture, so this
stage runs natively one time for all targets.
3. runtime: `node_modules` from `deps` (never from `build`, which has
amd64 modules) and `build/` from `build`. Entrypoint (`tini`), `CMD`,
`USER nodeuser`, `WORKDIR`, `EXPOSE 8080` and `HEALTHCHECK` do not
change.

All stages use `node:24-alpine@sha256:ebfe2f90…`, a multi-arch index
(amd64, arm64/v8, s390x). Node 24 is an LTS line, supported to
2028-04-30. It replaces `node:25-alpine` from #239, which is
end-of-life.

Measured results:

| | before | after |
|---|---|---|
| `build-and-deploy`, CI | 18m41s (amd64 + arm64, single stage) | ~4m37s
(amd64 + arm64) |
| image size, amd64 | 1.30 GB | 222 MB |
| runtime `node_modules` | 780.7 MB | ~54 MB |

The runtime image has no `typescript`, `artillery`, `eslint`, `o1js` or
Playwright, and no native `.node` files.

### Versioning policy (`docs/versioning.md`)

A move of the container image between LTS lines is **minor**, if
`engines` and the image's HTTP contract (port, endpoints, environment
variables, entrypoint, user) do not change. A move to a non-LTS line
(Current or end-of-life) is still breaking. A raise of `engines`, or of
the Node version that CI uses to publish, is still breaking. #235 makes
the same edit, so the two PRs merge cleanly.

## Release notes

- **The container runtime moves from Node 22 (1.0.x) to Node 24.** Put
this in the release notes of the next release. Under the policy above,
it is a minor change.
- Merge #235 directly after this PR. Until then, the CI test jobs run on
Node 22 only, and the image runs Node 24.

## How to verify after the first tag build

```sh
docker buildx imagetools inspect ghcr.io/o1-labs/archive-node-api:latest   # linux/amd64 + linux/arm64
docker run --rm --platform linux/arm64 --entrypoint node ghcr.io/o1-labs/archive-node-api:latest -e 'console.log(process.version, process.arch)'
docker run --rm --platform linux/amd64 --entrypoint node ghcr.io/o1-labs/archive-node-api:latest -e 'console.log(process.version, process.arch)'
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants