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 --json

jup 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.lock

Then jup cache install uses the recorded version.

#Prepare dependencies too

For pnpm, a simple connected preparation step is:

jup cache install
pnpm fetch --frozen-lockfile

Then test both layers offline:

JUP_ENABLE_NETWORK=0 pnpm install --offline --frozen-lockfile

JUP_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 /app

For 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-lockfile

For 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-lockfile

jup 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@^12

Make 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@^4

Import it without changing global fallbacks:

JUP_ENABLE_NETWORK=0 jup cache install -g --cache-only tools.tgz

pack 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 --all

The 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.

jup  Pin and run the right package manager or runtime for every project.