# CI/CD (/docs/ci-cd)



snpm is built for CI: deterministic installs, a hot path that exits in milliseconds when nothing changed, parallelism that scales with available bandwidth, and SARIF audit output for security tabs.

Quick start [#quick-start]

```bash
# Install snpm
npm install -g snpm

# Reproducible install
snpm install --frozen-lockfile

# Build / test
snpm run build
snpm run test
```

In a monorepo:

```bash
snpm install --frozen-lockfile
snpm run build -r
snpm run test --filter "[origin/main]"   # only what changed
```

GitHub Actions [#github-actions]

```yaml title=".github/workflows/ci.yml"
name: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install snpm
        run: npm install -g snpm

      - name: Cache snpm store
        uses: actions/cache@v4
        with:
          path: ~/.local/share/snpm
          key: ${{ runner.os }}-snpm-${{ hashFiles('**/snpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-snpm-

      - name: Install dependencies
        env:
          SNPM_MIN_PACKAGE_AGE_DAYS: "7"
          SNPM_ALLOW_SCRIPTS: "esbuild,sharp"
        run: snpm install --frozen-lockfile

      - name: Build
        run: snpm run build

      - name: Test
        run: snpm run test
```

Audit with SARIF [#audit-with-sarif]

```yaml
      - name: Audit dependencies
        run: snpm audit --format sarif > snpm-audit.sarif
        continue-on-error: true

      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: snpm-audit.sarif
```

GitLab CI [#gitlab-ci]

```yaml title=".gitlab-ci.yml"
image: node:20

cache:
  key:
    files:
      - snpm-lock.yaml
  paths:
    - ~/.local/share/snpm

stages:
  - install
  - test

install:
  stage: install
  script:
    - npm install -g snpm
    - snpm install --frozen-lockfile

test:
  stage: test
  variables:
    SNPM_MIN_PACKAGE_AGE_DAYS: "7"
  script:
    - npm install -g snpm
    - snpm install --frozen-lockfile
    - snpm run test
```

CircleCI [#circleci]

```yaml title=".circleci/config.yml"
version: 2.1

jobs:
  build:
    docker:
      - image: cimg/node:20.0
    steps:
      - checkout
      - restore_cache:
          keys:
            - snpm-{{ checksum "snpm-lock.yaml" }}
            - snpm-
      - run: npm install -g snpm
      - run: snpm install --frozen-lockfile
      - run: snpm run build
      - run: snpm run test
      - save_cache:
          paths:
            - ~/.local/share/snpm
          key: snpm-{{ checksum "snpm-lock.yaml" }}

workflows:
  build-and-test:
    jobs:
      - build
```

Docker [#docker]

```dockerfile
FROM node:20-slim AS deps

WORKDIR /app
RUN npm install -g snpm

COPY package.json snpm-lock.yaml ./
COPY snpm-workspace.yaml* snpm-catalog.yaml* ./
COPY packages packages
COPY apps apps

RUN snpm install --production --frozen-lockfile

FROM node:20-slim
WORKDIR /app
COPY --from=deps /app /app
CMD ["node", "apps/web/dist/index.js"]
```

For monorepos you can copy only the workspace metadata first to cache the install layer, then copy source files for the build step.

Common patterns [#common-patterns]

Production install [#production-install]

```bash
snpm install --production --frozen-lockfile
```

Skips `devDependencies` and refuses to touch the lockfile.

Min package age [#min-package-age]

```bash
SNPM_MIN_PACKAGE_AGE_DAYS=7 snpm install --frozen-lockfile
```

Refuses to use versions published within the last 7 days. Combine with `--frozen-lockfile` to be sure CI never silently bumps to a fresh release.

Bumping concurrency [#bumping-concurrency]

`SNPM_REGISTRY_CONCURRENCY` defaults to `128`. Most CI runners can push it higher on fast networks; lower it on memory-constrained runners or when the registry rate-limits.

Caching the store [#caching-the-store]

| Cache                | What to cache                                                                                                                           |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Shared package store | `~/.local/share/snpm` (Linux), `~/Library/Application Support/io.snpm.snpm` (macOS) — or whatever `snpm config` reports as the data dir |
| Metadata cache       | Already inside the data dir                                                                                                             |

Key the cache on `snpm-lock.yaml` so it invalidates when dependencies change. snpm only writes new entries; cache restore is purely additive.

Workspaces [#workspaces]

```bash
# install everything once
snpm install --frozen-lockfile

# build everything
snpm run build -r

# only run tests for projects whose files changed since main
snpm run test --filter "[origin/main]"
```

`snpm publish -r --filter ./packages` publishes every project under `packages/` (handy for release pipelines).

Authenticating to a private registry [#authenticating-to-a-private-registry]

GitHub Actions example:

```yaml
- name: Install dependencies
  env:
    SNPM_CONFIG_REGISTRY: https://npm.mycompany.com
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
  run: snpm install --frozen-lockfile
```

Or commit a `.snpmrc` that interpolates env vars:

```ini title=".snpmrc"
registry=https://npm.mycompany.com/
//npm.mycompany.com/:_authToken=${NPM_TOKEN}
```

Troubleshooting [#troubleshooting]

--frozen-lockfile fails [#--frozen-lockfile-fails]

The lockfile is out of date. Run `snpm install` locally, commit the resulting `snpm-lock.yaml`, and push.

Install scripts blocked [#install-scripts-blocked]

snpm blocks dependency lifecycle scripts by default. Add the trusted package to the allow-list:

```bash
SNPM_ALLOW_SCRIPTS="esbuild,sharp" snpm install --frozen-lockfile
```

After widening the allow-list, run `snpm rebuild` to apply it to packages that already extracted.

Slow cold installs [#slow-cold-installs]

* Make sure the store cache key is correct (`hashFiles('**/snpm-lock.yaml')`).
* Bump `SNPM_REGISTRY_CONCURRENCY` if the network has headroom.
* Use `--prefer-frozen-lockfile` on retries to reuse the lockfile but re-resolve only what changed.

Authentication failures [#authentication-failures]

* Verify the auth token is exported in the job's environment.
* Confirm `snpm config` shows the expected default registry and `default auth` status.
* For scoped registries, make sure both `@scope:registry=...` and the matching `//host:_authToken=...` are set.
