Publish private npm package with github actions and packages

Open source projects are great, but there are times when we really need our NPM packages to be private. npmjs is the default registry for node packages, and although it is possible to host private packages there, it costs! Using github packages instead also has the advantage of keeping both our codebase and published package within the same ecosystem. In this article we'll look at how to use github actions to create a new github package.

Published:

Getting started

We'll use the commitizen/eslint/prettier/husky starter code which supports the Commitizen/ESLint/Prettier/Husky article as a starter. This is a simple project that just integrates some front end tooling which makes our development process a bit saner - since we setup Commitizen and conventional changelog in that project to help enforce standardised commit messages, we have a great starting point from which to commence. To follow along at home, feel free to clone that project and read the article, particularly if you want to understand Commitizen and it's associated tooling in more detail.

Public vs private github packages

Github packages has the ability to publish both private and public packages. Whether a package is private or public is inherited from the repository setting. If the repository is public, the package will be public. And if the repository is private, the package will be private too.

The only grey area would be if we published a package when the repository was public and then make it private afterwards. If this happens, our package would remain public in order to prevent anyone else's code depending on this package from breaking.

For the purposes of this exercise, create a private repository from the outset, and the package will be private too.

Update package.json

The package.json name must match your github repository URL, prefixed with an @ sign. The repsitory URL should also be updated at the same time. The publishConfig is also crucial - if you run an npm publish locally (although not recommended with this workflow) it lets NPM know that you intend to publish to the Github registry rather than the standard registry.

package.jsonjson
{
  "name": "@YOUR_USERNAME/YOUR_REPO_NAME",
  "repository": {
      "type": "git",
      "url": "git+https://github.com/YOUR_USERNAME/YOUR_REPO_NAME.git"
    },
  "publishConfig": {
    "@YOUR_USERNAME:registry": "https://npm.pkg.github.com"
  },
  ...
}

Set up Github workflows

We are now going to create two Github workflows. The first one's responsiblity is to prepare our package for release, and the second one will actually create a release of that package.

Build, bump, and tag workflow

Firstly, we create the file .github/workflows/build-bump-and-tag.yml This code should look as follows:

build-bump-and-tag.yml
name: Build bump tag

on:
    push:
        branches:
            - "master"

jobs:
    install-and-test:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v2
            - uses: actions/setup-node@v2
              with:
                  node-version: 12
            - run: npm i -g husky
            - run: npm ci
            - run: npm test
    tag-and-bump:
        if: "!startsWith(github.event.head_commit.message, 'bump:')"
        needs: install-and-test
        runs-on: ubuntu-latest
        name: "Bump version and create changelog with commitizen"
        steps:
            - name: Check out
              uses: actions/checkout@v2
              with:
                fetch-depth: 0
                token: "${{ secrets.GITHUB_TOKEN }}"
            - id: cz
              name: Bump package
              uses: commitizen-tools/commitizen-action@master
              with:
               github_token: ${{ secrets.GITHUB_TOKEN }}
            - name: Print Version
              run: echo "Bumped to version ${{ steps.cz.outputs.version }}"

This workflow will be trigged with any pushes to the master branch. This first job in this workflow installs dependencies and runs any tests which we might have.

Because the starting code is setup with Commitizen and conventional changelog, we are using commitizen action which is an action that automatically bumps our version and generate a changelog. Commitizen will decide whether to release a major, minor or patch version of our package according to semantic versioning, based on the commit type(s) it encounters.

Note that the tagging and bumping is disabled if the latest commit start with the phrase 'bump:'. When commitizen-action gets to work, once it has decided which version number is appropriate, it'll make a commit to the master branch which begins with this phrase. Without checking if the latest commit on master has this phrase, we'd get stuck in a bit of a loop.

At the same time, Commitizen generates (or updates) a CHANGELOG.md file in the root of our project which is great for automatically communicating the changes in a release to ourselves or other developers working with the codebase.

Release workflow

For our second workflow, create the file .github/workflows/release-package.yml. It should look like this:

release-package
name: Release

on:
    workflow_run:
        workflows: ["Build bump tag"]
        types:
            - completed
jobs:
    publish-gpr:
        runs-on: ubuntu-latest
        if: ${{ github.event.workflow_run.conclusion == 'success' }}
        permissions:
            packages: write
            contents: read
        steps:
            - uses: actions/checkout@v2
            - uses: actions/setup-node@v2
              with:
                  node-version: 12
                  registry-url: https://npm.pkg.github.com/
            - run: npm publish
              env:
                  NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}

This release workflow is triggered only if or when the first workflow has completed. Note the workflows: "Build bump tag" line - this has to match the name of the first workflow that we created earlier.

We then check whether the first workflow not just completed, but completed successfully. If it did, we proceed with publishing to the Github repository.

Create a commitizen .cz.toml file

The final piece of the puzzle is to add a .cz.toml file to the root of our project:

.cz.tomlbash
[tool.commitizen]
version = "1.0.0"
version_files = [
  "package.json:version"
]

We ensure that we set the version property to the same version that is currently in our package.json file.

The version_files array is used to instruct Commitizen to look for the version number in the listed file(s) and update it. In the example above, Commitizen will look in the package.json for the 'version' key, and update it with the new version number.

Using our private package in an app

Now, we can add our private package as a dependency in an app in the usual way - by adding it as a dependency/peer dependency/dev dependency in our package.json. However, we have to tell NPM where this package resides, as by default it'll look in the NPM registry and not find it.

This is easily done by adding an .npmrc file in the root of the app that wishes to depend on our private package:

.npmrcbash
@YOUR_USERNAME:registry=https://npm.pkg.github.com

This will ensure that when looking for packages with our username, it'll look in the Github packages registry instead.

Wrap up

Now we have learnt how to store private packages within Github packages, using Commitizen and Github actions to manage our release process for us!