Skip to content

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

Open
oidacra wants to merge 18 commits into
mainfrom
issue-37553-migrate-core-web-to-pnpm-12
Open

oidacra wants to merge 18 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

Why pnpm 12 rather than staying on 10 or stopping at 11

pnpm 12 breaks dependency cycles canonically during peer resolution, so the lockfile becomes a pure function of the dependency graph: repeated installs and reordered importers produce byte-identical lockfiles, and peer resolution is 2-3x faster on cycle-heavy trees like this one (33k-line lockfile, 3,011 packages, Angular 22 + PrimeNG + NgRx). Install-side there is a SQLite store index, direct-to-CAS writes, undici, and hardlink-before-clone on Linux. pnpm 11 alone would have been a toll booth, since 12 keeps 11's configuration model.

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.

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.

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.

…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 39s —— View job


Review: pnpm 10 → 12 migration

I reviewed the diff against origin/main, focusing on bugs introduced by this PR (the lockfiles were not line-reviewed). This has already been through several thorough rounds; my pass confirms the two blocking findings are resolved and I found no new blocking issues.

Resolved

  • nodejs-parent/pom.xml:25-27, core-web/pom.xml, dotcms-postman/pom.xml, core-web/apps/dotcms-ui-e2e/pom.xml — the "pnpm off PATH" blocker is fixed. pnpm.bin.dir (${node.install.dir}/bin) is prepended ahead of ${node.install.dir} in all six <PATH> blocks, so pnpm exec in dotcms-webcomponents' target and verify-package.sh now resolve the build's own pnpm rather than an ambient one. I confirmed all four poms inherit dotcms-nodejs-parent, so ${pnpm.bin.dir} / ${pnpm.executable} resolve everywhere they're used.
  • dotcms-postman/pnpm-workspace.yaml — the module no longer silently runs on pnpm 12 defaults; verifyDepsBeforeRun: warn (correct for the skippable-install + pnpm run start flow) plus the three supply-chain keys are declared.
  • .github/actions/core-cicd/maven-job/action.yml:216-250 — sync check now covers packageManager, devEngines.packageManager and devEngines.runtime across both manifests, collects all mismatches before failing, and correctly strips the v prefix from <node.js.version>. The check() helper's use of a plain if (not &&) under set -e is the right call.
  • core-web/.npmrc / core-web/pnpm-workspace.yaml — config move is complete and the maturity gate is now enforced (minimumReleaseAge: 10080, minimumReleaseAgeIgnoreMissingTime: false).

New Issues

  • 🟡 Medium: core-web/pnpm-workspace.yaml:113 — the comment states that declaring minimumReleaseAge explicitly "also turns on minimumReleaseAgeStrict". Copilot flagged this as a separate setting that is not auto-enabled. Assumption: if strict mode is off, a range with no version old enough falls back to a younger version instead of failing. What to verify: whether minimumReleaseAgeStrict is implicitly enabled in pnpm 12.4.2, or set it explicitly (minimumReleaseAgeStrict: true) to make the posture match the comment. Note the counter-evidence in @nicobytes' live test: an exact pin inside the window failed with ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION, which suggests the gate does apply to exact resolutions — so this is a comment-accuracy / confirmation item, not a demonstrated hole. Non-blocking.

Existing (non-blocking, already discussed in-thread)

  • 🟡 Medium: .github/actions/core-cicd/maven-job/action.yml:265 and deploy-javascript-sdk/action.yml:162 — with no runtime: input, pnpm/setup installs the devEngines.runtime Node (22.22.3) on top of the Node that frontend-maven-plugin / actions/setup-node already provide. Harmless and acknowledged by the author as a redundancy pending a "who owns the Node runtime" decision. Not a defect introduced here.
  • 🟡 Medium: .github/actions/core-cicd/maven-job/action.yml:265 — the pnpm store cache (cache: true, working-directory: core-web) keys only on core-web/pnpm-lock.yaml, so dotcms-postman dependency changes won't invalidate it. This matches the previous behavior (the old key was also hashFiles('core-web/pnpm-lock.yaml')), so it's a pre-existing cache-efficiency gap, not a regression.

Everything else in the CI/pom/workspace changes reads correctly. The remaining items from prior reviews (Windows PATH separator, virtualStoreType: global) are pre-existing or consciously deferred with evidence, not blockers for this PR.

issue-37553-migrate-core-web-to-pnpm-12

Comment thread core-web/.npmrc
Comment on lines +1 to 3
# From pnpm 11 on this file is read for registry and auth only; every other setting
# is ignored in silence. See pnpm-workspace.yaml for the rest of the configuration.
registry=https://dotcms-npm.b-cdn.net

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.

Semgrep identified a blocking 🔴 issue in your code:
This .npmrc does not set a minimum release age or sets it too low. Newly published packages can be malicious or unstable. Add min-release-age = 7 to wait 7 days before resolving newly published package versions. Added in: v11.10 Reference: https://github.blog/changelog/2026-02-18-npm-bulk-trusted-publishing-config-and-script-security-now-generally-available/

Why this might be safe to ignore:

The rule matched the registry line because its broad pattern treats any .npmrc content without min-release-age as a finding. This file explicitly states that pnpm 11 reads it only for registry and authentication, so adding min-release-age here would be ineffective and the relevant setting belongs in pnpm-workspace.yaml.

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by npm-missing-minimum-release-age.

If this is a critical or high severity finding, please also link this issue in the #security channel in Slack.

You can view more details about this finding in the Semgrep AppSec Platform.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

/fp From pnpm 11 onward .npmrc is read for registry and authentication only; every other setting is ignored there, so adding min-release-age to this file would have no effect. The control the rule is asking for is in place, in the file pnpm actually reads: core-web/pnpm-workspace.yaml sets minimumReleaseAge: 10080 (7 days, against pnpm's 1-day default) plus minimumReleaseAgeIgnoreMissingTime: false so the check fails loudly rather than skipping when publish dates are unavailable.

Verified live: raising the window to a year fails resolution with ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION naming the computed cutoff, so the gate reads real publish dates rather than passing vacuously.

Worth flagging to whoever maintains the rule: it will report this on every pnpm 11+ repository, since the setting can no longer live where it looks.

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.

/fp The rule asks for a 7-day minimum release age. This repository already enforces exactly that — in the file pnpm actually reads.

min-release-age is npm's setting. This project is on pnpm 12, where .npmrc is consulted for registry and authentication only; every other key there is ignored in silence, which is why this PR moved the rest of the configuration out of it. Adding min-release-age to core-web/.npmrc would be a no-op that reads as protection — strictly worse than not having it.

The equivalent control lives in core-web/pnpm-workspace.yaml:

minimumReleaseAge: 10080            # 7 days, the value this rule asks for
minimumReleaseAgeIgnoreMissingTime: false

The second line matters as much as the first: it closes the silent hole where pnpm skips the check without saying so when the registry omits publish timestamps.

Verified live rather than assumed. With the window raised to a year, pnpm add vite through our registry resolves 7.1.5 (published 2025-09-08) instead of latest 8.3.0 (published 2026-09-10); and an exact pin inside the window fails with ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION, naming the computed cutoff. The gate is live and reading real publish dates.

@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>
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

3 participants