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



This guide covers the moving parts of running LPM CLI in CI: pinning the binary, authenticating reproducibly, gating on quality and security signals, and producing reliable artifacts. We'll go through GitHub Actions in detail; GitLab CI follows the same shape.

## 1. Install LPM CLI in CI [#1-install-lpm-cli-in-ci]

The fastest path is npm — it works everywhere:

```yaml
- run: npm install -g @lpm-registry/cli
```

Pin the version explicitly. Don't `@latest` in CI — reproducible builds depend on a fixed toolchain.

For Linux runners that don't have npm bootstrapped, use the standalone installer:

```yaml
- run: curl -fsSL https://cli.lpm.dev/install | sh
- run: echo "$HOME/.lpm/bin" >> $GITHUB_PATH
```

## 2. Authenticate reproducibly [#2-authenticate-reproducibly]

For private packages on lpm.dev, two approaches:

### OIDC (recommended for GitHub Actions, GitLab CI) [#oidc-recommended-for-github-actions-gitlab-ci]

```yaml
permissions:
  id-token: write # required for OIDC

jobs:
  install:
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @lpm-registry/cli
      - run: lpm setup ci npmrc --oidc # writes .npmrc by exchanging OIDC token at install time
      - run: lpm ci --offline --strict-integrity
```

`lpm setup ci npmrc --oidc` exchanges the workflow's OIDC token for an LPM.dev Registry session token at runtime. Nothing committed, nothing in secret storage — the trust model is the workflow's identity.

### Static token (the universal fallback) [#static-token-the-universal-fallback]

```yaml
- run: npm install -g @lpm-registry/cli
- run: lpm setup ci npmrc # writes .npmrc with ${LPM_TOKEN} placeholder
  env:
    LPM_TOKEN: ${{ secrets.LPM_TOKEN }}
- run: lpm ci --offline --strict-integrity
  env:
    LPM_TOKEN: ${{ secrets.LPM_TOKEN }}
```

Generate the token via `lpm setup local` locally with a short lifetime:

```bash
lpm setup local -d 30        # 30-day read-only token
```

Store the printed token as `LPM_TOKEN` in your CI's secret store.

