The integration of pnpm into continuous integration and continuous deployment (CI/CD) pipelines, specifically within the GitHub Actions ecosystem, represents a critical shift toward more efficient dependency management. By leveraging a content-addressable store, pnpm fundamentally alters how packages are handled compared to traditional npm or yarn installations. In the context of GitHub Actions, the primary mechanism for this integration is the pnpm/action-setup action, which provides a standardized method for installing the pnpm CLI, managing specific versions, and optimizing the build lifecycle through sophisticated caching strategies.
The necessity of a dedicated setup action arises from the fact that GitHub-hosted runners typically come with npm pre-installed, but not pnpm. Manually installing pnpm via npm install -g pnpm in every job is inefficient and lacks version pinning, which can lead to non-deterministic builds. The pnpm/action-setup action solves this by ensuring the exact version of the package manager is present before the installation phase begins, allowing developers to maintain parity between local development environments and the remote CI runner.
High-Level Configuration of pnpm/action-setup
The core of implementing pnpm in a GitHub Actions workflow is the utilization of the pnpm/action-setup action. This action is designed to prepare the environment by installing the pnpm CLI and optionally triggering the initial dependency installation.
The basic implementation follows a specific sequence within the .github/workflows/NAME.yml file. A standard deployment requires the checkout of the repository, the setup of the pnpm CLI, and the configuration of the Node.js environment.
yaml
name: pnpm Example Workflow
on:
push:
jobs:
build:
runs-on: ubuntu-24.04
strategy:
matrix:
node-version: [24]
steps:
- uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5
with:
version: 11
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install
In this configuration, the version: 11 parameter ensures that the environment uses a specific major version of pnpm. If this parameter is omitted, the action can automatically detect the required version if the packageManager field is correctly defined within the project's package.json file. This creates a seamless link between the project metadata and the CI infrastructure, reducing the risk of version mismatch errors.
Advanced Caching Strategies and Store Management
One of the most significant challenges in CI environments is the time spent downloading dependencies. While actions/setup-node provides a basic cache: "pnpm" option, some users have reported that the standard template provided in the documentation does not always function as expected. To achieve maximum efficiency and reliability, a manual caching approach using the pnpm store directory is recommended.
The failure of standard caching often stems from the dynamic nature of the pnpm store path. To resolve this, developers must explicitly retrieve the store path and pass it to the actions/cache action.
The following sequence provides a robust implementation for caching:
yaml
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v4
- name: 🤌 Setup pnpm
uses: 'pnpm/action-setup@v4'
with:
version: 9.0.4
run_install: false
- name: ⎔ Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: ⎔ Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: ⎔ Setup node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: 📥 Download deps
run: pnpm install --frozen-lockfile
The logic applied here involves several critical layers:
- The
run_install: falseflag prevents the setup action from running an immediate installation, allowing the cache to be restored first. - The command
pnpm store path --silentretrieves the absolute path to the pnpm content-addressable store on the runner. - This path is written to the
$GITHUB_ENVfile, making it available as a variable for subsequent steps. - The
actions/cacheaction uses a composite key based on the runner's operating system and a hash of thepnpm-lock.yamlfile. This ensures that if the lockfile changes, a new cache is generated, while remaining consistent across unchanged builds. - The use of
--frozen-lockfileduring thepnpm installphase is mandatory for CI. It prevents the package manager from updating the lockfile, ensuring that the exact versions specified during development are the ones installed in the pipeline.
Specialized Implementations for Expo and React Native
Integrating pnpm with Expo in GitHub Actions presents a unique set of challenges because Expo's remote build environments often default to npm. This leads to a conflict where the project is configured for pnpm, but the Expo build process attempts to run npm install, triggering errors from the only-allow package which is designed to enforce the use of pnpm.
To force Expo to recognize and utilize pnpm, a multi-pronged configuration approach is required.
The first method involves the .npmrc file. Adding a specific configuration to the project root forces the environment to acknowledge pnpm.
- In the project root, create or edit the
.npmrcfile and add the lineuse-pnpm=true. - In the GitHub Actions workflow, explicitly ensure this setting is present before the Expo steps run:
yaml
- run: echo "use-pnpm=true" >> .npmrc
The second method utilizes the eas.json configuration, which governs the EAS (Expo Application Services) build process. By injecting an environment variable into the build profile, the Expo remote environment can be instructed to switch package managers.
Within the eas.json file, the EXPO_USE_PNPM variable must be set to 1:
json
{
"build": {
"preview": {
"android": {
"buildType": "apk"
},
"ios": {
"simulator": true
},
"env": {
"EXPO_USE_PNPM": "1"
}
}
}
}
Furthermore, this environment variable must be mirrored in the GitHub Actions workflow file to ensure consistency across the local runner and the remote build server:
yaml
env:
EXPO_USE_PNPM: 1
This dual-layer configuration (both in eas.json and the workflow env block) effectively overrides the default npm behavior and allows the Expo build process to successfully leverage the pnpm installation.
Customizable Installation and Dependency Arguments
The pnpm/action-setup action offers flexibility beyond simple installation. It allows for complex installation arguments and the management of global packages, which is essential for tools like TypeScript, Prettier, or Gulp that may need to be available globally on the runner.
For projects requiring a recursive installation across a monorepo or specific flags for peer dependency handling, the run_install parameter can be expanded into an object.
Example of a detailed installation configuration:
yaml
- uses: pnpm/action-setup@v4
with:
version: 8
run_install: |
- recursive: true
args: [--frozen-lockfile, --strict-peer-dependencies]
- args: [--global, gulp, prettier, typescript]
In this configuration:
- The recursive: true flag tells pnpm to install dependencies across all workspace members in a monorepo.
- The args array allows for the passing of specific CLI flags. --frozen-lockfile ensures build reproducibility.
- --strict-peer-dependencies forces the build to fail if peer dependencies are not met, preventing runtime crashes in production.
- A separate args block is used to install global utilities, ensuring the environment has the necessary tooling for linting and compilation.
Comparative Analysis of pnpm Integration Across CI Platforms
While GitHub Actions is a primary focus, pnpm's integration patterns are consistent across other major CI providers. Understanding these patterns helps in migrating workflows or maintaining hybrid CI setups.
CircleCI Configuration
On CircleCI, pnpm is managed through the .circleci/config.yml file. Unlike GitHub Actions, which uses a dedicated action, CircleCI requires a manual setup using Corepack.
yaml
version: 2.1
jobs:
build:
docker:
- image: node:18
resource_class: large
parallelism: 10
steps:
- checkout
- restore_cache:
name: Restore pnpm Package Cache
keys:
- pnpm-packages-{{ checksum "pnpm-lock.yaml" }}
- run:
name: Install pnpm package manager
command: |
npm install --global corepack@latest
corepack enable
corepack prepare pnpm@latest-11 --activate
pnpm config set store-dir .pnpm-store
- run:
name: Install Dependencies
command: |
pnpm install
- save_cache:
name: Save pnpm Package Cache
key: pnpm-packages-{{ checksum "pnpm-lock.yaml" }}
paths:
- .pnpm-store
The critical difference here is the use of Corepack, a Node.js tool that manages package manager versions. By running corepack enable and corepack prepare, the environment ensures that the specific version of pnpm is activated.
GitLab CI Configuration
GitLab CI utilizes the .gitlab-ci.yml file and relies on a before_script block to prepare the environment.
```yaml
stages:
- build
build:
stage: build
image: node:24.14.1
before_script:
- npm install --global corepack@latest
- corepack enable
- corepack prepare pnpm@latest-11 --activate
- pnpm config set store-dir .pnpm-store
script:
- pnpm install
cache:
key:
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
```
In GitLab, the cache is managed at the job level, where the pnpm-lock.yaml file serves as the key to determine when the cache should be invalidated.
Jenkins Pipeline
Jenkins utilizes a Groovy-based DSL for its pipeline definitions. The pnpm setup is handled within a sh block inside a Docker agent.
groovy
pipeline {
agent {
docker {
image 'node:lts-bullseye-slim'
args '-p 3000:3000'
}
}
stages {
stage('Build') {
steps {
sh 'npm install --global corepack@latest'
sh 'corepack enable'
sh 'corepack prepare pnpm@latest-11 --activate'
sh 'pnpm install'
}
}
}
}
Semaphore CI
Semaphore uses a YAML configuration in .semaphore/ lsemaphore.yml and handles caching through a dedicated cache restore and cache store mechanism.
yaml
version: v1.0
name: Semaphore CI pnpm example
agent:
machine:
type: e1-standard-2
os_image: ubuntu2404
blocks:
- name: Install dependencies
task:
jobs:
- name: pnpm install
commands:
- npm install --global corepack@latest
- corepack enable
- corepack prepare pnpm@latest-11 --activate
- checkout
- cache restore node-$(checksum pnpm-lock.yaml)
- pnpm install
- cache store node-$(checksum pnpm-lock.yaml) $(pnpm store path)
Technical Summary of pnpm Setup Components
The following table provides a structured comparison of the components involved in the pnpm setup process across different environments.
| Component | GitHub Actions | CircleCI | GitLab CI | Jenkins |
|---|---|---|---|---|
| Primary Setup Method | pnpm/action-setup |
Corepack via npm |
Corepack via before_script |
Shell commands in Pipeline |
| Cache Mechanism | actions/cache |
restore_cache/save_cache |
cache: paths |
Manual/Plugin based |
| Versioning | version input |
corepack prepare |
corepack prepare |
corepack prepare |
| Store Path Handling | Dynamic via pnpm store path |
Manual .pnpm-store config |
Manual .pnpm-store config |
Default pnpm path |
| Lockfile Enforcement | --frozen-lockfile |
pnpm install |
pnpm install |
pnpm install |
Conclusion
The transition to pnpm within GitHub Actions, while offering significant performance gains, requires a deliberate approach to configuration, particularly regarding the installation of the CLI and the management of the content-addressable store. The pnpm/action-setup action serves as the foundational block, but the true optimization lies in the manual orchestration of the store path and the use of actions/cache to ensure that the dependency layer does not become a bottleneck in the CI pipeline.
Furthermore, the integration with specialized frameworks like Expo highlights the necessity of overriding default environment behaviors using .npmrc and specific environment variables such as EXPO_USE_PNPM. By combining the pnpm/action-setup action with precise cache keys and explicit environment configurations, developers can achieve a deterministic, fast, and scalable build process that leverages the full potential of the pnpm ecosystem. The use of Corepack across other platforms like CircleCI and GitLab CI underscores a broader industry trend toward standardized package manager versioning, moving away from the unpredictability of global npm installations.