Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
99884d0
chore(build): move pnpm configuration to pnpm-workspace.yaml and pin …
oidacra Sep 15, 2026
6a19f22
chore(build): install pnpm with npm instead of corepack in the Maven …
oidacra Sep 15, 2026
3e0a772
ci: bump pnpm/action-setup to v6.1.0 and cover the postman pin
oidacra Sep 15, 2026
8ae0c2d
chore(build): regenerate both lockfiles with pnpm 12.4.2
oidacra Sep 15, 2026
d06298d
docs(core-web): record where pnpm configuration lives after the pnpm …
oidacra Sep 15, 2026
cd1a6d1
fix(build): put the build's own pnpm on PATH
oidacra Sep 16, 2026
0a6d2cb
chore(build): declare dotcms-postman's pnpm posture instead of inheri…
oidacra Sep 16, 2026
1069b4b
chore(build): pin Node and pnpm through devEngines
oidacra Sep 16, 2026
125dc57
ci: replace pnpm/action-setup with pnpm/setup
oidacra Sep 16, 2026
28c758c
ci: report every pnpm version mismatch at once
oidacra Sep 16, 2026
600cccc
fix(build): keep pnpm on PATH for Maven-spawned processes
nicobytes Sep 16, 2026
cfafbb6
fix(build): give dotcms-postman an explicit pnpm 12 configuration
nicobytes Sep 16, 2026
1fe9b97
chore(build): correct stale pnpm comments and report every version mi…
nicobytes Sep 16, 2026
ab2f008
Merge branch 'issue-37553-migrate-core-web-to-pnpm-12' of github.com:…
nicobytes Sep 16, 2026
c12174d
feat(build): enforce the pnpm maturity gate now that the mirror retur…
oidacra Sep 16, 2026
1bee367
Merge remote-tracking branch 'origin/main' into issue-37553-migrate-c…
oidacra Sep 16, 2026
5da1ed4
ci: check every pinned toolchain declaration, not just packageManager
nicobytes Sep 16, 2026
d10a2c1
Merge branch 'main' into issue-37553-migrate-core-web-to-pnpm-12
nicobytes Sep 16, 2026
1837d3c
Merge branch 'main' into issue-37553-migrate-core-web-to-pnpm-12
nicobytes Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -149,28 +149,22 @@ runs:
echo "::endgroup::"
shell: bash

# pnpm/setup downloads pnpm's own standalone binary — no Node and no npm
# involved — and with `cache: true` restores and saves the pnpm store itself,
# keyed on core-web/pnpm-lock.yaml, replacing the `pnpm store path` probe and
# the paired restore/save cache steps this action used to carry.
#
# No `version` input: it comes from core-web/package.json
# (devEngines.packageManager, falling back to packageManager).
#
# install: false because the install is run explicitly in the next step, which
# also records the resolved pnpm and Node versions in the job log.
- name: Set up pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0
with:
package_json_file: core-web/package.json
run_install: false

- name: Get pnpm store directory path
id: pnpm-info
shell: bash
run: |
echo "dir=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
echo "version=$(pnpm -v)" >> $GITHUB_OUTPUT

