🤖 Automatically publish your Node package to NPM (with PNPM and GitHub actions)
Tags: javascript, npm, github, tutorial
Published: 2024-01-12 11:35:55

I'm excited to share my experience in setting up GitHub Actions for automatically publishing a package. While I use `pnpm` as an alternative to `npm`, this guide should also work for you if you if you use `npm` instead.

## The Setup

The core of this setup lies in a specific YAML file located in the `.github/workflows` directory of the repository.

Here is a detailed description of each section in the file. I will add the contents of the full file at the end of this article.

### 1. Name
```yaml
name: Release components package
```
Specifies the name of the GitHub Action workflow, this is what you will see in the GitHub user interface:


![The GitHub user interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g9m099pwvp3snxlpna5t.png)

### 2. Trigger
```yaml
on:
  release:
    types: [published]
```
This defines the event that triggers the workflow. This workflow is triggered when a release is _published_.

> Initially, I used `types: [created]` but I encountered issues with triggering the action. It turns out that creating a draft release before the actual release will not trigger _created_
[GitHub Docs: Events that trigger workflows](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release)

### 3. Jobs
```yaml
jobs:
  build:
    runs-on: ubuntu-latest
```
This workflow defines one single job named "build" that runs on the latest Ubuntu virtual environment.

### 4. Steps
#### Step 1: Checkout Repository
```yaml
    steps:
      - name: Checkout
        uses: actions/checkout@v4
```
The [checkout@v4](https://github.com/marketplace/actions/checkout) action clones the repository into the runner.

#### Step 2: Install pnpm
```yaml
      - uses: pnpm/action-setup@v2
        name: Install pnpm
        with:
          version: 8
          run_install: false
```
Sets up pnpm in the GitHub runner using [pnpm/action-setup@v2](https://github.com/marketplace/actions/setup-pnpm) to install pnpm version 8. The `run_install: false` parameter prevents automatic installation of dependencies at this stage.

> 🚀 The pnpm setup must precede the Node.js setup to take advantage of the `cache: pnpm` property in the setup-node action. This will significantly speed up build times by reusing the stored dependencies across workflow executions.

#### Step 3: Install Node.js
```yaml
      - name: Install Node.js
        uses: actions/setup-node@v3
        with:
          node-version-file: ".nvmrc"
          cache: pnpm
          registry-url: https://registry.npmjs.org
```
Run [actions/setup-node@v3](https://github.com/marketplace/actions/setup-node-js-environment) to install Node.js. While it is not mandatory in this case I like to explicitly define to read the node version from the ".nvmrc" file. It also sets up caching for pnpm.

> 💡 A crucial discovery was the need to explicitly set the registry URL in the Node.js setup for npm. Otherwise you will run into an authentication issue.

#### Step 4: Install Dependencies
```yaml
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
```
Executes `pnpm install --frozen-lockfile` to install dependencies based on the lock file, ensuring consistent versions across builds. This is the equivalent to `npm ci` which is like `npm install` but based on the lock file.

#### Step 5: Publish Package
```yaml
      - name: Publish 🚀
        shell: bash
        run: pnpm publish packages/components --access public --no-git-checks
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
Runs the command to publish the package. In my case the package is inside a monorepo and located in `packages/components`. 

The `--access public` flag has to be set explicitly in the command line the first time you publish a public package to npm. This is to prevent private packages from being published accidentally. 

The `--no-git-checks` flag is needed to bypass [an issue with pnpm](https://github.com/pnpm/pnpm/issues/5894) related to GitHub's detached HEAD state during releases.

Authentication is provided via `NODE_AUTH_TOKEN`, set to a value from a [GitHub secret](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) called `NPM_TOKEN`.

> 💡 An interesting hiccup occurred with the naming of the authentication environment variable. While using `NPM_TOKEN` seemed correct (it kind of worked), it had to be `NODE_AUTH_TOKEN` for the action to work correctly. https://github.com/npm/cli/issues/1637#issuecomment-1888709776

### Authentication and Security

The GitHub repository's Secrets feature is used to store the npm token. To get this token you need to log in to your npm account and generate a token that with permission to publish a new version of your package.

I chose a "Granular Access Token" that is valid for a limited time and has access to all packages of my organization.

![The npm user interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s738fhtxay076dsa7vqy.png)

### Conclusion

Automating package publishing with GitHub Actions was generally straightforward, yet it required some fine-tuning to address certain issues. These included configuring workflow triggers, optimizing caching mechanisms, managing authentication tokens, and navigating the nuances of npm and GitHub environments for seamless operation.

Hope you found this guide helpful. Happy coding, and have a great day! 🚀👨‍💻👩‍💻

---

And here is full content of my YAML file (tested and working):

```yml
name: Release components package

on:
  release:
    types: [published]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2
        name: Install pnpm
        with:
          version: 8
          run_install: false

      - name: Install Node.js
        uses: actions/setup-node@v3
        with:
          node-version-file: ".nvmrc"
          cache: pnpm
          registry-url: https://registry.npmjs.org

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Publish 🚀
        shell: bash
        run: pnpm publish packages/components --access public --no-git-checks
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```