Skip to content

chore(build): migrate core-web from pnpm 10.17.1 to pnpm 12.4.2 - #37563

Merged
nicobytes merged 22 commits into
mainfrom
issue-37553-migrate-core-web-to-pnpm-12
Sep 17, 2026
Merged

nicobytes merged 22 commits into
mainfrom
issue-37553-migrate-core-web-to-pnpm-12

Conversation

@oidacra

@oidacra oidacra commented Sep 15, 2026

Copy link
Copy Markdown
Member

Summary

Moves core-web from pnpm 10.17.1 to pnpm 12.4.2, crossing two majors. The version bump is the small part; what changes is where pnpm configuration lives and what pnpm does by default.

  • Configuration moved. pnpm 11 stopped reading package.json#pnpm and reads .npmrc for registry and auth only, so everything now lives in core-web/pnpm-workspace.yaml, with dotcms-postman declaring its own rather than inheriting bare defaults.
  • onlyBuiltDependencies is gone, replaced by the allowBuilds map. less needed an explicit entry: pnpm 10 printed Ignored build scripts: less and carried on, while pnpm 12's strictDepBuilds raises ERR_PNPM_IGNORED_BUILDS and fails the install.
  • Supply-chain settings are declared, not inherited: trustPolicy: no-downgrade, blockExoticSubdeps, strictDepBuilds, verifyDepsBeforeRun: warn, and a maturity gate at 7 days (see below).
  • Maven installs pnpm with npm instead of corepack. pnpm 12 ships as a native binary through @pnpm/exe.* optional dependencies; npm runs the wrapper's install.js and links the platform binary inside installs/, which CI's node-binary cache already restores. Corepack runs no lifecycle scripts and downloads that binary on first use into ~/.cache/node/corepack, outside the cache — a fetch plus a signature check on every job.
  • Node and pnpm are pinned through devEngines. The lockfile now records node@runtime:22.22.3 with a sha256 per platform, so the runtime pin is verified rather than three files agreeing by convention. .nvmrc stays; it has seven consumers.
  • CI uses pnpm/setup v2.1.0, which installs pnpm and handles the store cache itself, replacing the manual pnpm store path probe and the paired restore/save steps.

Closes #37553

The maturity gate, and the mirror fix behind it

minimumReleaseAge refuses to resolve a version until it has been public for a given period. It is pnpm's defence against a freshly compromised release, which is usually detected and pulled within hours, and it needs the registry's time field.

This PR originally shipped it documented as inert. The registry mirror dotcms-npm.b-cdn.net was ignoring the Accept header and serving whichever of npm's two metadata documents its CDN cache happened to hold — the abbreviated one carries no time — so the check applied to an arbitrary and shifting subset of packages while reading as enabled. The Cloud team has since fixed the pull zone to vary on Accept, verified across eight packages against registry.npmjs.org (#37562, closed).

