Skip to content

fix(market): give every install staging tree its own pnpm workspace - #487

Open
elkaix wants to merge 1 commit into
dataelement:mainfrom
elkaix:fix/generation-staging-workspace-isolation
Open

elkaix wants to merge 1 commit into
dataelement:mainfrom
elkaix:fix/generation-staging-workspace-isolation

Conversation

@elkaix

@elkaix elkaix commented Sep 21, 2026

Copy link
Copy Markdown

Problem

installGeneration writes a pnpm-workspace.yaml into the staging workspace only when build approvals exist to forward. Without one, pnpm resolves the workspace root by walking the ancestor chain — and when a directory above the staging tree is itself a pnpm workspace (a home folder used as one is the usual case), that workspace claims the install together with its store. Its node_modules/.modules.yaml records the store of a different pnpm major than the bundled one, so pnpm exits with ERR_PNPM_UNEXPECTED_STORE in a few hundred milliseconds and the market reports the plugin as unbuildable.

Reproduction on macOS (~/pnpm-workspace.yaml + ~/node_modules linked to store v11, packageManager: pnpm@11, bundled pnpm 10.34.5):

staging tree result
in /tmp, no workspace file install succeeds
under $HOME, no workspace file ERR_PNPM_UNEXPECTED_STORE (v11 vs v10), exit 1 in ~234 ms
under $HOME, with pnpm-workspace.yaml install succeeds

dshmarket classifies the failure and advises relinking the profile's node_modules — which never mismatched, so the advice cannot fix it.

Fix

  • stageBuildApprovals always writes the workspace file now. With no approvals it is a minimal manifest (packages: ['.']) that pins the workspace root to the staging tree itself.
  • storeMismatch() recognizes the failure by its code and extracts both named stores; a mismatch that survives triggers exactly one retry in a fresh staging workspace (disposable by construction — a failed install already discards staging, so the retry cannot touch anything outside it).
  • The terminal failure detail names both stores and the ancestor-workspace cause instead of a bare pnpm code line.

Testing

End to end with the app's bundled node+pnpm against the real registry flow (installGeneration imported from the patched file):

  • install with no ancestor workspace: ok: true
  • simulated first-attempt store mismatch: retry engages (2 pnpm calls, traced) and succeeds
  • persistent mismatch: ok: false with the actionable detail naming both stores
  • storeMismatch() returns undefined for unrelated failures
  • node --check clean; diff is confined to packages/dsh-desktop-market-installer/generations/installer.mjs

installGeneration wrote a pnpm-workspace.yaml into the staging workspace only
when build approvals existed to forward. Without one, pnpm resolved the
workspace root by walking the ancestor chain — and when a directory above the
staging tree is itself a pnpm workspace (a home folder used as one is the
usual case), that workspace claimed the install together with ITS store. Its
node_modules records the store of a different pnpm major than the bundled one,
so pnpm exits with ERR_PNPM_UNEXPECTED_STORE in a few hundred milliseconds and
the market reports the plugin as unbuildable.

Reproduced on macOS with ~/pnpm-workspace.yaml + ~/node_modules linked to
store v11 (packageManager pnpm@11): identical staging trees succeed in /tmp,
fail under $HOME without a workspace file, and succeed with one. dshmarket
classifies the failure and points the user at the PROFILE's node_modules,
which never mismatched — the wrong advice for this variant.

- stageBuildApprovals always writes the workspace file now; with no approvals
  it is a minimal manifest (packages: ['.']) that pins the workspace root to
  the staging tree itself.
- storeMismatch() recognizes the failure by its code and extracts both stores;
  a mismatch that survives triggers exactly one retry in a fresh staging
  workspace (disposable by construction — a failed install discards it).
- The terminal failure detail names both stores and the ancestor-workspace
  cause instead of a bare code line.

Verified end to end with the app's bundled node+pnpm against the shipped
registry flow: install without any ancestor workspace succeeds; a simulated
first-attempt mismatch retries and succeeds (2 pnpm calls, traced); a
persistent mismatch fails with the actionable detail; storeMismatch() returns
undefined for unrelated failures.
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.

1 participant