Summary
This repo has two npm publish workflows on different tag patterns. One is a well-built, gated, provenance-enforcing release. The other is an ungated legacy path that can publish a prerelease to latest with a long-lived token — and NPM_TOKEN is currently set on the repo, so it is armed.
release.yml already knows this. Its own header says so:
publish.yml (tags: v*) publishes with NO gate; this workflow is the guarded [one]
The guarded one landed in #44 (2026-07-02, fix/npm-oidc-trusted-publishing). The ungated one was never removed.
The two paths
|
release.yml — guarded |
publish.yml — ungated |
| Trigger |
tags: ['sdk-v*'] |
tags: ['v*'] |
| Auth |
OIDC trusted publishing, no token |
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} |
| Provenance |
--provenance, enforced |
--no-provenance, explicitly stripped |
| Version check |
tag must match package.json or it refuses |
none |
| dist-tag |
routed — *-next.* → next |
none → latest |
Why this is live, not theoretical
package.json on main is at 2.1.0-next.0. npm currently serves 2.0.14 as latest.
npm publish with no --tag publishes to latest regardless of whether the version is a semver prerelease. So a v*-shaped tag pushed to this repo today would put 2.1.0-next.0 on the latest dist-tag, and every npm install @wave-av/sdk would start resolving to a prerelease.
release.yml handles exactly this correctly:
case "$PKG_VERSION" in
*-next.*) DIST_TAG=next ;; # explicit prerelease channel
publish.yml has no equivalent.
Two smaller problems in the same file:
--no-provenance overrides the package's own stated intent. package.json declares publishConfig: { "provenance": true }. This workflow passes a flag that cancels it, so a release down this path is unattested even though the package asked to be attested — and this is a public repo, where provenance actually works.
- Unpinned actions.
actions/checkout@v4 and actions/setup-node@v4 are mutable tags in a workflow that holds publish rights. release.yml is pinned; this one isn't.
Suggested fix
Delete .github/workflows/publish.yml. It is fully superseded — release.yml does the same job with a version gate, correct dist-tag routing, provenance, and no long-lived credential.
Then revoke NPM_TOKEN on this repo. Once the ungated path is gone, nothing reads it, and an unused publish token on a public repo is a standing liability rather than a fallback. Trusted publishing is already registered for @wave-av/sdk against release.yml, so there is no path that still needs it.
If a v* trigger is wanted for ergonomics, add v* to release.yml's tag list rather than keeping a second workflow — one publisher, one gate.
Why I'm filing rather than fixing
Deleting a publish workflow and revoking a publish credential on a public package is a release-infrastructure change with a blast radius beyond my lane, and CI can't currently verify anything (org Actions are billing-locked, so every check on every repo fails at 0 jobs). Happy to open the PR once someone confirms the intent — the change itself is two deletions.
Summary
This repo has two npm publish workflows on different tag patterns. One is a well-built, gated, provenance-enforcing release. The other is an ungated legacy path that can publish a prerelease to
latestwith a long-lived token — andNPM_TOKENis currently set on the repo, so it is armed.release.ymlalready knows this. Its own header says so:The guarded one landed in #44 (2026-07-02,
fix/npm-oidc-trusted-publishing). The ungated one was never removed.The two paths
release.yml— guardedpublish.yml— ungatedtags: ['sdk-v*']tags: ['v*']NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}--provenance, enforced--no-provenance, explicitly strippedpackage.jsonor it refuses*-next.*→nextlatestWhy this is live, not theoretical
package.jsononmainis at2.1.0-next.0. npm currently serves2.0.14aslatest.npm publishwith no--tagpublishes tolatestregardless of whether the version is a semver prerelease. So av*-shaped tag pushed to this repo today would put2.1.0-next.0on thelatestdist-tag, and everynpm install @wave-av/sdkwould start resolving to a prerelease.release.ymlhandles exactly this correctly:publish.ymlhas no equivalent.Two smaller problems in the same file:
--no-provenanceoverrides the package's own stated intent.package.jsondeclarespublishConfig: { "provenance": true }. This workflow passes a flag that cancels it, so a release down this path is unattested even though the package asked to be attested — and this is a public repo, where provenance actually works.actions/checkout@v4andactions/setup-node@v4are mutable tags in a workflow that holds publish rights.release.ymlis pinned; this one isn't.Suggested fix
Delete
.github/workflows/publish.yml. It is fully superseded —release.ymldoes the same job with a version gate, correct dist-tag routing, provenance, and no long-lived credential.Then revoke
NPM_TOKENon this repo. Once the ungated path is gone, nothing reads it, and an unused publish token on a public repo is a standing liability rather than a fallback. Trusted publishing is already registered for@wave-av/sdkagainstrelease.yml, so there is no path that still needs it.If a
v*trigger is wanted for ergonomics, addv*torelease.yml's tag list rather than keeping a second workflow — one publisher, one gate.Why I'm filing rather than fixing
Deleting a publish workflow and revoking a publish credential on a public package is a release-infrastructure change with a blast radius beyond my lane, and CI can't currently verify anything (org Actions are billing-locked, so every check on every repo fails at 0 jobs). Happy to open the PR once someone confirms the intent — the change itself is two deletions.