With the data reliably there, the gate is enforced rather than deferred: minimumReleaseAge: 10080 (7 days, against pnpm's 1-day default) and minimumReleaseAgeIgnoreMissingTime: false, so if the publish dates ever stop coming back the install fails loudly instead of silently disabling the check. Declaring the window explicitly also enables minimumReleaseAgeStrict.

What this costs in CI, measured

This migration should not be sold on pipeline time. The install engine is genuinely faster, and it does not matter at this scale — the honest numbers, from Frontend Unit Tests and Initial Artifact Build on Ubuntu runners, against a pnpm 10 PR from the same day and the same runner pool:

pnpm install --frozen-lockfile, core-web pnpm 10.17.1 pnpm 12.4.2, new lockfile pnpm 12.4.2, unchanged lockfile
total reported by pnpm 10s 34.7s 36.3s
— supply-chain verification n/a 32.5s 32.9s
— install work ~10s ~2.2s ~3.4s
packages 2682 2678 2678

The install engine is ~3x faster (10s → 3.4s), consistent with pnpm's own published clean-install benchmarks. Everything else is the maturity gate and trustPolicy verifying 3,009 lockfile entries, which pnpm 10 did not do at all. Net today: about +26s on any job that installs core-web.

For scale, the longest job in this pipeline is Integration Tests - MainSuite 1a at 2,510s and the whole run is ~42 minutes. Both the cost and the potential saving are ~1% of the pipeline. Nobody will notice either.

One recoverable second-order cost

pnpm/setup memoizes the verification verdict and restores it across runs, so an unchanged lockfile should not be re-verified. It works — but not for core-web. In a single job:

core-web (3009 entries): "3009 entries in 32.9s"      ← recomputed
postman  (134 entries):  "verified 4h ago"  → 223ms   ← cache hit

That is on a run whose lockfile was byte-identical to the previous one. So ~33s per job is recoverable and something about the core-web case defeats the memo. Worth chasing as hygiene, not as a headline: see the follow-up issue.

What actually justifies this change

  • A deterministic lockfile. pnpm 12 breaks dependency cycles canonically during peer resolution, so the lockfile is a pure function of the dependency graph. On a 3,000-package Angular 22 / PrimeNG / NgRx tree that removes a whole class of noisy diffs and "it resolves differently on my machine".
  • The security posture. trustPolicy: no-downgrade, blockExoticSubdeps, strictDepBuilds and a 7-day maturity gate. The 33s buys the defence against a freshly compromised release.
  • Not compounding the jump. Staying on 10 only makes the next migration larger, and 12 keeps 11's configuration model, so stopping at 11 would have been a toll booth.

trustPolicy exclusions

trustPolicy: no-downgrade fails an install when a version carries weaker publish-trust evidence than earlier releases of the same package. Eight lockfile entries trip it. None is a takeover; each is an older release of a package whose other releases carry provenance, and the check compares against the package's whole history regardless of semver line. Every one is listed in trustPolicyExclude with its publish date and reason, and trustPolicyExcludePrune drops entries once the lockfile stops resolving them.

Package Published Why it trips
semver@5.7.2, semver@6.3.1 2023-07-10 legacy lines; 7.5.1 attested since 2023-05-12
css-declaration-sorter@6.4.1 2023-07-08 7.0.0 attested since 2023-05-29
undici-types@6.21.0 2024-11-13 6.13.0 through 6.16.x were attested
detect-port@1.6.1 2024-05-08 1.6.0 was attested the same day
chokidar@4.0.3 2024-12-18 3.6.0 attested since 2024-02-06
@modern-js/node-bundle-require@2.68.2, @modern-js/utils@2.68.2 2025-07-11 over a thousand earlier versions attested

A regression this PR introduced and then fixed

The corepack shim used to sit at ${node.install.dir}/pnpm, which is exactly what every pom exports as PATH. npm puts the binary at ${node.install.dir}/bin/pnpm, which was not exported, so anything resolving pnpm from PATHdotcms-webcomponents' stencil build, verify-package.sh — silently fell through to whatever ambient pnpm the machine had: a developer shell locally, the CI action's pnpm on runners. It passed verification while being wrong. A pnpm.bin.dir property is now exported first in all six <PATH> blocks:

PATH=installs/node:/usr/bin:/bin           -> command -v pnpm finds nothing
PATH=installs/node/bin:installs/node:...   -> installs/node/bin/pnpm, 12.4.2

Verified locally

Check Result
pnpm install (core-web, full resolution) green
pnpm install --frozen-lockfile (what CI and Maven run) green, 140ms warm
pnpm install + --frozen-lockfile (dotcms-postman) green
nx run-many -t build --exclude='tag:skip:build' --skip-nx-cache 20/20 projects
./mvnw test -pl :dotcms-core-web -Pvalidate -Dmaven.build.cache.enabled=false, from a deleted installs/ Node installed, pnpm 12.4.2 installed by npm into the prefix, pnpm install 207ms, lint, format:check, build, build-analytics and 43 test suites green
Maturity gate is live, not vacuous window raised to a year fails with ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION naming the computed cutoff; at 7 days a full resolution passes in 16s
strictDepBuilds is live removing less from allowBuilds fails a forced install with ERR_PNPM_IGNORED_BUILDS
Lockfiles two YAML documents each, lockfileVersion: 9.0, no dependency version drift
Prettier clean on every changed file

One task failed in the Maven run, sdk-create-app:verify-compose-static, for an unrelated local reason: its script calls python3 -c, which a wrapper on that machine refuses in favour of uv run python3. It never executes the check, and no such wrapper exists on CI.

Only CI can verify these

  • The native binary resolving on linux-x64 — both install mechanisms were exercised on darwin-arm64 here, and npm picks the platform package from @pnpm/exe.*.
  • postman, karate and e2e suites (Docker and license gated).
  • deploy-javascript-sdk, which matters because pnpm 11 replaced the npm-delegating publish flow with a native one.

Considered and not shipped

virtualStoreType: global. The payoff is real — core-web/node_modules drops from 2.1 GB to 10 MB per worktree — but Nx cannot build its project graph with it: chokidar@5.0.0 importing ReaddirpStream from readdirp resolves to the CommonJS copy through the store links and throws, and nx's own postinstall fails the same way (TypeError: chalk.blue is not a function) without pnpm surfacing it as an install failure. Reverting restores both. Worth its own change rather than riding along here. Tracked in #37573, with the resolution mechanism and a tested-and-rejected preserveSymlinks lead recorded there.

core-web/apps/ai-evals' prune-lockfile target is the only thing in the repo that parses pnpm-lock.yaml rather than hashing it. It fails today on main for an unrelated reason — apps/ai-evals/package.json does not exist — and nothing in CI invokes it, so whether Nx 23.1.1 reads the two-document lockfile correctly is untested. Pre-existing.

Documentation. .github/copilot-instructions.md and .github/instructions/frontend.instructions.md needed no change: neither pins a pnpm version nor mentions package.json#pnpm. core-web/CLAUDE.md gains a short section on where configuration now lives and the two installs that now fail where they used to warn.

This PR fixes: #37553

…pnpm 12.4.2

pnpm 11 stopped reading the pnpm block in package.json and now consults .npmrc
for registry and auth only, so overrides, peerDependencyRules,
ignoredOptionalDependencies, engineStrict, strictPeerDependencies and
fetchTimeout move into the new core-web/pnpm-workspace.yaml.

onlyBuiltDependencies was removed along with its siblings; its ten entries
become an allowBuilds map. less joins them as an explicit false: pnpm 10 only
printed "Ignored build scripts: less", while pnpm 12's strictDepBuilds turns
that into ERR_PNPM_IGNORED_BUILDS and fails the install.

Declares the supply-chain posture explicitly rather than inheriting it:
trustPolicy no-downgrade, blockExoticSubdeps, strictDepBuilds, and
verifyDepsBeforeRun warn (pnpm 11 changed that default to install, which would
inject a dependency install into Maven and CI builds). Eight older versions fail
no-downgrade because newer releases of the same packages carry provenance they
do not; each is listed in trustPolicyExclude with its publish date and reason,
with trustPolicyExcludePrune on so the list cannot rot.

minimumReleaseAge is left at its default and documented as inert: the registry
mirror returns no time field, so pnpm cannot evaluate the maturity check through
it. Tracked in #37562.

The dead stylus override is dropped: it pointed at a git source for an optional
peer that is never installed, and blockExoticSubdeps is now on.

Refs #37553
…build

pnpm 12 ships as a native binary through @pnpm/exe.* optional dependencies.
Installing it with npm runs the wrapper's install.js, which links the platform
binary in place, so everything lands inside installs/ where CI's node-binary
cache already picks it up. Corepack runs no lifecycle scripts: measured on a
clean environment, its shim downloads the native binary on first use into
~/.cache/node/corepack, outside that cache, which would mean a fetch and a
signature check on every job.

npm exposes the binary at <prefix>/bin/pnpm rather than at the path corepack's
shim used to occupy, so nodejs-parent publishes a pnpm.executable property and
the consuming modules invoke it instead of hardcoding the old location.

Refs #37553
v6.1.0 is the first release that understands pnpm 12. Both call sites read the
version from core-web/package.json, so the action version is the only manual
part of the bump.

The version-sync guard compared core-web/package.json against
nodejs-parent/pom.xml and left dotcms-postman/package.json, which carries its
own packageManager pin, with no safety net. It now checks all three.

Refs #37553
From pnpm 11 the lockfile is a two-document YAML: the second document carries
the environment and config-dependency data. lockfileVersion stays at 9.0 and no
dependency version changes; the churn is pnpm 12 re-keying the peer variants of
cyclic packages once, which it does deterministically from now on.

Refs #37553
…12 move

Points at pnpm-workspace.yaml and names the two installs that now fail where
they used to warn, so a failing install is diagnosable without reading the pnpm
changelog.

Refs #37553
@github-actions github-actions Bot added Area : Backend PR changes Java/Maven backend code Area : CI/CD PR changes GitHub Actions/workflows Area : Documentation PR changes documentation files Area : Frontend PR changes Angular/TypeScript frontend code labels Sep 15, 2026
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @nicobytes's task in 2m 21s —— View job


Review: pnpm 10 → 12 migration

  • Read the diff against origin/main
  • Review CI/pom changes for correctness
  • Review pnpm-workspace configs & supply-chain settings
  • Recheck open reviewer findings
  • Post review

I reviewed the diff for correctness bugs introduced by this PR (build/CI wiring, PATH handling, and the two new pnpm-workspace.yaml supply-chain postures). No new blocking issues. Both prior blockers are fixed, and the medium items below are all already-acknowledged trade-offs rather than defects.

New Issues

None blocking.

  • 🟡 Medium: core-web/pnpm-workspace.yaml:116 — the comment asserts "Declaring this explicitly also turns on minimumReleaseAgeStrict". I can't confirm from the repo that pnpm auto-enables minimumReleaseAgeStrict as a side effect of setting minimumReleaseAge (Copilot flagged the same, claiming it's an independent flag). Assumption: if it is independent, an exact-version resolution could still fall back to a young version, so the posture is weaker than the comment reads — the gate itself still works, this is a documentation-accuracy risk, not a broken install. What to verify: run a resolution that would violate the window with a single-version range and confirm it fails; if it doesn't, add minimumReleaseAgeStrict: true explicitly. Non-blocking.

