Skip to content

feat(containers): Node 24 LTS from nodejs.org, out of conda - #166

Merged
mmcky merged 2 commits into
mainfrom
feat/images-node24
Sep 18, 2026
Merged

mmcky merged 2 commits into
mainfrom
feat/images-node24

Conversation

@mmcky

@mmcky mmcky commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Both images carry Node 20, which reached end-of-life on 2026-04-30: the lean quantecon-build has 20.17.0 and the full quantecon has 20.20.2. This moves them to Node 24 LTS. It is the image half of moving the fleet to Node 24; GitHub's own actions already run on node24.

Why not just change the conda spec

It cannot solve, and it would have failed only after merge. Every jupyterlab below 4.6 on the defaults channel carries constrains: nodejs >=20.0.0,<21.0.0. Two things hold the images below 4.6:

  • the lean image's notebook 7.5.x requires jupyterlab <4.6;
  • the full image's anaconda=2026.06 pins jupyterlab 4.5.7 exactly.

So nodejs>=24,<25, and nodejs=24, have no solution in either image. That is also why #81's widening to <25 never took effect. Image builds don't run on pull requests, so a conda-spec bump would have broken main's image build with nothing in review to catch it. This was checked with dry-run linux-64 solves using micromamba and the image's own conda 25.11.1. The unchanged spec reproduces both live images package for package, 317 of 317 and 506 of 506.

What changes

File Change
both Dockerfiles install Node 24.21.0 from nodejs.org, pinned by version and SHA256 like Miniconda. The layer comes after conda, so a Node bump never re-runs the solve. /opt/node/bin goes on PATH ahead of the conda env, so a lecture environment can't shadow it. The build asserts the exact version, where until now nothing checked which Node the image had.
both environment.ymls drop nodejs. The lean image holds icu=73.1, the version the old conda nodejs pinned. Without it, dropping nodejs re-solves the native stack: qt 5→6, pyqt 5→6, pillow, lxml, and a tbb downgrade that numba's threading uses, about 90 packages in all.
.github/dependabot.yml remove the nodejs cap, which no longer applies, and ignore icu until the next Anaconda baseline
preview-netlify, preview-cloudflare READMEs, CHANGELOG.md document it

Nothing in the kernel moves

The proposed environments were solved for linux-64 with the image's own conda. In the full image only nodejs leaves; in the lean image only nodejs and libuv leave. Python, ipykernel, jupyter_server, numpy, scipy, matplotlib, pandas and plotly are all unchanged.

Who uses Node in the image

Consumer Needs On Node 20 today
preview-netlifynpm install -g netlify-cli@latest (27.x) node >=22.13 runs with about 100 EBADENGINE warnings (canary run, 2026-08-18)
preview-cloudflarewrangler@latest node >=22; bin/wrangler.js exits below 22 would fail outright
JS actions in container jobs the runner's own Node, mounted at /__e unaffected
kaleido, plotly, the book theme, jupyter-book, sphinx-* no Node at all unaffected

