
# 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`](/actions). This page covers other CI systems,
containers, and offline use.
::

## Warm the jup store

From a pinned project, while connected:

```sh
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:

```sh
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:

```sh
jup cache install
pnpm fetch --frozen-lockfile
```

Then test both layers offline:

```sh
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:

```sh
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](/actions) 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:

```sh
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:

```dockerfile
FROM node:22
RUN npm install -g jup && jup enable --system
WORKDIR /app
```

For an exact pin:

```dockerfile
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:

```dockerfile
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:

```sh
jup cache install -g --cache-only pnpm@^12
```

Make it the fallback outside pinned projects by leaving off `--cache-only`:

```sh
jup cache install -g pnpm@^12
```

## Move tools to an offline machine

Create an archive on a connected, compatible machine:

```sh
jup pack -o tools.tgz pnpm@^12 yarn@^4
```

Import it without changing global fallbacks:

```sh
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

```sh
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`.
