Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@
node-version: '22'
cache: 'pnpm'

- name: Check and upgrade npm
run: |
echo "Current npm version:"
npm --version

Check failure on line 57 in .github/workflows/publish.yaml

View check run for this annotation

Claude / Claude Code Review

npm@latest is unpinned - non-deterministic and supply chain risk

The new "Check and upgrade npm" step uses `npm install -g npm@latest`, which is unpinned — every workflow run may install a different npm version, making builds non-deterministic. More critically, in a publish workflow with registry credentials, a compromised or broken version briefly tagged `@latest` on the npm registry would be automatically adopted; pin to a specific version like `npm@10.9.2` instead.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The new "Check and upgrade npm" step uses npm install -g npm@latest, which is unpinned — every workflow run may install a different npm version, making builds non-deterministic. More critically, in a publish workflow with registry credentials, a compromised or broken version briefly tagged @latest on the npm registry would be automatically adopted; pin to a specific version like npm@10.9.2 instead.

Extended reasoning...

What the bug is and how it manifests

The newly added step runs npm install -g npm@latest without pinning to a specific version. The @latest dist-tag on the npm registry is a floating pointer that changes whenever a new npm release is published. This means the version of npm installed in the workflow is determined at runtime by whatever is currently tagged @latest, not by anything recorded in source control.

The specific code path that triggers it

Line 57 of .github/workflows/publish.yaml (the new step named "Check and upgrade npm") runs:

npm install -g npm@latest

This executes on every trigger of the publish job — both on push to main (the release path) and on canary/RC issue_comment triggers.

Why existing code doesn't prevent it

There is no version lock or checksum verification anywhere in the workflow for this global npm install. GitHub Actions runners do cache some tooling, but a fresh npm install -g bypasses any such caching and fetches directly from the registry. Nothing in the repository enforces which npm version is installed.

What the impact would be

Two distinct risks apply:

  1. Non-determinism: Different workflow runs (e.g., a hotfix publish vs. a regular release one week later) may use different npm versions. If a newer npm changes behavior around package resolution, lockfile handling, or publish semantics, the discrepancy would be silent and hard to debug.
  2. Supply chain risk: This is a publish workflow that pushes packages to the npm registry using pnpm release (backed by changeset-actions). Downstream consumers of these packages trust that the publishing toolchain was not tampered with. If a malicious or broken npm release is briefly tagged @latest on the registry — a known attack category — the workflow would install it and use it for the publish operation before anyone can react.

How to fix it

Replace the floating tag with a pinned version:

- name: Check and upgrade npm
  run: |
      echo "Current npm version:"
      npm --version
      npm install -g npm@10.9.2
      echo "Upgraded npm version:"
      npm --version

The pinned version should be deliberately updated in a PR when an upgrade is intentional, making the change explicit and reviewable.

Step-by-step proof

  1. Workflow run A executes today: npm@latest resolves to 10.9.2. Packages are published successfully.
  2. npm Inc. releases npm@11.0.0 tomorrow and tags it @latest.
  3. Workflow run B executes on the next push to main: npm install -g npm@latest now installs 11.0.0.
  4. npm@11.0.0 has a breaking change in its publish behavior (or, in the attack scenario, is a compromised build).
  5. pnpm release (or changeset-actions) calls npm under the hood; packages are published with the new/compromised npm binary without any human review of the npm upgrade.
  6. Downstream consumers install the affected package versions.

npm install -g npm@latest
echo "Upgraded npm version:"
npm --version

- name: Install Dependencies
run: pnpm install --frozen-lockfile

Expand Down
Loading