CI integration
Use pcpm ci for strict, reproducible installs on CI.
CI is where PCPM pays for itself. A clean-room build of a large
.NET solution that used to take minutes becomes a few seconds, because
the global store is already warm on most CI runners, and pcpm ci
is strict enough to catch “works on my machine” drift.
The shape of a PCPM CI run
- Restore:
pcpm ci— installs exactly whatpcpm.locksays, no floating, no surprises, fails fast if the lockfile is stale. - Build:
dotnet build— unchanged from a non-PCPM pipeline. - Test:
dotnet test— unchanged.
You can do steps 2 and 3 with a single dotnet test invocation; PCPM
only owns step 1.
Why pcpm ci and not pcpm install?
pcpm install is a developer’s command. It does its best to keep going
even if the lockfile is out of date — for example, if you pcpm add Foo and forget to commit pcpm.lock, pcpm install will resolve
fresh and update the lockfile.
pcpm ci is a CI command. It:
- Refuses to run if
pcpm.lockis missing. - Refuses to run if
Directory.Packages.propsandpcpm.lockdisagree. - Never writes to
pcpm.lock. It either succeeds with the exact versions in the lockfile, or it fails with a non-zero exit.
This catches a class of bugs that are otherwise silent: someone bumps
a version in CPM and forgets to update the lockfile. On a developer
machine, pcpm install would re-resolve and the new version would
slip in. On CI, pcpm ci would fail and the build would be red.
GitHub Actions
name: build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- name: Cache PCPM store
uses: actions/cache@v4
with:
path: |
~/.local/share/pcpm/store
~/.nuget/packages
key: pcpm-${{ hashFiles('**/pcpm.lock') }}
restore-keys: |
pcpm-
- name: Install PCPM
run: dotnet tool install --global pcpm
- name: Restore
run: pcpm ci
- name: Build
run: dotnet build --no-restore -c Release
- name: Test
run: dotnet test --no-build -c Release
The cache step is the part that makes this fast. The cache key is
pcpm.lock, so the store is reused as long as the lockfile is the
same. A typical warm restore takes 2–5 seconds on a 50-project
solution.
GitLab CI
build:
image: mcr.microsoft.com/dotnet/sdk:10.0
variables:
PCPM_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pcpm"
cache:
key:
files:
- pcpm.lock
paths:
- .cache/pcpm
before_script:
- dotnet tool install --global pcpm
script:
- pcpm ci
- dotnet build --no-restore -c Release
- dotnet test --no-build -c Release
Caching notes
- Cache key:
pcpm.lockis the right granularity. A change in the lockfile invalidates the cache, by design. - Cache path: This is the global store. On Linux it’s
~/.local/share/pcpm/store; on macOS,~/Library/Application Support/pcpm/store; on Windows,%LOCALAPPDATA%\pcpm\store. Usepcpm store pathto get the exact value at runtime. - Also cache
~/.nuget/packages: the hardlinked layout means this is small (a few hundred MB on large solutions), but caching it makesdotnet restoreitself a no-op. - Don’t cache across branches in PR builds: a PR that adds a dependency shouldn’t pollute the main branch’s cache. Most CI providers handle this automatically (the cache key includes the branch).
Local pre-flight
Before pushing, you can run the same command locally:
pcpm ci
If it passes locally, it will pass on CI. This is intentional — pcpm ci
is the same binary in both environments.