@alexstormwood/gha-npm-publishing-demo
v1.0.4
Published
Demo project to showcase automatic NPM package publishing using the newly-required OIDC auth flow.
Downloads
23
Readme
@alexstormwood/gha-npm-publishing-demo
Demo project to showcase automatic NPM package publishing using the newly-required OIDC auth flow.
Problem
Publishing things can be very manual. Automation is our friend, and makes publishing easier!
However, NPM recently made some authentication changes to their systems. Now we have to keep in mind these facts:
- NPM packages are most-secure when published via a "Trusted Publisher" workflow, such as a git platform's continuous integration (CI) system (e.g. GitHub Actions for GitHub). Trusted publishing can only be configured on existing NPM packages, since it requires an update to a package's settings. If a package doesn't exist yet, it has no settings to edit!
- This also means that new NPM packages cannot be created via a "Trusted Publisher" workflow, so you must do some manual, human-involved process to initialise a new package on NPM.
We can make granular access tokens in NPM 4 times per year and use those tokens in NPM publishing automation, and may want to do that to at least initialise a new package. But we shouldn't aim to use that for ongoing publishing of a package's new versions, as security/compliance features like package publishing provenance reports don't happen if you are not using Trusted Publishing.
We can also publish packages manually, from our local command line... but that kinda defeats the whole point of even glancing at CI platforms such as GitHub Actions.
It's kinda dumb right now, but it also kinda makes sense:
- Initialise a new package with human involvement.
- Configure automation infrastructure for that new package now that the package exists, not before it exists.
- Never manually be involved in package publishing for that specific package again.
Solution
Repository Setup
- Make a new repository on GitHub via its web interface, with a name like
gha-npm-publishing-demo. - Clone the project to your programming environment.
NPM Setup
- Log in to NPM, making an account if you don't have on already.
- Go to your account's Access Tokens page from the profile menu:

- From the Access Tokens page, we want to generate a new token. This should be a "granular access token", the only type that NPM allows any more. For the token configuration:
- Name: Something easy for you to remember, such as
GitHub Actions Publisher 1-Day - Description: Something easy for you to remember, such as
For manually or semi-manually publishing an NPM package, such as a new package's first version. - Bypasss two-factor authentication: Tick this box. If we want to use this token in a CI workflow, then we cannot have 2FA enabled for this token.
- Allowed IP ranges: Skip this, as finding the IP addresses of your GitHub Actions runners can be a pain and doesn't help for creating the first version of an NPM package.
- Packages and Scopes Permissions: Read and Write, all packages
- Organizations Permissions: Only relevant if you're publishing packages to an NPM organisation. If you need it, set it to Read and Write for the relevant organisation.
- Expiration: Pick a custom date 1 day from the day that you're making this token.
Save that NPM token to a text file for now, we'll need it shortly.
Go to your repository's webpage in GitHub and open its Settings section.
Navigate to the "Secrets and variables" heading in the Settings section, which should be under the "Security" subheading.
Create a new secret variable in your Actions section.
Name the secret something recognisable such as "NPM_AUTH_TOKEN, and give it the contents of the NPM token you've just made.
Save the secret to GitHub, and delete the local text file containing the NPM token. The token should only be used by one thing - GitHub Actions.

