Publishing JavaScript and TypeScript packages from GitHub Actions requires careful handling of npm credentials. A package publishing workflow usually needs authentication, but storing a long-lived npm token inside a repository or CI configuration can create unnecessary security risk.
npm has introduced trusted publishing and newer token controls to reduce the need for long-lived credentials. One of the useful concepts for CI environments is a stage-only token, which can be used for a limited publishing workflow without behaving like a general-purpose npm credential.
For GitHub Actions users, understanding where these tokens fit is important because npm authentication, GitHub Actions permissions, and package publishing are separate parts of the workflow.
This article explains how to use stage-only npm tokens in GitHub Actions, how they differ from traditional tokens, how to configure the workflow, and what to check when authentication fails.
Why npm Authentication Matters in GitHub Actions
A typical package publishing workflow looks like this:
Developer
|
v
Git Push / Tag
|
v
GitHub Actions
|
v
Build + Test
|
v
npm Authentication
|
v
npm Publish
The workflow needs permission to publish the package.
Historically, this often meant putting an npm access token into GitHub repository secrets:
NPM_TOKEN
The workflow then used that secret during npm publish.
The problem is that a long-lived credential can remain valid even after the workflow that originally used it has changed.
That is why short-lived or narrowly scoped authentication mechanisms are useful for CI/CD.
What Is a Stage-Only Token?
A stage-only token is designed for a limited stage of an npm publishing workflow rather than acting as a general credential for every npm operation.
The important idea is limited purpose.
Instead of treating a token as:
Token
|
+-- Install packages
+-- Read packages
+-- Publish packages
+-- Modify packages
a stage-specific credential can be restricted to the operation where it is needed.
For CI/CD, this can reduce the amount of access a compromised workflow could potentially obtain.
The exact token capabilities and availability depend on npm's current token model and account or organization settings, so administrators should verify the current npm documentation before changing production authentication.
Why Use a Limited Token in GitHub Actions?
GitHub Actions workflows often execute automatically.
For example:
on:
push:
tags:
- "v*"
A tagged commit can trigger a publishing workflow without a developer manually entering credentials.
That automation is convenient, but it also means the workflow becomes an important security boundary.
If the workflow has access to a broad npm token, an attacker who manages to execute unauthorized commands inside the workflow may be able to use that credential.
A narrower credential reduces unnecessary access.
Traditional npm Token vs Stage-Only Token
Area | Traditional long-lived token | Stage-only token |
|---|---|---|
Lifetime | Can remain valid until revoked or expired | Intended for a limited workflow stage |
CI usage | Common | Designed for restricted use cases |
Blast radius | Depends on token permissions | Can be reduced by narrower scope |
Rotation | Often requires manual management | Can reduce long-lived credential dependence |
Security model | Credential-based | More narrowly controlled |
Best use | General npm authentication where required | Specific CI/CD stages that support it |
The exact behavior depends on the npm token type and current npm account configuration.
The Better Pattern: Separate Build and Publish
A production workflow should not publish every time code is pushed.
A cleaner pipeline is:
Pull Request
|
v
Build
|
v
Test
|
v
Merge
|
v
Release Tag
|
v
Publish
This gives you a clear boundary between normal development and package publication.
For example:
name: Publish Package
on:
push:
tags:
- "v*"
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build package
run: npm run build
- name: Publish package
run: npm publish
The authentication configuration should be added only where the publishing step actually requires it.
Store Secrets in GitHub, Not in the Repository
Never put an npm credential directly into a committed file.
Do not do this:
env:
NPM_TOKEN: "npm_xxxxxxxxxxxxxxxxx"
Instead, use a GitHub Actions secret where a token-based workflow requires one:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Then configure npm authentication through the workflow.
For example:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- name: Publish package
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
The credential remains outside the repository source code.
Use .npmrc Carefully
A common pattern is to create an npm configuration file through setup-node.
For example:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
This allows the action to configure npm authentication for the specified registry.
Avoid committing a file containing a real token.
Bad:
//registry.npmjs.org/:_authToken=npm_real_secret
Good:
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
The environment variable is resolved during the workflow.
Where Stage-Only Tokens Fit
A stage-only token should be introduced at the narrowest point where authentication is needed.
For example:
Build
|
+-- No npm publish credential
|
v
Test
|
+-- No npm publish credential
|
v
Publish
|
+-- Stage-specific npm credential
|
v
Registry
This is preferable to exposing the credential to every step.
For example, avoid:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- run: npm ci
- run: npm test
- run: npm run build
- run: npm publish
when only the publish operation needs the credential.
Instead:
steps:
- run: npm ci
- run: npm test
- run: npm run build
- name: Publish package
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
The second pattern makes the credential's purpose much clearer.
Use Environment Protection for Production Publishing
GitHub Actions environments can add another layer of protection.
For example:
Repository
|
v
GitHub Actions
|
v
Production Environment
|
+-- Required reviewers
+-- Environment secrets
|
v
npm Publish
A production publishing environment can keep the publishing credential separate from ordinary repository secrets.
A workflow might reference:
jobs:
publish:
environment: npm-production
Then the credential can be associated with that environment.
This is particularly useful when publishing packages is a sensitive production operation.
Restrict the Trigger
A publishing workflow should have a predictable trigger.
For example:
on:
push:
tags:
- "v*.*.*"
This is easier to reason about than publishing on every branch push.
Another approach is to trigger publishing manually:
on:
workflow_dispatch:
This gives maintainers explicit control over when the package is released.
The appropriate choice depends on the team's release process.
Protect the Release Branch
If your workflow publishes based on tags or a release branch, protect the process that creates those tags.
For example:
Pull Request
|
v
Review
|
v
Merge
|
v
Release Process
|
v
Version Tag
|
v
Publish Workflow
The npm credential should not be the only security control.
The release process itself should be protected.
Test the Workflow Without Publishing
One of the most useful practices is separating authentication testing from actual publishing.
You can first verify the package:
npm pack --dry-run
This helps identify which files will be included in the package without publishing it.
You can also run:
npm test
npm run build
before introducing the production publishing step.
A safe progression is:
Build
|
v
Test
|
v
Package validation
|
v
Authentication test
|
v
Production publish
Use a Separate Test Package
When changing authentication, consider testing with a package that is not used by production applications.
For example:
@company/npm-auth-test
The test package can validate:
Authentication
Registry configuration
Publishing permissions
Workflow triggers
Environment protection
without changing a production package.
This is especially useful when migrating from a traditional token-based workflow.
Verify the Registry
A surprisingly common problem is authenticating against the wrong registry.
For npmjs.com, the registry is:
https://registry.npmjs.org
Check your configuration:
npm config get registry
A project may also contain a custom registry:
registry=https://registry.example.com
If the package is intended for npmjs.com but npm is configured to use an internal registry, authentication can appear to be broken even when the credential itself is valid.
Check Package Scope
Scoped packages use names such as:
@my-company/payment-client
The package scope may require a corresponding registry configuration.
For example:
@my-company:registry=https://registry.npmjs.org
If the scope is configured incorrectly, the publishing command may use the wrong registry.
Always verify:
npm config get registry
npm config get @my-company:registry
where appropriate.
Verify Package Ownership and Permissions
Authentication and authorization are different.
A valid token does not necessarily mean the account can publish a specific package.
For example:
Token
|
v
Authenticated
|
v
Package permission?
|
+----+
| |
Yes No
| |
v v
Publish 403
If npm returns a permission error, check package ownership, organization membership, package scope, and publishing permissions.
Do not immediately assume the token is invalid.
Common Authentication Errors
Authentication Failed
Check:
Token value
Token type
Token status
GitHub secret name
Environment secret
Registry URL
npm configuration
For example:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Make sure the secret name matches exactly.
E401
An HTTP 401 response generally indicates that authentication failed.
Check whether the credential is valid and whether npm is reading it from the expected environment variable.
E403
A 403 response often indicates an authorization or policy problem.
Check:
Package permissions
Organization policy
Package ownership
Token restrictions
Publishing restrictions
Package Already Exists
Publishing a package version that already exists can fail even when authentication is correct.
Check:
npm pkg get version
Then verify that the version is intended for release.
Prevent Accidental Credential Exposure
Never print the token.
Avoid commands such as:
echo "$NODE_AUTH_TOKEN"
Even if GitHub masks known secrets, deliberately printing credentials is unnecessary.
Also avoid writing authentication configuration into build artifacts.
For example:
find . -name ".npmrc" -print
can be useful during debugging, but inspect files carefully before exposing their contents in logs.
Do Not Give Pull Requests Unnecessary Publish Credentials
This is especially important for workflows triggered by pull requests from forks.
A publishing credential should not be available to untrusted code simply because a workflow needs to run tests.
Separate workflows can help:
Pull Request
|
+-- Build
+-- Test
+-- Lint
Release
|
+-- Build
+-- Test
+-- Publish
The release workflow can have access to the publishing credential while the pull-request workflow does not.
Keep Publishing Credentials Out of Build Steps
Even inside a trusted workflow, avoid exposing the credential to every command.
Prefer:
- name: Build
run: npm run build
- name: Test
run: npm test
- name: Publish
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
instead of defining the token at the job level.
This reduces the number of commands that can access the credential.
Consider Trusted Publishing
npm supports trusted publishing for supported CI/CD environments, including GitHub Actions.
With trusted publishing, the workflow can authenticate using an OIDC-based identity rather than relying on a long-lived npm access token.
The general model is:
GitHub Actions
|
v
OIDC Identity
|
v
npm Trust Relationship
|
v
Package Publish
This can remove the need to store a long-lived npm publishing token in GitHub secrets.
When trusted publishing is supported for the package and workflow, it should be evaluated before introducing another long-lived credential.
Stage-Only Tokens vs Trusted Publishing
These approaches solve related but different problems.
Area | Stage-only token | Trusted publishing |
|---|---|---|
Credential model | Token-based | OIDC-based |
Stored secret | Depends on configuration | Can avoid long-lived npm publishing token |
GitHub Actions support | Depends on npm token capabilities | Designed for supported trusted CI/CD environments |
Setup | Token and workflow configuration | Trust relationship and workflow configuration |
Main security benefit | Restricts credential use | Reduces long-lived credential storage |
The appropriate option depends on the package, npm account configuration, and publishing workflow.
Production Workflow Example
A practical package workflow can look like this:
name: Publish Package
on:
push:
tags:
- "v*.*.*"
permissions:
contents: read
jobs:
publish:
runs-on: ubuntu-latest
environment: npm-production
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build package
run: npm run build
- name: Validate package
run: npm pack --dry-run
- name: Publish package
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
This example uses a token-based publishing model for illustration.
If the package is configured for trusted publishing, the authentication portion should follow the supported npm and GitHub Actions setup instead of storing an npm publishing token.
Common Mistakes
Putting the npm Token in YAML
Never commit a real token.
Use GitHub secrets or a supported tokenless authentication mechanism.
Giving the Token to Every Step
Expose credentials only to the command that requires them.
Publishing on Every Push
Use a deliberate release trigger such as a protected tag or manually approved release workflow.
Testing With the Production Package
Use a test package when possible.
Ignoring Registry Configuration
Always confirm which registry npm is using.
Assuming Authentication Means Authorization
A valid credential does not guarantee permission to publish a specific package.
Skipping Package Validation
Run:
npm pack --dry-run
before publishing to verify package contents.
Best Practices Checklist
[ ] Use GitHub Actions secrets for token-based publishing
[ ] Keep publish credentials out of source control
[ ] Expose credentials only to the publish step
[ ] Use a protected release trigger
[ ] Consider a GitHub environment for production publishing
[ ] Verify the npm registry
[ ] Verify package scope
[ ] Check package permissions
[ ] Run tests before publishing
[ ] Validate package contents
[ ] Avoid publishing from untrusted pull requests
[ ] Consider npm trusted publishing where supported
[ ] Review token permissions and lifetime
[ ] Rotate or revoke credentials when no longer needed
Advantages and Disadvantages
Advantages | Disadvantages |
|---|---|
Reduces unnecessary credential exposure | Requires careful workflow configuration |
Fits automated CI/CD workflows | Token behavior depends on npm's current token model |
Can limit the scope of authentication | Existing workflows may require migration |
Works with protected release environments | Debugging authentication can be confusing |
Can complement GitHub Actions security controls | Trusted publishing may be preferable where supported |
Summary
Publishing npm packages through GitHub Actions requires more than adding a token to a workflow. The authentication method, registry configuration, package permissions, workflow trigger, and GitHub environment all contribute to the security of the release process.
Stage-only or narrowly scoped credentials can help reduce unnecessary access when a token-based workflow is required. The key principle is to expose the credential only at the stage where it is needed and avoid making it available to builds, tests, or untrusted pull-request workflows.
For new or modernized publishing pipelines, also evaluate npm trusted publishing where it is supported. OIDC-based authentication can reduce dependence on long-lived npm publishing tokens.
Whatever authentication method you choose, keep the publishing process predictable: build the package, run tests, validate its contents, authenticate only when required, and publish through a protected release workflow. This gives developers a practical CI/CD pipeline without making npm credentials broader or more permanent than necessary.
Join the conversation! Your thoughts help the community grow.