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:

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:

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 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.