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 execlooks for the binary in the localnode_modules/.binof 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-librarywas created via a build process (likely copying files), it does not contain its ownnode_modulesfolder or thesemantic-releasebinary. - Strictness: Unlike npm, which often relies on a flattened, global-style
node_modulesstructure 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 thepnpm-workspace.yaml. Therefore, pnpm treatsdist/my-libraryas an isolated directory with no knowledge of the dependencies installed in the project root. - Shell Execution Context: Using
cd <dir> && commandchanges 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-releaseis explicitly listed indevDependenciesof the rootpackage.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 (thecdcommand) that caused the error. - Ignoring Symlinks: pnpm’s heavy use of symlinks and a non-flat
node_modulesstructure is fundamentally different from npm. Juniors accustomed to npm’s “hoisting” behavior often struggle with pnpm’s strict dependency resolution.