GitHub Actions for NPM packages
Tags: github, webdev, npm, javascript
Published: 2022-11-23 13:18:51

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:

```bash
.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 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:

```yaml
name: Merge to Main
```

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

```yaml
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:

```yaml
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:

```yml
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:

```yaml
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:

```bash
//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. 


![Github Secrets](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b20ja1ubz8fc6nvgq3lj.png)


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:

```yaml
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:

```yaml
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:

```yaml
- 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:

![Successful releases](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ell1kcikv0c985x1tghm.png)

## Update

After I wrote this post I found out a slightly simpler way, straight from the [official GitHub documentation](https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages), 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:

```yaml
# 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.