Note: the result of all these steps can be found here, in the workflow file I actually used for my package.
The easiest way I found was just to put the two jobs in the same workflow, and having them both fire on every push
event: the publish job was then limited to execute only if on the main
branch and after the first one.
- Build job:
It needs to build the new version of the package, then commit it to the repository: committing is crucial because that allows the other job to pick the built version. To commit the changes made inside a workflow run, you can use one of my actions, add-and-commit
: it will push the changes to the GitHub repository using a "fake" git user.
You workflow job should look something like this:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Install dependencies
run: npm install --only=prod
- name: Compile build
run: npm run build # This can be whatever command you use to build your package
- name: Commit changes
uses: EndBug/add-and-commit@v9
with: # More info about the arguments on the action page
author_name: Displayed name
author_email: Displayed email
message: 'Message for the commit'
add: local/path/to/built/version/"*.js --force
- Publish job:
We want it to run only in the main
branch after build
is completed, so we can set it like this:
publish:
name: Publish to NPM & GitHub Package Registry
runs-on: ubuntu-latest
if: contains(github.ref, 'main') # This sets the branch
needs: build # This makes it wait for the build job
I didn't find a good way to do that so I made another action, version-check
: this action scans the commits of every push and tries to figure out whether they include a version change. Remeber to set eventual needed arguments/inputs!
You need to set up these two steps:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: main
- name: Check version changes
uses: EndBug/version-check@v2 # More info about the arguments on the action page
id: check # This will be the reference for later
You could also use the static-check
and file-url
option to detect teh version change, but if more checks run at the same time this can make it try to publish it multiple times.
version-check
provides three outputs: changed
(whether there has been an update), type
(the type of update, like "patch", "minor", ...) and version
(the new version).
These outputs can be accessed through the steps
context and you can use them to decide whether to run a step or not, using the if
property. This is an example:
# check is the id we gave to the check step in the previous paragraph
- name: Version update detected
if: steps.check.outputs.changed == 'true'
run: 'echo "Version change found! New version: ${{ steps.check.outputs.version }} (${{ steps.check.outputs.type }})"'
Publishing a package to NPM is pretty straight-forward: you only need to set up Node.js and then use npm publish
, like you would on your own machine. The only difference is that you'll need a token to authenticate: you can create one on npmjs.com. After you created it DO NOT put it in the workflow itself: you can store it as a "secret", you can find more info about those here.
In this example, I'll assume your secret is called NPM_TOKEN
:
- name: Set up Node.js for NPM
if: steps.check.outputs.changed == 'true'
uses: actions/setup-node@v1
with:
registry-url: 'https://registry.npmjs.org' # This is just the default registry URL
- name: Install dependencies
if: steps.check.outputs.changed == 'true'
run: npm install
- name: Publish the package to NPM
if: steps.check.outputs.changed == 'true'
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # NPM will automatically authenticate with this
As for now, GitHub Package Registry is not very pleasant to work with if you want to keep publishing your existing package to npm: that's why it requires packages to be scoped, and that can mess thing up (your package may not be scoped or be scoped under a different name).
I found that the easiest way to deal with that is doing this workaround:
- In your workflow, re-setup Node.js but adding GPR's registry URL and your name-scope
- Create an npm script that edits your package.json so that it changes the original name of the package to the one you need to publish to GPR (scope included)
- After calling that script in your workflow, use
npm publish
as before, but this time using the built-inGITHUB_TOKEN
asNODE_AUTH_TOKEN
.
{
"name": "YourPackageName",
...
"scripts": {
"gpr-setup": "node scripts/gpr.js"
}
}
// scripts/gpr.js
const fs = require('fs')
const { join } = require('path')
// Get the package obejct and change the name
const pkg = require('../package.json')
pkg.name = '@yourscope/YourPackageName'
// Update package.json with the udpated name
fs.writeFileSync(join(__dirname, '../package.json'), JSON.stringify(pkg))
- name: Set up Node.js for GPR
if: steps.check.outputs.changed == 'true'
uses: actions/setup-node@v1
with:
registry-url: 'https://npm.pkg.github.com/'
scope: '@yourscope'
- name: Set up the package for GPR
if: steps.check.outputs.changed == 'true'
run: npm run gpr-setup
- name: Publish the package to GPR
if: steps.check.outputs.changed == 'true'
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Your package is now published both to NPM and GPR (a description needs to be manually added to GPR though). You can find all of the stuff I'm referring to in the 4.0.3 version of uptime-monitor: