I wanted to share a few GitHub workflows I use for NPM projects, and I want to make it clear from the start that this are not the best workflows (and if you have suggestions are more than welcome), but they work very well for me.
Note
I'm going to assume that you already know a few concepts, like creating a Pull Request, or a Release version in GitHub.
What are GitHub Actions?
If you landed here but not sure what GitHub Actions are, in a few words, it’s a simple way to create your CI/CD workflow, that allows you to run specific workflows, like to run tests, linting, etc… when some specific events are triggered, like creating a Pull Request or merging a branch into another.
This is very similar to other popular solution for CI/CD like JenkinsCI, CircleCI, TravisCI, etc… But instead of using an external agent, everything is right in GitHub, where you code lives!
Use case
These workflows will work for an NPM package, and will perform the following events:
- Check the code when a PR is created against the
main
branch. - Check the code again when the PR is merged.
- Publish the package to npmjs.com repository once a release is created.
Pull Requests actions
When we create a PR, or we merge into the main
branch, we want to do a few things:
- Checkout the code
- Install dependencies
- Check the linting of my code
- Run unit tests to ensure my code is running well
- Repeat all the above for currently supported Node.js LTS versions (14, 16 and 18)
Release actions
When I think my code is good to go, as it’s tested and code is properly formatted, I use GitHub releases to publish the package to the NPM repository.
This is helpful because we define what changes on every release, and we have a proper version history on GitHub.
When the release is done, a workflow will be triggered to perform the following actions:
- Checkout the code
- Install dependencies
- Set the container to use Node.js latest LTS version (18 currently)
- Use the
JS-DevTools/npm-publish@v1
action to publish my code and use my access token that it’s stored as a GitHub secret
So now that we have a plan, let’s see how this is translated into code.
Create the file structure
To enable GitHub Actions, you will need to create a .github
folder in your root directory.
In this directory I also normally add the CODEOWNERS
file and a PR template.
In here you will have to create a workflows
subfolder, and then create the various workflows you want to create, which they will be YAML files.
The naming of folders is important, but not how you call the configuration files, as long they are all .yml
.
In my case I’ll have something like this:
.github
├── CODEOWNERS
├── PULL_REQUEST_TEMPLATE.md
└── workflows
├── main.yml
├── pr.yml
└── release.yml
Where the workflows will be the following:
-
main.yml
to catch when some code is merged intomain
branch -
pr.yml
to catch when a PR is created/updated -
release.ytml
to catch when a new release is created
Add our workflows
Merging code to main
First thing to add, is the name
, which will be shown in the actions
page:
name: Merge to Main
Now we need to define when this workflow will be triggered:
on:
push:
branches: [ main ]
In this case we want to intercept when a user pushes code into the main
branch, of course you can add as many branches you want, but in our case we just want to listen for one.
Jobs
Now that we gave a name and when to run a job, we need to define the job itself:
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
strategy:
matrix:
node: [ 14, 16, 18 ]
name: Node ${{ matrix.node }} sample
steps:
- uses: actions/checkout@v3
- name: Run linting rules and tests
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm run lint
- run: npm run test
The code above will do the following:
- Uses the latest ubuntu docker image to create a docker container to run our code.
- Creates a strategy, where we say that we want to run the job 3 times, and each time we just change a different version of Node.js.
- Gives a name to the job for each version.
- Uses the
actions/checkout@v3
action, this will simply clone and checkout the branch into the container. - Tells the container, with the
actions/setup-node@v3
, to setup Node.js using the version in the matrix. - Runs the command to install the dependencies, using the
ci
command. - Runs the npm script to trigger the linter.
- Runs the npm script to trigger the unit tests.
Once you glue all the above together, you will have a file like this:
name: Merge to Main
on:
push:
branches: [ main ]
jobs:
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
strategy:
matrix:
node: [ 14, 16, 18 ]
name: Node ${{ matrix.node }} sample
steps:
- uses: actions/checkout@v3
- name: Run linting rules and tests
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm run lint
- run: npm run test
Creating a Pull Request
When you create a PR, you want to execute all the above instructions, the only difference between the main.yml
and pr.yml
file, is when the event is triggered, so our only changes in the code is the name
and the on
event:
name: Pull Request
on:
pull_request:
branches: [ main ]
Note
I separate the two workflows mostly to have a clear naming when executing the workflows, and sometimes I add some differences on the jobs itself, but if you want, you can keep everything in one file, and add the events trigger in the same file.
GitHub Secrets
Before we go to the next step, we need to save our NPM Token as a secret in GitHub, so we can use later when we need to deploy it.
Where is the NPM Token?
Well, the first question you should ask is: “Do I have a NPM account?”.
If no, go create one and come back here once done.
Once you have an account, you simply need to run npm login
, enter your authentication credentials, and once logged in, the token will be available in your home folder (on Mac OS/Linux), and you will see a file called .npmrc
, which it will contain something like this:
//registry.npmjs.org/:_authToken=npm_S0M3AuTh3nT1Cat10n
That’s your auth token!
Add it as a secret
You can add secrets in multiple places, either on the repository itself, under Settings -> Security -> Secrets -> Actions
, or you can add it to an organisation level if you have one, or you can save it as environment secret, and the only difference is where your secret is available.
To keep it simple we’ll use the repository settings and use a Repository Secrets
, but just know that you have options.
Once in the Secrets/Actions
section, just click New repository secret
, give it a name of NPM_TOKEN
(or whatever it’s clear for you) and paste the token in it, and that’s it!
Create the release workflow
Now, the release workflow will be triggered when you create a new release version, and essentially what you want to do is the following:
- Run a container as before.
- Checkout the code.
- Setup Node.js, in this case only the latest version.
- Install the dependencies.
- Use the
JS-DevTools/npm-publish@v1
action to publish your package to NPM. - Add the token and the access level to that action.
The name and event will be the following:
name: Publish Package to npmjs
on:
release:
types: [created]
So it will be run only once, when you create the release.
And the job will be the following:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm install
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Again, you want to be sure that you just use one version of Node, if we used a matrix as before, the action will try to publish a package for each version, which will succeed once, and fail the others.
With this code:
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
We tell to the action to use the token we stored in the secrets to authenticate, and our project is publicly accessible on npmjs.org repository by default.
We are all set!
Now you just need to push the workflows to your repository, and you are ready to go!
If everything goes to plan, you might see something like this
every time you have a successful release:
Update
After I wrote this post I found out a slightly simpler way, straight from the official GitHub documentation, to publish packages that doesn't rely on an github action maintained by a third party, but essentially what it changed on the release.yml
file is the following changes at the very end:
# Original code was
- run: npm install
- uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
# Now is this
- run: npm install
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
You need the flag npm publish --access public
only if your package is scoped, otherwise it's not needed.
Top comments (0)