- id: restore-cache-pnpm
name: Restore pnpm Store Cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.pnpm-info.outputs.dir }}
key: ${{ runner.os }}-pnpm-${{ steps.pnpm-info.outputs.version }}-${{ hashFiles('core-web/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-${{ steps.pnpm-info.outputs.version }}
${{ runner.os }}-pnpm-
working-directory: core-web
install: false
cache: true

- name: 'Install project'
working-directory: ${{ github.workspace }}/core-web/
Expand All @@ -181,14 +175,6 @@ runs:
node --version
shell: bash

- id: save-cache-pnpm
name: Save pnpm Store Cache
if: ${{ steps.restore-cache-pnpm.outputs.cache-hit != 'true' }}
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.pnpm-info.outputs.dir }}
key: ${{ steps.restore-cache-pnpm.outputs.cache-primary-key }}

- name: 'Build SDK packages'
working-directory: ${{ github.workspace }}/core-web/
run: |
Expand Down
98 changes: 55 additions & 43 deletions .github/actions/core-cicd/maven-job/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -205,50 +205,70 @@ runs:
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: installs
# hash nodejs-parent/pom.xml so the pnpm-era layout (install-node-and-npm
# + corepack shim) is not mixed with the yarn-era layout from cached
# builds of the previous toolchain.
# hash nodejs-parent/pom.xml because that pom is what decides the contents
# of installs/: it holds <pnpm.version> and installs pnpm into the same
# prefix as Node, so bumping the pinned pnpm (or Node) invalidates this key
# instead of restoring a cache built for the previous versions.
key: node-binary-${{ hashFiles('core-web/.nvmrc', 'nodejs-parent/pom.xml') }}

- name: Verify pnpm version is in sync between package.json and nodejs-parent/pom.xml
- name: Verify the pinned toolchain versions are in sync
if: ${{ inputs.requires-node == 'true' }}
shell: bash
run: |
set -euo pipefail
pkg_json_pm=$(jq -r '.packageManager' core-web/package.json)
pkg_json_version="${pkg_json_pm#pnpm@}"
pom_version=$(grep -oE '<pnpm\.version>[^<]+</pnpm\.version>' nodejs-parent/pom.xml | sed -E 's|</?pnpm\.version>||g')
if [ "$pkg_json_version" != "$pom_version" ]; then
echo "::error::pnpm version mismatch: core-web/package.json=$pkg_json_version vs nodejs-parent/pom.xml=$pom_version. Keep these in sync — Maven uses the pom value while the pnpm store cache key uses the package.json value." >&2
exit 1
fi
echo "pnpm version in sync: $pom_version"

# nodejs-parent/pom.xml is the reference: Maven installs those versions and every
# module runs them. Every other declaration has to agree, including the ones that
# only the workflow reads — pnpm/setup resolves its version from
# devEngines.packageManager *before* falling back to packageManager, so checking
# packageManager alone would let a devEngines-only bump install a different pnpm
# than Maven's, with nothing failing. Same for devEngines.runtime and Node.
# Collect every mismatch before failing, so a contributor who forgot to bump one
# declaration does not fix it, re-run CI, and only then learn about the next.
pom_pnpm=$(grep -oE '<pnpm\.version>[^<]+</pnpm\.version>' nodejs-parent/pom.xml | sed -E 's|</?pnpm\.version>||g')
pom_node=$(grep -oE '<node\.js\.version>[^<]+</node\.js\.version>' nodejs-parent/pom.xml | sed -E 's|</?node\.js\.version>||g')
pom_node=${pom_node#v}
fail=0

# An absent declaration is fine; a present one must match. Written as a plain `if`
# rather than a `&&` chain because under `set -e` a short-circuited chain returns
# non-zero and would abort the step instead of recording the mismatch.
check() { # <file> <label> <declared> <expected>
if [ -n "$3" ] && [ "$3" != "$4" ]; then
echo "::error file=$1::$2 declares $3, nodejs-parent/pom.xml pins $4. Maven installs the pom version and every module runs it, so all declarations must agree." >&2
fail=1
fi
}

for manifest in core-web/package.json dotcms-postman/package.json; do
declared=$(jq -r '.packageManager // ""' "$manifest")
check "$manifest" "$manifest packageManager" "${declared#pnpm@}" "$pom_pnpm"
check "$manifest" "$manifest devEngines.packageManager" \
"$(jq -r 'if .devEngines.packageManager.name == "pnpm" then .devEngines.packageManager.version else "" end' "$manifest")" "$pom_pnpm"
check "$manifest" "$manifest devEngines.runtime" \
"$(jq -r 'if .devEngines.runtime.name == "node" then .devEngines.runtime.version else "" end' "$manifest")" "$pom_node"
done

[ "$fail" -eq 0 ] || exit 1
echo "toolchain in sync: pnpm $pom_pnpm, node $pom_node"

# pnpm/setup downloads pnpm's own standalone binary — no Node and no npm
# involved — and with `cache: true` restores and saves the pnpm store itself,
# keyed on core-web/pnpm-lock.yaml. That replaces the `pnpm store path` probe
# and the paired restore/save cache steps this action used to carry.
#
# No `version` input: the action reads it from core-web/package.json
# (devEngines.packageManager, falling back to packageManager), which the sync
# check above already holds against nodejs-parent/pom.xml.
#
# install: false — the Maven build runs the install itself, in the phase and
# with the flags the poms decide.
- name: Set up pnpm
if: ${{ inputs.requires-node == 'true' }}
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0
with:
package_json_file: core-web/package.json
run_install: false

- name: Get pnpm store directory path
id: pnpm-info
if: ${{ inputs.requires-node == 'true' }}
shell: bash
run: |
echo "dir=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
echo "version=$(pnpm -v)" >> $GITHUB_OUTPUT

- id: restore-cache-pnpm
name: Restore pnpm Store Cache
if: ${{ inputs.requires-node == 'true' }}
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.pnpm-info.outputs.dir }}
key: ${{ runner.os }}-pnpm-${{ steps.pnpm-info.outputs.version }}-${{ hashFiles('core-web/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-${{ steps.pnpm-info.outputs.version }}
${{ runner.os }}-pnpm-
working-directory: core-web
install: false
cache: true
Comment thread
nicobytes marked this conversation as resolved.


- id: cache-sonar
Expand Down Expand Up @@ -445,14 +465,6 @@ runs:
path: installs
key: ${{ steps.restore-cache-node.outputs.cache-primary-key }}

- id: save-cache-pnpm
name: Save pnpm Store Cache
if: ${{ inputs.requires-node == 'true' && inputs.generate-artifacts == 'true' && steps.restore-cache-pnpm.outputs.cache-hit != 'true' }}
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.pnpm-info.outputs.dir }}
key: ${{ steps.restore-cache-pnpm.outputs.cache-primary-key }}

- id: save-cache-sonar
name: Save SonarQube Cache
if: ${{ inputs.cache-sonar == 'true' && steps.cache-sonar.outputs.cache-hit != 'true' }}
Expand Down
10 changes: 2 additions & 8 deletions core-web/.npmrc
Original file line number Diff line number Diff line change
@@ -1,9 +1,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
Comment on lines +1 to 3

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.

Comment thread
oidacra marked this conversation as resolved.
engine-strict=true
# Keep strict-peer-dependencies=false (pnpm default would block the install on
# pre-existing peer conflicts: codelyzer, ng-mocks, ngx-markdown, etc.).
# These mirror what yarn 1 warned about silently. Dropping this flag requires
# fixing each conflict first, tracked as separate follow-up work.
strict-peer-dependencies=false
# Mirrors `yarn config set network-timeout 6000000` from the previous toolchain.
fetch-timeout=6000000
20 changes: 20 additions & 0 deletions core-web/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ This file provides guidance to Claude Code when working with code in this reposi

DotCMS Core-Web monorepo — Angular + Nx. Uses **pnpm** as package manager. Nx is not installed globally — always use `pnpm nx`.

### pnpm Configuration

All of it lives in **`pnpm-workspace.yaml`** — `overrides`, `peerDependencyRules`,
`ignoredOptionalDependencies`, `engineStrict`, `strictPeerDependencies`, `fetchTimeout`,
`allowBuilds` (replaces the removed `onlyBuiltDependencies`) and the supply-chain settings
(`trustPolicy`, `trustPolicyExclude`, `trustPolicyExcludePrune`, `blockExoticSubdeps`,
`strictDepBuilds`, `verifyDepsBeforeRun`). The `pnpm` block in `package.json` is **not read at
all**, and `.npmrc` is consulted for registry and auth only — don't add settings to either.

Two installs that now fail where they used to warn:

- **`ERR_PNPM_IGNORED_BUILDS`** (`strictDepBuilds`) — a dependency with a build script that is not
in `allowBuilds` now aborts the install; pnpm 10 only printed `Ignored build scripts: ...` and
carried on. Add the package to `allowBuilds` with `true` to let it build or `false` to deny it
explicitly.
- **`trustPolicy: no-downgrade`** — fails when a version carries weaker publish-trust evidence than
earlier releases of the same package. Eight known older versions sit in `trustPolicyExclude`, each
with its reason and publish date. A new one is not automatically an attack: check that version's
publish date and provenance history before adding a line.

### MCP Servers

Configured in `/.mcp.json`. Use these instead of guessing:
Expand Down
6 changes: 3 additions & 3 deletions core-web/apps/dotcms-ui-e2e/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
<executable>${node.install.dir}/pnpm</executable>
<executable>${pnpm.executable}</executable>
<workingDirectory>${e2e.frontend.dir}</workingDirectory>
<environmentVariables>
<PATH>${node.install.dir}:${env.PATH}</PATH>
<PATH>${pnpm.bin.dir}:${node.install.dir}:${env.PATH}</PATH>
</environmentVariables>
</configuration>
<executions>
Expand Down Expand Up @@ -103,7 +103,7 @@
<CI>true</CI>
<CURRENT_ENV>${e2e.test.env}</CURRENT_ENV>
<NODE_OPTIONS>${e2e.node.options}</NODE_OPTIONS>
<PATH>${node.install.dir}:${env.PATH}</PATH>
<PATH>${pnpm.bin.dir}:${node.install.dir}:${env.PATH}</PATH>
</environmentVariables>
</configuration>
</execution>
Expand Down
5 changes: 3 additions & 2 deletions core-web/libs/sdk/create-app/scripts/verify-package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ fi
#
# Both packers are tried because neither is present everywhere. `npm` is missing from a
# pnpm-managed local toolchain (`pnpm env` installs node without it), which is what made
# `npm pack` report a false failure on a dev machine. `pnpm` — the corepack shim the Maven
# build puts on PATH — is the one that returned nothing on the CI runner. Whichever answers
# `npm pack` report a false failure on a dev machine. `pnpm` — installed with npm into
# installs/node by the Maven build, which puts installs/node/bin on PATH — is the one that
# returned nothing on the CI runner. Whichever answers
# first decides the check; if neither does, each packer's stderr is printed, because a
# silent packer is exactly what made this failure unreadable the first time.
PACKED=""
Expand Down
54 changes: 12 additions & 42 deletions core-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -326,47 +326,17 @@
"nx": {
"includedScripts": []
},
"pnpm": {
"overrides": {
"@babel/helper-define-polyfill-provider": "0.6.6",
"@tiptap/core": "3.22.2",
"@vue/shared": "3.5.29",
"http-cache-semantics": "4.1.1",
"pdfjs-dist": "4.2.67",
"stylus": "github:stylus/stylus#0.59.0"
"packageManager": "pnpm@12.4.2",
"devEngines": {
"runtime": {
"name": "node",
"version": "22.22.3",
"onFail": "download"
},
"peerDependencyRules": {
"allowedVersions": {
"@angular/core": "22",
"@angular/common": "22",
"@angular/forms": "22",
"@angular/router": "22",
"@angular/platform-browser": "22",
"@angular/cdk": "22",
"primeng>@angular/core": "22",
"primeng>@angular/common": "22",
"primeng>@angular/forms": "22",
"primeng>@angular/router": "22",
"primeng>@angular/platform-browser": "22",
"@ngrx/signals>@angular/core": "22",
"@ngrx/component-store>@angular/core": "22"
}
},
"ignoredOptionalDependencies": [
"canvas"
],
"onlyBuiltDependencies": [
"canvas",
"esbuild",
"@swc/core",
"nx",
"@parcel/watcher",
"core-js",
"document-register-element",
"lmdb",
"msgpackr-extract",
"unrs-resolver"
]
},
"packageManager": "pnpm@10.17.1"
"packageManager": {
"name": "pnpm",
"version": "12.4.2",
"onFail": "download"
}
}
}
Loading
Loading