From 8c7fc91c95e5f8e3d51629d61aed0de4d46d88d6 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Tue, 4 Aug 2026 11:24:08 -0700 Subject: [PATCH 1/2] chore(build): move the Admin UI toolchain to Node 24 Node 18 and Node 20 both reached end of life (2025-04-30 and 2026-04-30), and the release images along with every CI lane were still building on them. Node 24 is the current LTS through 2028-04-30, so this moves the four UI build images, the CircleCI lanes, and the four GitHub Actions workflows onto it Node 24 also ships npm 11.17, which is the first line that implements the min-release-age setting this repo already carries in its .npmrc files. On npm 10 the key is parsed and discarded, so the release-age gate has had no effect regardless of its value. Tightening the dashboard's engines range and turning on engine-strict makes an unsupported npm fail loudly rather than skip the gate quietly, and a new step in the UI build workflow probes an impossible cooldown so an inert setting cannot pass unnoticed again Node 24's bundled undici tightened its brand check on RequestInit.signal, which rejects the AbortSignal jsdom installs and broke the two cases in src/lib/http/api.test.ts that rebase a request onto a runtime base url. Under jsdom the Request global comes from Node while AbortSignal comes from jsdom; tests/jsdomFetchEnv.ts delegates to the jsdom environment and then restores Node's native AbortController and AbortSignal so both come from one realm. Upgrading jsdom does not address this, as jsdom still does not own Request The workflows now read ui/litellm-dashboard/.nvmrc instead of repeating a literal, so the Node version has a single source of truth, and ui/Dockerfile is pinned by digest to match the other three build images. The lockfile changes are npm 11 normalising the engines range and dropping optional peer entries it no longer records --- .circleci/config.yml | 12 ++--- .github/workflows/check-ui-api-types.yml | 2 +- .github/workflows/test-litellm-ui-build.yml | 21 +++++++- .github/workflows/test-litellm-ui-lint.yml | 2 +- .github/workflows/test-litellm-ui-unit.yml | 2 +- Dockerfile | 2 +- docker/Dockerfile.database | 2 +- docker/Dockerfile.non_root | 2 +- ui/Dockerfile | 4 +- ui/litellm-dashboard/.npmrc | 1 + ui/litellm-dashboard/.nvmrc | 2 +- ui/litellm-dashboard/package-lock.json | 56 ++------------------- ui/litellm-dashboard/package.json | 4 +- ui/litellm-dashboard/tests/jsdomFetchEnv.ts | 24 +++++++++ ui/litellm-dashboard/vitest.config.ts | 2 +- 15 files changed, 66 insertions(+), 72 deletions(-) create mode 100644 ui/litellm-dashboard/tests/jsdomFetchEnv.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 790cd6c7010e..80dc1c8cbc56 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2592,14 +2592,14 @@ jobs: bundle exec rspec no_output_timeout: 30m # Install Node.js directly from nodejs.org with SHA256 verification, - # instead of piping NodeSource's setup_18.x apt-repo installer into + # instead of piping NodeSource's setup_24.x apt-repo installer into # sudo bash (which runs a mutable upstream script unattended). - run: - name: Install Node.js 18.20.8 + name: Install Node.js 24.19.0 command: | - NODE_VERSION="18.20.8" + NODE_VERSION="24.19.0" NODE_TARBALL="node-v${NODE_VERSION}-linux-x64.tar.xz" - NODE_EXPECTED_SHA="5467ee62d6af1411d46b6a10e3fb5cacc92734dbcef465fea14e7b90993001c9" + NODE_EXPECTED_SHA="14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647" curl -sSLf -o "/tmp/${NODE_TARBALL}" "https://nodejs.org/dist/v${NODE_VERSION}/${NODE_TARBALL}" echo "${NODE_EXPECTED_SHA} /tmp/${NODE_TARBALL}" | sha256sum -c - sudo tar -xJf "/tmp/${NODE_TARBALL}" -C /usr/local --strip-components=1 @@ -2734,7 +2734,7 @@ jobs: ui_build: docker: - - image: cimg/node:20.19@sha256:35e64883e8d21bc345b0a7b04c35ee46442c127607ed1d8d7d37d8a1ed76db81 + - image: cimg/node:24.19@sha256:8966565f07189a67d64d6808a2b127f31dafae566508e3547f55640e1070bfad auth: username: ${DOCKERHUB_USERNAME} password: ${DOCKERHUB_PASSWORD} @@ -2778,7 +2778,7 @@ jobs: ui_unit_tests: docker: - - image: cimg/node:20.19@sha256:35e64883e8d21bc345b0a7b04c35ee46442c127607ed1d8d7d37d8a1ed76db81 + - image: cimg/node:24.19@sha256:8966565f07189a67d64d6808a2b127f31dafae566508e3547f55640e1070bfad auth: username: ${DOCKERHUB_USERNAME} password: ${DOCKERHUB_PASSWORD} diff --git a/.github/workflows/check-ui-api-types.yml b/.github/workflows/check-ui-api-types.yml index 9c24bad00f1a..597daebd7205 100644 --- a/.github/workflows/check-ui-api-types.yml +++ b/.github/workflows/check-ui-api-types.yml @@ -56,7 +56,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: - node-version: "20" + node-version-file: ui/litellm-dashboard/.nvmrc cache: "npm" cache-dependency-path: ui/litellm-dashboard/package-lock.json diff --git a/.github/workflows/test-litellm-ui-build.yml b/.github/workflows/test-litellm-ui-build.yml index 525e2c5b949a..b1a8216f70c6 100644 --- a/.github/workflows/test-litellm-ui-build.yml +++ b/.github/workflows/test-litellm-ui-build.yml @@ -27,10 +27,29 @@ jobs: - name: Setup Node.js uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: - node-version: "20" + node-version-file: ui/litellm-dashboard/.nvmrc cache: "npm" cache-dependency-path: ui/litellm-dashboard/package-lock.json + - name: Verify the dependency cooldown is enforced + run: | + set -euo pipefail + configured=$(npm config get min-release-age) + if [ "$configured" = "null" ] || [ -z "$configured" ]; then + echo "::error::min-release-age is not set for ui/litellm-dashboard" + exit 1 + fi + echo "min-release-age=$configured on npm $(npm --version)" + + probe=$(mktemp -d) + printf '{"name":"cooldown-probe","version":"1.0.0","private":true}\n' > "$probe/package.json" + printf 'min-release-age=999999\n' > "$probe/.npmrc" + if (cd "$probe" && npm install --package-lock-only --no-audit --no-fund react >/dev/null 2>&1); then + echo "::error::npm resolved a version despite a 999999-day cooldown; min-release-age is inert on npm $(npm --version)" + exit 1 + fi + echo "cooldown enforced: resolution refused under an impossible min-release-age" + - name: Install dependencies run: npm ci diff --git a/.github/workflows/test-litellm-ui-lint.yml b/.github/workflows/test-litellm-ui-lint.yml index 5173eb6da35c..2c31abc66097 100644 --- a/.github/workflows/test-litellm-ui-lint.yml +++ b/.github/workflows/test-litellm-ui-lint.yml @@ -61,7 +61,7 @@ jobs: if: steps.changed.outputs.has_files == 'true' uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: - node-version: "20" + node-version-file: ui/litellm-dashboard/.nvmrc cache: "npm" cache-dependency-path: ui/litellm-dashboard/package-lock.json diff --git a/.github/workflows/test-litellm-ui-unit.yml b/.github/workflows/test-litellm-ui-unit.yml index 5374a0059de8..20c3611eb384 100644 --- a/.github/workflows/test-litellm-ui-unit.yml +++ b/.github/workflows/test-litellm-ui-unit.yml @@ -35,7 +35,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: - node-version: "20" + node-version-file: ui/litellm-dashboard/.nvmrc cache: "npm" cache-dependency-path: ui/litellm-dashboard/package-lock.json diff --git a/Dockerfile b/Dockerfile index a127cdabd599..1fb34f6ebf96 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17 ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a # Pinned by digest like the other base images; bump explicitly on Node upgrades. -ARG UI_BUILD_IMAGE=node:20.18-alpine3.20@sha256:3488b10bf958af7125a176419d2d8a9937d895bf124012aae811651988d2ffe6 +ARG UI_BUILD_IMAGE=node:24.19-alpine3.24@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 FROM $UV_IMAGE AS uvbin diff --git a/docker/Dockerfile.database b/docker/Dockerfile.database index 9ee076ce8256..c93a08409a2b 100644 --- a/docker/Dockerfile.database +++ b/docker/Dockerfile.database @@ -7,7 +7,7 @@ ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17 ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a # Pinned by digest like the other base images; bump explicitly on Node upgrades. -ARG UI_BUILD_IMAGE=node:20.18-alpine3.20@sha256:3488b10bf958af7125a176419d2d8a9937d895bf124012aae811651988d2ffe6 +ARG UI_BUILD_IMAGE=node:24.19-alpine3.24@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 FROM $UV_IMAGE AS uvbin diff --git a/docker/Dockerfile.non_root b/docker/Dockerfile.non_root index 946b4de6f5e6..1545a84d379c 100644 --- a/docker/Dockerfile.non_root +++ b/docker/Dockerfile.non_root @@ -6,7 +6,7 @@ ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b ARG PROXY_EXTRAS_SOURCE=published ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a # Pinned by digest like the other base images; bump explicitly on Node upgrades. -ARG UI_BUILD_IMAGE=node:20.18-alpine3.20@sha256:3488b10bf958af7125a176419d2d8a9937d895bf124012aae811651988d2ffe6 +ARG UI_BUILD_IMAGE=node:24.19-alpine3.24@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 FROM $UV_IMAGE AS uvbin diff --git a/ui/Dockerfile b/ui/Dockerfile index b75c4d0a0c63..0d184b74493c 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -2,11 +2,11 @@ # UI container — Next.js static export served by nginx. -ARG NODE_VERSION=20.18-alpine3.20 +ARG UI_BUILD_IMAGE=node:24.19-alpine3.24@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 ARG NGINX_VERSION=1.27-alpine # ---------- builder ---------- -FROM node:${NODE_VERSION} AS builder +FROM ${UI_BUILD_IMAGE} AS builder ENV NEXT_TELEMETRY_DISABLED=1 \ npm_config_fund=false \ diff --git a/ui/litellm-dashboard/.npmrc b/ui/litellm-dashboard/.npmrc index 7999681cc351..8865c7b3c2eb 100644 --- a/ui/litellm-dashboard/.npmrc +++ b/ui/litellm-dashboard/.npmrc @@ -3,3 +3,4 @@ ignore-scripts=true # Protects local npm install only — npm ci (used in CI) ignores this min-release-age=3 +engine-strict=true diff --git a/ui/litellm-dashboard/.nvmrc b/ui/litellm-dashboard/.nvmrc index 209e3ef4b624..60ade1ae01e8 100644 --- a/ui/litellm-dashboard/.nvmrc +++ b/ui/litellm-dashboard/.nvmrc @@ -1 +1 @@ -20 +24.19.0 diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index a1bd63151b4e..1cc35329dd90 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -76,8 +76,8 @@ "vitest": "3.2.6" }, "engines": { - "node": ">=20.9.0", - "npm": ">=8.3.0" + "node": ">=24.0.0", + "npm": ">=11.10.0" } }, "node_modules/@acemir/cssom": { @@ -2718,23 +2718,6 @@ "win32" ] }, - "node_modules/@playwright/test": { - "version": "1.58.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.1.tgz", - "integrity": "sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "playwright": "1.58.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -7427,6 +7410,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -11009,40 +10993,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/playwright": { - "version": "1.58.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz", - "integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "playwright-core": "1.58.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.58.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz", - "integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index 4760b622f9bd..428027f9580e 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -102,7 +102,7 @@ "sharp": "^0.35.0" }, "engines": { - "node": ">=20.9.0", - "npm": ">=8.3.0" + "node": ">=24.0.0", + "npm": ">=11.10.0" } } diff --git a/ui/litellm-dashboard/tests/jsdomFetchEnv.ts b/ui/litellm-dashboard/tests/jsdomFetchEnv.ts new file mode 100644 index 000000000000..b3c0239d1085 --- /dev/null +++ b/ui/litellm-dashboard/tests/jsdomFetchEnv.ts @@ -0,0 +1,24 @@ +import { builtinEnvironments, type Environment } from "vitest/environments"; + +const env: Environment = { + name: "jsdom-fetch", + transformMode: "web", + async setup(global, options) { + const nativeAbortController = global.AbortController; + const nativeAbortSignal = global.AbortSignal; + const { teardown } = await builtinEnvironments.jsdom.setup(global, options); + Object.defineProperty(global, "AbortController", { + configurable: true, + writable: true, + value: nativeAbortController, + }); + Object.defineProperty(global, "AbortSignal", { + configurable: true, + writable: true, + value: nativeAbortSignal, + }); + return { teardown }; + }, +}; + +export default env; diff --git a/ui/litellm-dashboard/vitest.config.ts b/ui/litellm-dashboard/vitest.config.ts index da4734eeaf2c..19af41ab8edc 100644 --- a/ui/litellm-dashboard/vitest.config.ts +++ b/ui/litellm-dashboard/vitest.config.ts @@ -15,7 +15,7 @@ const staticImageData: Plugin = { const config: ViteUserConfig = { plugins: [staticImageData], test: { - environment: "jsdom", + environment: "./tests/jsdomFetchEnv.ts", setupFiles: ["tests/setupTests.ts"], globals: true, css: true, // lets you import CSS/modules without extra mocks From 888193ecbdffeb063bd244bf591cc4664a5b4fcb Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Tue, 4 Aug 2026 12:03:24 -0700 Subject: [PATCH 2/2] fix(build): point every Admin UI build script at .nvmrc The enterprise Docker path was left on Node 18. docker/build_admin_ui.sh runs only when enterprise/enterprise_ui/enterprise_colors.json is present, which it never is in the OSS tree, so neither CI nor a default image build reaches it; it pinned nvm to v18.17.0 and then built the dashboard, which now requires Node 24, so a customized enterprise image would have failed EBADENGINE All three UI build scripts now resolve the version from ui/litellm-dashboard/.nvmrc rather than carrying their own pin, so the Node version has a single home across Docker, CI, and local builds. build_ui.sh was on v20 and build_ui_custom_path.sh on v18.17.0 Also drops the dependency-cooldown probe from the UI build workflow. The engines floor plus engine-strict already fails an unsupported npm loudly at install time, so the probe was redundant, and treating any nonzero exit from a live registry call as proof of enforcement made it unsound besides --- .github/workflows/test-litellm-ui-build.yml | 19 ------------------- docker/build_admin_ui.sh | 5 +++-- ui/litellm-dashboard/build_ui.sh | 5 +++-- ui/litellm-dashboard/build_ui_custom_path.sh | 5 +++-- 4 files changed, 9 insertions(+), 25 deletions(-) diff --git a/.github/workflows/test-litellm-ui-build.yml b/.github/workflows/test-litellm-ui-build.yml index b1a8216f70c6..39f4bc1428a4 100644 --- a/.github/workflows/test-litellm-ui-build.yml +++ b/.github/workflows/test-litellm-ui-build.yml @@ -31,25 +31,6 @@ jobs: cache: "npm" cache-dependency-path: ui/litellm-dashboard/package-lock.json - - name: Verify the dependency cooldown is enforced - run: | - set -euo pipefail - configured=$(npm config get min-release-age) - if [ "$configured" = "null" ] || [ -z "$configured" ]; then - echo "::error::min-release-age is not set for ui/litellm-dashboard" - exit 1 - fi - echo "min-release-age=$configured on npm $(npm --version)" - - probe=$(mktemp -d) - printf '{"name":"cooldown-probe","version":"1.0.0","private":true}\n' > "$probe/package.json" - printf 'min-release-age=999999\n' > "$probe/.npmrc" - if (cd "$probe" && npm install --package-lock-only --no-audit --no-fund react >/dev/null 2>&1); then - echo "::error::npm resolved a version despite a 999999-day cooldown; min-release-age is inert on npm $(npm --version)" - exit 1 - fi - echo "cooldown enforced: resolution refused under an impossible min-release-age" - - name: Install dependencies run: npm ci diff --git a/docker/build_admin_ui.sh b/docker/build_admin_ui.sh index 68acdd78e3e4..f84aba7c0537 100755 --- a/docker/build_admin_ui.sh +++ b/docker/build_admin_ui.sh @@ -54,8 +54,9 @@ else fi || { echo "nvm checksum verification failed"; exit 1; } bash "$NVM_SCRIPT" source ~/.nvm/nvm.sh -nvm install v18.17.0 -nvm use v18.17.0 +NODE_VERSION="$(cat ui/litellm-dashboard/.nvmrc)" +nvm install "v${NODE_VERSION}" +nvm use "v${NODE_VERSION}" # cd in to /ui/litellm-dashboard diff --git a/ui/litellm-dashboard/build_ui.sh b/ui/litellm-dashboard/build_ui.sh index a3ab475dc58d..3cdf4305d617 100755 --- a/ui/litellm-dashboard/build_ui.sh +++ b/ui/litellm-dashboard/build_ui.sh @@ -23,11 +23,12 @@ if ! command -v nvm &> /dev/null; then fi # Use nvm to set the required Node.js version -nvm use v20 +nvm install +nvm use # Check if nvm use was successful if [ $? -ne 0 ]; then - echo "Error: Failed to switch to Node.js v20. Deployment aborted." + echo "Error: Failed to switch to the Node.js version in .nvmrc. Deployment aborted." exit 1 fi diff --git a/ui/litellm-dashboard/build_ui_custom_path.sh b/ui/litellm-dashboard/build_ui_custom_path.sh index a92927f8ea7b..2f9905126d6a 100755 --- a/ui/litellm-dashboard/build_ui_custom_path.sh +++ b/ui/litellm-dashboard/build_ui_custom_path.sh @@ -33,11 +33,12 @@ if ! command -v nvm &> /dev/null; then fi # Use nvm to set the required Node.js version -nvm use v18.17.0 +nvm install +nvm use # Check if nvm use was successful if [ $? -ne 0 ]; then - echo "Error: Failed to switch to Node.js v18.17.0. Deployment aborted." + echo "Error: Failed to switch to the Node.js version in .nvmrc. Deployment aborted." exit 1 fi