Existing (non-blocking, already discussed in threads)

  • 🟡 Medium: dotcms-postman/pnpm-workspace.yaml — declares trustPolicy/blockExoticSubdeps/strictDepBuilds but not minimumReleaseAge, so the 7-day maturity gate doesn't cover this module. Confirmed harmless today: its lockfile carries only semver@7.5.4/7.6.3 (attested lines, not the downgraded ones), no exotic sources, and no build-script deps — so no-downgrade, blockExoticSubdeps and strictDepBuilds won't trip on the current tree. The gap only matters on a future lockfile refresh. Author decision recorded.
  • 🟡 Medium: .github/actions/core-cicd/maven-job/action.yml:271 / deploy-javascript-sdk/action.ymlpnpm/setup with no runtime: installs Node 22.22.3 from devEngines.runtime on top of the Node frontend-maven-plugin installs into installs/. Redundant, not incorrect (author offered to pin runtime: explicitly). The store cache is also keyed only on core-web/pnpm-lock.yaml, so Postman dependency changes won't invalidate it — a cache miss/re-fetch, not a correctness bug.

Resolved

  • nodejs-parent/pom.xml + all six <PATH> blocks — blocker 1 fixed: pnpm.bin.dir is now prepended everywhere, so pnpm exec and verify-package.sh resolve the pinned pnpm from installs/node/bin instead of an ambient one. No dangling references to the removed pnpm-info/restore-cache-pnpm/save-cache-pnpm steps.
  • dotcms-postman/pnpm-workspace.yaml + .../pom.xml — blocker 2 fixed: module now carries verifyDepsBeforeRun: warn and the security keys rather than silently inheriting pnpm 12 defaults.
  • .github/actions/core-cicd/maven-job/action.yml:216 — version-sync guard now checks packageManager, devEngines.packageManager and devEngines.runtime across both manifests, collects all mismatches before failing, and correctly no-ops on absent declarations (verified against the postman manifest which has no devEngines).
  • core-web/package.json / core-web/.npmrc — config move to pnpm-workspace.yaml is complete and internally consistent (stylus override dropped, less: false added for strictDepBuilds, .npmrc reduced to registry only).

