Skip to content

pm(ci): make nub ci produce a project-local, COPY-safe node_modules#261

Merged
colinhacks merged 2 commits into
mainfrom
ci-gvs-off
Jul 1, 2026
Merged

pm(ci): make nub ci produce a project-local, COPY-safe node_modules#261
colinhacks merged 2 commits into
mainfrom
ci-gvs-off

Conversation

@colinhacks

Copy link
Copy Markdown
Contributor

Under the isolated + global-virtual-store default (#238), a nub ci node_modules has .nub/<dep> entries that are absolute symlinks into the machine-global store (~/.cache/nub/pm/virtual-store). That store does not travel with a multi-stage Docker COPY --from, so every dependency dangles in the final image (#241).

nub ci is the frozen, deploy-oriented install whose output is almost always relocated or thrown away, so the global store buys nothing there. This forces its virtual store project-local (GVS off): real .nub/<dep> dirs + relative symlinks — a self-contained tree that survives the COPY. Isolation / phantom-dep protection is preserved; only the store location moves. Plain nub install is unchanged (GVS stays the default), and an explicit user enableGlobalVirtualStore / nodeLinker still wins.

Mechanism

A new engine_session_ci threads VirtualStoreLocality::ProjectLocal into the embedder-defaults tier, pushing enableGlobalVirtualStore=false alongside the existing isolated + hoist=false layout. Only run_ci uses it. Embedder-tier, so any user source overrides it — mirroring how aube dlx defaults GVS off for scratch installs.

Verification

Linux Docker (node:22-slim, multi-stage COPY --from=build /app/node_modules):

Install global-store symlinks final image require('express')
nub ci 0 resolves
nub install 68 Cannot find module 'express'

Local ad-hoc confirms user enableGlobalVirtualStore=true and node-linker=hoisted under ci both override correctly. Adds a Multi-stage builds section to the Docker docs.

Refs #241

`nub ci` is the frozen, ephemeral, deploy-oriented install; its
node_modules is almost always relocated across a multi-stage Docker
`COPY --from` or thrown away. Under the isolated+GVS default (#238) the
`.nub/<dep>` entries are absolute symlinks into the machine-global
virtual store (`~/.cache/nub/pm/virtual-store`), which does not travel
with the COPY, leaving every dep dangling (#241).

Force the virtual store project-local for `nub ci` (GVS off) via a new
`engine_session_ci` that threads `VirtualStoreLocality::ProjectLocal`
into the embedder-defaults tier, pushing `enableGlobalVirtualStore=false`
alongside the existing isolated+hoist=false layout. Result: real
`node_modules/.nub/<dep>` dirs + relative symlinks — a self-contained,
COPY-relocatable tree that keeps the isolated layout's phantom-dep
protection. Plain `nub install` is unchanged (GVS stays the default).
An explicit user `enableGlobalVirtualStore`/`nodeLinker` still wins
(embedder-tier default, mirroring how `aube dlx` defaults GVS off).

Refs #241
The multi-stage COPY-relocatability of node_modules follows from the ci
GVS-off change: document `nub ci` as the self-contained install for the
build stage.

Refs #241
Copilot AI review requested due to automatic review settings July 1, 2026 00:58
@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nub Ready Ready Preview, Comment Jul 1, 2026 12:58am

Request Review

@pullfrog pullfrog Bot 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.

ℹ️ No critical issues — one doc gap worth a look. The code change is sound and well-tested.

Reviewed changes — makes nub ci install with a project-local virtual store (GVS off) so its frozen node_modules survives a multi-stage-Docker COPY --from (#241), while leaving plain nub install on the machine-global store.

  • Add VirtualStoreLocality enum + engine_session_ci — a new Default/ProjectLocal knob threaded through engine_session_innernub_setting_defaults; run_ci is the only caller that requests ProjectLocal, pushing enableGlobalVirtualStore=false at the embedder-defaults tier so a user enableGlobalVirtualStore/nodeLinker still wins.
  • Preserve isolation — the isolated + hoist=false layout (phantom-dep protection) is unchanged; only the store location moves.
  • Add ci_forces_project_local_store_but_keeps_isolation test — asserts ci forces GVS off + keeps isolated/hoist=false, and that a plain install never forces GVS off.
  • Docs — new "Multi-stage builds" section recommending nub ci in the build stage.

I traced the mechanism through the vendored aube: resolve_global_virtual_store_override reads the resolved enable_global_virtual_store setting as the explicit override, so an embedder default of false yields planned_gvs=false regardless of !$CI — the change does what the description claims, and set_embedder_defaults being the lowest-priority tier confirms user config wins. The new engine_session_ci mirrors the existing engine_session_transient/_quiet/_global wrapper pattern, so it's idiomatic for this file.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY . .
CMD ["nub", "run", "start"]

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.

ℹ️ The final stage runs nub but never installs it. It starts from a bare node:26-slim and only COPY --from=build /app/node_modules — but a global npm install -g @nubjs/nub lives in /usr/local (bin + lib), which is not under /app/node_modules, so nub won't be on PATH here and CMD ["nub", "run", "start"] fails with nub: command not found.

Technical details
# Final stage lacks nub on PATH

## Affected sites
- site/content/docs/docker.mdx:63-67 — final `FROM node:26-slim` stage copies only `/app/node_modules` + source, then `CMD ["nub", "run", "start"]`.

## Required outcome
- The final stage must have the `nub` binary available, or the example must not invoke it.

## Suggested approach (optional)
- Add `RUN npm install -g @nubjs/nub` to the final stage, OR base the final stage on `ghcr.io/nubjs/nub` (which bakes nub in, matching the page's other examples). The former keeps the base symmetric with the build stage; the latter is shorter and consistent with the top-of-page official-image example.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Makes nub ci produce a relocatable, project-local node_modules by forcing the isolated layout’s virtual store to be project-local (GVS off) for CI installs, so multi-stage Docker COPY --from doesn’t leave dangling links.

Changes:

  • Add a dedicated engine_session_ci that threads a VirtualStoreLocality::ProjectLocal choice into embedder defaults (disabling enableGlobalVirtualStore).
  • Switch run_ci to use the CI session and add unit coverage for the new defaulting behavior.
  • Document multi-stage Docker builds to prefer nub ci for copy-safe dependency trees.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
site/content/docs/docker.mdx Adds a “Multi-stage builds” section recommending nub ci for COPY-safe node_modules.
crates/nub-cli/src/pm_engine/mod.rs Introduces engine_session_ci, threads VirtualStoreLocality into session/default resolution, and adds tests.
crates/nub-cli/src/pm_engine/install_family.rs Updates nub ci to use engine_session_ci so the new locality behavior actually applies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +632 to +635
/// install; its `node_modules` is almost always COPY-relocated (multi-stage
/// Docker) or thrown away, and a machine-global virtual store makes that tree
/// non-relocatable — every `.nub/<dep>` becomes an absolute symlink into
/// `~/.cache/nub/pm/virtual-store` that a `COPY --from` leaves dangling (#241).
Comment on lines +2078 to +2080
// contained tree that survives a multi-stage-Docker `COPY --from`, unlike
// the machine-global store the default install engages (#241). Isolation
// (phantom-dep protection) is unchanged; only the store LOCATION moves.
Comment on lines +2648 to +2651
// `nub ci` (VirtualStoreLocality::ProjectLocal) pushes
// `enableGlobalVirtualStore=false` so the frozen node_modules is
// COPY-relocatable (#241), while keeping the isolated+hoist=false layout
// (phantom-dep protection). A plain install (Default) leaves GVS on.
Comment on lines +1162 to +1164
// `engine_session_ci` forces a project-local virtual store (GVS off) so
// ci's frozen node_modules is COPY-relocatable across multi-stage Docker
// (#241); isolation/phantom-dep protection is preserved.
Comment on lines +54 to +55
Install with `nub ci` in the build stage. It writes a self-contained `node_modules` — real files, project-local links — that survives a `COPY --from` into the final stage. A plain `nub install` shares packages through a machine-global store the final stage doesn't have, so the copied tree would point at files that aren't there.

Comment on lines +63 to +66
FROM node:26-slim
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY . .
@colinhacks colinhacks merged commit 1c1dac6 into main Jul 1, 2026
60 checks passed
@colinhacks

Copy link
Copy Markdown
Contributor Author

Shipped in v0.3.0: https://github.com/nubjs/nub/releases/tag/v0.3.0

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