feat(containers): Node 24 LTS from nodejs.org, out of conda - #166
Merged
Merged
Conversation
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>
Contributor
There was a problem hiding this comment.
🟡 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
nodejsfrom the full image environment; in the lean image, removenodejsbut pinicu=73.1to 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.
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Both images carry Node 20, which reached end-of-life on 2026-04-30: the lean
quantecon-buildhas 20.17.0 and the fullquanteconhas 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
jupyterlabbelow 4.6 on the defaults channel carriesconstrains: nodejs >=20.0.0,<21.0.0. Two things hold the images below 4.6:notebook7.5.x requiresjupyterlab <4.6;anaconda=2026.06pinsjupyterlab 4.5.7exactly.So
nodejs>=24,<25, andnodejs=24, have no solution in either image. That is also why #81's widening to<25never took effect. Image builds don't run on pull requests, so a conda-spec bump would have brokenmain'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
Dockerfiles/opt/node/bingoes onPATHahead 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.environment.ymlsnodejs. The lean image holdsicu=73.1, the version the old condanodejspinned. Without it, droppingnodejsre-solves the native stack:qt5→6,pyqt5→6,pillow,lxml, and atbbdowngrade that numba's threading uses, about 90 packages in all..github/dependabot.ymlnodejscap, which no longer applies, and ignoreicuuntil the next Anaconda baselinepreview-netlify,preview-cloudflareREADMEs,CHANGELOG.mdNothing in the kernel moves
The proposed environments were solved for linux-64 with the image's own conda. In the full image only
nodejsleaves; in the lean image onlynodejsandlibuvleave. Python, ipykernel, jupyter_server, numpy, scipy, matplotlib, pandas and plotly are all unchanged.Who uses Node in the image
preview-netlify→npm install -g netlify-cli@latest(27.x)>=22.13EBADENGINEwarnings (canary run, 2026-08-18)preview-cloudflare→wrangler@latest>=22;bin/wrangler.jsexits below 22/__enodejsfirst 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
https://nodejs.org/dist/v24.21.0/SHASUMS256.txtin both Dockerfiles.curlis installed by apt in both images and already fetches Miniconda.What only the post-merge build can prove
As with #165,
build-containers.ymlruns onmain, not on this PR. After merge:node -p process.versions.node=24.21.0).test-container.ymlsmoke tests stay green.EBADENGINE.A failed build pushes nothing, so
:latestkeeps the current image; the failure mode is safe.Two things this surfaced, not fixed here
notebook7.6, the next Monday rebuild movesjupyterlabto 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-serverandplotlyare unpinned in the lean image, so any re-solve can move the kernel under:latestconsumers such aslecture-dp. The theme-parity corpora are protected, because they pin the image by digest (QuantEcon/project-theme-parity#22).🤖 Generated with Claude Code