The Windows PATH-separator point (Copilot) is not a regression this PR introduces — these poms have no Windows branches and ${node.install.dir} was already joined with : before this change.
• branch issue-37553-migrate-core-web-to-pnpm-12

Comment thread core-web/.npmrc

@nicobytes nicobytes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed across correctness / readability / architecture / security / performance, and additionally against the four pnpm guides this migration should be aligned with: docker, supply-chain-security, continuous-integration#github-actions and git-worktrees.

Overall this is a careful and unusually well-documented migration. The configuration move to pnpm-workspace.yaml is correct and complete, dropping corepack is the right call and matches upstream (the pnpm CI docs no longer recommend it), the trustPolicyExclude entries are each justified with a publish date rather than waved through with trustPolicyIgnoreAfter, and trustPolicyExcludePrune: true is a good touch because it stops that list from rotting.

Two things I think have to change before merge, then five alignments with the docs above.


Blocking

1. pnpm is no longer on PATH for anything the Maven build spawns

Full detail inline on nodejs-parent/pom.xml. Short version: the corepack shim used to sit at ${node.install.dir}/pnpm, and ${node.install.dir} is exactly what every pom exports as PATH. npm puts the binary at ${node.install.dir}/bin/pnpm, and ${node.install.dir}/bin is not on that PATH. The new ${pnpm.executable} property fixes the five places that invoke pnpm directly, but not the places that resolve it from PATH:

  • core-web/libs/dotcms-webcomponents/project.json:34"command": "pnpm exec stencil build ..." under nx:run-commands. The project has no tags, so --exclude='tag:skip:build' does not skip it; it runs inside the nx run-many -t build that Maven drives.
  • core-web/libs/sdk/create-app/scripts/verify-package.sh:61for candidate in pnpm npm; do ... command -v "$candidate". Its comment on line 53 still reads "pnpm — the corepack shim the Maven build puts on PATH", which this PR makes false.

I verified pnpm does not paper over this. With pnpm 12, pnpm exec prepends only node_modules/.bin to the child environment — it does not add the directory holding the pnpm executable:

$ env PATH="$NODE_DIR:/usr/bin:/bin" "$PNPM_DIR/pnpm" exec node -e '...command -v pnpm...'
PATH= .../node_modules/.bin:.../fakenode:/usr/bin:/bin
which pnpm -> NOT FOUND

Both verification environments hide this: your dev shell has a global pnpm, and CI gets one from pnpm/action-setup before Maven runs. So the nested pnpm exec silently resolves to that pnpm — a version nothing in this repo pins — instead of failing. That is a correctness regression against the self-contained toolchain the previous setup actually had, and it will only surface somewhere without an ambient pnpm.

2. dotcms-postman is moved to pnpm 12 with none of this PR's posture

dotcms-postman/package.json is bumped to pnpm@12.4.2, but there is no dotcms-postman/pnpm-workspace.yaml and no dotcms-postman/.npmrc. Detail inline.


Alignment with the pnpm docs

3. devEngines instead of .nvmrc (+ packageManager)

Inline on core-web/package.json. This is the change that makes the Node pin a verified pin rather than a string three files agree on.

4. Git worktrees — virtualStoreType: global is missing

Inline on pnpm-workspace.yaml. Straight out of https://pnpm.io/git-worktrees, and this repo is routinely worked on in several worktrees at once.

5. minimumReleaseAge is inert, and the fix may be in this PR rather than in a follow-up

Inline on core-web/.npmrc. Shipping trustPolicy while pnpm's strongest defence against a freshly compromised release silently does nothing is the weaker half of the posture, and the PR description already contains the evidence that the mirror may be droppable today.

