Publishing to npm from GitHub Actions without a token
npx hostc@latest runs whatever we published last. So the release workflow has two rules: store no long-lived secret, and test the packed tarball before publishing it.
Publishing without a token
A classic npm token in CI secrets lasts until someone revokes it. With trusted publishing, npm trusts a specific workflow in a specific repository instead. GitHub Actions proves its identity with a short-lived OIDC token, and no npm token is stored anywhere.
On npm, you name the repository and the workflow file. In the workflow, you allow the identity token:
permissions:
contents: read
id-token: writeThree things to know:
- It needs npm 11.5.1 or later, newer than the npm that ships with Node.js 22. We build releases on Node.js 24.
- npm adds provenance by default for a public package from a public repository, so anyone can see which commit built a release.
- npm doesn't validate the settings when you save them. A misspelled workflow name shows up only when you publish.
Pin your actions
id-token: write means every step in that job can obtain the identity that publishes the package. uses: some/action@v6 follows a tag its owner can move. If that action is compromised, an attacker's code runs with your publishing rights.
So every action is pinned to a full commit:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: falsepersist-credentials: false keeps the checkout's Git token out of later steps, because this workflow doesn't push code.
Why 2.0.0 had no executable
hostc 2.0.0 went out without its executable. The dist folder was missing from the package.
We pack with pnpm and publish the resulting tarball with npm, and prepublishOnly doesn't run for pnpm pack or for npm publish <tarball>. Nothing built the CLI. Tests on the source tree can't catch that, because everything exists there.
The build moved to prepack, which does run. We also added a test of the package itself.
Test the tarball
The release now installs the packed file the way a user would, and runs the end-to-end tests against that copy:
pnpm -F hostc pack --pack-destination "$RUNNER_TEMP"
pnpm -F hostc test:package "$RUNNER_TEMP"/hostc-*.tgz
npm publish "$RUNNER_TEMP"/hostc-*.tgz --tag latesttest:package creates an empty directory, runs npm install on the tarball, checks that the installed CLI prints the right version, and points the end-to-end tests at it. Those start a real server, a real local origin, and traffic through the tunnel.
That catches a missing file, and a dependency that only resolves inside the workspace. CI runs the same step on every pull request.
If a release goes wrong
A published npm version can't be changed. We released 2.0.1 as the same code packaged correctly. Moving a dist-tag needs interactive 2FA, which trusted publishing doesn't cover, so the fix for a bad release is a new version.
Give localhost a public URL, free and with no account
npx hostc@latest 3000