Skip to content

Fix offline/air-gapped Docker boot: stop pnpm self-provisioning the pinned version (#7911)#7918

Merged
JohnMcLear merged 2 commits into
developfrom
pnpm-offline-startup-7911
Jun 9, 2026
Merged

Fix offline/air-gapped Docker boot: stop pnpm self-provisioning the pinned version (#7911)#7918
JohnMcLear merged 2 commits into
developfrom
pnpm-offline-startup-7911

Conversation

@JohnMcLear

@JohnMcLear JohnMcLear commented Jun 9, 2026

Copy link
Copy Markdown
Member

PR Summary by Qodo

Fix offline Docker startup by preventing pnpm self-provisioning
🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 📝 Documentation 🕐 20-40 Minutes

Grey Divider

Walkthroughs

User Description

What

Addresses item 1 of #7911 — the reporter confirmed they hit it on the official Docker image running node directly, with:

[WARN] plugins - Failed to get pnpm version: Error: Command exited with code 1: pnpm --version

Root cause

The official image installs pnpm directly via npm (corepack was dropped for Node 25+). Standalone pnpm still honours the "packageManager" pin in package.json via its built-in version management (the pnpm 11 pmOnFail setting, successor to managePackageManagerVersions). The image shipped pnpm 11.0.6 against a pnpm@11.1.2 pin — so every pnpm call, including the informational pnpm --version probe Etherpad runs at startup (plugins.ts), tried to download and run the pinned 11.1.2 build. Behind a firewall that download fails → non-zero exit → the warning above and a broken offline boot.

Note: this is a standalone-pnpm behaviour, not corepack — which is why the earlier "no package manager runs at startup" read was incomplete. pnpm --version itself self-provisions.

Fix (defence-in-depth)

  • Dockerfile: realign ARG PnpmVersion to the package.json pin (11.1.2) so there's no mismatch to trigger a download, and set pnpm_config_pm_on_fail=ignore in the build + runtime stages so any future drift — or any other offline pnpm call in the container (updater, ad-hoc pnpm in an exec shell) — uses the installed pnpm instead of reaching for the network.
  • App level (covers non-Docker offline installs too): the startup pnpm --version probe in plugins.ts and the updater's pnpmOnPath checks (updater/index.ts, updateActions.ts) now run with pnpm_config_pm_on_fail=ignore. The probe is purely informational — it should only ever read the local version, never self-provision.

Tests

New backend spec dockerfilePnpmPin.ts fails CI if (a) the Dockerfile pnpm version drifts from the packageManager pin again — the exact regression that caused this — or (b) the offline flag is dropped. Existing updater specs (updateActions, updater-integration, scheduler/window) still green on Node 24.

Verification

Reproduced with a standalone (non-corepack) pnpm — the Docker setup:

invocation result
pnpm --version with a mismatched packageManager pin (default) tries to fetch the pinned build → exit 1 (offline failure)
same, with pnpm_config_pm_on_fail=ignore prints the local version → exit 0, no network

Scope

Item 1 only. Item 2 (env-var overrides for the outbound calls) is #7917. Together they close #7911.

🤖 Generated with Claude Code

AI Description
• Align Docker image pnpm version with the package.json packageManager pin to avoid network
  self-provisioning.
• Force pnpm invocations at startup and during updates to ignore pin mismatches in
  offline/air-gapped environments.
• Add CI regression coverage to prevent future Dockerfile pnpm pin/env drift.
Diagram
graph TD
A["package.json (pnpm pin)"] --> B["Dockerfile (PnpmVersion + pm_on_fail)"] --> C["Container runtime"]
C --> D["plugins.ts ('pnpm --version' probe)"] --> F["pnpm CLI"] --> G{{"Pinned build download"}}
C --> E["Updater (pnpmOnPath checks)"] --> F
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Remove the startup pnpm version probe entirely
  • ➕ Eliminates any chance of pnpm-related startup failures
  • ➕ Slightly reduces startup work/log noise
  • ➖ Loses useful diagnostics for support/debugging
  • ➖ Does not address updater pnpm checks or ad-hoc pnpm use in containers
2. Rely on corepack-managed pnpm in the Docker image
  • ➕ Corepack can enforce the pinned version consistently
  • ➕ Avoids standalone pnpm’s version-management surprises
  • ➖ Corepack is no longer shipped in Node 25+ base images (per PR context)
  • ➖ Adds complexity/variance across Node image versions
3. Detect pnpm availability/version without executing pnpm (e.g., inspect installed package files)
  • ➕ Avoids triggering pnpm’s self-provisioning behavior by design
  • ➕ No subprocess execution needed
  • ➖ More brittle across pnpm install layouts and packaging methods
  • ➖ Still wouldn’t protect other runtime pnpm invocations from self-provisioning

Recommendation: Keep the PR’s approach: (1) align the Docker-installed pnpm version with the repo’s packageManager pin and (2) defensively set pnpm_config_pm_on_fail=ignore for all pnpm probes (Docker + app code). This directly targets the failure mode, improves offline robustness beyond Docker, and the added regression test prevents recurrence with minimal ongoing maintenance.

Grey Divider

File Changes

Bug fix (3)
updateActions.ts Make updater preflight pnpm detection offline-safe +5/-1

Make updater preflight pnpm detection offline-safe

• Runs the pnpm --version probe with pnpm_config_pm_on_fail=ignore so a packageManager pin mismatch does not cause a non-zero exit (and false pnpm absence) in offline environments.

src/node/hooks/express/updateActions.ts


index.ts Harden scheduler updater pnpmOnPath checks for offline installs +5/-1

Harden scheduler updater pnpmOnPath checks for offline installs

• Applies pnpm_config_pm_on_fail=ignore to the updater scheduler’s pnpm --version check to prevent self-provisioning attempts and false negatives when offline.

src/node/updater/index.ts


plugins.ts Prevent startup pnpm version probe from triggering network downloads +8/-1

Prevent startup pnpm version probe from triggering network downloads

• Executes the informational pnpm --version startup probe with pnpm_config_pm_on_fail=ignore so it only reports the locally installed pnpm and does not fail in air-gapped/firewalled setups.

src/static/js/pluginfw/plugins.ts


Tests (1)
dockerfilePnpmPin.ts Add regression test for Docker pnpm pin + offline flag +55/-0

Add regression test for Docker pnpm pin + offline flag

• Introduces a backend spec that asserts Dockerfile ARG PnpmVersion matches the packageManager pin in package.json and that pnpm_config_pm_on_fail=ignore is set, preventing regressions that break offline Docker startup.

src/tests/backend/specs/dockerfilePnpmPin.ts


Documentation (1)
CHANGELOG.md Document offline Docker pnpm self-provisioning fix +1/-0

Document offline Docker pnpm self-provisioning fix

• Adds a changelog entry explaining the offline/air-gapped Docker startup failure mode and the defence-in-depth fix (pin alignment + pm_on_fail ignore + regression test).

CHANGELOG.md


Other (1)
Dockerfile Pin pnpm to packageManager and disable self-provisioning on mismatch +20/-1

Pin pnpm to packageManager and disable self-provisioning on mismatch

• Updates ARG PnpmVersion to match the package.json packageManager pin and adds pnpm_config_pm_on_fail=ignore to build and runtime stages. This prevents pnpm from trying to download a pinned build when offline and versions drift.

Dockerfile


Grey Divider

Qodo Logo

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Grey Divider


Remediation recommended

1. Env test misses regressions 🐞 Bug ☼ Reliability
Description
The new Dockerfile regression spec only checks that ENV pnpm_config_pm_on_fail=ignore appears
somewhere in the Dockerfile, so CI would still pass if it is accidentally removed from the
runtime-inherited build stage (reintroducing offline failures) but left in the unrelated
adminbuild stage.
Code

src/tests/backend/specs/dockerfilePnpmPin.ts[R50-52]

+    it('sets pnpm_config_pm_on_fail=ignore so offline boots do not self-provision', function () {
+      assert.match(dockerfile, /ENV\s+pnpm_config_pm_on_fail=ignore/,
+          'Dockerfile must set pnpm_config_pm_on_fail=ignore for offline robustness (issue #7911).');
Evidence
The test’s regex matches the entire Dockerfile content, but runtime stages inherit from the build
stage, not adminbuild. If the build stage ENV is removed while adminbuild keeps it, the
current test would still pass even though runtime offline behavior regresses.

src/tests/backend/specs/dockerfilePnpmPin.ts[41-53]
Dockerfile[18-49]
Dockerfile[167-190]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The regression test `dockerfilePnpmPin.ts` currently uses a whole-file regex to assert `ENV pnpm_config_pm_on_fail=ignore` exists. This can miss the real regression you care about: removing the ENV from the `build` stage (which runtime stages inherit from) while leaving it in `adminbuild`.

## Issue Context
The Dockerfile sets `ENV pnpm_config_pm_on_fail=ignore` in both `adminbuild` and `build`. Runtime stages (`development`, `production`) inherit from `build_${BUILD_ENV}` (which derives from `build`), not from `adminbuild`.

## Fix Focus Areas
- src/tests/backend/specs/dockerfilePnpmPin.ts[41-53]

### Implementation notes
Update the spec to verify the ENV is set in the runtime-inherited stage. For example:
- Extract the `build` stage block by finding `^FROM .* AS build$` and slicing until the next `^FROM ` line, then assert that block contains `ENV pnpm_config_pm_on_fail=ignore`.
 - or, assert there is an `ENV pnpm_config_pm_on_fail=ignore` after the `FROM ... AS build` line (and before the next `FROM`).
- Optionally also assert there is no conflicting later `ENV pnpm_config_pm_on_fail=` override in runtime stages.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

…e boot (#7911)

The official Docker image installs pnpm directly via npm (corepack was dropped
for Node 25+). Standalone pnpm still honours the "packageManager" pin in
package.json: the image's pnpm intentionally lags that pin (pnpm 11.1.x enforces
a minimum-release-age policy the frozen-lockfile build can't satisfy), so pnpm
treats every invocation — including the informational `pnpm --version` probe
Etherpad runs at startup — as a request to download and run the pinned build.
Behind a corporate firewall / in an air-gapped install that download fails:

  [WARN] plugins - Failed to get pnpm version: Error: Command exited with
  code 1: pnpm --version

which is what #7911 reported.

Fix — neutralise the gap instead of closing it (closing it would break the
frozen-lockfile build on 11.1.x):

  - Dockerfile build stage sets `pnpm_config_pm_on_fail=ignore` (the pnpm 11
    successor to managePackageManagerVersions), inherited by the development and
    production runtime stages. pnpm then uses the installed pnpm instead of
    fetching the pinned one. It does not change which pnpm runs the build-time
    install, so the frozen-lockfile build is unaffected.
  - plugins.ts startup probe and the updater's pnpm-on-PATH checks run with the
    same flag, so the fix also covers non-Docker offline installs and the probe
    can never fail-loud.

Add a backend spec that fails CI if the offline guard is dropped while the image
pnpm differs from the package.json pin.

Verified with a standalone (non-corepack) pnpm: a "packageManager" mismatch
makes `pnpm --version` exit 1 by default (tries to fetch the pinned build), and
exit 0 reading the local version with pm_on_fail=ignore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JohnMcLear JohnMcLear force-pushed the pnpm-offline-startup-7911 branch from 9623f9c to d589296 Compare June 9, 2026 08:03
…tage

Address Qodo review: the regression spec matched ENV pnpm_config_pm_on_fail
anywhere in the Dockerfile, so it would still pass if the guard were removed
from the `build` stage (which the runtime stages inherit) but left in the
throwaway `adminbuild` stage — reintroducing the offline failure. Extract the
`build` stage block and assert the ENV is present there specifically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JohnMcLear

Copy link
Copy Markdown
Member Author

Thanks Qodo — actioned in 87215a7. You're right that matching ENV pnpm_config_pm_on_fail=ignore anywhere in the Dockerfile was too weak: only the build stage is inherited by the runtime (development/production) stages, so the guard could be dropped there but survive in the throwaway adminbuild stage and the test would still pass. The spec now extracts the FROM … AS build block and asserts the ENV is present there specifically (verified the extracted block excludes adminbuild).

@JohnMcLear JohnMcLear merged commit b19ad89 into develop Jun 9, 2026
32 checks passed
@JohnMcLear JohnMcLear deleted the pnpm-offline-startup-7911 branch June 9, 2026 08:31
JohnMcLear added a commit that referenced this pull request Jun 9, 2026
…7921)

The `# 3.3.0` changelog section was written before ~8 commits landed on
develop. #7909 (dark mode) and #7918/#7911 (offline Docker boot) were already
captured; this adds the remaining user-facing change and dependency bumps so
the 3.3.0 release notes are complete:

- Notable fixes: #7910 — Firefox authorship-colour flake (early keystrokes
  tagged author='' before the async userAuthor propagated, producing an
  unattributed insert the pad-corruption guard rejected).
- Dependencies: mysql2 →3.22.5 (#7915), undici →8.4.1 (#7914), pdfkit
  →0.19.0 (#7916), @radix-ui/react-switch →1.3.0 (#7913), and the extra
  dev-dependency group bump (#7912).

Localisation (translatewiki) is already covered by the existing entry.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Etherpad does not start behind corporate firewall.

1 participant