nodejs first entered the full image for netlify-cli (2afe4db) and was copied into the lean image as "Required for some Jupyter extensions" (#17). Nothing ever builds one.

Checked before opening

  • v24.21.0 is a real release: LTS "Krypton", 2026-09-07, the latest 24.x.
  • The SHA256 matches https://nodejs.org/dist/v24.21.0/SHASUMS256.txt in both Dockerfiles.
  • curl is installed by apt in both images and already fetches Miniconda.
  • The Node binary is linux-x64 glibc. Node 24 needs glibc 2.28 or later; ubuntu 24.04 has 2.39.

What only the post-merge build can prove

As with #165, build-containers.yml runs on main, not on this PR. After merge:

  1. Both image builds pass their verify step (node -p process.versions.node = 24.21.0).
  2. The test-container.yml smoke tests stay green.
  3. The next preview-netlify run shows no EBADENGINE.

A failed build pushes nothing, so :latest keeps the current image; the failure mode is safe.

Two things this surfaced, not fixed here

  • The lean spec can shift on its own. Once defaults publishes notebook 7.6, the next Monday rebuild moves jupyterlab to 4.6 without anyone editing a file. Taking Node out of conda closes the Node half of that; the Jupyter stack still floats.
  • ipykernel, jupyter-server and plotly are unpinned in the lean image, so any re-solve can move the kernel under :latest consumers such as lecture-dp. The theme-parity corpora are protected, because they pin the image by digest (QuantEcon/project-theme-parity#22).

🤖 Generated with Claude Code

Both images carried Node 20 (lean 20.17.0, full 20.20.2), end-of-life since
2026-04-30. It cannot move to 24 through conda: every jupyterlab below 4.6 on
the defaults channel -- including the 4.5.7 that anaconda=2026.06 pins --
constrains nodejs to 20.x, so `nodejs>=24` has no solution in either image,
and the widening to `<25` in #81 never took effect. A conda-spec bump would
have failed only after merge, since image builds do not run on pull requests.

Node now comes from nodejs.org, pinned by version and SHA256 like Miniconda
(24.21.0, the latest 24.x LTS; hash checked against SHASUMS256.txt), in a
layer after conda so a Node bump never re-runs the solve, and on PATH ahead of
the conda env so a lecture environment cannot shadow it. The build asserts the
version -- until now nothing checked which Node the image had.

Dropping the conda nodejs leaves the full image's solve unchanged. The lean
image holds icu=73.1, the version the old nodejs pinned; without that hold the
native stack re-solves (qt 5 -> 6, pillow, lxml, tbb and about 90 others).
Solved for linux-64 with the image's own conda 25.11.1: no Python kernel
package moves in either image.

Who needed it: preview-netlify installs netlify-cli@latest (27.x, node
>=22.13) into the image, and has been running it on Node 20 with EBADENGINE
warnings; preview-cloudflare's wrangler refuses to start below node 22.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 18, 2026 06:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Both preview action READMEs currently show actions/setup-node without an @v* ref, which is not a valid uses: reference and will break copied workflow snippets.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR updates the QuantEcon container images to provide Node.js 24 LTS by installing it directly from nodejs.org (pinned by version + SHA256) instead of via conda, and updates accompanying documentation/config to reflect the new Node provisioning approach.

Changes:

  • Install Node.js 24.21.0 (pinned + SHA256-verified) in both container Dockerfiles and verify the installed version during image build.
  • Remove conda nodejs from the full image environment; in the lean image, remove nodejs but pin icu=73.1 to avoid an unrelated native-stack re-solve.
  • Update Dependabot guidance and action READMEs / CHANGELOG to document the new Node requirements and provenance.
File summaries
File Description
containers/quantecon/Dockerfile Installs Node 24 from nodejs.org with SHA256 verification; adds build-time verification of Node/NPM.
containers/quantecon/environment.yml Removes conda-managed nodejs from the full image environment.
containers/quantecon-build/Dockerfile Installs Node 24 from nodejs.org with SHA256 verification; adds build-time verification of Node/NPM.
containers/quantecon-build/environment.yml Drops conda nodejs and pins icu=73.1 to prevent unrelated dependency churn.
preview-netlify/README.md Updates stated Node requirements and container coverage (but the setup-node usage string needs a version tag fix).
preview-cloudflare/README.md Updates stated Node requirements and container coverage (but the setup-node usage string needs a version tag fix).
.github/dependabot.yml Removes nodejs-specific guidance/ignores and adds an icu ignore note aligned with the lean image pin.
CHANGELOG.md Documents the Node move from conda to nodejs.org and the motivation/impact.
Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 2
  • Review effort level: Lite

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

Comment thread preview-cloudflare/README.md Outdated
Comment thread preview-netlify/README.md Outdated
Review feedback on #166. The Node note named `actions/setup-node` without a
version; it now says `@v7`. Copilot suggested `@v4`, but v4 runs on Node 20,
the runtime this PR moves the images off; v5 onwards run on node24.

The same READMEs pointed readers at `actions/checkout@v4` -- in the
changed-lecture note and in each usage example -- which is also Node 20.
Both now say `@v7`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mmcky
mmcky merged commit 195b9f2 into main Sep 18, 2026
17 checks passed
@mmcky
mmcky deleted the feat/images-node24 branch September 18, 2026 06:47
mmcky added a commit to QuantEcon/test-lecture-theme-sphinx that referenced this pull request Sep 18, 2026
QuantEcon/actions#166 rebuilt the images with Node 24.21.0 from nodejs.org in
place of conda's end-of-life Node 20; the build asserts the version. All three
workflows move from sha256:5d256c23... to that image's index digest,
sha256:937af97c..., which `latest` and `main-195b9f2` name. The conda env is
unchanged apart from nodejs leaving it, so the kernel should be too; the
build_info page on this PR's artefact is the check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mmcky added a commit to QuantEcon/test-lecture-theme-mystmd that referenced this pull request Sep 18, 2026
… probe

The image moves from sha256:5d256c23... to sha256:937af97c..., the index
QuantEcon/actions#166 built with Node 24.21.0 in place of conda's end-of-life
Node 20 -- the same digest the Sphinx corpus now pins.

The diagnostic step confirmed why HTML export failed. In this container Node
resolves "localhost" to ::1 first, so a server bound to "localhost" listens on
IPv6 loopback only, while an http client dials 127.0.0.1 and is refused:
measured, the server bound ::1 and http.get(localhost) got ECONNREFUSED
127.0.0.1; with --dns-result-order=ipv4first it bound 127.0.0.1 and got 200.
That is myst's content server against the theme's node-fetch. The option stays,
with the measured mechanism in its comment; the probe step goes.

The README said to `pip install -r requirements.txt`, a file this PR deletes,
and named qe-v10. It now says qe-v11 and that the cells run in the image's
conda env (review feedback on this PR).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mmcky added a commit to QuantEcon/test-lecture-theme-sphinx that referenced this pull request Sep 18, 2026
…4) (#8)

* ci: pin the image by digest, fetch full history, upload PR builds

Decision D14 of the theme-parity project has both test corpora execute
in one image, so the theme is the only variable. All three workflows now
name ghcr.io/quantecon/quantecon-build by its index digest,
sha256:5d256c23..., which the `latest` and `main-3adfcf3` tags named on
2026-09-18, instead of `:latest`, which moves every Monday. A job-level
QE_CONTAINER carries the same ref, because container.image cannot read
env; build_info.md prints it.

Every page on the live site has lost its "Last changed" header: git
refused the runner-owned checkout from the root container, and the theme
drops the header silently when git fails. The image now trusts every
work tree at system scope (see QuantEcon/actions#165). Each workflow
also adds a global safe.directory entry for the checkout as a backstop
against an image that loses that entry, and fails if git still cannot
read the tree. Checkout fetches full history, because a shallow clone
dates every page to HEAD.

CI Preview uploads _build/html as the sphinx-html artefact on pull
requests, kept for 7 days, so a pull request can be inspected before it
merges; Netlify is not configured here, and its guarded step is
unchanged.

actions/checkout moves from v4 to v7, off Node 20 (end-of-life
2026-04-30) onto node24, as part of moving the fleet to Node 24. v6's
credential file does not matter here: nothing in these jobs pushes with
the job token.

environment.yml and lectures/ are untouched, so the build-cache key is
unchanged.

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

* ci: pin the Node 24 image (actions 195b9f2) instead of the Node 20 one

QuantEcon/actions#166 rebuilt the images with Node 24.21.0 from nodejs.org in
place of conda's end-of-life Node 20; the build asserts the version. All three
workflows move from sha256:5d256c23... to that image's index digest,
sha256:937af97c..., which `latest` and `main-195b9f2` name. The conda env is
unchanged apart from nodejs leaving it, so the kernel should be too; the
build_info page on this PR's artefact is the check.

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

* ci: build PR previews without the build cache, so build_info is true

After the image was re-pinned to the Node 24 digest, this PR's preview pulled
the new image and then restored the build cache from the previous preview
run; every notebook came from the cache ("Using cached notebook"), and the
build_info page reported the old image's digest, Node 20.17 and build date --
the environment of whichever run last executed it, not this one.

The cache key folds environment.yml, not the image digest, so changing the
image never invalidates it; and even on an unchanged image a cached build_info
shows another run's commit and build time. For a corpus whose purpose is
accurate provenance that is disqualifying. It is 14 pages and builds uncached
in about three minutes. publish.yml never restored the cache, so the live site
was always accurate; this brings pull-request previews into line.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
mmcky added a commit to QuantEcon/test-lecture-theme-mystmd that referenced this pull request Sep 18, 2026
* ci: build in the digest-pinned image on qe-v11, with Node 24

Decision D14 of the theme-parity project has both test corpora execute in
one image, so the theme is the only variable. The build job now runs inside
ghcr.io/quantecon/quantecon-build@sha256:5d256c23..., the image the Sphinx
corpus pins, and the code cells execute in that image's `quantecon` conda env
instead of a setup-python 3.12 env resolved from an unpinned requirements.txt.
requirements.txt is removed; nothing reads it.

The fork moves to qe-v11 (83685347, cut 2026-09-18). It changes no dependency
and, on this corpus, no rendered output once per-build ids are masked; the old
pin was c1f274b8, nine commits past qe-v10, which still printed "qe-v10". The
install step now checks that `myst --version` names the same qe-vN as the
fork's VERSION.yml -- qe-v11's VERSION.yml grew 13 comment lines, so the old
`head -40` would have printed nothing useful.

Three things the container needs, each of which fails silently without it:
- the Jupyter server refuses to start as root unless allow_root is set, and
  myst treats that as non-fatal -- the build went green with no executed
  output on any page;
- git refuses a runner-owned checkout from root, which drops every page's
  "Last changed" history; the image now trusts the tree itself
  (QuantEcon/actions#165) and a global safe.directory step is kept as a
  backstop;
- setup-bun needs unzip, which the image lacks, so bun is installed with npm
  at the fork's own packageManager version.
A check step then fails the job unless build-info shows the image's Python and
every page carries its git history.

Node is not taken from the image: it runs the engine and the theme server,
never a code cell, so it sits outside what D14 holds constant. It is pinned to
24 through setup-node, and the job fails if the image's Node 20 wins on PATH.
Every action here now runs on node24: checkout v7, setup-node v7,
upload-pages-artifact v5 (no dotfiles in this site to lose to v4's exclusion),
deploy-pages v5. The Netlify guard requires both credentials, and netlify-cli
is no longer capped at 26 now Node is 24.

Every edit is under jobs.build except QE_MYSTMD_SHA and its comment.

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

* ci: resolve localhost to IPv4 first for the HTML export, and diagnose it

The first build in the container executed every notebook and then failed at
HTML export: the theme's server could not fetch
http://localhost:3100/config.json from myst's content server, so every route
returned 404. The same step passes on the runner.

Both of myst's servers bind "localhost" (start.ts, DEFAULT_HOST, passed to the
theme as HOST), and the theme builds its content URL as
`http://localhost:${CONTENT_CDN_PORT}` and fetches it with a bundled
node-fetch v2 -- whose failure it swallows into "No site configuration
found". myst's own fetch to the theme on :3000 worked; the theme's fetch to
:3100 did not, which points at the two resolving localhost to different
address families. mystmd already pins HOST to 127.0.0.1 for ReadTheDocs, the
other containerised builder, for what looks like the same reason.

NODE_OPTIONS=--dns-result-order=ipv4first makes every Node process resolve
localhost to 127.0.0.1, so the bind and the fetch agree, without hard-coding
the content server's port. A temporary, non-fatal step records /etc/hosts,
how localhost resolves and whether loopback has IPv6, and reproduces the
bind-then-fetch pattern with and without the option; it goes once the cause
is confirmed.

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

* ci: pin the Node 24 image; the localhost cause is confirmed, drop the probe

The image moves from sha256:5d256c23... to sha256:937af97c..., the index
QuantEcon/actions#166 built with Node 24.21.0 in place of conda's end-of-life
Node 20 -- the same digest the Sphinx corpus now pins.

The diagnostic step confirmed why HTML export failed. In this container Node
resolves "localhost" to ::1 first, so a server bound to "localhost" listens on
IPv6 loopback only, while an http client dials 127.0.0.1 and is refused:
measured, the server bound ::1 and http.get(localhost) got ECONNREFUSED
127.0.0.1; with --dns-result-order=ipv4first it bound 127.0.0.1 and got 200.
That is myst's content server against the theme's node-fetch. The option stays,
with the measured mechanism in its comment; the probe step goes.

The README said to `pip install -r requirements.txt`, a file this PR deletes,
and named qe-v10. It now says qe-v11 and that the cells run in the image's
conda env (review feedback on this PR).

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

---------

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.

2 participants