For GitLab CI, mint `LPM_OIDC_TOKEN` via the `id_tokens` block with `aud: https://lpm.dev` — `lpm setup ci gitlab` emits a ready-to-paste snippet. The legacy `LPM_GITLAB_OIDC_TOKEN` env var is accepted as a back-compat alias. See [Environment variables](/docs/reference/env-vars#auth-and-routing).

## 3. Generate the workflow with `lpm setup ci` [#3-generate-the-workflow-with-lpm-setup-ci]

```bash
lpm setup ci github-actions       # prints a starter workflow snippet + auth command
lpm setup ci gitlab               # GitLab equivalent
```

Prints a starter snippet (OIDC permissions, `lpm env pull`, a deploy step) to stdout, followed by the `lpm env oidc allow` command you need to run once to authorize the repo. No files are written — copy the snippet into `.github/workflows/<name>.yml` (or `.gitlab-ci.yml`) yourself and adapt. `--env=<mode>` controls which env mode the snippet references (default `production`).

## 4. Reproducible installs [#4-reproducible-installs]

```bash
lpm ci --offline --strict-integrity --no-skills --no-editor-setup --no-security-summary
```

| Flag                    | What it does in CI                                                                                                   |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `--offline`             | No network. Replays entirely from `lpm.lock` + the global store. Errors if anything is missing.                      |
| `--strict-integrity`    | Refuse to install tarball-URL deps that don't declare an inline SRI. Disables trust-on-first-use for fresh installs. |
| `--no-skills`           | Skip skills auto-install. Saves time.                                                                                |
| `--no-editor-setup`     | Compatibility flag. Package skills do not create editor integrations.                                                |
| `--no-security-summary` | Skip the post-install security report. Pair with `lpm audit` as a separate explicit step.                            |

If `lpm ci` fails, the lockfile is missing, stale, or cannot be replayed from the available store. Run `lpm install` locally, commit the updated `lpm.lock`, and commit `lpm.lockb` only when LPM CLI writes it.

## 5. npm firewall in CI [#5-npm-firewall-in-ci]

The npm firewall is a network verdict check, so the command that runs it cannot be `--offline`. Use `LPM_NPM_FIREWALL=monitor` or `LPM_NPM_FIREWALL=enforce` for a per-job policy, or run `lpm config firewall --set enforce` on runners where the home directory is intentionally persistent.

### Static token [#static-token]

```yaml
- run: npm install -g @lpm-registry/cli
- run: LPM_NPM_FIREWALL=enforce lpm ci --strict-integrity
  env:
    LPM_TOKEN: ${{ secrets.LPM_TOKEN }}
```

`LPM_TOKEN` is used for the firewall verdict API and any lpm.dev package auth in the same run.

### OIDC [#oidc]

On GitHub Actions, the same `permissions: id-token: write` setting used by `lpm setup ci npmrc --oidc` is enough for firewall checks:

```yaml
permissions:
  id-token: write
  contents: read

jobs:
  install:
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @lpm-registry/cli
      - run: LPM_NPM_FIREWALL=enforce lpm ci --strict-integrity
```

When active LPM Firewall checks need auth and no `LPM_TOKEN` or stored LPM.dev Registry auth is already available, LPM CLI exchanges the CI OIDC token for a short-lived LPM.dev Registry token. If `LPM_TOKEN` or stored auth is available, there is no OIDC exchange.

On GitLab CI, mint `LPM_OIDC_TOKEN` with audience `https://lpm.dev`:

```yaml
install:
  id_tokens:
    LPM_OIDC_TOKEN:
      aud: https://lpm.dev
  script:
    - npm install -g @lpm-registry/cli
    - LPM_NPM_FIREWALL=enforce lpm ci --strict-integrity
```

If the workflow deliberately separates an online store warm from an offline replay, put the firewall on the online step and keep the replay offline:

```yaml
- run: LPM_NPM_FIREWALL=enforce lpm fetch
- run: lpm ci --offline --strict-integrity
```

## 6. Gating [#6-gating]

The standard gate sequence:

```yaml
- run: lpm ci --offline --strict-integrity
- run: lpm trust diff --assert-none
- run: lpm audit --fail-on vuln # fail on vulnerabilities (not behavior signals)
- run: lpm licenses --fail-on copyleft --deny GPL-3.0 # optional license policy gate
- run: lpm test # forwards vitest/jest exit code
- run: lpm lint # forwards oxlint exit code
- run: lpm fmt --check # fails if anything is unformatted
- run: lpm check # tsc --noEmit
```

Each step exits non-zero on failure — no special handling needed. See [Exit codes](/docs/reference/exit-codes). `lpm trust diff` is informational by default; add `--assert-none` to gate on `trustedDependencies` drift. Use `lpm trust diff --assert-none --json` if you also want the structured diff envelope in CI logs.

For finer-grained security gates, use [`lpm query`](/docs/packages/query):

```yaml
- run: lpm query ":vulnerable:not(:built)" --assert-none
- run: lpm query ":eval:scripts" --assert-none
```

For compliance gates, use [`lpm licenses`](/docs/packages/licenses):

```yaml
- run: lpm licenses --fail-on copyleft,missing
- run: lpm licenses --deny GPL-3.0 --deny AGPL-3.0
```

## 7. Caching `~/.lpm/` [#7-caching-lpm]

Cache the content LPM CLI can safely reuse. Do **not** cache `node_modules/`; it is a generated layout that LPM CLI recreates from the lockfile and store.

| Path               | Cache key                                                   | Use                                                                                                |
| ------------------ | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `~/.lpm/store`     | `lpm-store-${{ runner.os }}-${{ hashFiles('lpm.lock') }}`   | Package store used by `lpm ci --offline` and `lpm install --offline`.                              |
| `~/.lpm/plugins`   | `lpm-plugins-${{ runner.os }}-${{ hashFiles('lpm.json') }}` | Managed Oxlint, Biome, and Rolldown plugin binaries used by built-in tools.                        |
| `~/.lpm/cache/dlx` | `lpm-dlx-${{ runner.os }}-${{ hashFiles('lpm.lock') }}`     | Optional. Speeds up repeated `lpm dlx` / `lpx` calls; entries still expire after their normal TTL. |

The store is the important cache:

```yaml
- name: Cache LPM CLI store
  uses: actions/cache@v4
  with:
    path: ~/.lpm/store
    key: lpm-store-${{ runner.os }}-${{ hashFiles('lpm.lock') }}
    restore-keys: lpm-store-${{ runner.os }}-
```

If your workflow runs managed tools, cache plugins separately so tool downloads do not share the dependency-store key:

```yaml
- name: Cache LPM CLI plugins
  uses: actions/cache@v4
  with:
    path: ~/.lpm/plugins
    key: lpm-plugins-${{ runner.os }}-${{ hashFiles('lpm.json') }}
    restore-keys: lpm-plugins-${{ runner.os }}-
```

`lpm install --offline` is fast in benchmarks when the store + lockfile match. On the VitePress docs fixture, up-to-date checks land around 14 ms and warm reinstalls around 387 ms. Caching `~/.lpm/store` turns a cold CI install into a warm one.

The cache key is the lockfile hash. Typical CI runners are short-lived enough that orphan cleanup isn't needed; if you do persist the store across runs and want to bound its size, run `lpm cache prune --apply` periodically.

For persistent self-hosted runners, also clean short-lived caches on your own schedule:

```bash
lpm cache clean dlx
lpm cache prune --apply
```

## 8. Publishing from CI [#8-publishing-from-ci]

```yaml
- run: lpm setup ci npmrc --oidc
- run: lpm publish --provenance --min-score 80 -y
```

`--provenance` requires OIDC and produces a Sigstore attestation. `--min-score 80` blocks publishing if quality drops. `-y` skips the interactive prompt.

For npm-compatible publish provenance, target npm explicitly:

```yaml
- run: lpm publish --npm --provenance --min-score 80 -y
```

Generated npm provenance requires public access. LPM CLI's npm default is public; if your config sets restricted access, change it to public or skip generated provenance.

For multi-registry publishing in one job, configure `lpm.json > publish.registries` and pass nothing — `lpm publish` walks every configured target. Custom npm-compatible registries need an exact registry-scoped token from `lpm login --login-registry <URL> --token <T>`; `NPM_TOKEN` only authenticates `https://registry.npmjs.org`.

## Common pitfalls [#common-pitfalls]

* **`@latest` in CI.** Don't pin the binary to floating tags. Reproducible builds need a fixed toolchain.
* **`lpm install` without `--offline` in CI.** Lets lockfile drift slip through unnoticed.
* **Combining active npm firewall with `--offline`.** Firewall verdicts need network access. Run the firewall-protected install online, or firewall-gate an online `lpm fetch` before the offline replay.
* **Forgetting `permissions: id-token: write`** for OIDC in GitHub Actions.
* **Caching `node_modules/` instead of `~/.lpm/store/`.** The LPM CLI store is the right cache key — `node_modules` is a layout, the store is content.
* **Skipping `lpm audit` to "save time".** It's a few hundred ms; the time-not-paid is paid back on the next CVE.

## See also [#see-also]

* [`lpm setup ci npmrc`](/docs/infra/setup) — generate `.npmrc` for CI
* [`lpm setup ci <platform>`](/docs/infra/setup) — generate OIDC workflow snippets
* [`lpm ci`](/docs/packages/install#frozen-lockfile-and-ci) — frozen lockfile install
* [`lpm env print --ci`](/docs/dev/env#ci-output) — CI-native env output
* [`lpm audit`](/docs/packages/audit) — security gating
* [Exit codes](/docs/reference/exit-codes) — what each non-zero code means
* [Environment variables](/docs/reference/env-vars) — `LPM_TOKEN`, `LPM_OIDC_TOKEN`
