npm publish going through felt great.
The trouble came after. The moment I moved to GitHub Actions, three things broke. The Trusted Publisher setup, provenance-related metadata, and the ordering of tags and versions—each failed for a different reason, in a different place.
I’m writing this down so the next person doesn’t spend time on the same issues.
Why I Wanted to Publish It
@hsblabs/web-stream-extras is a small package of utility helpers around the Web Streams API. I originally wrote it for internal reuse, but decided it was general enough to open source.
Why I Published 0.0.1 Manually from Local
For the first release, I ran npm publish locally before setting up Actions. The goal was to confirm that install and import actually work once the package is on npm. I left the release flow for later—wiring up automation before things are stable just adds another layer to debug.
There’s another reason. To configure Trusted Publisher on the npmjs side, the package has to already exist on npm. That means the very first publish has to happen some other way. There’s no getting to the settings screen otherwise.
Moving to GitHub Actions + Trusted Publisher for 0.1.0
I kept the manual publish to a single time and moved everything after that to GitHub Actions.
npm’s Trusted Publisher lets you publish via OIDC without issuing or managing a token. It uses a short-lived token generated by the workflow, so there’s no need to store NPM_TOKEN as a secret.
Two things matter for the setup. On the npm side, you specify the workflow filename when adding a Trusted Publisher. On the GitHub Actions side, you add id-token: write permission and leave out NODE_AUTH_TOKEN.
permissions: id-token: write contents: readLeaving NODE_AUTH_TOKEN in place can cause the Trusted Publisher OIDC flow to fail. If token-based instructions are still sitting in the README or TODO, they’ll cause confusion—clear them out during the migration.
Failures I Actually Hit
Publish Stopping with 422 Unprocessable Entity
Only the publish step in Actions was failing. The error was 422 Unprocessable Entity. Everything else passed, so at first I had no idea where to look.
The cause was an empty repository.url in package.json. When provenance is enabled, npm uses repository.url to link the package to its repo. Leave it out and you get a 422.
// before"repository": {}
// after"repository": { "type": "git", "url": "https://github.com/hsblabs/web-stream-extras"}Even if you’ve checked everything else in the metadata, this one is easy to miss.
Tag and package.json.version Ordering
The workflow validates that the tag name matches package.json.version.
If you push the tag before bumping the version, the check fails. The correct order is:
- Bump
package.json.versionand commit - Tag that commit
- Push
If the order is reversed, the version in the commit the tag points to won’t match the tag name. If it fails, delete the tag and reattach it.
git tag -d v0.1.0git push origin :refs/tags/v0.1.0# after committing the version bumpgit tag v0.1.0git push origin v0.1.0The Release Flow That Finally Stabilized
This is how it works now:
- Implement and test
- Bump
package.json.versionand commit - Push a
v{version}tag - Actions detects the tag push and runs the publish
Pre-publish validation is consolidated into a publish:check command—lint and type checks without --write. Placing this before the publish step prevents uncommitted changes from sneaking in. Chaining auto-fixing commands directly can produce uncommitted changes right before publish, which is worth watching out for.
I also ran pnpm pack --dry-run to inspect the tarball contents. Even with files pointing only to dist/, sourcemaps can end up in there. If you don’t want them published, cutting them at the build level is the safest approach.
What I Want to Improve Next
I want to wire changelog generation into the flow. Right now it’s manual, and something slips through every time I push a tag.
The steps for moving from manual publish to Actions are straightforward. Where things get tricky is the configuration details. Trusted Publisher removes the need to manage tokens, but it requires the OIDC settings to line up precisely. Confirming that the npm side and the Actions side correspond one-to-one before running anything is the fastest path forward.
hsb.horse