From dca7cce833ececc6282fd67549a2d8a19b950f0c Mon Sep 17 00:00:00 2001 From: SamTV12345 Date: Wed, 16 Sep 2026 19:10:12 +0200 Subject: [PATCH 1/3] chore: update to pnpm v12 --- .github/workflows/docker.yml | 2 +- .github/workflows/release.yml | 2 +- Dockerfile | 32 ++-- bin/release.ts | 2 +- package.json | 4 +- packaging/README.md | 2 +- pnpm-lock.yaml | 158 +++++++++++++++++++ snap/snapcraft.yaml | 2 +- src/tests/backend/specs/dockerfilePnpmPin.ts | 12 +- 9 files changed, 186 insertions(+), 30 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 76c044e2e37..d88adf56072 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -119,7 +119,7 @@ jobs: # Regression test for #7718. Reproduces the production # docker-compose layout reported in that issue: a named volume # mounted on src/plugin_packages with no TTY allocated. Under - # the previous `CMD ["pnpm", "run", "prod"]`, pnpm 11's + # the previous `CMD ["pnpm", "run", "prod"]`, pnpm's # runDepsStatusCheck spuriously decided node_modules was out of # sync at boot and tried to wipe + reinstall, aborting with # ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY before the HTTP diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 310151b8244..55509388ec4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,7 +69,7 @@ jobs: cache-dependency-path: etherpad/pnpm-lock.yaml - name: Install dependencies ether.github.com # ether.github.com depends on sharp (Next.js image pipeline), whose - # install script must run to fetch the platform binary. pnpm 11 + # install script must run to fetch the platform binary. pnpm # turned ignored-builds into an error; allow all builds for this # external repo since we don't control its pnpm-workspace.yaml. run: pnpm install --frozen-lockfile --config.dangerously-allow-all-builds=true diff --git a/Dockerfile b/Dockerfile index f0ad8351c1b..892295d0c5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,12 +7,12 @@ # docker build --build-arg BUILD_ENV=copy . ARG BUILD_ENV=git -# NOTE: this intentionally lags the "packageManager" pin in package.json. pnpm -# 11.1.x enforces the minimum-release-age supply-chain policy during install, -# which the frozen-lockfile Docker build can't satisfy, so the image stays on -# 11.0.x. The version gap is made harmless by pnpm_config_pm_on_fail=ignore in -# the build stage below — see ether/etherpad#7911. -ARG PnpmVersion=11.0.6 +# NOTE: this tracks the "packageManager" pin in package.json. pnpm's +# minimum-release-age supply-chain policy (enforced during install) is satisfied +# by the `minimumReleaseAgeExclude` list in pnpm-workspace.yaml, so the image no +# longer needs to lag the pin the way it did during the pnpm 11.1.x era — see +# ether/etherpad#7911. +ARG PnpmVersion=12.4.2 FROM node:24-alpine AS adminbuild # Install pnpm directly via npm (rather than via corepack) so the same @@ -33,15 +33,13 @@ RUN pnpm run build:ui FROM node:24-alpine AS build LABEL maintainer="Etherpad team, https://github.com/ether/etherpad" -# The image's pnpm intentionally lags the "packageManager" pin (see the ARG -# note above). pnpm would otherwise try to self-provision the pinned version on -# invocation — including the informational `pnpm --version` probe Etherpad runs -# at startup — which fails closed with no network and breaks air-gapped boots -# (ether/etherpad#7911). pm_on_fail=ignore makes pnpm use the installed version -# instead. Inherited by the development and production runtime stages, so it -# also covers the updater's pnpm-on-PATH check and ad-hoc `pnpm` in an exec -# shell. It does not change which pnpm runs the build-time install (still the -# installed 11.0.x), so the frozen-lockfile build is unaffected. +# The image's pnpm matches the "packageManager" pin (see the ARG note above), +# so pnpm does not self-provision on invocation. pm_on_fail=ignore is kept as +# belt-and-suspenders: if the pin and the image ever drift again, pnpm falls +# back to the installed version instead of downloading the pinned build — which +# fails closed with no network and breaks air-gapped boots (ether/etherpad#7911). +# Inherited by the development and production runtime stages, so it also covers +# the updater's pnpm-on-PATH check and ad-hoc `pnpm` in an exec shell. ENV pnpm_config_pm_on_fail=ignore # Set these arguments when building the image from behind a proxy @@ -202,7 +200,7 @@ ENV ETHERPAD_PRODUCTION=true # needed at runtime. Overwrite it with a production-only version so # pnpm install doesn't warn about missing workspace directories. # Preserve the build-script policy from the source workspace file so -# pnpm 11 doesn't error out with ERR_PNPM_IGNORED_BUILDS for transitive +# pnpm doesn't error out with ERR_PNPM_IGNORED_BUILDS for transitive # postinstalls (e.g. @scarf/scarf via swagger-ui-dist). RUN printf 'packages:\n - src\n - bin\nonlyBuiltDependencies:\n - esbuild\nignoredBuiltDependencies:\n - "@scarf/scarf"\nstrictDepBuilds: false\n' > pnpm-workspace.yaml @@ -239,7 +237,7 @@ HEALTHCHECK --interval=5s --timeout=3s \ CMD wget -qO- http://127.0.0.1:9001/health | grep -E "pass|ok|up" > /dev/null || exit 1 EXPOSE 9001 -# Run node directly instead of via `pnpm run prod`. pnpm 11's +# Run node directly instead of via `pnpm run prod`. pnpm's # `runDepsStatusCheck` fires before every `pnpm run …` and spuriously # decides node_modules is out of sync on first start under the named- # volume layout used by docker-compose (mounting src/plugin_packages). diff --git a/bin/release.ts b/bin/release.ts index ae87c036dc7..c1488fc59b4 100644 --- a/bin/release.ts +++ b/bin/release.ts @@ -204,7 +204,7 @@ try { run('git pull --ff-only', {cwd: '../ether.github.com/'}); console.log('Committing documentation...'); run(`cp -R out/doc/ ../ether.github.com/public/doc/v'${newVersion}'`); - // pnpm 11 refuses `pnpm version` on a dirty tree (the doc copy above + // pnpm refuses `pnpm version` on a dirty tree (the doc copy above // dirties it) even with --no-git-tag-version, so write the bump with jq — // same pattern used for the etherpad package.json files at the top of // this script. The git add+commit below picks up both the bump and the diff --git a/package.json b/package.json index a0f8b50ea3f..ebebca25831 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,9 @@ }, "engines": { "node": ">=24.0.0", - "pnpm": ">=11.1.2" + "pnpm": ">=12.0.0" }, - "packageManager": "pnpm@11.10.0", + "packageManager": "pnpm@12.4.2", "repository": { "type": "git", "url": "https://github.com/ether/etherpad.git" diff --git a/packaging/README.md b/packaging/README.md index 9e170bb6aa0..4f2e97dd178 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -19,7 +19,7 @@ Built artefacts land in `./dist/`. ## Building locally -Prereqs: Node 24 (current LTS; matches `engines.node` floor), pnpm 11.1.2+, nfpm. +Prereqs: Node 24 (current LTS; matches `engines.node` floor), pnpm 12+, nfpm. ```sh pnpm install --frozen-lockfile diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49b04c1fce5..b000c2143c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,3 +1,161 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.4.2 + version: 12.4.2 + +packages: + + '@pnpm/exe.android-arm64@12.4.2': + resolution: {integrity: sha512-E255MbcQ0V1577M2BV0ajWuP7KmTPYA0jqZnk3rK6Lq3kTvZWulMMb7iqRmnLsQbyWTkMEB2CfyM70loVDA8xg==} + cpu: [arm64] + os: [android] + + '@pnpm/exe.android-x64@12.4.2': + resolution: {integrity: sha512-J1pSeCUwuKxMG70ZzpWn8JzElxiEa8NN/3N0SlZRtU2xbztChaJCKF3HaBNIj9yPfeofWzJo/lwNvDDjraQuZw==} + cpu: [x64] + os: [android] + + '@pnpm/exe.darwin-arm64@12.4.2': + resolution: {integrity: sha512-A0WDo8iErfZBXgrLseQxw8i8Y9ctUpOEl/Uu+cubnTzpD8tT9ykIB548L8YTM2WD4OS+ZOHSxy8aGZcvKq8PaQ==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.4.2': + resolution: {integrity: sha512-MSgJdovBWHcb5DEOvfPH9yNi/T5O1Xa4ess+E1ESGo/5yuty7S4JoiIjami+fsNXfnoQMlwsMUqYU4zAvBcNCw==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.freebsd-x64@12.4.2': + resolution: {integrity: sha512-h2YumlQSNvgbRPv+RXwABohX65f9bOBZn+jMIt7bFDISZPCzQ+Nvpt6Awbp4ip5PwQgYxbu5iREJ1fHE39Fm8A==} + cpu: [x64] + os: [freebsd] + + '@pnpm/exe.linux-arm64-musl@12.4.2': + resolution: {integrity: sha512-LwSEtSEDTv6S51YLs3YvSkPyun/QmfMic1UGICUkPWFu6ByP43RdMlkKvmVkfGhAYCpnxO057vrmyJqtfZrPCA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.4.2': + resolution: {integrity: sha512-2dSiDXyhx+RTHsewxex8f/jVjqQXWJ2oow4kCVHEWdZKeBpgMxvZ6fHkTAUBJXgqhbKIDHvuNlZBP7gJfUWL5A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-ppc64@12.4.2': + resolution: {integrity: sha512-8Itc+jQk+MTz04LS9D1RcH+VctAWmzM4l1cJQ+Sx7pAJNo7EKHdRMbC0Tok1jyQ7eEHNJZD5EleYneZqWfZM+g==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-riscv64@12.4.2': + resolution: {integrity: sha512-hleOeqhTVpH+z9RVMGnxvU4ZnrkBUClWjCbHD9u6kwyqhGSpevoU1wTGish+CBRhmIgMAy9pAfFpqhbAKOKNfw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-s390x@12.4.2': + resolution: {integrity: sha512-LAsQRRdP9aToENR6dtcIJ9l+e1zMYOX0tQcLGpRyLPVBQcYRLlvAPcmDshsiIHQjp05SDa9FI0czX1ZQ1a7a/A==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.4.2': + resolution: {integrity: sha512-kzfzH2/0BWdTABK14Yj5a1xsdkTEQUp2eXEPNakaD9jKL025lq3hyaKHIz/gIZaPDMe/1bFFK/En4ztFlbBJxw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.4.2': + resolution: {integrity: sha512-/pbt0UVTa8NMDhzOWLQRfZ6G9ROKXlJPZtx845BqyWfc7hCrWXhTLBd70yO2y8+E+IWN3oHM1s/JsIQNGk1yvg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.4.2': + resolution: {integrity: sha512-PsW19e4dAUNpZ0cS9flaxFuAmpt2dKlH/Vvi8TZ4qyJjjQzue/CEsWm+6wKVP4CJ7IT8RdL4qh+soOPWIgF2Zw==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.4.2': + resolution: {integrity: sha512-+xGoeE0g55ztWvl8i5QqdmNfW3nIrTVcoQrNshEOwxthm9Ag48oXton3uxOA3/SANyz5AXexEjj2KO20NnOrEw==} + cpu: [x64] + os: [win32] + + pnpm@12.4.2: + resolution: {integrity: sha512-CK3GYTGAJ1x8ntraOdzwjJxhrU5+rzMKTzRh8QKw+QdCNFTRF/mOctR/7wYWBwZE17/8lzpqV/UJCm18NosHyQ==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.android-arm64@12.4.2': + optional: true + + '@pnpm/exe.android-x64@12.4.2': + optional: true + + '@pnpm/exe.darwin-arm64@12.4.2': + optional: true + + '@pnpm/exe.darwin-x64@12.4.2': + optional: true + + '@pnpm/exe.freebsd-x64@12.4.2': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.4.2': + optional: true + + '@pnpm/exe.linux-arm64@12.4.2': + optional: true + + '@pnpm/exe.linux-ppc64@12.4.2': + optional: true + + '@pnpm/exe.linux-riscv64@12.4.2': + optional: true + + '@pnpm/exe.linux-s390x@12.4.2': + optional: true + + '@pnpm/exe.linux-x64-musl@12.4.2': + optional: true + + '@pnpm/exe.linux-x64@12.4.2': + optional: true + + '@pnpm/exe.win32-arm64@12.4.2': + optional: true + + '@pnpm/exe.win32-x64@12.4.2': + optional: true + + pnpm@12.4.2: + optionalDependencies: + '@pnpm/exe.android-arm64': 12.4.2 + '@pnpm/exe.android-x64': 12.4.2 + '@pnpm/exe.darwin-arm64': 12.4.2 + '@pnpm/exe.darwin-x64': 12.4.2 + '@pnpm/exe.freebsd-x64': 12.4.2 + '@pnpm/exe.linux-arm64': 12.4.2 + '@pnpm/exe.linux-arm64-musl': 12.4.2 + '@pnpm/exe.linux-ppc64': 12.4.2 + '@pnpm/exe.linux-riscv64': 12.4.2 + '@pnpm/exe.linux-s390x': 12.4.2 + '@pnpm/exe.linux-x64': 12.4.2 + '@pnpm/exe.linux-x64-musl': 12.4.2 + '@pnpm/exe.win32-arm64': 12.4.2 + '@pnpm/exe.win32-x64': 12.4.2 + +--- lockfileVersion: '9.0' settings: diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 2098dc8ae48..23b9d544828 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -115,7 +115,7 @@ parts: "${CRAFT_PART_INSTALL}/opt/node/bin/npm" install \ --prefix "${CRAFT_PART_INSTALL}/opt/node" -g corepack@latest corepack enable --install-directory "${CRAFT_PART_INSTALL}/opt/node/bin" - corepack prepare pnpm@11.0.6 --activate + corepack prepare pnpm@12.4.2 --activate # -- 3. Copy source into install dir and build. APP_DIR="${CRAFT_PART_INSTALL}/opt/etherpad" diff --git a/src/tests/backend/specs/dockerfilePnpmPin.ts b/src/tests/backend/specs/dockerfilePnpmPin.ts index 94a15a8c0dc..6b524427cd6 100644 --- a/src/tests/backend/specs/dockerfilePnpmPin.ts +++ b/src/tests/backend/specs/dockerfilePnpmPin.ts @@ -11,12 +11,12 @@ // download fails and pnpm exits non-zero, surfacing as `Failed to get pnpm // version` and breaking offline boots. // -// The image deliberately lags the pin (pnpm 11.1.x enforces a minimum-release- -// age policy the frozen-lockfile build can't satisfy), so the guard is not to -// force the versions equal but to neutralise the gap: the Dockerfile must set -// pnpm_config_pm_on_fail=ignore so pnpm uses the installed version instead of -// reaching for the network. This test fails if that guard is dropped while a -// version gap exists. +// The image and the pin are kept in sync, so the guard is belt-and-suspenders +// rather than strictly required. It exists to neutralise any future gap: if the +// image pnpm ever drifts from the "packageManager" pin again, the Dockerfile +// must set pnpm_config_pm_on_fail=ignore so pnpm uses the installed version +// instead of reaching for the network. This test fails if that guard is dropped +// while a version gap exists. const assert = require('assert').strict; import fs from 'fs'; From 2629bcdc2d6a14a42e4c627ea95d7fac6a6cac86 Mon Sep 17 00:00:00 2001 From: SamTV12345 Date: Wed, 16 Sep 2026 19:21:34 +0200 Subject: [PATCH 2/3] chore: use new flag --- src/static/js/pluginfw/installer.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/static/js/pluginfw/installer.ts b/src/static/js/pluginfw/installer.ts index ad1f8dcfc53..e419eaa2d17 100644 --- a/src/static/js/pluginfw/installer.ts +++ b/src/static/js/pluginfw/installer.ts @@ -53,11 +53,12 @@ const wrapTaskCb = (cb: Function | null) => taskQueue.wrap(cb); const migratePluginsFromNodeModules = async () => { logger.info('start migration of plugins in node_modules'); // Notes: - // * Do not pass `--prod` otherwise `npm ls` will fail if there is no `package.json`. - // * The `--no-production` flag is required (or the `NODE_ENV` environment variable must be - // unset or set to `development`) because otherwise `npm ls` will not mention any packages + // * Do not pass `--prod` otherwise `pnpm ls` will fail if there is no `package.json`. + // * The `--no-prod` flag is required (or the `NODE_ENV` environment variable must be + // unset or set to `development`) because otherwise `pnpm ls` will not mention any packages // that are not included in `package.json` (which is expected to not exist). - const cmd = ['pnpm', 'ls', '--long', '--json', '--depth=0', '--no-production']; + // (pnpm 12 renamed `--no-production` to `--no-prod`.) + const cmd = ['pnpm', 'ls', '--long', '--json', '--depth=0', '--no-prod']; const [{dependencies = {}}] = JSON.parse(await runCmd(cmd, {stdio: [null, 'string']})); From 46c23329a58ac927a801c36221da1a45786fd292 Mon Sep 17 00:00:00 2001 From: SamTV12345 Date: Wed, 16 Sep 2026 19:31:30 +0200 Subject: [PATCH 3/3] chore: use ./local_plugins --- bin/installLocalPlugins.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/installLocalPlugins.sh b/bin/installLocalPlugins.sh index 15442e6463d..a6ad75b94cc 100755 --- a/bin/installLocalPlugins.sh +++ b/bin/installLocalPlugins.sh @@ -39,7 +39,9 @@ if [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then plugin=$(trim "$plugin") if [ -d "local_plugins/${plugin}" ]; then echo "Installing plugin: '${plugin}'" - pnpm install -w ${PNPM_OPTIONS:-} "local_plugins/${plugin}/" + # The `./` prefix is required: without it pnpm treats `local_plugins/` + # as a scoped package name (or git URL) rather than a local directory. + pnpm install -w ${PNPM_OPTIONS:-} "./local_plugins/${plugin}/" else ( echo "Error. Directory 'local_plugins/${plugin}' for local plugin " \ "'${plugin}' missing" >&2 )