Project Setup
- Run
npm init -yto initialise the project as a NodeJS project. - Modify the
package.jsonfile so that itsnamefield is scoped to an account or team or organisation that you have publishing permissions to in NPM, such as"name": "@alexstormwood/gha-npm-publishing-demo",- using your GitHub username and ensuring that your GitHub username matches your NPM username keeps this simple.
- (Optional) Read up on how scoped or namespaced packaging is helpful with resources like these:
- Karrys, L., & Thomson, E. (2023, October 23). About scopes | npm Docs. Npmjs.com. https://docs.npmjs.com/about-scopes
- Modify the
package.jsonfile so that itsmainfield will match a soon-to-exist JavaScript file in asrcdirectory, such as"main": "src/index.js", - Create the
srcdirectory and an emptyindex.jsfile within thatsrcdirectory, matching themainfield you just edited from thepackage.json. - Add some "whatever" code to that just-made JavaScript file. The code should export something, so we can confirm if the package functionality works as intended later. Code like this is great:
let wordOfTheDay = "bananas";
function getWotD(){
return wordOfTheDay;
}
module.exports = {
wordOfTheDay,
getWotD
}- Modify the
package.jsonfile so that it gains anexportsfield, with content like this:
"exports": {
".":"./src/index.js"
},- (Optional) Read up about the concept of "entry points" and what that looks like in more-complex NPM/NodeJS projects here:
- Modules: Packages | Node.js v25.2.1 Documentation. (2025). Nodejs.org. https://nodejs.org/api/packages.html#package-entry-points
- Create a
.githubdirectory at the root of the repository (e.g. the same level as thesrcdirectory, they are sibling directories). - Create a
workflowsdirectory within the.githubdirectory. - Create a
cd_npm.ymlfile within theworkflowsdirectory. This will be processed as a GitHub Actions workflow because it's a YAML file in a.github/workflows/directory path. - Add this code to the
cd_npm.ymlfile:
name: CD NPM
on:
workflow_dispatch:
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
npm-publisher:
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
registry-url: 'https://registry.npmjs.org/'
node-version: 24
- name: Install project dependencies
run: npm install
- name: Build the package
run: npm run build --if-present
- name: Publish the package
if: ${{ success() }}
run: NODE_AUTH_TOKEN="" npm publish --access public --provenance- Create a
first_publish_npm.ymlfile within theworkflowsdirectory. This will be processed as a GitHub Actions workflow because it's a YAML file in a.github/workflows/directory path. - Add this code to the
first_publish_npm.ymlfile:
name: NPM Package First-Time Publish
on:
workflow_dispatch:
permissions:
contents: read
jobs:
npm-publisher:
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
registry-url: 'https://registry.npmjs.org/'
node-version: 24
token: ${{secrets.NPM_AUTH_TOKEN}}
- name: Install project dependencies
run: npm install
- name: Build the package
run: npm run build --if-present
- name: Publish the package
if: ${{ success() }}
run: NODE_AUTH_TOKEN=${{ secrets.NPM_AUTH_TOKEN}} npm publish --access public- Note the differences between the two workflows:
cd_npmhas provenance functionality, and interacts with anid-tokenpermission. This is related to the OIDC stuff that we must set up soon to enable Trusted Publishing - it won't work just yet.first_publish_npmhas no provenance orid-tokenthings, but does have aNODE_AUTH_TOKENvalue. Thecd_npmworkflow specifically sets that to a blank string, as it sometimes causes issues with OIDC functionality - but it's the key to manually publishing that first version of our new NPM package to NPM, so we need it infirst_publish_npm!
- Save and commit your changes to your repository.
- Push your repository's commits to the remote repository (which already exists if you made your new repository via the GitHub website).
Enabling Trusted Publishing
First of all: Trusted Publishing - as a system - cannot create new packages. So, navigate to the GitHub repository's Actions section, click through to your "NPM Package First-Time Publish" action, and click its "Run workflow" manual dispatcher button. Let it run - if your NPM token as configured properly and added to the repository as a secret, it will allow the "NPM Package First-Time Publish" action to create a new NPM package.
Visit your new NPM package on the NPM website. Find its Settings page.
Find the "Trusted Publisher" section of your NPM package's settings. Click through to your chosen provider - for this demo project, it's GitHub Actions.

- Fill out the "Trusted Publisher" settings as appropriate for your repository. For example:

Click "Set up connection" on that NPM Trusted Publisher screen to finish setting things up. You should be asked for a 2FA code, too - do that.
Jump back to your repository code and bump the version of the project in the project's
package.jsonfile. A singular NPM package cannot have two releases with the same version - change its right-most number to be a "1", so the full version field should look like:"version": "1.0.1",Save, commit, and push the local changes to the remote repository.
Jump back to your GitHub repository's Actions section, and find the "CD NPM" action. From there, click its "Run workflow" manual dispatcher button. Let it run.
Confirming The Package Works
Make a new NodeJS project and do the usual
npm initsetup stuff.Run
npm install @alexstormwood/gha-npm-publishing-demoor the equivalent using your own published package.Write this code, run this code, enjoy:
const ghaPackage = require("@alexstormwood/gha-npm-publishing-demo");
console.log(ghaPackage.getWotD());
console.log(ghaPackage.wordOfTheDay());Common Problems When Setting This Up
- Older versions of NodeJS in the GitHub Actions runners don't have the right NPM to deal with this new OIDC way of doing things. Specify NodeJS 24 when setting up NodeJS in the job runners.
- Trusted Publisher configuration is specific - capitalisation and special characters matter! No "@" in organisation or usernames, no character cases that don't exactly match the target name.
- That package version property in the
package.jsonfile must always be some semver increment above the previously-published version of the package.
More-Real Usage
Honestly, this just comes down to triggers for your workflows and things like CI/CD environment configuration. Go nuts. That stuff is beyond the scope of this repository. Look at other open-source projects to see what type of CI/CD they implement, and what type of events or triggers they use to publish their packages.
