diff --git a/tests/test_litellm/ui/test_ui_build_pinning_lit_2723.py b/tests/test_litellm/ui/test_ui_build_pinning_lit_2723.py new file mode 100644 index 000000000000..17a11bb44d7c --- /dev/null +++ b/tests/test_litellm/ui/test_ui_build_pinning_lit_2723.py @@ -0,0 +1,62 @@ +""" +Regression tests for LIT-2723 - pin the UI build output layout and Node version +so committed `litellm/proxy/_experimental/out/` diffs stop flipping between +`.html` and `/index.html`, and so the build does not silently +fall back to a non-v20 Node. + +These are file-level assertions on the build config + script. They do not run +npm or restructure the export; they only guard the locking lines. +""" + +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] +NEXT_CONFIG = REPO_ROOT / "ui" / "litellm-dashboard" / "next.config.mjs" +BUILD_UI_SH = REPO_ROOT / "ui" / "litellm-dashboard" / "build_ui.sh" + + +def test_next_config_pins_trailing_slash_true(): + """next.config.mjs must declare `trailingSlash: true` so every build emits + the `/index.html` directory-index layout. Flipping back to the + `.html` form rewrites every file in `_experimental/out` and breaks + deployments that rely on directory-index routing (e.g. the MCP OAuth + callback).""" + assert NEXT_CONFIG.is_file(), f"next.config.mjs missing at {NEXT_CONFIG}" + text = NEXT_CONFIG.read_text() + assert "trailingSlash: true" in text or "trailingSlash:true" in text, ( + "next.config.mjs must set `trailingSlash: true` (LIT-2723). " + "Removing it makes the static export flip back to the bare " + ".html form on the next local build." + ) + + +def test_build_ui_sh_installs_node_v20_and_verifies(): + """build_ui.sh must install v20 (not just `nvm use`), error-check both + steps, and verify `node -v` actually reports v20 before running the + build. Without these guards the script silently succeeds against any + Node version on PATH in non-interactive shells, and the committed + artifacts pick up the wrong Node.""" + assert BUILD_UI_SH.is_file(), f"build_ui.sh missing at {BUILD_UI_SH}" + text = BUILD_UI_SH.read_text() + + assert "nvm install v20" in text, ( + "build_ui.sh must `nvm install v20` before `nvm use v20` - " + "`use` alone silently no-ops when v20 is not already installed." + ) + assert "nvm install v20 failed" in text, ( + "build_ui.sh must surface `nvm install v20` failure with an " + "error message and non-zero exit (LIT-2723)." + ) + assert "Failed to switch to Node.js v20" in text, ( + "build_ui.sh must surface `nvm use v20` failure with an error " + "message and non-zero exit." + ) + assert "node -v" in text, ( + "build_ui.sh must read `node -v` to verify the active Node " + "version after `nvm use v20`." + ) + assert "v20." in text, ( + "build_ui.sh must check that `node -v` output starts with " + "`v20.` and abort otherwise (LIT-2723)." + ) diff --git a/ui/litellm-dashboard/build_ui.sh b/ui/litellm-dashboard/build_ui.sh index aa346c12edc4..faeb13efd4e3 100755 --- a/ui/litellm-dashboard/build_ui.sh +++ b/ui/litellm-dashboard/build_ui.sh @@ -22,15 +22,29 @@ if ! command -v nvm &> /dev/null; then [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" fi -# Use nvm to set the required Node.js version -nvm use v20 +# Install + use the pinned Node.js version. `nvm use` alone silently no-ops +# when v20 isn't installed in the current shell (e.g. non-interactive CI), +# and the build then runs against whatever node is on PATH. Install first, use +# second, and verify `node -v` actually reports v20 before continuing. +if ! nvm install v20; then + echo "Error: nvm install v20 failed. Deployment aborted." + exit 1 +fi -# Check if nvm use was successful -if [ $? -ne 0 ]; then +if ! nvm use v20; then echo "Error: Failed to switch to Node.js v20. Deployment aborted." exit 1 fi +NODE_VER="$(node -v 2>/dev/null || true)" +case "$NODE_VER" in + v20.*) echo "Confirmed Node.js $NODE_VER" ;; + *) + echo "Error: expected Node.js v20.* but got '$NODE_VER'. Deployment aborted." + exit 1 + ;; +esac + # print contents of ui_colors.json echo "Contents of ui_colors.json:" cat ui_colors.json @@ -60,4 +74,6 @@ if [ $? -eq 0 ]; then echo "Deployment completed." else echo "Build failed. Deployment aborted." + exit 1 fi + diff --git a/ui/litellm-dashboard/next.config.mjs b/ui/litellm-dashboard/next.config.mjs index cfaeb24dc5d4..3d9b8e3463e4 100644 --- a/ui/litellm-dashboard/next.config.mjs +++ b/ui/litellm-dashboard/next.config.mjs @@ -7,6 +7,12 @@ const __dirname = path.dirname(__filename); const nextConfig = { output: "export", + // Pin the directory-index layout (/index.html). Otherwise different + // Next.js patch versions or stale .next caches flip the export between + // foo.html and foo/index.html, producing massive rename-only diffs in + // litellm/proxy/_experimental/out and breaking deployments that rely on + // directory-index routing for nested paths (e.g. /ui/mcp/oauth/callback). + trailingSlash: true, // Required with output: "export" — default image optimizer runs only in server mode. // See https://nextjs.org/docs/messages/export-image-api images: {