feat(admin): show "requires newer Etherpad" when installing incompatible plugin (#7763)#7771
Conversation
…ble plugin (#7763) Old admins on out-of-date Etherpad installations get no feedback when they click Install on a plugin that needs a newer core. live-plugin-manager doesn't honor engines.node, and the admin UI dropped the error payload that adminplugins.ts already emits. This wires up an end-to-end signal: - pluginEngineCheck.ts: pure helper comparing a plugin's engines.node range against process.version, with a stable error code (PLUGIN_REQUIRES_NEWER_ETHERPAD) and a message that avoids leaking the Node-version implementation detail. Unparseable ranges fall through as compatible so the preflight is opportunistic, not a gate. 8 unit tests cover the happy + edge paths. - installer.ts: install() now best-effort fetches the published plugin's engines.node from npmjs.org, runs the preflight, and short-circuits with the typed error before invoking live-plugin-manager. Also wraps the body in try/catch so the error reaches the socket callback (it was silently dropped on every install failure today, including network errors). - HomePage.tsx: surfaces finished:install.error as a toast, using a dedicated i18n key when the code is PLUGIN_REQUIRES_NEWER_ETHERPAD and a generic fallback otherwise. - en.json: two new strings, parameterized by {{plugin}} and {{error}}. The message admins see is intentionally about Etherpad, not Node — upgrading Etherpad pulls the Node requirement along with it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
Review Summary by QodoAdd plugin engine compatibility check and error feedback
WalkthroughsDescription• Add engine compatibility check before plugin installation - Fetches plugin's engines.node from npm registry - Prevents incompatible plugins from installing on outdated Etherpad • Wire error feedback to admin UI via socket payload - Wraps install logic in try/catch to capture all failures - Surfaces install errors as toast notifications • Add user-facing messages for incompatible plugins - Dedicated message for newer Etherpad requirement - Generic fallback for other installation errors Diagramflowchart LR
A["Admin clicks Install"] --> B["Fetch engines.node from npm"]
B --> C["Check compatibility"]
C -->|Compatible| D["Install plugin"]
C -->|Incompatible| E["Throw EngineIncompatibleError"]
D --> F["Success callback"]
E --> G["Error callback"]
F --> H["Socket: finished:install"]
G --> H
H --> I["Admin UI displays toast"]
File Changes1. src/static/js/pluginfw/pluginEngineCheck.ts
|
Code Review by Qodo
1. No Node.js 25 mentioned
|
| "admin_plugins.install_error": "Failed to install {{plugin}}: {{error}}", | ||
| "admin_plugins.install_error_requires_newer_etherpad": "Cannot install {{plugin}}: it requires a newer version of Etherpad. Please upgrade Etherpad and try again.", |
There was a problem hiding this comment.
1. No node.js 25 mentioned 📎 Requirement gap ≡ Correctness
The new user-facing install error text explicitly avoids mentioning the Node.js 25 requirement, but the checklist requires clear messaging that Node.js 25 is needed (and that upgrading Etherpad/core/runtime is required). Admins will only see “upgrade Etherpad” without the mandated Node.js 25 requirement callout.
Agent Prompt
## Issue description
PR Compliance ID 1 requires the plugin install flow to clearly communicate the Node.js 25 requirement and the need to update Etherpad/core/runtime. The newly added admin toast strings and error message do not mention Node.js 25 (and tests enforce that), which violates the requirement.
## Issue Context
The new incompatibility handling uses `PLUGIN_REQUIRES_NEWER_ETHERPAD` and shows `admin_plugins.install_error_requires_newer_etherpad`, but the text only instructs to upgrade Etherpad.
## Fix Focus Areas
- src/locales/en.json[69-70]
- src/static/js/pluginfw/pluginEngineCheck.ts[41-44]
- src/tests/backend-new/specs/pluginEngineCheck.test.ts[52-58]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Disagreeing with this one — the absence of "Node.js 25" in the user-facing string is intentional, not an oversight.
Per the design discussion on #7763, the message is deliberately phrased in terms admins can act on directly: "upgrade Etherpad." Three reasons:
- The plugin doesn't really need Node 25 — it needs an Etherpad version that itself requires Node 25. Node is a downstream constraint, not the actual gate.
- The upgrade chain handles Node automatically. When the admin upgrades Etherpad, Etherpad's own
engines.nodeenforcement (enforceMinNodeVersionat startup) raises the Node-version concern at the right layer — with the right error message, in the right context. - "You need Node 25" is the wrong abstraction for an admin clicking Install in the plugin manager. They didn't ask about runtimes; they asked about plugins. Telling them about Node would make them ask "do I have to install Node manually first? then Etherpad? in what order?" The single signal "upgrade Etherpad" is concrete and complete.
The test asserting the message doesn't mention Node is enforcing this design, not papering over a gap. The compliance checklist's "mention Node 25" rule conflicts with the UX intent agreed on the issue thread.
Happy to revisit if there's a separate concern (e.g. logs/error class — EngineIncompatibleError.required already carries the raw range for log forensics), but the admin-facing string stays as-is.
…iled Qodo PR review on #7771 caught a side effect introduced earlier in this PR. Before this PR, install() never invoked its callback on error, so a failed install left the task counter inflated and onAllTasksFinished() never ran — masking, but not fixing, the bug. Once install() correctly propagates errors to its cb, the counter hits zero on failure too, and onAllTasksFinished() fires hooks.aCallAll('restartServer'). A no-op preflight rejection (EngineIncompatibleError) would then disconnect every connected pad. Fix: extract wrapTaskCb + task state into InstallerTaskQueue and track whether at least one task in the current batch succeeded. Only fire the "all finished" side effect when something actually changed. Failed-only batches do nothing. Seven unit tests cover the matrix: single success, single failure (the regression), mixed batch (still restarts), all-failed batch, batch reset, null cb, two-task drain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Qodo PR review on #7771 flagged that fetchPluginEnginesNode awaits fetch() with no timeout. A stalled DNS lookup or hung connection to registry.npmjs.org would block install() forever — the finished:install socket event would never fire and the admin UI would stay spinning with no error to surface. Wrap the fetch in AbortSignal.timeout(5000). On any failure (network, HTTP error, abort) fetchPluginEnginesNode returns undefined, which the preflight then treats as "no engines info → compatible," so a slow registry never blocks an install that would otherwise succeed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Working through Qodo's second pass. Finding 4 — "Registry fetch can hang" — accepted, fixed in 912da05. Wrapped the Finding 3 — "Preflight lacks feature flag" — disagreeing. The cited AGENTS.MD rule ("New features should be placed behind feature flags and disabled by default") exists and is real. The question is whether this PR introduces a "new feature" in the sense that rule protects against. Concretely, the PR has three changes:
(1) and (2) are bug fixes, not features — same shape as fixing a swallowed exception or a missing error display. Flagging those default-off would mean "by default, continue dropping errors silently," which is the opposite of what every PR comment, every test, and the issue itself asks for. (3) is the only piece that's genuinely new behavior. But its failure modes are bounded by design:
The rule exists to prevent novel failure modes from being enabled-by-default in production. This preflight's failure modes are: (a) intended (correctly rejecting incompatible plugins, with a clearer message than the silent crash they'd get otherwise) and (b) silent fallthrough (never worse than today). Gating it default-off would mean the admins #7763 is about — old installs who don't know to look — would still get silent failures, defeating the entire PR. I considered adding a
If a maintainer wants this gated regardless, I'll add |
|
This is 100% a bug fix, not a feature — Qodo's feature-flag finding (#3) doesn't apply. Closing that thread. |
Closes #7763.
Summary
When an admin on an out-of-date Etherpad clicks Install on a plugin whose
engines.nodedoesn't match the running Node (because the plugin was published against a newer Etherpad that requires newer Node), today they get nothing — no message, no toast, no row state. The plugin silently fails to install or installs and then crashes at hook-load time.Three problems combined:
live-plugin-manager@1.1.0doesn't honorengines.nodeat install time (confirmed: noenginesreference in itsdist/).installer.install()swallowed exceptions — the callback never fired with an error, so even pre-existing install failures (network, 404, dependency conflicts) were dropped before reaching the admin UI.onFinishedInstallignored theerrorfield in the socket payload.Change
pluginEngineCheck.ts— pure helper: takes a plugin'sengines.noderange andprocess.version, returns a typed compatibility result. Unparseable ranges fall through as compatible so the preflight stays opportunistic. Throws anEngineIncompatibleErrorcarrying a stablecode: 'PLUGIN_REQUIRES_NEWER_ETHERPAD'. The user-facing message intentionally avoids mentioning Node — admins are told to upgrade Etherpad, since upgrading core pulls the Node bump along with it.installer.ts—install()now best-effort fetches the published plugin'sengines.nodefrom npmjs.org, runs the preflight, and short-circuits before invoking live-plugin-manager. Body wrapped in try/catch so the typed error (and any other install failure) reachescb— and therefore thefinished:installsocket payload.HomePage.tsx—onFinishedInstallnow surfacesdata.erroras a toast. Uses thePLUGIN_REQUIRES_NEWER_ETHERPADcode to pick a dedicated i18n key; falls back to a genericinstall_errorstring for everything else.en.json— two new strings parameterized by{{plugin}}and{{error}}.Test plan
pnpm exec vitest run— 36 files / 610 tests pass (incl. 8 new tests inpluginEngineCheck.test.tscovering compatible / below-range / no-engines / unparseable-range / error-class shape / message-does-not-mention-Node)pnpm exec tsc --noEmitclean (src + admin)pnpm run buildclean (admin)engines.node: '>=99.0.0'→ click Install → toast"Cannot install ep_xxx: it requires a newer version of Etherpad..."Notes
npm registry → engines.nodefetch is best-effort: any failure (network, 404, parse) falls through to the existing install path. The preflight is a UX shortcut, not a gate.static.etherpad.org/plugins.json), which doesn't currently carry engines data. A future PR could add a "Requires newer Etherpad" badge to the available-plugins table by populating that field; the code-side preflight is the priority because it's the only thing the admin sees at the install moment.pnpm lintconfig is broken ondevelop(ESLint 10 expectseslint.config.js); verified the same failure exists ondevelopwithout my changes. Out of scope here.🤖 Generated with Claude Code