LPM CLI

CI/CD setup

Configure LPM CLI for GitHub Actions, GitLab CI, and other CI systems.

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

The fastest path is npm — it works everywhere:

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

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

2. Authenticate reproducibly

For private packages on lpm.dev, use a read-scoped Registry token.

Static token

- run: npm install -g @lpm-registry/cli
- run: lpm fetch
  env:
    LPM_TOKEN: ${{ secrets.LPM_TOKEN }}
- run: lpm ci --offline --strict-integrity

Create a read-scoped token and store it as LPM_TOKEN in your CI's secret store. LPM CLI reads it directly and routes @lpm.dev/* packages without an .npmrc; use lpm setup ci npmrc only when another npm-compatible client in the job needs Registry authentication. That command must resolve a bearer first and writes the literal only to an atomically replaced, owner-only project file; its JSON output is redacted.

3. Reproducible installs

lpm ci --offline --strict-integrity --no-skills --no-editor-setup --no-security-summary
FlagWhat it does in CI
--offlineNo network. Replays entirely from lpm.lock + the global store. Errors if anything is missing.
--strict-integrityRefuse to install tarball-URL deps that don't declare an inline SRI. Disables trust-on-first-use for fresh installs.
--no-skillsSkip skills auto-install. Saves time.
--no-editor-setupCompatibility flag. Package skills do not create editor integrations.
--no-security-summarySkip 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.

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

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

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:

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

5. Gating

The standard gate sequence:

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

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

For compliance gates, use lpm licenses:

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

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

PathCache keyUse
~/.lpm/storelpm-store-${{ runner.os }}-${{ hashFiles('lpm.lock') }}Package store used by lpm ci --offline and lpm install --offline.
~/.lpm/pluginslpm-plugins-${{ runner.os }}-${{ hashFiles('lpm.json') }}Managed Oxlint, Biome, and Rolldown plugin binaries used by built-in tools.
~/.lpm/cache/dlxlpm-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:

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

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

lpm cache clean dlx
lpm cache prune --apply

7. Publishing from CI

- run: lpm publish --provenance --min-score 60 --wait -y

--provenance requires OIDC and produces a Sigstore attestation. --min-score 60 blocks a low local quality score.

--wait keeps the job active until the LPM.dev Registry version becomes active under its distribution rules. The default wait limit is 20 minutes.

Some quality points remain pending until Registry analysis. Set the threshold from a representative lpm publish --check result. -y skips the prompt.

For npm-compatible publish provenance, target npm explicitly:

- run: lpm publish --npm --provenance -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. A normal npm-only publish does not run the LPM.dev quality scorer, so --min-score is not a quality gate for this command; keep the project's lint, test, type-check, and audit jobs explicit in CI.

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

  • @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 when publishing with OIDC or generated provenance 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