fix(studio): serve container-baked assets, and stop shipping a partial Studio bundle [ASTD-354] - #1080
Merged
Merged
Conversation
The nmp-api image copies the Studio bundle to /static/studio, but the packaged nmp/studio/static dir is only populated by the wheel build, so any non-Helm run of the image (docker run, compose, quickstart) fell through to the "assets are not built" page. Helm masked this by setting studio.static_files_path explicitly. Add /static/studio to the _get_static_files_path fallback chain rather than baking NMP_STUDIO_STATIC_FILES_PATH into the Dockerfile: ServiceConfig derives from EnvironmentFirstSettings, which orders env_settings ahead of init_settings, so the env var would silently override an operator's studio.static_files_path. The fallback also covers future images that lay the bundle down without setting the var. Also split the error page's recovery block. The nvm / make bootstrap-studio instructions only apply to a source checkout; packaged installs now get the NMP_STUDIO_STATIC_FILES_PATH knob and the container bundle location instead. Signed-off-by: mschwab <mschwab@nvidia.com>
…ist [ASTD-354] `status="$?"` sat after `fi`, so it captured the exit status of the `if` compound command (always 0 when the condition fails and there is no else) rather than the build's. Every studio build failure therefore fell into `[ 0 != 124 ] && [ 0 != 137 ]` and ran `exit 0`. The RUN succeeded, the three-attempt retry never once fired, and whatever was in dist/ shipped. Vite copies publicDir into outDir from the `vite:prepare-out-dir` renderStart hook, before chunks are rendered, so a build killed during chunk rendering leaves dist/ holding only public/ files. That is exactly what the arm64 nmp-api:0.3.0 image carries at /static/studio: favicon.svg, sample-agents/, sample-datasets/, no index.html, no assets/. The amd64 variant of the same tag is fine, which matches the QEMU hang the existing comment describes. Capture the status with `|| status="$?"` outside the `if`, and assert dist/index.html exists before reporting success so a silently truncated bundle fails the build rather than reaching an image. Signed-off-by: mschwab <mschwab@nvidia.com>
Contributor
📝 WalkthroughWalkthroughThe Studio UI Docker build now verifies ChangesStudio asset delivery
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Contributor
Signed-off-by: mschwab <mschwab@nvidia.com>
Contributor
|
nmp-studio-ui declares platforms = [linux/amd64, linux/arm64], so buildkit ran the whole node + pnpm + vite chain twice and emulated whichever half did not match the builder. dist/ is architecture-independent JS/CSS/HTML, so the second chain bought nothing and supplied the failure mode: emulated arm64 hangs in Vite chunk rendering, which is how nmp-api:0.3.0 shipped an arm64 bundle holding only public/ files. Pin the node stages to $BUILDPLATFORM. The scratch artifacts stage is still stamped per target platform, so nmp-api consumes it unchanged. Verified by building both target platforms on an arm64 host. Before: a full linux/amd64 base chain runs under emulation and dies in `pnpm install` with a Go nil-pointer panic in orval SDK generation, failing the build. After: only native arm64 stages run, and the two exported trees are byte-identical (sha256 d6dd8e84..., 331 assets, index.html 8710 B each). Signed-off-by: mschwab <mschwab@nvidia.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.
Problem
Running
nvcr.io/nvidia/nemo-platform/nmp-apidirectly —docker run, compose, quickstart, anything that is not the Helm chart — lands on the error page at/studio:Helm deployments are unaffected because
k8s/helm/values.yamlsetsstudio.static_files_path: "/static/studio"explicitly, masking the gap.Investigating this turned up a second, independent problem: the arm64 image at that tag ships a Studio bundle with no
index.htmlat all. Three bugs in total — the assets were unreachable (1), a broken bundle could ship green (2), and the build chain that produced the broken bundle should never have been running (3).Bug 1 — the container bundle was never consulted
docker/Dockerfile.nmp-apicopies the built bundle to/static/studio, but sets no config pointing at it.StudioConfig.static_files_pathdefaults toNone, so_get_static_files_pathfalls back to the packagedstatic/dir beside thenmp.studiopackage — which is only populated by the wheel build (packages/nemo_platform/pyproject.tomlforce_include). The container builder stage runsuv syncwithoutweb/distpresent, so it stays empty. Nothing ever looked at/static/studio.Fix: add
/static/studioto the_get_static_files_pathfallback chain — configured → packaged → container → source checkout.Why not bake
NMP_STUDIO_STATIC_FILES_PATHinto the DockerfileThat was the obvious one-line fix, and it is wrong here.
ServiceConfigderives fromEnvironmentFirstSettings(packages/nemo_platform_plugin/src/nemo_platform_plugin/config.py:143-160), whosesettings_customise_sourcesreturns(env_settings, dotenv_settings, init_settings, file_secret_settings)— env ahead of init kwargs, the reverse of the pydantic-settings default. So the env var would silently override an operator'sstudio.static_files_path. Confirmed:The fallback chain fixes this image, covers future images that lay the bundle down without setting a var, and cannot shadow config.
docker/Dockerfile.nmp-apiis untouched. A regression test pins the env-over-YAML ordering so nobody re-adds the env var later.Bug 2 — the arm64 image ships a public-only
dist//static/studioin the arm64 image at that tag contains exactlyfavicon.svg,sample-agents/,sample-datasets/— byte-for-byte the contents ofweb/packages/studio/public/. Noindex.html, noassets/. 877 kB.It is arch-specific. Comparing the two manifests layer-by-layer, every layer matches within a few percent except one:
Why public-only: Vite 8 copies
publicDir→outDirfrom thevite:prepare-out-dirrenderStarthook — before chunks are rendered and written. Kill the build during chunk rendering anddist/holds public files and nothing else. The Dockerfile's own comment names that failure mode: "QEMU arm64 builds occasionally hang in Vite chunk rendering."Why it did not fail the build —
docker/base/Dockerfile.nmp-studio-ui:$?afterfiis the status of theifcompound command, not the failed build — a failing condition with noelseleaves theifitself at 0. So a SIGKILL at the 15m timeout produced 124, was captured as 0, fell through0 != 124 && 0 != 137, and ranexit 0. The RUN succeeded, the three-attempt retry never once fired, and the partialdist/shipped.Fix (backstop): capture the status with
|| status="$?"outside theif, and assertdist/index.htmlexists before reporting success. That converts a silently broken image into a failed build — necessary, but it only makes the failure loud.Bug 3 — the emulated build chain should not exist
Making the build fail loudly would have turned arm64 release builds red rather than green-and-broken. The actual fix is to remove the emulation.
nmp-studio-uideclaresplatforms = [linux/amd64, linux/arm64], so buildkit runs the entire node + pnpm + vite chain twice and emulates whichever half does not match the builder. Butdist/is architecture-independent JS/CSS/HTML — the second chain produces the same bytes and contributes nothing except the failure mode.Fix: pin the node stages to the builder's native arch.
FROM --platform=$BUILDPLATFORM ${DOCKERHUB_MIRROR}/node:${NODE_VERSION}-bookworm AS base$BUILDPLATFORMis whatever the builder host is, so this is symmetric — an amd64 runner builds amd64 natively, an arm64 runner builds arm64 natively, and neither emulates. TheFROM scratch AS artifactsstage is still stamped per target platform, sonmp-apiconsumes it unchanged. Also collapses the expensivepnpm install+ vite build from twice to once.nmp-studio-uihas nocache-to/cache-from(unlikenmp-api-docker) and its only consumer is thenmp-api-dockernamed context, so there are no cache implications.Also in scope
The error page's recovery block printed nvm /
make bootstrap-studio/nemo services restartunconditionally — meaningless inside a container, and it sent the reporting user down the wrong path. It now branches on whether a source checkout is detected.Source checkouts keep the build tips. Packaged installs get no remediation steps at all, because a packaged install ships with the bundle and there is nothing for the operator to build — a missing bundle there is a packaging defect, not a setup mistake. They get a plain error plus a link to the docs site, the only outward-facing help pointer the repo has (README badge,
web/packages/studio/src/constants/links.ts):Heading changed from "assets are not built" to "assets were not found".
Verification
Bug 1, against a real
nmp-apiimage with the patchednmp/studiopackage bind-mounted:/studio/→200 text/html, SPA index servedstudio.static_files_path→ resolves to the configured path; container fallback losesBug 2, real builds of the
nmp-studio-uitarget:✓ built in 3.49s, 12.28 MB exported,/artifactshasindex.html+assets/(331 files). Exit 0.&& rm -f dist/index.htmlspliced in:Exit-path matrix (harness mirroring the RUN body):
index.htmlindex.htmlgoneCaveat on the happy-path build above: this host is arm64, so it ran natively and did not exercise the emulated path that broke 0.3.0. It proves the guard and the happy path, not that emulated arm64 succeeds. Bug 3 is what addresses the emulated path.
Bug 3, both target platforms built on an arm64 host:
before — buildkit spawns a full second chain,
[linux/amd64 base 10/29]…[linux/amd64 base 21/29] RUN pnpm install --frozen-lockfile, which dies under emulation:A Go nil-pointer panic in orval SDK generation. The whole build exits 1. This host is arm64, so the emulated half is amd64 — the mirror image of CI, where an amd64 runner emulates arm64. Different symptom from the chunk-rendering hang, same root cause: the node toolchain does not survive QEMU.
after — only native arm64 stages execute (no
linux/amd64 base/buildat all), and the two exported trees are byte-identical:diff -rclean, 331 assets each,index.html8710 B each.Tests:
pytest services/studio/tests/unit→ 107 passed. New coverage for the container fallback, configured-path precedence, the/static/studioconstant, both error-page branches, and the env-over-YAML ordering.ruff check/ruff format/ty checkclean.Note for reviewers
The
config-reference-docspre-commit hook cannot run from a git worktree —uvfails to fetch thenooagit dependency (fatal: not a git repository) before it reaches doc generation. I skipped it and hand-editeddocs/set-up/config-reference.mdx; the line is byte-identical to what the generator emits for that field.Summary by CodeRabbit
Bug Fixes
Documentation