6. pnpm/setup@v2 — optional, but it collapses ~25 lines of CI

Inline on maven-job/action.yml.

7. Docker — nothing to do, and that is worth recording

I checked: no Dockerfile in this repo installs pnpm or builds the frontend (the image consumes the already-built WAR). So ghcr.io/pnpm/pnpm:12, RUN --mount=type=cache,id=pnpm,target=/pnpm/store and pnpm fetch from https://pnpm.io/docker have no surface to apply to here. Noting it so the question is closed rather than left open.


Verified, no action needed

  • pnpm/action-setup is not deprecated. Its README states it supports pnpm v11 and v12 and that continuing to use it with actions/setup-node is supported. So the bump is a legitimate choice, not debt.
  • The v6.1.0 pin is correct: the annotated tag v6.1.0 dereferences to ea17c68df8912ef543352723c149a84f56e3d413.
  • The AC about .github/copilot-instructions.md and .github/instructions/frontend.instructions.md is satisfied by "nothing to change" — neither file pins a pnpm version or mentions package.json#pnpm. Worth saying so explicitly in the PR body so the checklist can be closed honestly.
  • The stylus override removal is right, and consistent with turning on blockExoticSubdeps — it would have been the only exotic source in the tree.

Stale comments to sweep

  • .github/actions/core-cicd/maven-job/action.yml:208-210 — "the pnpm-era layout (install-node-and-npm + corepack shim)". Corepack is gone; the reason to hash nodejs-parent/pom.xml is now that <pnpm.version> lives there, which is a better reason and worth stating.
  • core-web/libs/sdk/create-app/scripts/verify-package.sh:53 — see blocker 1.
  • core-web/pom.xml:126-130 and core-web/apps/dotcms-ui-e2e/pom.xml — see inline.

Verdict

Request changes — items 1 and 2. Items 3-6 are alignments I'd like a decision on (accept, or reply with why not) rather than silent deferral; 4 in particular is a small diff with a real payoff. Everything else is FYI.

Comment thread nodejs-parent/pom.xml Outdated
Comment thread dotcms-postman/package.json
Comment thread core-web/.npmrc
Comment thread core-web/pnpm-workspace.yaml
Comment thread core-web/package.json Outdated
Comment thread .github/actions/core-cicd/maven-job/action.yml Outdated
Comment thread .github/actions/core-cicd/maven-job/action.yml Outdated
Comment thread core-web/pom.xml Outdated
Comment thread core-web/pnpm-workspace.yaml
oidacra and others added 8 commits September 16, 2026 10:00
The corepack shim used to sit at ${node.install.dir}/pnpm, which is exactly what
every pom exports as PATH, so pnpm was on PATH for everything the build spawned.
npm puts the binary at ${node.install.dir}/bin/pnpm, and that directory was not
exported: with the old PATH, `command -v pnpm` finds nothing.

Two callers on the build's critical path resolve pnpm from PATH rather than
through ${pnpm.executable} — libs/dotcms-webcomponents/project.json runs
`pnpm exec stencil build` and carries no tags, so --exclude='tag:skip:build'
does not skip it, and libs/sdk/create-app/scripts/verify-package.sh looks pnpm up
with command -v. Both silently fell through to whatever ambient pnpm the machine
happened to have: a developer shell locally, pnpm/action-setup on CI. That is how
this passed verification while being wrong, and it would have failed anywhere
without an ambient pnpm.

A new pnpm.bin.dir property is exported first in all six PATH blocks, so the pnpm
the build installed wins over any ambient one:

  PATH=${pnpm.bin.dir}:${node.install.dir}:${env.PATH}
  command -v pnpm -> installs/node/bin/pnpm, 12.4.2

npm install also gains --prefer-offline: with the version already installed it
takes 0.82s against 1.30s, and still succeeds with a warm npm cache and no
network at all.

Sweeps the comments the mechanism change made false: the PATH rationale in
core-web/pom.xml described a shim shebang that no longer exists, and
verify-package.sh still described the corepack shim.

Refs #37553
…ting it

The module has its own package.json, lockfile and pom, so nothing in core-web
reaches it. Bumping it to pnpm 12 without a config of its own left it on the bare
defaults while its larger sibling declared a posture — including
verifyDepsBeforeRun: install, the one default core-web deliberately overrides
because this module's pom also runs its install and then execs through pnpm
inside a Maven build that expects an already-installed tree.

Refs #37553
.nvmrc records a string that nothing verifies. devEngines.runtime makes pnpm
resolve the version and record it in the lockfile with a checksum per platform,
so the runtime becomes a verified pin rather than three files agreeing by
convention, and scripts run against that pinned runtime.

Added alongside .nvmrc and packageManager rather than replacing them: .nvmrc has
seven consumers across the workflows and two cache keys, and the CI version-sync
guard compares packageManager. Retiring those belongs in its own change.

Refs #37553
pnpm/setup installs pnpm's standalone binary and, with cache: true, restores and
saves the pnpm store itself keyed on the lockfile. That removes the `pnpm store
path` probe and the paired restore/save cache steps, about 25 lines. It reads the
version from devEngines.packageManager or packageManager, so the pin stays where
the sync guard already checks it.

