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.
{
"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:
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:
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:
[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:
@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!