CI Pipeline Bugs in pnpm: PNPM Import Issues

Summary

A CI pipeline transition from npm to pnpm failed during the release stage. The job failed with ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL, stating that the command semantic-release could not be found. While the build step succeeded, the execution of the release tool within a sub-directory failed, causing the entire deployment pipeline to crash.

Root Cause

The failure stems from a misunderstanding of dependency isolation and workspace hoisting in pnpm compared to npm.

  • Scope Misalignment: The command pnpm exec looks for the binary in the local node_modules/.bin of the current working directory.
  • Directory Context: The CI script performs cd dist/my-library. This directory is a build artifact, not a managed pnpm workspace member.
  • Missing Binaries: Because dist/my-library was created via a build process (likely copying files), it does not contain its own node_modules folder or the semantic-release binary.
  • Strictness: Unlike npm, which often relies on a flattened, global-style node_modules structure that might accidentally “leak” binaries into sub-directories, pnpm uses a content-addressable store and symlinks, making it much stricter about where a binary is actually declared and installed.

Why This Happens in Real Systems

In modern monorepos or complex build pipelines, we often separate source code from distribution artifacts.

  • Build vs. Runtime: Engineers often assume that if a package is installed in the root of the project, it is available globally across all shell commands.
  • Artifact Isolation: The dist/ folder is typically ignored by version control and is not part of the pnpm-workspace.yaml. Therefore, pnpm treats dist/my-library as an isolated directory with no knowledge of the dependencies installed in the project root.
  • Shell Execution Context: Using cd <dir> && command changes the execution context of the shell, effectively “blinding” the package manager to the dependencies installed in the parent directory.

Real-World Impact

  • Deployment Blockage: Automated release cycles (CD) are halted, preventing critical bug fixes or features from reaching production.
  • Broken Versioning: If the release step fails after a build but before a tag is pushed, it can lead to desynchronized version numbers between the code and the registry.
  • CI/CD Flakiness: Teams may attempt to “fix” this by installing dependencies globally, which leads to non-deterministic builds and “it works on my machine” syndromes.

Example or Code

The problematic command in the CI YAML:

run: cd dist/my-library && pnpm exec semantic-release --branches main

The corrected approach involves running the command from the project root where the dependencies actually reside, or explicitly pointing to the binary:

# Option 1: Run from root (Recommended)
run: pnpm exec semantic-release --branches main

# Option 2: If you must be in the directory, use the root's binary path
run: cd dist/my-library && ../../node_modules/.bin/semantic-release --branches main

How Senior Engineers Fix It

A senior engineer looks beyond the immediate error and addresses the architectural flow of the CI pipeline.

  • Unified Execution Context: Instead of jumping into build artifacts, keep the execution context in the workspace root. Release tools should be run against the project root to ensure all workspace plugins and configurations are correctly loaded.
  • Dependency Declaration: Ensure that semantic-release is explicitly listed in devDependencies of the root package.json.
  • Pipeline Abstraction: Instead of raw shell commands in YAML, wrap complex release logic into a dedicated script (e.g., scripts/release.sh) located in the root. This makes testing the release logic locally much easier.
  • Idempotency and Safety: Ensure that the build step and the release step are decoupled such that a failure in one does not leave the system in a partially-deployed state.

Why Juniors Miss It

  • The “Path” Mental Model: Juniors often view the file system as a single bucket where “if it’s installed, it exists everywhere.” They don’t account for how package managers manage path resolution.
  • Tooling Over-Reliance: They focus on the error message (Command not found) rather than the environmental context (the cd command) that caused the error.
  • Ignoring Symlinks: pnpm’s heavy use of symlinks and a non-flat node_modules structure is fundamentally different from npm. Juniors accustomed to npm’s “hoisting” behavior often struggle with pnpm’s strict dependency resolution.

Leave a Comment