pnpm/action-setup is not deprecated and the v6.1.0 bump it replaces was
legitimate; this is the action the pnpm CI docs now recommend, not a fix for
anything broken.

Refs #37553
With three sources the guard exited on the first mismatch, so a contributor who
forgot to bump both manifests would fix one, re-run CI, and only then learn about
the other. It now collects them, annotates each offending file, and fails once.

Refs #37553
Installing pnpm with npm moved the binary from ${node.install.dir}/pnpm, where
corepack's shim used to sit, to ${node.install.dir}/bin/pnpm. ${pnpm.executable}
covers the direct invocations, but every pom exports ${node.install.dir} as PATH
and nothing put the new bin directory there, so anything resolving pnpm from PATH
stopped finding the pnpm this repo pins:

  - core-web/libs/dotcms-webcomponents/project.json runs `pnpm exec stencil build`
    through nx:run-commands, and the project carries no tags, so it is not skipped
    by --exclude='tag:skip:build'
  - core-web/libs/sdk/create-app/scripts/verify-package.sh probes `command -v pnpm`

pnpm does not compensate: it prepends only node_modules/.bin to a child process's
environment, never the directory holding its own executable. The failure stayed
invisible because both verification environments supply an ambient pnpm — a dev
shell locally, pnpm/action-setup on CI — so the nested calls silently resolved to
a pnpm that nothing here pins.

Add ${pnpm.bin.dir} next to ${pnpm.executable} and prepend it in all six PATH
blocks. Verified that the property resolves in dotcms-nodejs-parent and inherits
into dotcms-core-web, dotcms-postman and dotcms-ui-e2e.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dotcms-postman/package.json was bumped to pnpm@12.4.2 but the module has its own
package.json and lockfile and no pnpm-workspace.yaml, so pnpm read the pnpm 12
defaults there rather than the posture declared for core-web.

The one that bites is verifyDepsBeforeRun, whose default changed to `install` in
pnpm 11. pom.xml runs `pnpm install --frozen-lockfile` at generate-resources behind
a skippable flag, then `pnpm run start` at integration-test. Reproduced on a copy of
this module's manifest and lockfile: with the default, a `pnpm exec`/`pnpm run` on a
drifted tree aborts with ERR_PNPM_OUTDATED_LOCKFILE and never runs the command, so a
build using -Dskip.npm.install=true fails in the integration-test phase. With
verifyDepsBeforeRun: warn it reports the drift and proceeds.

Also declares trustPolicy, blockExoticSubdeps and strictDepBuilds so the two modules
cannot drift apart silently. `pnpm install --frozen-lockfile` passes all supply-chain
policies here with no trustPolicyExclude entries needed (134 entries, 5.5s).

No overrides, peerDependencyRules or allowBuilds: this is a three-dependency Newman
runner with no peer conflicts and nothing that builds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…smatch

Comments left over from the corepack era described the current mechanism
incorrectly: the node-binary cache key no longer guards a "corepack shim" (it
guards <pnpm.version> and the installs/ layout), and pnpm 12 is a native binary
with no `#!/usr/bin/env node` shebang to resolve.

The version-sync check grew a third source in this branch but still exited on the
first mismatch, so a contributor who forgot to bump both package.json files would
fix one, re-run CI, and only then learn about the second. It now loops over the
manifests, annotates each offending file, and fails once.

Also records why allowBuilds keeps `canvas` even though ignoredOptionalDependencies
means it is never installed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added Area : SDK PR changes SDK libraries and removed AI: Safe To Rollback labels Sep 16, 2026
…dotCMS/core into issue-37553-migrate-core-web-to-pnpm-12
…ns publish dates

minimumReleaseAge refuses to resolve a version until it has been public for a
given period. It is pnpm's defence against a freshly compromised release, which
is usually detected and pulled within hours.

