Skip to content

Framework: Update npm to v12 #82328

Description

@manzoorwanijk

What problem does this address?

npm 12 shipped on 8 July 2026 as a security release. Three things npm install does automatically today become opt-in: dependency lifecycle scripts (allowScripts), git dependencies (--allow-git), and remote URL dependencies (--allow-remote). See the announcement and RFC 54.

We normally pick up an npm major by moving to the Node.js version that bundles it. That path is closed here. The Node.js Release WG decided not to land npm 12 on Node.js 26, 24 or 22. Node.js 26 becomes Active LTS on 28 October 2026 still bundling npm 11, and under the new annual release schedule the next major, Node.js 27, is Current on 22 April 2027 and LTS that October. Waiting for a bundled npm 12 keeps us on npm 11 for another year with the supply chain protections switched off.

Meanwhile npm@latest already resolves to 12.0.2, so contributors installing the latest npm are on a major we have never tested against. Related: #72143.

What is your proposed solution?

Install npm 12 explicitly, decoupled from the bundled Node.js version. npm 12 requires Node.js ^22.22.2 || ^24.15.0 || >=26.0.0, so the 24.18 line we are moving to in #72973 already satisfies it.

Steps

  1. Land the Node.js 24 / npm 11 update first (Framework: Update Node.js to v24 LTS and npm to v11 #80395, tracked in Framework: Update Node.js version to v24 LTS #72973).
  2. Audit what the new defaults block (see below) and decide what we approve.
  3. Request that Systems install npm 12 on the wordpress.org build server. It will not arrive with a Node.js update, so it has to be installed explicitly, as Node.js itself was in 2023.
  4. Bump the repo pins. Since CI: Read the required npm version from a single source #82235, devEngines.packageManager.version in the root package.json is the single source of truth for the npm version CI and the release tooling install, so this is a one-line change plus engines.npm.
  5. Update wordpress-develop and the bundled default themes to match, with a Core ticket, coordinated so that both repositories move around the same time.
  6. Update the WordPress Branches and Node.js/npm Versions handbook page and our contributor docs.

What the new defaults hit on trunk

  • Ten dependency packages declare install scripts and will be blocked unless listed in allowScripts: @parcel/watcher, @swc/core, core-js, core-js-pure, esbuild, fs-ext, fsevents, leveldown, nx, unrs-resolver. Several fetch or build native binaries, and fs-ext and leveldown are node-gyp builds.
  • No git or remote URL dependencies, so --allow-git and --allow-remote can stay at their new defaults.
  • packages/icons generates src/library from a prepare script. Workspace packages are link dependencies and npm 12 blocks prepare for those, so this needs verifying before the bump.

Thoughts?

CC: @aduth @desrosj @johnbillion @tyxla @jsnajdr @Mamaduka @ciampo @lancewillett @adimoldovan

Tasks

  • Update trunk on both Gutenberg and wordpress-develop
  • Update release branches in Gutenberg
  • Update release branches in wordpress-develop

Activity

  1. johnbillion commented on Sep 2, 2026

    @johnbillion
    Member

    Related: #74877

  2. Mamaduka commented on Sep 3, 2026

    @Mamaduka
    Member

    It's worth testing whether package managers like nvm and fnm can automatically pick up the required versions.

  3. manzoorwanijk commented on Sep 3, 2026

    @manzoorwanijk
    MemberAuthor

    It's worth testing whether package managers like nvm and fnm can automatically pick up the required versions.

    No, they don't. They only set the runtime, i.e. NodeJS and it sets the package manager version that comes bundled with it.

  4. jsnajdr commented on Sep 3, 2026

    @jsnajdr
    Member

    It's worth testing whether package managers like nvm and fnm can automatically pick up the required versions.

    There is the corepack tool that used to be bundled with Node (but was removed). It's something like "nvm for package managers". Maintaining multiple versions, automatically switching according to package.json. Not sure if it could be useful for us in any way.

  5. manzoorwanijk commented on Sep 3, 2026

    @manzoorwanijk
    MemberAuthor

    Yes, Corepack can help but then we have one more tool to explicitly install after we switch to Node 26.

    And TIL that Corepack now supports devEngines

  6. manzoorwanijk commented on Sep 10, 2026

    @manzoorwanijk
    MemberAuthor

    I am planning to create draft PRs for both Gutenberg and wordpress-develop with npm v12, and then someone like @desrosj can file a Systems request to have npm v12 installed on the build server.

  7. added a commit that references this issue on Sep 10, 2026
    19c2e92
  8. 34 remaining items

  9. added 9 commits that reference this issue on Oct 5, 2026
    25af696
    bfd0a36
    af1a067
    c131acd
    f8b05e7
    b7c251f
    864089d
    b5e438a
    927a931
  10. manzoorwanijk commented on Oct 7, 2026

    @manzoorwanijk
    MemberAuthor

    Due to some concerns about release/older branchs, we have put the upgrade on hold - #82732 (comment).

    Meanwhile, I am firing up some AI agents to see what needs to be done for older branches.

  11. manzoorwanijk commented on Oct 7, 2026

    @manzoorwanijk
    MemberAuthor

    Here is what Claude Code (opus-5.5) and Codex (gpt-5.6-sol) together came up with:

    Node 24, npm 11/12 and the supply chain policy on wp/* release branches

    Goal

    Every supported wp/X.Y branch can ship a security release through trunk's Publish npm packages workflow (release_type: wp) without manual intervention. Each branch is considered done only after a release candidate (RC, see RC procedure) passes on it.

    Scope

    • Branches: wp/5.1 to wp/7.1.
    • Port: Node 24 runtime, npm 11.18 floor that also accepts npm 12, and the npm v12 supply chain defaults (allow-git, allow-remote, allow-file, strict-allow-scripts with allowScripts), as described in trunk's supply chain policy.
    • Not ported: install-strategy = "linked", strict-peer-deps, isolated dependency moves, React 19, TypeScript 7, or any other trunk change that a security release does not need.
    • Lockfiles may be rewritten. They are always regenerated in place from the existing lockfile, never deleted and resolved from scratch.

    Constraint: how trunk publishes a wp/X.Y branch

    publish-npm-packages.yml checks out the release CLI from trunk and the publishing branch side by side. It installs Node from cli/.nvmrc and npm from trunk's devEngines via setup-npm (currently ^11.18.0, soon 12), then the CLI runs these in the branch checkout:

    1. npm ci
    2. npm exec --no -- lerna version <bump> --no-private --no-push --yes
    3. npm exec --no -- lerna publish from-package --dist-tag wp-X.Y --git-head <sha> --yes --no-verify-access, which runs the root prepublishOnly (the package build).

    So every branch must install, build its packages and run its Lerna under trunk's Node and npm. The branch's own .npmrc and package.json apply to steps 1 to 3.

    Verified facts

    Checked locally with Node 24.21 and npm 11.18.0 unless noted.

    # Fact Evidence
    F1 engine-strict plus an npm range that excludes 11 (<7 on 5.8 to 6.3, >=8 <9 on 6.4) fails npm ci with EBADENGINE Minimal reproduction
    F2 npm ci on an unchanged v1 lockfile fails the "not in sync" check wp/5.6 and wp/6.3 trials
    F3 On wp/6.3, engine-strict also fails on transitive engines, for example @es-joy/jsdoccomment@0.36.1 (node ^14 || ... || ^19) wp/6.3 trial
    F4 With engine-strict removed and legacy-peer-deps added, npm 11 converts the wp/6.3 v1 lockfile and resolves the tree. The run hit the 10 minute cap during install scripts, so the build is unverified wp/6.3 trial
    F5 wp/6.4 with only .nvmrc and engines changed installs, runs build:packages and passes the blocks and post-date unit tests (525 tests) wp/6.4 trial
    F6 allow-file = none does not block file: directory dependencies Minimal reproduction
    F7 strict-allow-scripts gates install scripts of non-workspace file: dependencies. They match only when keyed by spec ("file:packages/a" or "a@file:packages/a"), not by name. Workspaces are exempt Minimal reproduction
    F8 Lerna below 3.21 rejects --no-private (yargs .strict()). @lerna/publish below roughly 3.13 also rejects --git-head Package source of the locked Lerna versions
    F9 Every git and remote tarball dependency on 5.5 to 7.0 is declared only by packages/react-native-editor. No published package depends on @wordpress/react-native-* Branch lockfiles and packages/*/package.json
    F10 The August 2026 security release was published manually for wp/5.8 to wp/6.9. The wp-5.2 to wp-5.7 dist-tags were last updated in 2022 npm dist-tags and time for @wordpress/block-library, #81295, #81325
    F11 Without strict-allow-scripts, npm 11.18 ignores allowScripts and runs every install script, false entries included. npm 12 blocks unlisted and false scripts by default, with only a warning Minimal reproduction on npm 11.18.0 and 12.2.0

    Common target state

    Applies to every branch unless the per-branch table says otherwise.

    ID File Change
    C1 .nvmrc 24.20 (trunk value)
    C2 package.json engines set to { "node": ">=24.18.0", "npm": ">=11.18.0" } and the trunk devEngines block (runtime node >=24.18.0, packageManager npm >=11.18.0, onFail: error). Mirror engines into packages[""] of the lockfile
    C3 .npmrc Add min-release-age = 1, strict-allow-scripts = true, allow-git = none, allow-remote = none, allow-file = none, legacy-peer-deps = true (if missing). Keep the existing save-exact and prefer-dedupe. Set lockfile-version = 3. Do not add install-strategy, strict-peer-deps or engine-strict where F3 applies
    C4 package.json allowScripts lists every dependency that ships an install script, all false, same policy as trunk. Non-workspace branches key local packages by spec (F7). Derive the list from ESTRICTALLOWSCRIPTS output, then review each entry. strict-allow-scripts stays on every branch: without it npm 11 runs every script (F11), including native builds that fail on Node 24 (node-sass, nodegit)
    C5 package.json, lockfile Remove the React Native packages from the install (F9). Workspace branches (6.8 to 7.0): add "!packages/react-native-*" to workspaces. Non-workspace branches (5.5 to 6.7): delete the @wordpress/react-native-{aztec,bridge,editor} file: entries from root dependencies. Delete patches/*.patch files whose target package is no longer installed (for example react-native-reanimated on 6.4), and drop patch-xcode.js from postinstall if react-native leaves the tree, otherwise patch-package fails the install. Remove the test:native* scripts and the native unit test job. After C6, assert the lockfile has no git+, github: or non-registry https: sources
    C6 package-lock.json Regenerate in place with npm 11.18 on Node 24 (npm install with the existing lockfile present), producing lockfile v3. Review the diff: published packages' dependency versions must not move except for entries removed with C5 or pruned as extraneous
    C7 .github/ Port .github/setup-npm/action.yml and the trunk .github/setup-node/action.yml (introducing it where the branch calls actions/setup-node directly), with the node_modules cache disabled: trunk's cache-hit path replays npm run postinstall and lerna run postinstall, which fails on branches without a root postinstall (5.2 to 5.5) and reaches the React Native packages C5 drops from the install. The npm download cache stays. Pin every action to the SHA trunk uses. This retires actions/cache below 4.2 (6.7 and older) and upload-artifact and download-artifact v2/v3 (6.4 and older), which GitHub has shut down (cache, artifacts)
    C8 .github/workflows/ Keep every workflow, including branch management (cherry-pick-wp-release, sync-backport-changelog). Change only what C1 to C7 break: Node version source, setup actions, retired action versions, and the native jobs C5 removes (rnmobile-*, which hold every macOS runner, and native unit tests). unit-test.yml (JS and PHP), static-checks.yml and the build job of build-plugin-zip.yml must pass for the RC. build-plugin-zip.yml first exists on 5.7; on 5.1 to 5.6 the RC runs npm run build instead
    C9 Lerna Lerna 3.21 or newer (F8)

    Per-branch changes

    Branch Lockfile Changes beyond C1 to C9 Status
    7.1 v3 Already on Node 24.18, npm 11.16, min-release-age. Needs C1, C2 (npm floor 11.18, devEngines), C3, C4, C7. No React Native packages, so no C5 Not run
    7.0, 6.9 v3 All of C1 to C8. Publish path verified on Node 24 in #82122 before C3 to C5 Partly verified
    6.8 v3 All of C1 to C8 Not run
    6.5 to 6.7 v2 All of C1 to C8. C6 upgrades to v3 Not run
    6.4 v2 All of C1 to C8. C6 upgrades to v3 F5 (before C3 to C5)
    6.2, 6.3 v1 All of C1 to C8. Drop engine-strict (F3) F4
    5.9 to 6.1 v1 As 6.3 Not run
    5.8 v1 As 6.3. Webpack 4.46 needs NODE_OPTIONS=--openssl-legacy-provider for npm run build (plugin build only, not build:packages) Not run
    5.7 v1 As 5.8. nodegit (via @wordpress/env) is denied by C4, so confirm the PHP unit test job still starts wp-env Not run
    5.5, 5.6 v1 As 5.7, plus: replace node-sass 4 with sass in bin/packages/build-worker.js and root devDependencies (the render legacy API stays). Bump Lerna to 3.22.1 (C9) Not run
    5.3, 5.4 v1 As 5.5, except no React Native packages (no C5). Only pull-request-automation.yml exists, so C8 adds unit-test.yml and static-checks.yml from 5.5 adapted to C7 Not run
    5.1, 5.2 v1 As 5.3, plus: replace github:yoavf/webpack-rtl-plugin#develop with webpack-rtl-plugin@2.0.0 from the registry (or drop it as 5.3 did) and confirm the RTL CSS output is unchanged. node-sass lives in bin/packages/build.js on 5.1 and 5.2, which have no build-worker.js. No .github/workflows, so the RC is the only check Not run

    RC procedure

    An RC is a full rehearsal of trunk's npm-wp flow on the ported branch, without touching npm or the real remote.

    1. Branch CI on the port PR is green for the C8 required workflows.
    2. Start a local Verdaccio registry and create a throwaway user. Write a temporary .npmrc with registry=http://localhost:4873/ and a registry-scoped _authToken, and export it as NPM_CONFIG_USERCONFIG. This mirrors what actions/setup-node with registry-url does in the workflow. npm whoami must print the throwaway user.
    3. Record the branch SHA, seed a local bare repository with wp/X.Y at that SHA, clone it, and confirm origin is that file: path. Add a throwaway commit touching one published package (for example its README.md) so lerna version has something to bump.
    4. Guard before running anything: abort unless npm config get registry is a loopback URL and git -C <clone> remote get-url --push origin is a local path. No real npm token or GitHub credential may be present in the environment.
    5. Under trunk's Node and npm (same steps as publish-npm-packages.yml), run npm ci in a trunk checkout, then npm exec --no release-cli -- npm-wp --wp-version X.Y --ci --repository-path <clone>.
    6. Pass criteria: npm ci in the clone succeeds with no EALLOW* or ESTRICTALLOWSCRIPTS, prepublishOnly builds, exactly the set of packages lerna version bumped lands in Verdaccio under wp-X.Y, and the CLI's publication verification passes. Then repeat steps 2 to 6 with npm 12 from a clean slate: new Verdaccio storage, a new bare origin seeded from the same recorded branch SHA, and the identical throwaway commit.
    7. Build parity: before step 2, record each package's wp-X.Y version and download its tarball with npm pack <name>@wp-X.Y --registry=https://registry.npmjs.org and no user config, so the baseline can never resolve to Verdaccio. Diff each Verdaccio tarball against that baseline. Only version, gitHead, changelog, the throwaway change and source changes merged since that release may differ. Any change in build, build-module, build-style or build-types is a regression from the Node 24 port to investigate.

    Rollout order

    1. 7.1, 7.0, 6.9 (smallest change, establishes the template and RC tooling).
    2. 6.8 down to 6.4.
    3. 6.3 down to 5.8.
    4. 5.7 down to 5.1, best effort (see "Decisions" below).

    Decisions

    1. 5.1 to 5.7 still need security releases. They are best effort: if a branch turns out too costly to port, skip it for now and record why in its tracking issue.
    2. The RC is the Verdaccio rehearsal only. No prerelease is published to npm.
  12. ciampo commented on Oct 7, 2026

    @ciampo
    Contributor

    Reviewing the plan with my local agents, it seems that the branch changes alone won't be enough to make the release flow pass. Here are some suggestions to the plan:

    • Isolate npm settings. npm exec exports trunk's settings to the release CLI. I reproduced engine-strict=true and install-strategy=linked overriding the branch's .npmrc, so dropping engine-strict won't resolve F3. Could we invoke the CLI directly with Node or isolate the inherited settings?
    • Update the lockfile validator. On wp/5.5–wp/6.3, it reads packageLock.dependencies, which v3 omits. I reproduced the crash. This check runs in the required lint workflow.
    • Resolve the nodegit dependency. On wp/5.7, wp-env imports it unconditionally. Denying its install scripts leaves its native binding unavailable, so the PHP test runner needs a compatible replacement.
    • Specify Verdaccio's upstream or seed data. With neither, lerna publish from-package also selects unchanged packages, invalidating the "exactly the bumped packages" check.
  13. ciampo commented on Oct 7, 2026

    @ciampo
    Contributor

    Let's also make sure to coordinate with @desrosj , I know he's been working on improving the setup on all the past release branches.

  14. manzoorwanijk commented on Oct 7, 2026

    @manzoorwanijk
    MemberAuthor

    Thanks @ciampo, these match what I'm seeing while digging into porting 7.1, 7.0 and 6.9.

    1. npm settings: Confirmed. A rehearsal on wp/7.1 installed with trunk's install-strategy = linked and failed at the lerna version commit hook. I am fixing those issues in the release CLI rather than the workflow: commands run in the release branch drop the npm_config_* values copied from the CLI's .npmrc, so user config (registry, auth) and explicit overrides still apply, and a trunk target sees identical config. This also covers local npm exec runs and the CLI's own npm exec -- lerna calls. PR coming.
    2. Lockfile validator: Agreed. We should port it to read packages on 5.5 to 6.3 rather than keep lockfile v2 there.
    3. nodegit: Agreed. It will need to be resolved when we reach 5.7, either by backporting the wp-env change away from nodegit or by running the PHP job with a newer published @wordpress/env.
    4. Verdaccio: It proxies npmjs, so from-package skips versions already published. The gap I hit was elsewhere: the bare origin had no tags, so Lerna assumed every package changed. The rehearsal now seeds the tags a --depth=999 fetch auto-follows.
  15. peterwilsoncc commented on Oct 9, 2026

    @peterwilsoncc
    Contributor

    While I understand the desire and advantage of upgrading the supported version of NPM, I don't think it's practical to use a version of NPM that is out of sync with the node version specified in the .nvmrc file.

    The long term development process in both the Gutenberg and WordPress-Develop repos has been to use the bundled version of NPM to allow development environments to justwork™️.

    One of the main problems is that NVM has a bias towards prepending the directory to $PATH after running nvm use. I use FNM which shares the same bias. Using a version of NPM that is out of sync will require manually changing the $PATH each time nvm use is run.

    My personal workflow is to run nvm use (well, fnm use) as a post-checkout hook so I can be sure I am using the correct version of node for the required branch.

    In order to prevent any supply chain issues making it in to the Gutenberg plugin, the GitHub actions could be configured to use NPM 12 while allowing developers to use the version distributed with node for the development workflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions