Skip to content

Keep the pnpm lockfiles readable for GitHub's dependency graph - #7

Merged
martinfrancois merged 3 commits into
mainfrom
fix/pmonfail-dependency-graph
Oct 5, 2026
Merged

martinfrancois merged 3 commits into
mainfrom
fix/pmonfail-dependency-graph

Conversation

@martinfrancois

@martinfrancois martinfrancois commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Adds pmOnFail: ignore to the pnpm-workspace.yaml of each pnpm demo (jest-fail-on-console, jest-msw, jest-parameterized, webdriverio) and regenerates their four pnpm-lock.yaml files as single YAML documents, so GitHub's dependency graph and Dependabot can see the npm packages.

Why

pnpm 12 writes pnpm-lock.yaml as two YAML documents whenever packageManager pins pnpm 12 or newer. The first document lists only pnpm and its @pnpm/exe.* binaries. GitHub's static dependency-graph parser for pnpm-lock.yaml reads only that first document. So once the graph builds, it lists no npm package from this repository, Dependabot opens no alert for one, and Renovate's vulnerabilityAlerts pull requests, which start from those alerts, never appear.

Upstream:

  • dependabot/dependabot-core#15904 is the GitHub-side bug and is still open. A dependabot-core change merged in September did not reach the static parser the graph uses for pnpm.
  • pnpm/pnpm#13805: the pnpm maintainer declined to reorder the documents and named pmOnFail: ignore as the supported way to get a single document.

What changed

  • Each of the four pnpm-workspace.yaml files: pmOnFail: ignore, with a comment that links dependabot-core#15904 and says when the setting can go.
  • Each of the four pnpm-lock.yaml files: the first document (pnpm's own binaries) is gone. Line 1 is now lockfileVersion: '9.0' and no file has a --- separator.

The artillery demo has no lockfile, and nothing else in the repository changes.

Trade-off

With pmOnFail: ignore, pnpm stops checking the packageManager pin. It no longer switches itself to that version, and it does not warn or fail. Whatever pnpm is on PATH runs.

  • CI: pnpm/action-setup reads packageManager and installs that exact version itself, so CI does not rely on pnpm switching. The artillery job prepares pnpm 12.4.1 through Corepack and has no package.json, so it is unaffected.
  • Locally: .github/scripts/check_all_demos.sh enables Corepack when pnpm is missing, and Corepack runs the pinned version. If a pnpm is already on PATH that is not the Corepack shim, the script now runs that version in the demos instead of pnpm switching to 12.4.1. A pnpm installed another way runs as its own version. I ran pnpm 12.8.1 against each new lockfile: it reported "using pnpm v12.8.1" instead of switching, which shows pnpm reads the setting, and it left the lockfile byte-identical.
  • The lockfile no longer records the sha512 of the pnpm binaries, and pnpm install --frozen-lockfile no longer fails when packageManager is bumped without regenerating the lockfile, because nothing about pnpm is recorded there to drift.
  • If configDependencies is ever added, pnpm writes the first document again regardless of this setting.

The comment next to the setting links #15904 and says when it can go: once GitHub's graph lists the packages from the second document. Removing it then also means letting pnpm write the two-document lockfile again.

No resolved version changed in the first commit

pnpm does not delete an existing first document by itself: on a scratch project with pnpm 12.6.0, install --lockfile-only and install both left the two-document file in place after the setting was added. So for each lockfile I saved the old file, removed everything up to the second ---, ran corepack pnpm@12.4.1 install --lockfile-only (the pinned version), then corepack pnpm@12.4.1 install --frozen-lockfile.

diff of each old second document against the new lockfile is empty, and the sorted packages: keys match before and after: 324 (jest-fail-on-console), 357 (jest-msw), 323 (jest-parameterized), 551 (webdriverio).

In that commit each lockfile diff is 158 deleted lines: the 157-line pnpm document plus its --- separator. Nothing was added or changed.

Checks run

  • corepack pnpm@12.4.1 install --frozen-lockfile in all four demos, in this checkout and in a fresh git clone --depth 1 of the branch: all pass, the lockfiles stay single-document, the worktree stays clean.
  • The three Jest demos, run like CI (pnpm test --ci --json, then .github/scripts/check_results.py): jest-fail-on-console OK (3 results, with the deliberate console.error failure expected), jest-msw OK (1), jest-parameterized OK (3).
  • webdriverio: pnpm exec tsc --noEmit -p . passes. I did not run the browser test.
  • Not run: .github/scripts/check_all_demos.sh --no-codex, because it also builds the Java demos and starts containers. CI on this pull request runs every demo.

Dependency graph and Dependabot settings here

Read through the API, nothing changed:

  • GET /repos/martinfrancois/testing-toolbox/vulnerability-alerts returns 204, so Dependabot alerts are enabled. Dependabot security updates are disabled.
  • The SBOM endpoint returns 404 and GraphQL dependencyGraphManifests has totalCount: 0, not even the workflow files. The REST API has no field that shows whether the graph itself is switched on, and a public repository normally has it on, so why GitHub builds no graph here is still open.

Once the graph builds, it should list the npm packages from all four lockfiles. webdriverio's lockfile holds two versions with known high advisories that no alert shows today: extract-zip 2.0.1 (GHSA-jmr9-qjv8-65gv, GHSA-7pqw-9j4j-h8q3, no patched release) and braces 3.0.3 (GHSA-vfj7-8cjw-p6xm, no patched release). Expect those alerts then. basic-ftp 5.3.1 is a third (GHSA-c475-qrg2-pj4r, high). It stays at 5.3.1, see the last section.

basic-ftp stays at 5.3.1

The second commit, cb43506, moved webdriverio's basic-ftp from 5.3.1 to 6.2.2 with an override. The third commit, 59fed66, takes that back out. The repository owner decided to dismiss the basic-ftp alerts instead, on the condition that the current code shows the vulnerable code cannot be reached. It cannot:

  • What the advisories need. GHSA-c475-qrg2-pj4r (high, fixed in 6.2.1) and GHSA-5rfr-xx34-2xxv (moderate, fixed in 6.2.2) both need a basic-ftp client that connects to an FTP server the attacker controls and then calls list(), or falls back to PASV.
  • Dependency path. In all four lockfiles, basic-ftp appears only in webdriverio/pnpm-lock.yaml: @wdio/utils 9.31.5 > @puppeteer/browsers 2.13.2 > proxy-agent 6.5.0 > pac-proxy-agent 7.2.0 > get-uri 6.0.5 > basic-ftp 5.3.1. Each of those packages has exactly one dependent in the lockfile, and in node_modules only get-uri/dist/ftp.js requires basic-ftp.
  • Call chain. @puppeteer/browsers (lib/esm/httpUtil.js) builds new ProxyAgent() with no options, so proxy-agent asks proxy-from-env for the proxy, which reads only the npm_config_<proto>_proxy, <proto>_proxy, npm_config_proxy and all_proxy environment variables. proxy-agent loads pac-proxy-agent only for a pac+… proxy URL, pac-proxy-agent strips pac+ and hands the rest to get-uri, and get-uri uses basic-ftp only for an ftp: URL. So basic-ftp runs only when one of those variables holds a pac+ftp:// URL.
  • Nothing here sets one. No tracked source, config or script file mentions a proxy. A search for proxy and ftp over every tracked file finds only the lockfile, the unrelated Toxiproxy demo, Gradle wrapper jars and base64 data in the bundled Allure report, the repository has no .npmrc, wdio.conf.ts sets no proxy capability, and .github/workflows/ci.yml and .github/scripts/check_all_demos.sh set no proxy variable.
  • The artillery demo runs artillery@latest through pnpm dlx. That tree is in no lockfile, so it gets no alert, and an override in webdriverio/ would not reach it either.

After 59fed66 the webdriverio lockfile is byte for byte the one from the first commit: the overrides block is gone and get-uri resolves basic-ftp 5.3.1. The minimumReleaseAgeExclude entry is gone with it.

Checks for the third commit, corepack pnpm@12.4.1 in webdriverio/:

  • pnpm install --lockfile-only: lockfile byte-identical afterwards, still one YAML document, line 1 lockfileVersion: '9.0'.
  • pnpm install --frozen-lockfile with node_modules removed first: passed, worktree clean.
  • pnpm why basic-ftp: one version, 5.3.1. Resolving basic-ftp from get-uri's directory gives 5.3.1.
  • pnpm exec tsc --noEmit -p .: passed.
  • CI on this pull request runs every demo, the WebdriverIO browser test included.

🤖 Generated with Claude Code

pnpm 12 writes pnpm-lock.yaml as two YAML documents when packageManager
pins pnpm 12 or newer. The first document holds only pnpm's own binaries,
and GitHub's static dependency-graph parser reads only that one
(dependabot/dependabot-core#15904), so the dependency graph and Dependabot
alerts see no npm package from the Jest and WebdriverIO demos.

pmOnFail: ignore is the setting the pnpm maintainers name as the supported
way to get a single document (pnpm/pnpm#13805). It also stops pnpm from
switching itself to the packageManager version, so that pin now reaches CI
through pnpm/action-setup and local shells through Corepack.

The lockfile change only removes the first document. What remains is
byte-identical to the old second document, so no resolved version changed.
@chatgpt-codex-connector

Copy link
Copy Markdown

The account paying for this security review has reached its Codex usage limits. The payer can check the Codex usage dashboard. For personal accounts, using credits requires enabling “Use credits for security reviews” in Code review settings. If you do not manage the paying account, contact this repository's admins.

basic-ftp 5.3.1 is affected by GHSA-c475-qrg2-pj4r (high, fixed in 6.2.1)
and GHSA-5rfr-xx34-2xxv (moderate, fixed in 6.2.2), and no 5.x release
carries either fix. @wdio/utils reaches it through @puppeteer/browsers,
proxy-agent, pac-proxy-agent and get-uri 6.0.5, and get-uri asks for
^5.3.1 even in its newest release, 8.0.1, so no parent update gets out of
the 5.x line. An override scoped to basic-ftp@5 is the only way to the fix.
TooTallNate/proxy-agents#500 makes the same bump upstream.

basic-ftp 6.0 has one break: a passive data connection to a host other
than the control host now needs allowSeparateTransferHost. get-uri calls
access, lastMod, list and downloadTo, which did not change, and get-uri
6.0.5 with 6.2.2 fetched a PAC file from a local FTP server.

6.2.2 was published on 2026-10-04 and is younger than pnpm's built-in
one-day minimumReleaseAge. That default is not strict, so pnpm wrote
basic-ftp@6.2.2 into minimumReleaseAgeExclude on its own. The entry is
kept with a comment that says when it can go.

A plain pnpm install --lockfile-only also deduplicated @types/node 22.7.8
onto 24.13.4. The lockfile here changes only basic-ftp; pnpm 12.4.1 leaves
it byte-identical on install --lockfile-only and installs it with
--frozen-lockfile.
This undoes the basic-ftp override from the previous commit. basic-ftp
5.3.1 stays in the lockfile and its alerts get dismissed with a reason,
because nothing in this repository can reach the vulnerable code.

GHSA-c475-qrg2-pj4r and GHSA-5rfr-xx34-2xxv need a client that connects
to an FTP server the attacker controls and calls list() or falls back to
PASV. Here basic-ftp is loaded only by get-uri's ftp: handler. get-uri is
called only by pac-proxy-agent, which proxy-agent creates only for a
pac+ftp:// style proxy URL. @puppeteer/browsers builds new ProxyAgent()
with no options, so the proxy comes only from the npm_config_*_proxy,
*_proxy and all_proxy environment variables. No workflow, script,
config or .npmrc in this repository sets any of them.

The lockfile goes back byte for byte to the previous commit's version:
the overrides block is gone and get-uri resolves basic-ftp 5.3.1 again.
@martinfrancois
martinfrancois merged commit 9bbb7fa into main Oct 5, 2026
16 checks passed
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