|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +// Regression tests for ether/etherpad#7911. |
| 4 | +// |
| 5 | +// The official Docker image installs pnpm directly (corepack was dropped for |
| 6 | +// Node 25+). Standalone pnpm still honours the "packageManager" pin in |
| 7 | +// package.json: when the pnpm baked into the image differs from that pin, pnpm |
| 8 | +// treats every invocation — including the informational `pnpm --version` probe |
| 9 | +// Etherpad runs at startup — as a request to self-provision the pinned build. |
| 10 | +// With no outbound network (air-gapped / behind a corporate firewall) that |
| 11 | +// download fails and pnpm exits non-zero, surfacing as `Failed to get pnpm |
| 12 | +// version` and breaking offline boots. |
| 13 | +// |
| 14 | +// The image deliberately lags the pin (pnpm 11.1.x enforces a minimum-release- |
| 15 | +// age policy the frozen-lockfile build can't satisfy), so the guard is not to |
| 16 | +// force the versions equal but to neutralise the gap: the Dockerfile must set |
| 17 | +// pnpm_config_pm_on_fail=ignore so pnpm uses the installed version instead of |
| 18 | +// reaching for the network. This test fails if that guard is dropped while a |
| 19 | +// version gap exists. |
| 20 | + |
| 21 | +const assert = require('assert').strict; |
| 22 | +import fs from 'fs'; |
| 23 | +import path from 'path'; |
| 24 | + |
| 25 | +const repoRoot = path.join(__dirname, '../../../../'); |
| 26 | +const readRepoFile = (rel: string) => fs.readFileSync(path.join(repoRoot, rel), 'utf8'); |
| 27 | + |
| 28 | +describe(__filename, function () { |
| 29 | + describe('Docker pnpm offline guard (issue #7911)', function () { |
| 30 | + let dockerfile: string; |
| 31 | + let imagePnpm: string; |
| 32 | + let pinPnpm: string; |
| 33 | + let guardPresent: boolean; |
| 34 | + |
| 35 | + before(function () { |
| 36 | + dockerfile = readRepoFile('Dockerfile'); |
| 37 | + |
| 38 | + const argMatch = dockerfile.match(/^ARG PnpmVersion=(\S+)/m); |
| 39 | + assert.ok(argMatch, 'Dockerfile must declare `ARG PnpmVersion=<version>`'); |
| 40 | + imagePnpm = argMatch![1]; |
| 41 | + |
| 42 | + const pkg = JSON.parse(readRepoFile('package.json')); |
| 43 | + const pinMatch = String(pkg.packageManager || '').match(/^pnpm@(.+)$/); |
| 44 | + assert.ok(pinMatch, `expected packageManager "pnpm@<version>", got "${pkg.packageManager}"`); |
| 45 | + pinPnpm = pinMatch![1]; |
| 46 | + |
| 47 | + // The guard only protects runtime if it lives in the `build` stage, which |
| 48 | + // the development/production runtime stages inherit from. The same ENV in |
| 49 | + // the throwaway `adminbuild` stage would NOT reach runtime, so scope the |
| 50 | + // check to the build stage block (from `FROM ... AS build` to the next |
| 51 | + // `FROM`) rather than matching anywhere in the file. |
| 52 | + const buildStage = dockerfile.match( |
| 53 | + /^FROM\s+\S+\s+AS\s+build\b[\s\S]*?(?=^FROM\s)/m); |
| 54 | + assert.ok(buildStage, 'Dockerfile must define a `FROM ... AS build` stage'); |
| 55 | + guardPresent = /ENV\s+pnpm_config_pm_on_fail=ignore/.test(buildStage![0]); |
| 56 | + }); |
| 57 | + |
| 58 | + it('neutralises any pnpm version gap so offline boots do not self-provision', function () { |
| 59 | + // The actual safety property: a mismatch between the image pnpm and the |
| 60 | + // package.json pin is only safe when pm_on_fail=ignore is set. (If they |
| 61 | + // are ever realigned, the guard becomes belt-and-suspenders, not required.) |
| 62 | + if (imagePnpm !== pinPnpm) { |
| 63 | + assert.ok(guardPresent, |
| 64 | + `Dockerfile pnpm ${imagePnpm} differs from package.json pnpm@${pinPnpm}, ` + |
| 65 | + 'but pnpm_config_pm_on_fail=ignore is not set — pnpm will try to ' + |
| 66 | + 'self-provision the pinned version and break offline/air-gapped ' + |
| 67 | + 'startup (issue #7911).'); |
| 68 | + } |
| 69 | + }); |
| 70 | + |
| 71 | + it('sets pnpm_config_pm_on_fail=ignore in the runtime-inherited build stage', function () { |
| 72 | + assert.ok(guardPresent, |
| 73 | + 'The `build` stage must set pnpm_config_pm_on_fail=ignore (it is ' + |
| 74 | + 'inherited by the development/production runtime stages) so runtime ' + |
| 75 | + 'pnpm calls — the startup probe, the updater pnpm check — do not fail ' + |
| 76 | + 'closed when offline (issue #7911). Setting it only in `adminbuild` ' + |
| 77 | + 'would not reach runtime.'); |
| 78 | + }); |
| 79 | + }); |
| 80 | +}); |
0 commit comments