It was documented here as inert: the mirror dotcms-npm.b-cdn.net served whichever
metadata variant its CDN cache happened to hold, ignoring the Accept header, and
the abbreviated variant carries no `time` field. pnpm's default is to skip the
check when it cannot date a release, silently, so the setting read as enabled
while applying to an arbitrary and shifting subset of packages. The Cloud team
has since fixed the pull zone to vary on Accept (#37562), and the field now comes
back consistently.

So the gate is turned on for real: 7 days rather than the 1-day default, and
minimumReleaseAgeIgnoreMissingTime: false so that losing the data fails loudly
instead of disabling the gate in silence. Declaring the window explicitly also
enables minimumReleaseAgeStrict, so a range with no mature version fails rather
than falling back to a young one.

Verified: a full resolution passes (16s, nothing in the tree is younger than 7
days), and raising the window to a year fails with
ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION naming the computed cutoff, which proves
the check is live and reading real publish dates rather than passing vacuously.

Refs #37553
@oidacra

oidacra commented Sep 16, 2026

Copy link
Copy Markdown
Member Author

Our fixes for blockers 1 and 2 crossed on the wire — you pushed yours at 10:27, I pushed mine a few minutes earlier, and the merge kept both. I read yours against mine and kept yours in all three places where they overlap: the pnpm.bin.dir comment in nodejs-parent/pom.xml explains the PATH requirement far better than mine did, the canvas: true justification in allowBuilds is a catch I had missed (it is in ignoredOptionalDependencies, so the entry never fires today), and your dotcms-postman header traces the actual failure mode through <skip>${skip.npm.install}</skip> and pnpm run start in integration-test, which is the concrete answer to a question I had only argued by analogy. Nothing of mine was lost that I can see; shout if you spot otherwise.

On your item 5, the situation changed underneath us today. The mirror was not simply stripping time: it was ignoring the Accept header and serving whichever of npm's two metadata documents its cache happened to hold, which is why time was present for some packages and absent for others, and why the result moved between runs. The Cloud team has since fixed the pull zone to vary on Accept, verified across eight packages against registry.npmjs.org — details in #37562, now closed.

So the gate is enforced in this PR rather than deferred, which was your point: minimumReleaseAge: 10080 (7 days, against pnpm's 1-day default) and minimumReleaseAgeIgnoreMissingTime: false, so losing the publish dates again fails loudly instead of silently disabling the check. Declaring the window explicitly also turns on minimumReleaseAgeStrict. Verified both ways: a full resolution passes in 16s, and raising the window to a year fails with ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION naming the computed cutoff, which proves the check is reading real dates rather than passing vacuously.

Items 3 and 6 are in: devEngines alongside .nvmrc and packageManager as you staged it, and pnpm/setup at v2.1.0 rather than the v2.0.0 you cited. devEngines earned its place immediately — the lockfile now carries node@runtime:22.22.3 with a sha256 per platform, so the runtime pin is verified rather than three files agreeing by convention. One interaction neither of us had in view: with runtime omitted, pnpm/setup installs every runtime declared in devEngines.runtime, so items 3 and 6 together mean CI now installs Node 22.22.3 on top of the one frontend-maven-plugin installs. Harmless but redundant — happy to pin runtime: explicitly if you would rather not pay it.

Item 4 is the one I am not shipping. I tried it and the payoff is exactly what you said: core-web/node_modules drops from 2.1 GB to 10 MB. But Nx cannot build its project graph at all, because chokidar@5.0.0 importing ReaddirpStream from readdirp resolves to the CommonJS copy through the store links and throws. nx's own postinstall fails the same way (TypeError: chalk.blue is not a function), and pnpm does not surface that as an install failure. Reverting restores both, 20/20 projects build. I have the evidence written up for a follow-up issue rather than leaving it in a comment; it is not filed yet, so this is the record for now.

From the bot's review: measured npm install -g with the version already present — 1.30s plain, 0.82s with --prefer-offline, and it succeeds offline with a warm npm cache, so the concern about a mandatory network round-trip does not hold. Added the flag anyway for the 0.5s.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved Windows PATH, bootstrap verification, runtime ownership, cache invalidation, and maturity-gate issues remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Migrates core-web and dotcms-postman from pnpm 10.17.1 to 12.4.2, updating configuration, lockfiles, Maven integration, and CI setup.

Changes:

  • Moves pnpm configuration into workspace YAML and adds supply-chain policies.
  • Pins pnpm and Node versions and regenerates lockfiles.
  • Updates npm-based installation, PATH handling, CI setup, caching, and documentation.
File summaries
File Reviewed changes
nodejs-parent/pom.xml Installs pnpm via npm and exports its binary path. Critical (1 vote): Windows PATH handling uses the wrong separator and lacks pnpm.cmd. Moderate (1 vote): Corepack signature verification is removed.
dotcms-postman/pom.xml Uses the shared pnpm executable.
dotcms-postman/pnpm-workspace.yaml Adds Postman pnpm configuration. Moderate (1 vote): The seven-day maturity gate is not inherited.
dotcms-postman/pnpm-lock.yaml Regenerates the pnpm 12 lockfile.
dotcms-postman/package.json Pins pnpm 12.4.2.
core-web/pom.xml Updates pnpm execution and PATH handling.
core-web/pnpm-workspace.yaml Adds migrated and supply-chain settings. Moderate (1 vote): minimumReleaseAgeStrict is not explicitly enabled.
core-web/package.json Adds pnpm and Node development pins.
core-web/libs/sdk/create-app/scripts/verify-package.sh Updates pnpm path documentation.
core-web/CLAUDE.md Documents the new pnpm configuration model.
core-web/apps/dotcms-ui-e2e/pom.xml Updates pnpm execution paths.
core-web/.npmrc Retains registry configuration.
.github/actions/core-cicd/maven-job/action.yml Updates setup, version checks, and caching. Moderate (3 votes): the version guard checks only packageManager; Moderate (2 votes): setup installs a second runtime; Moderate (1 vote): the cache key does not cover the Postman lockfile.
.github/actions/core-cicd/deployment/deploy-javascript-sdk/action.yml Uses pnpm/setup. Moderate (1 vote): it installs a second Node runtime instead of using the existing setup-node runtime.
Review details

Files not reviewed (1)

  • dotcms-postman/pnpm-lock.yaml: Generated file

Suppressed comments (5)

.github/actions/core-cicd/deployment/deploy-javascript-sdk/action.yml:167

  • With no runtime input, pnpm/setup installs every runtime declared in devEngines.runtime. This action already runs actions/setup-node earlier, so the new step downloads and prepends a second Node installation; subsequent publish commands use the PNPM_HOME runtime rather than the setup-node installation. Remove one runtime owner or configure the setup action not to install the runtime.
      uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0
      with:
        working-directory: core-web
        install: false
        cache: true

.github/actions/core-cicd/maven-job/action.yml:251

  • This composite action is also used by the Postman matrix, but working-directory: core-web makes the new cache key depend only on core-web/pnpm-lock.yaml. Maven runs the Postman install from its separate dotcms-postman/pnpm-lock.yaml, so Postman dependency changes never invalidate this store cache and new packages repeatedly miss the intended cache. Use a module-specific lockfile key or include both lockfiles.
        working-directory: core-web
        install: false
        cache: true

core-web/pnpm-workspace.yaml:116

  • minimumReleaseAgeStrict is a separate pnpm setting; declaring minimumReleaseAge does not enable it. As written, an exact-version update can bypass the seven-day gate, so the security posture described here is not enforced for all installs. Set minimumReleaseAgeStrict: true explicitly and update this comment accordingly.
# Declaring this explicitly also turns on minimumReleaseAgeStrict (pnpm 12.3.0+): when no
# version in a range is old enough, resolution fails instead of quietly falling back to a
# young one. `pnpm audit --fix` records a security update that must skip the wait in
# minimumReleaseAgeExclude.

dotcms-postman/pnpm-workspace.yaml:23

  • Because dotcms-postman has its own pnpm-workspace.yaml, it does not inherit core-web's minimumReleaseAge: 10080 or minimumReleaseAgeIgnoreMissingTime: false. Its install therefore only gets pnpm's default maturity behavior, so the 7-day supply-chain gate described by this PR is not applied to Postman dependencies. Add the maturity settings here, or explicitly document and test this module's exemption.
trustPolicy: no-downgrade
blockExoticSubdeps: true
strictDepBuilds: true

nodejs-parent/pom.xml:99

  • Replacing Corepack's prepare with npm install -g pnpm@12.4.2 removes the signature verification that previously protected the pnpm bootstrap. npm resolves the exact version from registry metadata and immediately runs its install script, while the new trustPolicy only covers project dependencies, not this package-manager binary. Preserve signature verification or verify the exact pnpm tarball/signature before executing it.
                                <argument>${node.install.dir}/node_modules/npm/bin/npm-cli.js</argument>
                                <argument>install</argument>
                                <argument>-g</argument>
                                <argument>--prefix</argument>
                                <argument>${node.install.dir}</argument>
                                <!-- Measured with the version already installed: 1.30s plain, 0.82s
                                     with this flag, and it still succeeds with a warm npm cache and no
                                     network. Falls back to the registry when the cache cannot answer. -->
                                <argument>--prefer-offline</argument>
                                <argument>pnpm@${pnpm.version}</argument>
  • Files reviewed: 13/15 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread nodejs-parent/pom.xml
Comment thread .github/actions/core-cicd/maven-job/action.yml Outdated
Comment thread .github/actions/core-cicd/maven-job/action.yml
pnpm/setup resolves its version from devEngines.packageManager before falling back
to packageManager, but the guard compared only packageManager. A bump to the
devEngines value alone would pass the check while the action installed a different
pnpm than the one Maven installs from ${pnpm.version} — no step would fail, and the
build would run on two different pnpm versions depending on who invoked it.

Now checks both pnpm declarations, and devEngines.runtime against <node.js.version>
for the same reason. An absent declaration is still fine; a present one must agree.

The comparison is a plain `if` rather than a `&&` chain: under `set -e` a
short-circuited chain returns non-zero and would abort the step instead of
recording the mismatch and continuing to the next.

Verified by running the script against the current tree (passes, reporting
"pnpm 12.4.2, node 22.22.3") and against copies with devEngines.packageManager
bumped to 12.5.0 and devEngines.runtime to 24.0.0 — both are caught, and both
mismatches are reported in a single run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nicobytes
nicobytes enabled auto-merge September 17, 2026 18:19
@nicobytes
nicobytes added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit bb0071b Sep 17, 2026
69 of 70 checks passed
@nicobytes
nicobytes deleted the issue-37553-migrate-core-web-to-pnpm-12 branch September 17, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Backend PR changes Java/Maven backend code Area : CI/CD PR changes GitHub Actions/workflows Area : Documentation PR changes documentation files Area : Frontend PR changes Angular/TypeScript frontend code Area : SDK PR changes SDK libraries

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Build: migrate core-web from pnpm 10.17.1 to pnpm 12.4.2

4 participants