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/cliPin 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_PATH2. Authenticate reproducibly
For private packages on lpm.dev, two approaches:
OIDC (recommended for GitHub Actions, GitLab CI)
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-integritylpm 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)
- 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:
lpm setup local -d 30 # 30-day read-only tokenStore 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.
3. Generate the workflow with lpm setup ci
lpm setup ci github-actions # prints a starter workflow snippet + auth command
lpm setup ci gitlab # GitLab equivalentPrints 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
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
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
- 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
On GitHub Actions, the same permissions: id-token: write setting used by lpm setup ci npmrc --oidc is enough for firewall checks:
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-integrityWhen 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:
install:
id_tokens:
LPM_OIDC_TOKEN:
aud: https://lpm.dev
script:
- npm install -g @lpm-registry/cli
- LPM_NPM_FIREWALL=enforce lpm ci --strict-integrityIf 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-integrity6. 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 --noEmitEach 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-noneFor compliance gates, use lpm licenses:
- run: lpm licenses --fail-on copyleft,missing
- run: lpm licenses --deny GPL-3.0 --deny AGPL-3.07. 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:
- 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 --apply8. Publishing from CI
- 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:
- run: lpm publish --npm --provenance --min-score 80 -yGenerated 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
@latestin CI. Don't pin the binary to floating tags. Reproducible builds need a fixed toolchain.lpm installwithout--offlinein 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 onlinelpm fetchbefore the offline replay. - Forgetting
permissions: id-token: writefor OIDC in GitHub Actions. - Caching
node_modules/instead of~/.lpm/store/. The LPM CLI store is the right cache key —node_modulesis a layout, the store is content. - Skipping
lpm auditto "save time". It's a few hundred ms; the time-not-paid is paid back on the next CVE.
See also
lpm setup ci npmrc— generate.npmrcfor CIlpm setup ci <platform>— generate OIDC workflow snippetslpm ci— frozen lockfile installlpm env print --ci— CI-native env outputlpm audit— security gating- Exit codes — what each non-zero code means
- Environment variables —
LPM_TOKEN,LPM_OIDC_TOKEN