Two ways to automatically publish npm packages from GitHub Actions.
Trusted Publishing (OIDC) is the officially recommended approach — no tokens required.
| Trusted Publishing (OIDC) | Granular Access Token | |
|---|---|---|
| Security | No tokens, short-lived OIDC credentials | Long-lived token stored as secret |
| Configuration | npmjs.com package Settings | npmjs.com Access Tokens + GitHub Secrets |
| Provenance | Automatic | Requires --provenance flag |
| npm CLI required | 11.5.1+ / Node 22.14.0+ | 9.5.0+ |
| Runner support | GitHub-hosted only (no self-hosted) | Any runner |
| First publish | Not possible (package must already exist) | Supported |
| npm recommendation | ✅ Recommended | ⚠️ Security warning shown |
As of November 2025, npm has removed legacy tokens and only supports Granular Access Tokens.
When creating a token, npm displays a security warning recommending Trusted Publishing instead.
Add the fields required for npm publish to your package.json.
{
"name": "@your-scope/your-package",
"version": "1.0.0",
"main": "dist/index.js",
"bin": {
"your-package": "dist/index.js"
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"repository": {
"type": "git",
"url": "git+https://github.com/owner/repo.git"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"prepublishOnly": "npm run check && npm run build && npm test"
}
}
| Field | Purpose |
|---|---|
bin |
Makes the package executable via npx. Entry file needs a #!/usr/bin/env node shebang |
files |
Explicitly lists files/directories to include in the package. README*, LICENSE*, and package.json are always included automatically |
repository |
Shows GitHub link on npm page. Also used for provenance verification in Trusted Publishing (case-sensitive match required) |
publishConfig.access |
Scoped packages (@scope/name) default to restricted, so public must be set explicitly |
prepublishOnly |
Safety net for local npm publish (type check → build → test) |
Even with a
filesfield, npm always includes files matchingREADME*,LICENSE*, andCHANGELOG*.
Translated READMEs (e.g.README.ko.md) will be included and cannot be excluded via.npmignore.
GitHub Actions issues an OIDC token, which npm verifies for authentication. No long-lived tokens exist, so there is no risk of credential leakage.
- Requires npm CLI 11.5.1+ / Node 22.14.0+
- GitHub-hosted runners only (self-hosted not supported)
- Package must already exist on npm before configuring a Trusted Publisher
- Therefore, the first publish must be done manually from your local machine
# Log in to npm
npm login
# For scoped packages, verify your account is an owner in the org
npm org ls <your-org>
# First publish
npm publish --access public
# If 2FA is enabled, provide OTP
npm publish --access public --otp=123456
| Field | Value | Notes |
|---|---|---|
| Organization or user | owner |
GitHub username or org name |
| Repository | repo-name |
Repository name |
| Workflow filename | publish.yml |
Filename inside .github/workflows/ (include extension) |
| Environment name | (leave empty) | Only needed if using GitHub Environments |
npm does not validate your Trusted Publisher configuration when you save it.
Typos will only surface as errors at publish time, so double-check everything including case.
name: Publish to npm
on:
push:
tags:
- 'v*'
jobs:
ci:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm run check
- run: npm run build
- run: npm test
publish:
needs: ci
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for OIDC token issuance
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
cache: npm
- run: npm ci
- run: npm run build
- name: Verify tag matches package.json version
run: |
PKG_VERSION="v$(node -p "require('./package.json').version")"
GIT_TAG="${GITHUB_REF#refs/tags/}"
if [ "$PKG_VERSION" != "$GIT_TAG" ]; then
echo "::error::Tag $GIT_TAG does not match package.json version $PKG_VERSION"
exit 1
fi
- run: npm publish --access public
# No NODE_AUTH_TOKEN needed! OIDC handles auth automatically
# No --provenance flag needed! Auto-generated with Trusted Publishing
permissions.id-token: write — set on the publish job only (job-level preferred over top-level)NODE_AUTH_TOKEN / NPM_TOKEN — not needed. OIDC replaces them--provenance — not needed. Automatically generated with Trusted Publishing--access public — required for scoped packages on first publish (recommended to always include)registry-url — must be set in actions/setup-node so the npm CLI targets the correct registry# Update package.json version + create tag + commit
npm version patch # or minor, major
# Push (including tags)
git push && git push --tags
v* tag push → CI passes → npm publish runs automatically.
Use this when Trusted Publishing is not available (self-hosted runners, automating first publish, etc.).
| Setting | Value |
|---|---|
| Name | github-actions-publish |
| Expiration | Your preferred duration |
| Packages | Select target package(s), Read and Write |
| Bypass 2FA | true (CI cannot enter 2FA codes) |
GitHub repo → Settings → Secrets and variables → Actions → New repository secret:
NPM_TOKENname: Publish to npm
on:
push:
tags:
- 'v*'
permissions:
contents: read
id-token: write # Required for provenance generation
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
cache: npm
- run: npm ci
- run: npm run build
- name: Verify tag matches package.json version
run: |
PKG_VERSION="v$(node -p "require('./package.json').version")"
GIT_TAG="${GITHUB_REF#refs/tags/}"
if [ "$PKG_VERSION" != "$GIT_TAG" ]; then
echo "::error::Tag $GIT_TAG does not match package.json version $PKG_VERSION"
exit 1
fi
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# Trusted Publishing
- run: npm publish --access public
+ # No token needed, provenance auto-generated
# Token-based
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ # --provenance flag required, token required
Both methods include a verification step to prevent tag/version mismatches:
- name: Verify tag matches package.json version
run: |
PKG_VERSION="v$(node -p "require('./package.json').version")"
GIT_TAG="${GITHUB_REF#refs/tags/}"
if [ "$PKG_VERSION" != "$GIT_TAG" ]; then
echo "::error::Tag $GIT_TAG does not match package.json version $PKG_VERSION"
exit 1
fi
This ensures only tags created via npm version patch && git push --tags actually publish. Accidental or incorrect tags will fail safely.
Follow this order when transitioning from token-based publishing:
--provenance)NPM_TOKEN)Always confirm Trusted Publishing works correctly before revoking tokens.
Unable to authenticate Error.yml extension)id-token: write permission is set on the publish jobEOTP Error (Local Publish)npm publish --access public --otp=<AUTHENTICATOR_CODE>
Accounts with 2FA enabled require an OTP code for publishing.
404 Not Found on First PublishFor scoped packages, your account must be an owner in the npm org:
npm org ls <your-org> # Check permissions
on.push.tags pattern matches your tag name (e.g. v* matches v1.0.0)npm does not allow publishing the same version twice. Bump the version with npm version patch.