Context
@semantic-release/npm depends on the npm CLI package (npm@11.x), which ships its dependencies bundled inside its tarball (undici 6.27.0, tar 7.5.19, ip-address 10.2.0, brace-expansion 5.0.7, ...). Bundled deps are version-pinned and cannot be fixed from this repo's lockfile.
Consequences, all of them recurring:
npm audit fails on Node 24 (and in the release workflow) with findings that exist only at node_modules/npm/**. The newest npm (12.0.2) bundles the same vulnerable set — there is no npm version with clean bundles, so this never self-heals.
- Dependabot security-update runs fail permanently: "A patched version exists for undici, but the available update path still resolves it to 6.27.0" — the updater cannot construct a manifest fix for a bundled dep, so the job errors out. These runs recur on every advisory (undici, ip-address, tar, brace-expansion, js-yaml, ...).
- Workarounds so far: CI/release audit gates were scoped to
npm audit --omit=dev (audits the publishable tree; this library ships zero runtime deps).
Fix: remove the npm package from the dependency tree
@semantic-release/npm is the only path that pulls npm into the tree. Replace it with @semantic-release/exec (deps: debug, execa, lodash-es, parse-json, aggregate-error, @semantic-release/error — no npm) running custom commands, keeping the plugin's behavior via three phases:
| Plugin behavior today |
Replacement |
verifyConditions: registry auth (srnpm@13 does the GitHub-OIDC → npm token exchange internally — that's why release.yml asserts NODE_AUTH_TOKEN absent while ACTIONS_ID_TOKEN_REQUEST_URL is required) |
verifyConditionsCmd: explicit OIDC id-token exchange with npm's Trusted Publishing endpoint + npm whoami, failing closed before publish. Feed the token to npm via a temp .npmrc (never an env var), so the existing fail-closed token-absence evidence checks can stay. |
prepare: bump package.json + package-lock.json to the computed next version (semantic-release core then commits it) |
prepareCmd: npm version ${nextRelease.version} --no-git-tag-version |
publish: npm publish (provenance) |
publishCmd: npm publish --provenance (id-token: write already granted in release.yml) |
Steps:
- Remove
@semantic-release/npm, add @semantic-release/exec; update .releaserc.json plugin list.
- Add the OIDC exchange step (only piece with real unknowns — prototype and validate it first via the existing dry-run flow on a branch; never publish untested).
- Restore the audit gates to full-tree
npm audit --audit-level=moderate in both ci.yml and release.yml (they pass again once npm is out of the tree).
- Verify with a release dry-run + rerun-idempotency check, then a real (no-op) release on master.
Not gaps (same behavior either way): dist-tag latest (no prerelease channels configured), access: public via publishConfig, files filtering, dry-run version capture, rerun idempotency, GitHub release creation (@semantic-release/github stays).
Payoff
- Dependabot security-update red runs stop permanently (no bundled findings in the dependency graph).
- Full-tree
npm audit passes again — --omit=dev scoping can be reverted in both workflows.
- Dependency graph and lockfile stop carrying the vendored npm CLI.
Context
@semantic-release/npmdepends on thenpmCLI package (npm@11.x), which ships its dependencies bundled inside its tarball (undici 6.27.0, tar 7.5.19, ip-address 10.2.0, brace-expansion 5.0.7, ...). Bundled deps are version-pinned and cannot be fixed from this repo's lockfile.Consequences, all of them recurring:
npm auditfails on Node 24 (and in the release workflow) with findings that exist only atnode_modules/npm/**. The newest npm (12.0.2) bundles the same vulnerable set — there is no npm version with clean bundles, so this never self-heals.npm audit --omit=dev(audits the publishable tree; this library ships zero runtime deps).Fix: remove the
npmpackage from the dependency tree@semantic-release/npmis the only path that pullsnpminto the tree. Replace it with@semantic-release/exec(deps: debug, execa, lodash-es, parse-json, aggregate-error, @semantic-release/error — no npm) running custom commands, keeping the plugin's behavior via three phases:verifyConditions: registry auth (srnpm@13 does the GitHub-OIDC → npm token exchange internally — that's why release.yml assertsNODE_AUTH_TOKENabsent whileACTIONS_ID_TOKEN_REQUEST_URLis required)verifyConditionsCmd: explicit OIDC id-token exchange with npm's Trusted Publishing endpoint +npm whoami, failing closed before publish. Feed the token to npm via a temp.npmrc(never an env var), so the existing fail-closed token-absence evidence checks can stay.prepare: bumppackage.json+package-lock.jsonto the computed next version (semantic-release core then commits it)prepareCmd:npm version ${nextRelease.version} --no-git-tag-versionpublish:npm publish(provenance)publishCmd:npm publish --provenance(id-token: writealready granted in release.yml)Steps:
@semantic-release/npm, add@semantic-release/exec; update.releaserc.jsonplugin list.npm audit --audit-level=moderatein both ci.yml and release.yml (they pass again oncenpmis out of the tree).Not gaps (same behavior either way): dist-tag
latest(no prerelease channels configured),access: publicvia publishConfig,filesfiltering, dry-run version capture, rerun idempotency, GitHub release creation (@semantic-release/githubstays).Payoff
npm auditpasses again —--omit=devscoping can be reverted in both workflows.