Updated pnpm to 11.4.0#28197
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR upgrades pnpm from version 10.33.0 to 11.4.0 and restructures dependency management configuration. The package manager version is bumped in package.json and related configuration is migrated into pnpm-workspace.yaml, which now includes supply-chain hardening policies (minimumReleaseAge, blockExoticSubdeps, allowBuilds allowlist), extensive dependency overrides with version constraints, and packageExtensions to augment specific package dependencies. GitHub Actions workflows across CI and release jobs are updated to use pnpm/action-setup v6.0.8 instead of v4. The .npmrc file had two configuration entries removed. Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
5939993 to
8939d41
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/scripts/enforce-package-manager.js:
- Around line 7-14: The comment overstates certainty about pnpm-specific env
vars; update the explanatory comment above the fallback that checks
Object.keys(process.env).some(key => key.startsWith('pnpm_config_')) to use
softer wording—e.g., "pnpm-driven lifecycles typically set `pnpm_config_*`, so
we use this as a heuristic"—and remove or generalize the specific pnpm v10/v11
GitHub Actions shim claim so the note accurately reflects it as a heuristic
rather than an absolute guarantee.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b162616f-5e44-4201-82f5-2e958a82530e
📒 Files selected for processing (1)
.github/scripts/enforce-package-manager.js
| // Fallback for environments where pnpm 11 doesn't propagate | ||
| // npm_config_user_agent to lifecycle scripts — observed on GitHub Actions | ||
| // where the runner's pre-installed pnpm v10 shim layer strips the var even | ||
| // when pnpm 11 is the active binary. The `pnpm_config_*` env-var prefix is | ||
| // exclusive to pnpm and reliably set when pnpm 11 spawns a child process. | ||
| if (Object.keys(process.env).some(key => key.startsWith('pnpm_config_'))) { | ||
| process.exit(0); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify that pnpm sets pnpm_config_* environment variables in lifecycle scripts
# Create a minimal test package.json with a script that prints pnpm_config_* vars
mkdir -p /tmp/pnpm-env-test
cd /tmp/pnpm-env-test
cat > package.json << 'EOF'
{
"name": "pnpm-env-test",
"version": "1.0.0",
"scripts": {
"test": "node -e \"Object.keys(process.env).filter(k => k.startsWith('pnpm_config_')).forEach(k => console.log(k + '=' + process.env[k]))\""
}
}
EOF
echo "=== Testing pnpm ==="
pnpm run test 2>/dev/null || echo "No pnpm_config_* variables found or pnpm not available"
# Clean up
cd - > /dev/null
rm -rf /tmp/pnpm-env-testRepository: TryGhost/Ghost
Length of output: 173
🌐 Web query:
Does pnpm set environment variables with the pnpm_config_ prefix during lifecycle scripts?
💡 Result:
No, as of pnpm v11, pnpm does not set environment variables with the pnpm_config_ prefix during lifecycle scripts [1][2]. Historically, pnpm would populate npm_config_* environment variables with pnpm configuration values during lifecycle script execution [2]. However, this behavior was changed to reduce noise and avoid warnings when lifecycle scripts invoke npm internally [2]. Since pnpm v11, only well-known npm_* environment variables are set (such as npm_lifecycle_event, npm_package_name, and npm_package_version), aligning pnpm's behavior with Yarn [1][2].
Citations:
- 1: https://pnpm.io/scripts
- 2: fix: stop setting npm_config_ env vars during lifecycle scripts pnpm/pnpm#11116
🏁 Script executed:
#!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
cleanup() { rm -rf "$tmpdir"; }
trap cleanup EXIT
cd "$tmpdir"
cat > package.json <<'EOF'
{
"name": "env-test",
"version": "1.0.0",
"scripts": {
"print": "node -e \"const p=Object.keys(process.env).filter(k=>k.startsWith('pnpm_config_')).sort(); console.log('pnpm_config_keys=' + (p.length?p.join(','):'')); p.forEach(k=>console.log(k+'='+process.env[k])); const n=Object.keys(process.env).filter(k=>k.startsWith('npm_config_')).sort(); console.log('npm_config_keys=' + (n.length?n.join(','):''));\""
}
}
EOF
echo "=== pnpm ==="
if command -v pnpm >/dev/null 2>&1; then
pnpm -v
pnpm run print || true
else
echo "pnpm not found"
fi
echo "=== npm ==="
if command -v npm >/dev/null 2>&1; then
npm -v
npm run print || true
else
echo "npm not found"
fi
echo "=== yarn ==="
if command -v yarn >/dev/null 2>&1; then
yarn -v
yarn run print || true
else
echo "yarn not found"
fi
echo "=== bun ==="
if command -v bun >/dev/null 2>&1; then
bun -v
bun run print || true
else
echo "bun not found"
fiRepository: TryGhost/Ghost
Length of output: 2177
Soften the “exclusive/reliably set” wording for the pnpm_config_* fallback
- In lifecycle scripts, pnpm provides
pnpm_config_*(e.g.,pnpm_config_verify_deps_before_run), while npm and yarn did not set anypnpm_config_*keys—so the fallback won’t be bypassed by npm/yarn. - The explanatory comment’s “exclusive to pnpm” and the specific GitHub Actions “pnpm v10 shim strips … when pnpm 11 is active” scenario are stronger than the evidence shown here; consider rephrasing to a heuristic (pnpm-driven lifecycles typically set
pnpm_config_*) or removing the v10/11 shim specifics.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/enforce-package-manager.js around lines 7 - 14, The comment
overstates certainty about pnpm-specific env vars; update the explanatory
comment above the fallback that checks Object.keys(process.env).some(key =>
key.startsWith('pnpm_config_')) to use softer wording—e.g., "pnpm-driven
lifecycles typically set `pnpm_config_*`, so we use this as a heuristic"—and
remove or generalize the specific pnpm v10/v11 GitHub Actions shim claim so the
note accurately reflects it as a heuristic rather than an absolute guarantee.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #28197 +/- ##
=======================================
Coverage 73.88% 73.88%
=======================================
Files 1531 1531
Lines 129865 129865
Branches 15582 15585 +3
=======================================
+ Hits 95954 95956 +2
+ Misses 32949 32946 -3
- Partials 962 963 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
- pnpm 11 stops reading the `pnpm` field from package.json, so `overrides`, `packageExtensions`, and `onlyBuiltDependencies` move to pnpm-workspace.yaml. `onlyBuiltDependencies` is renamed to `allowBuilds` (map form). - Declared `minimumReleaseAge: 1440` (1 day, pnpm 11 default) and `blockExoticSubdeps: true` explicitly in pnpm-workspace.yaml. The 1-day floor matches what Renovate currently enforces in practice for regular updates (renovate.json5 only applies its 3-day soak inside `vulnerabilityAlerts`); aligning both to 3 days is tracked as a follow-up. - Added an override redirecting `perf-primitives` to the npm-published `^0.0.6`. liquid-wormhole@3.0.1 pins it via a personal-fork git URL, which blockExoticSubdeps (correctly) refuses; the override resolves to the upstream-maintained package at the same version. - `.npmrc` deleted: its only entries were pnpm 11 defaults that are no longer honored in .npmrc anyway. - Lockfile regenerated against the new policies under pnpm 11's v11 store. CI's pnpm cache and the Docker `pnpm-store` mount will cold-fill the v11 store on the first run after merge, then settle.
v4 predates pnpm 11; v6 (April) was the first release with pnpm 11 support. Under v4, pnpm 11 wasn't propagating `npm_config_user_agent` to lifecycle scripts in CI, which broke our preinstall guard (`.github/scripts/enforce-package-manager.js`) until the action was bumped. v6.0.0 also moved the action to Node 24 internally; this only affects the action's own runtime, not the Node version used for `pnpm install` (still controlled by actions/setup-node + NODE_VERSION).
The GitHub Actions Ubuntu runner pre-installs pnpm v10 at /home/runner/setup-pnpm/node_modules/.bin/. When pnpm/action-setup v6 installs pnpm 11 over it, the v10-style shim layer ends up stripping `npm_config_user_agent` from child processes — so our preinstall guard sees an empty user-agent string and exits 1 with "Detected package manager: unknown", even though pnpm 11 is in fact the active binary. Added a fallback that checks for any `pnpm_config_*` env var, a prefix exclusive to pnpm and reliably set when pnpm 11 spawns child processes regardless of how it was invoked. Original user-agent check still runs first, so pnpm 10 and older behavior is unchanged. This is a defence-in-depth pair with the pnpm/action-setup bump — the action bump is the correct action for our pnpm version; the script fallback covers the runner-layout case neither side fully resolves.
The pnpm 11 upgrade in #28197 deleted the root .npmrc, but two Dockerfiles still list it in their COPY directives: - Dockerfile.production (production image builds) - docker/ghost-dev/Dockerfile (devcontainer + local dev) Docker COPY fails when an explicitly-named source file doesn't exist, breaking the devcontainer image build on main and any subsequent prod build. I scanned both files for `.npmrc` references during the pnpm 11 PR but failed to update the COPY lines when the file itself was removed. Also re-applies the comment softening from the PR review on .github/scripts/enforce-package-manager.js — that change was reviewed and approved on #28197 but never made it into a commit before the rebase autostash on my end.
The pnpm 11 upgrade in #28197 deleted the root .npmrc, but several release-pipeline pieces still referenced it: 1. Dockerfile.production and docker/ghost-dev/Dockerfile listed .npmrc in their COPY directives — Docker fails when a named source is missing, breaking the devcontainer image build on main. 2. ghost/core/scripts/pack.js read root .npmrc to build the published Ghost tarball — would throw on the first release after merge. Also surfaced two latent pack.js issues in the process: - pnpm 11 strict-validates `catalog:` refs in workspace package.jsons. `pnpm pack` on @tryghost/i18n (etc.) needs pnpm-workspace.yaml in scope; pack.js wrote it AFTER the pack loop. Reordered to write it before. - The overrides merge into the packaged package.json read from root package.json's `pnpm.overrides`, which is empty under pnpm 11 (overrides moved to pnpm-workspace.yaml). Updated to read from workspace.yaml with the legacy path as fallback. Verified end-to-end: pack succeeds, tarball builds cleanly via Dockerfile.production. No .npmrc is produced or required — the only thing it carried for the published package was `frozen-lockfile=false`, which pnpm 11 ignores in .npmrc anyway. Also re-applies the comment softening from the #28197 review of .github/scripts/enforce-package-manager.js — that change was reviewed and agreed on the PR but never made it into a commit before the rebase autostash on my end.
The pnpm 11 upgrade in #28197 deleted the root .npmrc, but several release-pipeline pieces still referenced it: 1. Dockerfile.production and docker/ghost-dev/Dockerfile listed .npmrc in their COPY directives — Docker fails when a named source is missing, breaking the devcontainer image build on main. 2. ghost/core/scripts/pack.js read root .npmrc to build the published Ghost tarball — would throw on the first release after merge. Also surfaced two latent pack.js issues in the process: - pnpm 11 strict-validates `catalog:` refs in workspace package.jsons. `pnpm pack` on @tryghost/i18n (etc.) needs pnpm-workspace.yaml in scope; pack.js wrote it AFTER the pack loop. Reordered to write it before. - The overrides merge into the packaged package.json read from root package.json's `pnpm.overrides`, which is empty under pnpm 11 (overrides moved to pnpm-workspace.yaml). Updated to read from workspace.yaml with the legacy path as fallback. Verified end-to-end: pack succeeds, tarball builds cleanly via Dockerfile.production. No .npmrc is produced or required — the only thing it carried for the published package was `frozen-lockfile=false`, which pnpm 11 ignores in .npmrc anyway. Also re-applies the comment softening from the #28197 review of .github/scripts/enforce-package-manager.js — that change was reviewed and agreed on the PR but never made it into a commit before the rebase autostash on my end.
The pnpm 11 upgrade in #28197 deleted the root .npmrc, which removed both a file reference (broke Docker COPY) and the `frozen-lockfile=false` directive that had been silently absorbing several lockfile/workspace mismatches in published-tarball installs (pnpm 11 ignores .npmrc keys other than auth/registry, so it couldn't carry the directive anyway). Five distinct issues across the release pipeline: 1. `Dockerfile.production` and `docker/ghost-dev/Dockerfile` listed .npmrc in their COPY directives. Docker fails on missing sources — devcontainer image build went red on first push to main. 2. `pack.js` read root .npmrc to build the published tarball. Would throw on the next `pnpm archive` / release. 3. pnpm 11 strict-validates `catalog:` refs in workspace package.jsons, so `pnpm pack` of @tryghost/i18n etc. needs pnpm-workspace.yaml in scope. pack.js wrote it AFTER the pack loop. Reordered to write it first. 4. The packaged pnpm-workspace.yaml shipped overrides + packageExtensions verbatim from root, but `pnpm deploy` generates a lockfile that doesn't record them (they're already applied to the resolved package.json). pnpm 11's frozen-lockfile install (CI=true default for ghost-cli) compares config to lockfile as raw strings and fails ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. Solution: pack.js writes a trimmed workspace.yaml carrying only catalog/catalogs/allowBuilds/ strictDepBuilds — the things end-user installs actually need. 5. After pack.js's package.json post-processing (strip peer suffixes, rewrite file: refs to component tarballs), the deploy lockfile no longer matched. Under pnpm 10 this was masked by frozen-lockfile=false in the shipped .npmrc. Solution: pack.js regenerates the lockfile inside the deploy dir with `pnpm install --lockfile-only` so the shipped tarball is internally consistent. Also re-applies the comment softening from the #28197 review of .github/scripts/enforce-package-manager.js (was lost to a rebase autostash on my end before merge).
The pnpm 11 upgrade in #28197 deleted the root .npmrc, which removed both a file reference (broke Docker COPY) and the `frozen-lockfile=false` directive that had been silently absorbing several lockfile/workspace mismatches in published-tarball installs (pnpm 11 ignores .npmrc keys other than auth/registry, so it couldn't carry the directive anyway). Five distinct issues across the release pipeline: 1. `Dockerfile.production` and `docker/ghost-dev/Dockerfile` listed .npmrc in their COPY directives. Docker fails on missing sources — devcontainer image build went red on first push to main. 2. `pack.js` read root .npmrc to build the published tarball. Would throw on the next `pnpm archive` / release. 3. pnpm 11 strict-validates `catalog:` refs in workspace package.jsons, so `pnpm pack` of @tryghost/i18n etc. needs pnpm-workspace.yaml in scope. pack.js wrote it AFTER the pack loop. Reordered to write it first. 4. The packaged pnpm-workspace.yaml shipped overrides + packageExtensions verbatim from root, but `pnpm deploy` generates a lockfile that doesn't record them (they're already applied to the resolved package.json). pnpm 11's frozen-lockfile install (CI=true default for ghost-cli) compares config to lockfile as raw strings and fails ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. Solution: pack.js writes a trimmed workspace.yaml carrying only catalog/catalogs/allowBuilds/ strictDepBuilds — the things end-user installs actually need. 5. After pack.js's package.json post-processing (strip peer suffixes, rewrite file: refs to component tarballs), the deploy lockfile no longer matched. Under pnpm 10 this was masked by frozen-lockfile=false in the shipped .npmrc. Solution: pack.js regenerates the lockfile inside the deploy dir with `pnpm install --lockfile-only` so the shipped tarball is internally consistent. Also re-applies the comment softening from the #28197 review of .github/scripts/enforce-package-manager.js (was lost to a rebase autostash on my end before merge).
ref #28197 Five distinct release-pipeline files broke after the `.npmrc` deletion, but the deeper story is that `.npmrc` was also carrying `frozen-lockfile=false`, which was silently absorbing several pnpm-deploy-vs-lockfile mismatches under pnpm 10. pnpm 11 ignores `.npmrc` for that directive, so the mismatches surface. ## What was broken - `Dockerfile.production:28` — production image builds. `COPY` fails on missing source. - `docker/ghost-dev/Dockerfile:21` — devcontainer + local dev. Already failed on main: [run 26525156893](https://github.com/TryGhost/Ghost/actions/runs/26525156893). - `ghost/core/scripts/pack.js:124` — read root `.npmrc` to build the published Ghost tarball; would throw on next release. - `pack.js` — `pnpm pack` of `@tryghost/i18n`/etc. needs `pnpm-workspace.yaml` in scope (pnpm 11 strict-validates `catalog:` refs); pack.js wrote it AFTER the pack loop. - `pack.js` — the shipped tarball's `pnpm-workspace.yaml` carried `overrides` + `packageExtensions` verbatim from root, but `pnpm deploy`'s generated lockfile doesn't record them (they're already applied to the resolved package.json). pnpm 11's frozen-lockfile install (CI=true default for ghost-cli) failed `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH`. - `pack.js` — after the existing peer-suffix-stripping and component-file: rewriting on the deployed package.json, the deploy lockfile no longer matched. Under pnpm 10 this was masked by `frozen-lockfile=false` in `.npmrc`; pnpm 11 fails `ERR_PNPM_OUTDATED_LOCKFILE`. - `pack.js` — `minimumReleaseAge` policy in the shipped workspace.yaml 404'd on the private component tarballs and failed install inside `Dockerfile.production`.
ref #28197 `job_build_artifacts` is missing from `job_required_tests.needs`, so when it fails the gate doesn't see it. Its downstream jobs (`job_ghost-cli`, `job_build_e2e_image`, `job_e2e_tests`) cascade into "skipped" via their own `needs:` / `if:` checks, and the gate's `contains(needs.*.result, 'failure')` check sees only success/skipped — green merge despite a real release-pipeline failure. Surfaced concretely in #28197: pack.js broke under pnpm 11, `Build & Publish Artifacts` went red, the three downstream jobs skipped, gate passed, the .npmrc-deletion regression landed on main (devcontainer + production Docker + next release would all have failed).
refs #28197, #28205. This fixes the `ghost-dev` compose for pnpm 11. The dev Dockerfile (`docker/ghost-dev/Dockerfile`) only stages `ghost/{core,i18n,parse-email-address}` `package.json`s at build time. Compose then mounts `./ghost` into the container at runtime, which exposes `ghost/admin`'s `workspace:*` deps (e.g. `@tryghost/admin-x-framework` → lives in `apps/admin-x-framework/`). pnpm 10 was lenient about the missing workspace packages; pnpm 11 strict-validates the workspace graph and `pnpm dev` fails instead of being tolerated.
no ref
Updating pnpm 10.33>11.4.0:
pnpmfield frompackage.json.overrides,packageExtensions, andonlyBuiltDependencies(renamed toallowBuilds) now live inpnpm-workspace.yaml..npmrc.index/*.jsonfiles in the store with a single SQLiteindex.db(v11 store format) — faster lookups, fewer syscalls.minimumReleaseAge: 1440(1 day) andblockExoticSubdeps: true.Node version requirement is unchanged for us (already on 22.18.0).
What changed
pnpm: {…}block out ofpackage.jsonintopnpm-workspace.yaml.onlyBuiltDependencies(array) →allowBuilds(map of name →true).minimumReleaseAge: 4320(3 days, matches our Renovate cadence) andblockExoticSubdeps: trueexplicitly, even though the latter is now the default — keeps the supply-chain stance visible in the config rather than implicit.perf-primitivesto the npm-published^0.0.6.liquid-wormhole@3.0.1(transitive inghost/admin) pinsperf-primitivesvia a personal-fork git URL, whichblockExoticSubdeps(correctly) refuses; the override routes us to the upstream-maintained package at the same version..npmrc— both entries (shamefully-hoist=false,engine-strict=false) were pnpm 11 defaults that are no longer honored in.npmrcanyway.pnpm-lock.yamlagainst the new policies under pnpm 11's v11 store.Verification
pnpm installclean under pnpm 11.4.0.pnpm test:unitinghost/core: 6600 pass, 10 skip, 0 fail.pnpm nx run @tryghost/admin:build(Ember + 8 dependent Vite app builds): success.Notes for reviewers
perf-primitivesoverride —ghost/adminpreviously shipped a personal-fork git ref of this package; it now resolves to the upstream npm release at v0.0.6. The admin build is green; if anything regresses around liquid-wormhole transitions, that's the first place to look.pnpm-storemount will cold-fill the v11 store on the first run after merge, then settle.