Releasing GitHub npm packages

In this post I’ll give you an introduction into setting up a fully automated release process for GitHub packages. I use npm as an example because it is easy to use. But you can use the same approach for NuGet, Maven, or Gradle.

Semantic Versioning

Before we start with the release workflow, we have to take a look into versioning. We use version numbers for releases to indicate the consumers of our package if we have a breaking change or a bugfix. We also indicate the quality of the release with alpha or beta releases.

A formal convention for specifying version numbers for software is Semantic versioning. It consists of different parts with different meanings. Examples for semantic version numbers are 1.0.0 or 1.5.99-beta. The format is:

<major>.<minor>.<patch>-<pre>

Major version: A numeric identifier that gets increased if the version is not backwards compatible and has breaking changes. An update to a new major version must be handled with caution! A major version of zero is for the initial development.

Minor version: A numeric identifier that gets increased if new features are added but the version is backward compatible to the previous version and can be updated without breaking anything if you need the new functionality.

Patch: A numeric identifier that gets increased if you release bugfixes that are backwards compatible. New patches should always be installed.

Pre version: a text identifier that is appended using a hyphen. The identifier must only use ASCII alphanumeric characters and hyphens ([0-9A-Za-z-]).  The longer the text, the smaller the pre-version (meaning -alpha < -beta < -rc). A pre-release version is always smaller than a normal version (1.0.0-alpha < 1.0.0).

See https://semver.org/ for the complete specification.

You can use the Conventional Commits specification to provide information in your commit messages what kind of change the commit contains. 

Step-by-step to a release workflow

I created a demo repository on GitHub that you can use as a starting point. I’ll explain here the individual steps:

  1. Create a new repository called package-demo in GitHub and select Node as your .gitignore template:
  1. Clone your repository to your machine:
$ git clone https://github.com/<username>/package-demo.git 
$ cd package-demo
  1. Create a file index.js and add the following code:
alert("Hello, World!");
  1. Run npm init to initialize your package using the wizard. The package name is @<github-user-name>/package-demo. As the test command just enter exit 0. Leave the other default values
$ npm init
...
package name: @YOUR-USER/package-demo
...
test command: exit 0
...
If you don't have npm installed and just care about the release part you manually create a file package.json and package-lock.json in the root of your repo and copy the content from my repository to it. You have to edit the content and replace my username with yours.
  1. Commit and push your files:
$ git add index.js package.json package-lock.json
$ git commit -m "Initialize package"
$ git push
  1. Now create a workflow file .github/workflows/release-package.yml. You can copy the content from my repository – but I add here some additional comments that explain what the workflow does.
name: Node.js Package

# The workflow gets triggert when a new release is created
on:
  release:
    types: [created]

# You could also run it when you push a tag that start with 'v'.  
# Or a specific branch. It depends a lot on your workflow
# on:
#   push: 
#     tags:
#       - 'v*'

jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    permissions:
      packages: write
      contents: read
    steps:

      # You need a shallow clone for GitVersion to work.
      # That's done specifying fetch-depth: 0
      - uses: actions/checkout@v2
        with: 
          fetch-depth: 0 
          
      # Set up node and add the registry url
      - uses: actions/setup-node@v2
        with:
          node-version: 12
          registry-url: https://npm.pkg.github.com/
      
      # Install GitVersion to automatically create a semantic 
      # version from your repository: 
      - name: Install GitVersion
        uses: gittools/actions/gitversion/setup@v0.9.7
        with:
          versionSpec: '5.x'
         
      # Run GitVersion (set an ID to later fetch values)
      - name: Determine Version
        id:   gitversion
        uses: gittools/actions/gitversion/execute@v0.9.7
      
      # Now  happens the magic where you set the id of your package the default
      # would be $GITVERSION_SEMVER. But you could also use teh tag name
      # or access individual values from the gitversion task:
      - name: 'Change NPM version'
        uses: reedyuk/npm-version@1.1.1
        with:
          version: $GITVERSION_SEMVER
          # Use the tag name: version: ${{github.event.release.tag_name}}
          # Access individual values from gitversion:
          # ${{ steps.gitversion.outputs.major }}

      # Build
      - run: npm ci
      
      # Publish using the GITHUB_TOKEN to authenticate      
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
  1. Create an .npmrc file in the root of your repo with the following content:
@YOUR-USER:registry=https://npm.pkg.github.com
  1. Commit and push the new files:
$ git add .github/workflows/release-package.yml .npmrc
$ git commit -m "Add workflow to pusblish package"
$ git push
  1. Create a release. Click on Releases under Code and click Draft new Release. Create a new tag and enter a title. If you create the release, the workflow will automatically run.
  1. Wait until the workflow has completed. You can now find your package under Code | Packages:
  1. Click on the package to see the details:

Adjust to your workflow

The tool GitVersion is very powerful. It can create versions out of :

  • Tags
  • Versions in branches (i.e. release/2.0.0)
  • Merge-commit messages (i.e. “Merged branch ‘release/2.0.0.’ into master”)
  • A config file GitVersion.yml (i.e. next-version: 2.0.0.0)

It also support Conventional Commits using a special configuration:

mode: MainLine # Only add this if you want every version to be created automatically on your main branch.
major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)"
minor-version-bump-message: "^(feat)(\\([\\w\\s]*\\))?:"
patch-version-bump-message: "^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s]*\\))?:"

Consult the documentation of GitVersion to adjust it to fit to your workflow.

Conclusion

Setting up a fully automated release workflow for your GitHub packages is easy if you combine the right tools. I hope this post gives you a good jump start into semantic versioning, GitHub packages and automated release workflows so that you can build a workflow that fits your way of working.

I’m interested in what more automation and tools you add to your release workflows for packages. Leave a comment or contact me on Twitter (@mike_kaufmann) if you are willing to share.

Leave a comment