CI and offline use
Cache the tool and project dependencies as two separate steps.
jup stores package-manager programs. npm, pnpm, Yarn, and other tools maintain their own dependency stores. Preparing one does not prepare the other.
Tip
On GitHub, use setup-jup. This page covers other CI systems,
containers, and offline use.
#Warm the jup store
From a pinned project, while connected:
jup cache install
jup cache list --jsonjup cache install resolves and installs the project's selected tool. It does not
change the project pin, the committed jup.lock, or the global fallback.
For a range, create and commit the decision before CI:
jup use --lock pnpm@^12
git add package.json jup.lockThen jup cache install uses the recorded version.
#Prepare dependencies too
For pnpm, a simple connected preparation step is:
jup cache install
pnpm fetch --frozen-lockfileThen test both layers offline:
JUP_ENABLE_NETWORK=0 pnpm install --offline --frozen-lockfileJUP_ENABLE_NETWORK=0 blocks only jup's own metadata and artifact requests. The
package manager still needs its own offline flag and prepared dependency cache.
Use the equivalent commands for your manager.
To cache that second store between jobs, ask the manager where it is instead of hardcoding a command per tool:
jup cache install
dir="$(jup info --store-path)"It prints one absolute path, or nothing when the tool has no such command or is
not installed yet, and always exits 0 — so an optional cache step cannot fail
the job. Warm the store first: it runs the installed manager and never downloads
one.
#Cache keys and trust
The GitHub action handles this for you. Use the steps below in other CI jobs.
You may save JUP_HOME between jobs. Include the operating system and
architecture in the cache key. Include the Linux C library when your matrix uses
both glibc and musl.
For the project half of the key, ask jup which files decide what a run installs instead of listing them by hand:
jup info --json | jq -r '.inputs[].path'Each entry carries its kind (manifest, lockfile, version-file,
env-file), the tool it speaks for, whether it is present, and whether it is
the one of its kind discovery selected. Paths are absolute.
Feed the whole list to your hasher, absent paths included: a hasher that skips a path it cannot find costs nothing for one, and committing a file that was not there has to change the key. Three things this gets right that a hardcoded list does not:
- The manifest that speaks may not be the one in the working directory. Discovery climbs, so in a workspace the pin usually lives at the root.
- The version file's name is jup's to know, not yours to repeat. It comes from the built-in table, so a list built this way cannot drift from it.
- The env file is part of the answer, under either supported name.
The memo under node_modules is not listed. It is derived state, restored with
node_modules or not at all.
Warning
JUP_HOME contains executable code trusted on a cache hit. Restore it only from
trusted jobs. Do not let an untrusted fork or uploaded artifact replace a cache
used by a trusted release job.
Keep exact pins or committed range resolutions in the repository. A range
without jup.lock can resolve differently in separate jobs. A tag such as
latest is intentionally movable.
JUP_FROZEN_LOCKFILE=1 protects commands that could change jup.lock. Ordinary
proxy runs and jup cache install do not write that committed file.
#Container example
Install jup and system shims in one image layer:
FROM node:22
RUN npm install -g jup && jup enable --system
WORKDIR /appFor an exact pin:
COPY package.json pnpm-lock.yaml ./
RUN jup cache install && pnpm fetch --frozen-lockfile
COPY . .
ENV JUP_ENABLE_NETWORK=0
RUN pnpm install --offline --frozen-lockfileFor a range, copy the committed jup lockfile into the cacheable layer too:
COPY package.json jup.lock pnpm-lock.yaml ./
RUN jup cache install && pnpm fetch --frozen-lockfilejup enable --system uses the machine-wide bin directory and fails instead of
quietly choosing a user path. Add --exclude npm if the image's npm command must
remain untouched.
#Cache a named release
Install a tool without changing a project or global fallback:
jup cache install -g --cache-only pnpm@^12Make it the fallback outside pinned projects by leaving off --cache-only:
jup cache install -g pnpm@^12#Move tools to an offline machine
Create an archive on a connected, compatible machine:
jup pack -o tools.tgz pnpm@^12 yarn@^4Import it without changing global fallbacks:
JUP_ENABLE_NETWORK=0 jup cache install -g --cache-only tools.tgzpack copies complete store entries. Import validates the archive layout and
uses jup's safe extractor. It is not a substitute for authenticating who sent
the archive. Transfer it over a trusted channel or verify a separately published
signature/checksum first.
For host-specific native tools, create the archive on a machine compatible with the target OS, architecture, and Linux C library.
#Clean the store
jup cache clean
jup cache clear # alias
jup cache clean --allThe ordinary form removes installed tool versions but keeps global default
records. --all also removes those defaults. Signing-key state and jup's
self-installed copy stay outside this cleanup.
If a jup shim depends on a runtime inside the removable store, the ordinary
clean keeps that runtime. --all warns before removing it because the shims will
need to be repinned with jup enable.