Repository navigation
build(docker): move the image to Node 24 LTS, and test what we ship - #235
Conversation
|
Pushed one commit since your approval, What was wrong: adding the matrix renamed the checks. The fix: each workflow keeps its matrix under 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 🤖 Generated with Claude Code |
… 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>
|
Rebased onto #239 bumped the base image to
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 It also stops the recurrence. The Conflict resolution was only the two 🤖 Generated with Claude Code |
4724afd to
24dd28c
Compare
SanabriaRusso
left a comment
There was a problem hiding this comment.
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 isnode:24-alpine@sha256:ebfe2f90…in both stages. That deliberately replaces #239'snode:25-alpine(v25 ended 2026-06-01 and was never LTS). Agreed. - The pin is still what
24-alpineresolves to today (OCI index: amd64, arm64/v8, s390x). One small correction to the PR body:org.opencontainers.image.version=24-alpineis present, in the index annotations. - The runtime contract is unchanged versus
mainand versus the current release image (ghcr…:latest, Node 22.23.1): ENTRYPOINT (tini), CMD,USER nodeuser,WORKDIR /app,EXPOSE 8080and HEALTHCHECK are byte-identical.PLAYWRIGHT_SKIP_BROWSER_DOWNLOADis set only in the build stage. - npm consumers are unaffected:
engines(>=22.12.0) andvoltaare unchanged, and the bin shebang is intact. - CI:
unit-testsandRun-Testsshow 0 failures on both 22.23.x and 24.2x, and the aggregates reportmatrix result: success. Locally, on the merged tree,npm ci,build,lintandtest:unitall pass. - The GCP-auth ordering rule is unaffected.
build.yamlonly changes the setup-node value, which already runs beforeAuthenticate 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/networkStateand match onerrors[]wording. Neither runs this image or depends on its Node version. - The dependabot
semver-majorignore fornodeprevents 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.mdreturns nothing (option a), or the PR body states that the next release is a MAJOR (option b). -
grep -n "container image" docs/versioning.mdno 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)
- "Test what we ship" tests the Node major, not the artifact.
build.yamlbuilds and pushes the image but never runs it.smoke-load-test.yaml'spaths:filter excludesDockerfile, 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>, thencurl -sf :8080/healthcheck,curl -sf :8080/readiness, andPOST / {"query":"{ __typename }"}. - The
enginesfloor is no longer tested exactly.smoke-load-test.yamlwas the only job pinned to22.12.0, and the matrix value'22'resolves to 22.23.x. Consider['22.12.0', '24']inunit-tests.yaml; it is cheap and needs no Lightnet. The aggregate job keeps the required check name. AGENTS.md:63is 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.
- "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.
- Coordination with #233. It pins the same digest but replaces
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1/--no-audit --no-fundwithnpm ci [--omit=dev] --ignore-scriptsin a three-stage layout. Whichever PR lands second should resolve that deliberately: keep #233's three stages. With--ignore-scriptsthe Playwright ENV is redundant.
| - 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| # 22 is the `engines` floor, 24 is what the container image ships. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| # 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). |
There was a problem hiding this comment.
Nit: v24 moves to Maintenance LTS on 2026-10-20. "LTS line, supported to 2028-04-30" won't go stale.
There was a problem hiding this comment.
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.
… 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>
|
Thanks for the review. All blocking points are fixed in two commits:
Blocking: versioning policyWe took option (a).
#233 will get the same text, word for word, so the two PRs merge cleanly. Acceptance greps:
Non-blocking
The PR body also has a correction about the annotation: the OCI index does carry 🤖 Generated with Claude Code |
SanabriaRusso
left a comment
There was a problem hiding this comment.
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.mdnow treats an LTS-to-LTS image move, withenginesand the HTTP contract unchanged, as minor. The 1.0.0 migration note matchesv1.0.0:Dockerfile(Node 22).unit-testscovers the exact floor22.12.0and 24, andAGENTS.mdand the Dockerfile wording are fixed. - Against main: this replaces #239's
node:25-alpine, which is EOL and was never LTS, withnode:24-alpine@sha256:ebfe2f90…. Measured against the released 1.0.x image (Node 22) it is a minor LTS-to-LTS move, and the dependabotsemver-majorignore stops another #239. - Image, built from the head and run against the fixture DB: Node v24.21.0 LTS.
/healthcheckand/readinessreturn 200,{ __typename }andnetworkStaterespond correctly, tini is PID 1, it runs asnodeuseron 8080, the in-container wget healthcheck exits 0, and SIGTERM logs the shutdown and exits 0. The runtime stage differs frommainonly inFROM. The env, ports and entrypoint are unchanged, so there is no impact on HTTP consumers (mina-explorer, mina-explorer-api) or npmengines. - 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.
Co-authored-by: SanabriaRusso <luis@o1labs.org>
There was a problem hiding this comment.
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
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' |
| # --no-audit only suppresses npm's inline post-install summary; the audit gate is | ||
| # a separate job (.github/workflows/security.yaml). |
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>
## 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>


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.jsontoday (2026-09-21), not quoted from the review:Node 26 is still Current for another five weeks. Current lines take semver-major changes, so a routine Dependabot digest refresh on
26-alpinecan 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-resolved24-alpinefrom the registry: a genuine OCI index carryinglinux/amd64,linux/arm64/v8andlinux/s390x, image configNODE_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.versionlabel, but the OCI index annotations do carryorg.opencontainers.image.version=24-alpine.NODE_VERSION=24.21.0in the image config is further evidence.CI now runs the runtime we ship. This was the review's blocking objection.
run-tests.yaml'22'['22', '24']unit-tests.yaml'22'['22.12.0', '24']live-integration,publish-npm,build,graphql-inspector,lint2224smoke-load-test.yaml'22.12.0''24'security.yaml'20'— EOL 2026-04-3024nightly-devnet-dump.yaml'20'— EOL 2026-04-302422 stays in the matrix because it is the
enginesfloor; 24 is what the image runs.unit-tests.yamlpins the exact floor22.12.0, because a bare'22'resolves to the newest 22.x.run-tests.yamlkeeps'22', because it starts a Mina local network and is expensive. The two Node 20 jobs were running an EOL runtime onmainand 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 to24alone and leave the floor coverage tounit-tests.yaml.Docs —
docs/getting-started.mdsaid "Node.js 20+" whileenginesrequires>=22.12.0(wrong onmainalready);docs/versioning.mdnow names both the npm floor and the image's major.AGENTS.mdnow says Volta pins 22.12.0, not 20.18.0.enginesunchanged at>=22.12.0. Raising it breaks npm consumers and is a separate decision (criterion 5).volta.nodestays at22.12.0so local development runs the floor.Kept from the #222 branch, both cleared by the review:
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1(build stage only) andnpm ci --no-audit --no-fund. Confirmed in the built image:@playwrightis 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.ymlnow 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.mdclassed 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, withenginesand 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:
engines22.12, and the image on Node 22 (thev1.0.0tag'sDockerfilehasFROM 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:GET /healthcheckGET /readinessPOST / {"query":"{ __typename }"}{"data":{"__typename":"Query"}}/sbin/tini -- node build/src/index.jswget -qO- "http://127.0.0.1:${PORT:-8080}/healthcheck"inside the containerHEALTHCHECKstateSIGTERMShutting down (SIGTERM)…, exit code 0Runtime 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*.nodemodules. Image 1.31 GB.Acceptance criteria 1, 3 and 5 re-run and passing; both
FROMlines carry the identicaltag@digest, the LTS check printsLTS OKfor major 24, and no workflow pins an EOL major.Not fixed here
Three files this PR touches already fail
prettieronmain— as does the rest of the repo (47 files).lint.yamlrunsprettier --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