DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Alessio Michelini
Alessio Michelini

Posted on • Updated on

GitHub Actions for NPM packages

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
Enter fullscreen mode Exit fullscreen mode

Where the workflows will be the following:

  • main.yml to catch when some code is merged into main 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
Enter fullscreen mode Exit fullscreen mode

Now we need to define when this workflow will be triggered:

on:
  push:
    branches: [ main ]
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Github Secrets

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]
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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:

Successful releases

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 }}
Enter fullscreen mode Exit fullscreen mode

You need the flag npm publish --access public only if your package is scoped, otherwise it's not needed.

Top comments (0)

Let's Get Wacky


Use any Linode offering to create something unique or silly in the DEV x Linode Hackathon 2022 and win the Wacky Wildcard category

β†’ Join the Hackathon <-