fix: Keep dev-metrics shims off pnpm-managed binaries (no-changelog) (#37982)

Co-authored-by: Claude Fable 5 <[email protected]>
This commit is contained in:
Charlie Kolb
2026-09-08 13:16:20 +00:00
committed by GitHub
co-authored by Claude Fable 5
parent 793136b7e1
commit a3f53d66f5
3 changed files with 101 additions and 8 deletions
+38 -5
View File
@@ -149,10 +149,25 @@ function isOurShim(path) {
const pathDirs = () => (process.env.PATH ?? '').split(':').filter(Boolean);
/** First executable `bin` on PATH (may be our shim). */
// Binaries that pnpm manages itself: the `packageManager` version store
// (`<PNPM_HOME>/store/vN/links/...`) and its self-update install dir
// (`<PNPM_HOME>/.tools/pnpm/<version>/...`). pnpm prepends the running binary's
// dir to PATH for lifecycle scripts, so a `pnpm install` under a version-switched
// pnpm puts these first on PATH. Shimming them corrupts pnpm's version switching
// — never touch them, target the durable install instead. The shape checks match
// on any prefix because PNPM_HOME is not always set in the environment.
function isPnpmManagedPath(p) {
if (/\/store\/v\d+\/links\//.test(p) || /\/\.tools\/pnpm\//.test(p)) return true;
const home = process.env.PNPM_HOME;
if (!home) return false;
return p.startsWith(join(home, 'store') + '/') || p.startsWith(join(home, '.tools') + '/');
}
/** First shimmable `bin` on PATH (may be our shim; never a pnpm-managed one). */
function whichOnPath(bin) {
for (const d of pathDirs()) {
const p = join(d, bin);
if (isPnpmManagedPath(p)) continue;
if (isExecutable(p)) return p;
}
return '';
@@ -162,6 +177,7 @@ function whichOnPath(bin) {
function resolveRealBinary(bin) {
for (const d of pathDirs()) {
const p = join(d, bin);
if (isPnpmManagedPath(p)) continue;
if (!isExecutable(p)) continue;
if (!isOurShim(p)) return p; // genuine
const saved = p + SAVED_SUFFIX;
@@ -190,7 +206,27 @@ function renderShim(file, realExec, bin) {
chmodSync(file, 0o755);
}
/** Put the saved original back in place of a shim (or remove a stray shim). */
function restoreOne(p) {
const saved = p + SAVED_SUFFIX;
if (existsSync(saved))
renameSync(saved, p); // restore original
else rmSync(p, { force: true }); // stray shim with no saved original
}
function installOne(bin) {
// Heal installs made before the pnpm-managed-path guard existed: restore any
// shim inside a pnpm-managed dir, then shim the durable binary below.
for (const d of pathDirs()) {
const p = join(d, bin);
if (!isPnpmManagedPath(p) || !isOurShim(p)) continue;
try {
restoreOne(p);
} catch {
// unwritable managed dir — the durable binary below still gets the shim
}
}
const front = whichOnPath(bin);
// Already shimmed: re-render only if this checkout's template is newer
@@ -255,10 +291,7 @@ function uninstallBinaries() {
const restored = [];
for (const p of new Set([...recorded, ...onPath])) {
if (!isOurShim(p)) continue;
const saved = p + SAVED_SUFFIX;
if (existsSync(saved))
renameSync(saved, p); // restore original
else rmSync(p, { force: true }); // stray shim with no saved original
restoreOne(p);
restored.push(p);
}
return restored;
+6 -3
View File
@@ -1,5 +1,5 @@
#!/bin/sh
# n8n-shadow-shim-version: 1
# n8n-shadow-shim-version: 2
#
# n8n dev metrics — binary shim (template). setup.mjs renders this per shadowed
# CLI and installs it in place of the real binary (the original is saved next to
@@ -19,9 +19,12 @@ __bindir='__N8N_BINDIR__'
__tracker='__N8N_TRACKER__' # installed copy of track.mjs, refreshed on each install
# If the baked real binary moved (e.g. corepack/pnpm upgrade), re-resolve it via
# PATH with our own directory removed — never resolving back to this shim.
# PATH with our own directory removed — never resolving back to this shim. Also
# skip pnpm-managed dirs (version store, .tools/pnpm): resolving to one of those
# can run a different pnpm version than the project pinned.
if [ ! -x "$__real" ]; then
__cp=$(printf '%s' "${PATH:-}" | tr ':' '\n' | grep -vxF "$__bindir" | paste -sd: -)
__cp=$(printf '%s' "${PATH:-}" | tr ':' '\n' | grep -vxF "$__bindir" \
| grep -v -e '/store/v[0-9]*/links/' -e '/\.tools/pnpm/' | paste -sd: -)
__real=$(PATH="$__cp" command -v "$__bin" 2>/dev/null)
[ -n "$__real" ] || exec env PATH="$__cp" "$__bin" "$@" # last resort, no loop
fi
+57
View File
@@ -77,5 +77,62 @@ grep -q "n8n-shadow-shim-version" "$BIN/pnpm" && fail "shim left after reset"
echo "$out" | grep -q "NEW probe" || fail "upgrade downgraded via stale .n8n-real"
)
# Regression: never shim a binary inside pnpm's own management dirs (the
# packageManager version store, .tools) — a shim there corrupts pnpm's version
# switching. The durable binary further down PATH gets the shim instead, and a
# managed binary that an older setup already shimmed is restored.
(
PH="$T/pnpm-home"; STORE="$PH/store/v11/links/@/pnpm/9.9.9/hash/bin"; DUR="$T/bin3"
mkdir -p "$STORE" "$DUR"
printf '#!/bin/sh\necho "STORE $*"\n' > "$STORE/pnpm"; chmod +x "$STORE/pnpm"
printf '#!/bin/sh\necho "DURABLE $*"\n' > "$DUR/pnpm"; chmod +x "$DUR/pnpm"
export N8N_USER_FOLDER="$T/uf3" N8N_DEV_TELEMETRY=0 PNPM_HOME="$PH" PATH="$STORE:$DUR:$PATH"
node "$SRC/setup.mjs" --enable >/dev/null
grep -q "n8n-shadow-shim-version" "$STORE/pnpm" && fail "pnpm-managed store binary was shimmed"
grep -q "n8n-shadow-shim-version" "$DUR/pnpm" || fail "durable binary not shimmed behind a store binary"
# Simulate an older setup that shimmed the store binary: re-enable must heal it.
mv "$STORE/pnpm" "$STORE/pnpm.n8n-real"
sed "s|__N8N_BIN__|pnpm|;s|__N8N_REAL__|$STORE/pnpm.n8n-real|;s|__N8N_BINDIR__|$STORE|;s|__N8N_TRACKER__|/nonexistent|" \
"$SRC/shadow-shim.sh" > "$STORE/pnpm"
chmod +x "$STORE/pnpm"
node "$SRC/setup.mjs" --enable >/dev/null
grep -q "n8n-shadow-shim-version" "$STORE/pnpm" && fail "pre-existing store shim not healed"
out=$("$STORE/pnpm" probe 2>&1) || true
echo "$out" | grep -q "STORE probe" || fail "store binary not restored to the original"
)
# Regression: the `.tools` match is scoped to pnpm's own layout
# (.tools/pnpm/<version>/...) and works without PNPM_HOME. A durable binary
# under an unrelated .tools dir must still get the shim.
(
TOOLS="$T/dev/.tools/bin"; MANAGED="$T/ph2/.tools/pnpm/9.9.9/bin"
mkdir -p "$TOOLS" "$MANAGED"
printf '#!/bin/sh\necho "TOOLS $*"\n' > "$TOOLS/pnpm"; chmod +x "$TOOLS/pnpm"
printf '#!/bin/sh\necho "MANAGED $*"\n' > "$MANAGED/pnpm"; chmod +x "$MANAGED/pnpm"
export N8N_USER_FOLDER="$T/uf4" N8N_DEV_TELEMETRY=0 PATH="$MANAGED:$TOOLS:$PATH"
unset PNPM_HOME
node "$SRC/setup.mjs" --enable >/dev/null
grep -q "n8n-shadow-shim-version" "$MANAGED/pnpm" && fail "managed .tools binary was shimmed without PNPM_HOME"
grep -q "n8n-shadow-shim-version" "$TOOLS/pnpm" || fail "durable binary under unrelated .tools not shimmed"
)
# Regression: a heal failure (unwritable managed dir) must not abort the
# install — the durable binary still gets the shim.
(
PH="$T/ph3"; STORE="$PH/store/v11/links/@/pnpm/9.9.9/hash/bin"; DUR="$T/bin4"
mkdir -p "$STORE" "$DUR"
printf '#!/bin/sh\necho "DURABLE $*"\n' > "$DUR/pnpm"; chmod +x "$DUR/pnpm"
printf '#!/bin/sh\necho "STORE-REAL $*"\n' > "$STORE/pnpm.n8n-real"; chmod +x "$STORE/pnpm.n8n-real"
sed "s|__N8N_BIN__|pnpm|;s|__N8N_REAL__|$STORE/pnpm.n8n-real|;s|__N8N_BINDIR__|$STORE|;s|__N8N_TRACKER__|/nonexistent|" \
"$SRC/shadow-shim.sh" > "$STORE/pnpm"
chmod +x "$STORE/pnpm"
chmod a-w "$STORE" # healing the store shim cannot rename here
export N8N_USER_FOLDER="$T/uf5" N8N_DEV_TELEMETRY=0 PNPM_HOME="$PH" PATH="$STORE:$DUR:$PATH"
node "$SRC/setup.mjs" --enable >/dev/null
chmod u+w "$STORE" # so cleanup can remove the temp dir
grep -q "n8n-shadow-shim-version" "$DUR/pnpm" || fail "heal failure aborted the durable shim install"
)
rm -rf "$T" "$EV"
echo "ALL PASS"