Semantic versioning a Node.js app on Azure DevOps CI

Using semantic versioning allows you to completely automate updating versions in package.json and remove any arguments in your team about versioning. Here is how I use it in my Node.js apps.

Versioning and semver

If you deploy an application it’s really useful to know what version of the code is running on a given environment. A really common version system is “semver” or semantic versioning. Semver is used all over the JavaScript ecosystem.

A semver looks like 2.45.1 where the parts are [major].[minor].[patch]. The major version gets updated when there is a breaking change. The minor version gets updated when there is a new feature and the patch version gets updated when there is a bugfix or similar.

A package system can use this format to decide what version to upgrade automatically. It is assumed that major versions will break things so that needs a manually upgrade. But minor and patch versions can be updated safely by npm or yarn.

Conventional commits

If we want to automate setting the version then we need to be able to detect the type of change in a commit. This is where conventional commits come in.

If we use a convention when setting a commit message, tools can read the commit messages and decide if you have added a major change, a minor change or a patch.

The convention is to use a prefix. There are some well known prefixes that are understood by tools. feat, fix, docs, chore are common. Your libraries will list the ones it understands. I find it easier to use only 3-4 in my day to day work.

Example commit messages: feat: added the new menu item or fix: There was a typo on the menu button.

In those cases the commits would be parsed as feat - minor version bump and fix - patch version bump. If we know that we are introducing a breaking change we can add a bang to the prefix. So feat!: Change session cookie name would indicate that that this is a breaking change and would bump the major version.

The pattern we will use

  1. We make a change to the code. It goes in to PR and gets merged to master with a conventional commit message.
  2. Our CI system picks up the change and builds it
  3. The build completes without errors so we set the version in package.json based on the previous version and the commit message
  4. We also tag git with the new version

Enforcing conventional commits

To enforce commit format we will use a few libraries. Husky in particular is great for running checks each time we work with git. Install these with

yarn add -D husky @commitlint/cli @commitlint/config-conventional

Husky lets us add hooks when we work with git. Commitlint checks the commit message.

We need to configure the libraries. In your projects package.json file you can add

{
  "commitlint": {
    "extends": ["./node_modules/@commitlint/config-conventional"],
    "rules": {
      "subject-case": [0],
      "header-max-length": [0, "always", 120]
    }
  },
  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  }
}

This tells commit lint to use the conventional commit rule set as a base. And then tells husky to use commit lint whenever we submit a commit.

There are many linting rules available. Check out the commitlint cli documentation for more.

I use rules that turn off any subject case requirements. By default commitlint requires lowercase for everything but I sometimes use pascal case to start sentences out of habit.

I also set the header length to be a bit longer than the default.

-E HUSKY_GIT_PARAMS - husky puts all the variables that we passed to git in the variable HUSKY_GIT_PARAMS so that tools like commit lint can see and use them.

What will happen here is that every time a commit is attempted, it will get linted based on the conventional commit message rules. The developer will get an error if they deviate. This is painful to start with but it becomes second nature after a while!

Bumping versions based on commit message

When your CI tool builds a new change you can have it set the version before you package the code artifacts. For this we use a package called semantic-release in our CI step.

This setup means installing the following libraries

yarn add -D @semantic-release/changelog @semantic-release/exec

Then we add the following to our package.json file. We add a script that calls semantic-release.

We configure the release process. There is more detail on the semantic-release documentation but briefly,

  1. we check for any existing tag on our repository for the current version
  2. we parse the commit message for the next version
  3. we bump the package.json version to the new version
  4. we create a changelog
  5. we push the changelog to git in a release
  6. we tag git with the latest version
{
  "scripts": { "release": "npx semantic-release" },
  "release": {
    "plugins": [
      "@semantic-release/commit-analyzer",
      "@semantic-release/release-notes-generator",
      [
        "@semantic-release/npm",
        {
          "npmPublish": false
        }
      ],
      [
        "@semantic-release/changelog",
        {
          "changelogFile": "docs/CHANGELOG.md"
        }
      ],
      [
        "@semantic-release/github",
        {
          "assets": ["docs/CHANGELOG.md"]
        }
      ]
    ]
  }
}

Calling this on your CI system

After we have successfully built the change and tested it, basically when we know we have a valid version, we can bump the code version.

We run the release command we just configured. This will do the steps described above.

Note: We don’t push the updated package.json or other version changes to git on CI because it would cause another commit. So the version is only on the git tag. If we want to have the version anywhere else on CI we have to manually set it.

In the second command in this script we copy the version from the package.json file and put it in our front end project as a simple txt file.

This step has a condition that only runs for commits to master. We don’t want to version feature branches.

- script: |
    npm run release
    node -e 'console.log(require("./myProject/package.json").version)' | tee myFrontEndProject/build/static/version.txt
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
  displayName: 'Bump release version'

The result on our repository looks like

Git versions
The different versions applied by the script

Darragh ORiordan

Hi! I'm Darragh ORiordan.

I live and work in Sydney, Australia building and supporting happy teams that create high quality software for the web.

I also make tools for busy developers! Do you have a new M1 Mac to setup? Have you ever spent a week getting your dev environment just right?

My Universal DevShell tooling will save you 30+ hours of configuring your Windows or Mac dev environment with all the best, modern shell and dev tools.

Get DevShell here: ✨ https://usemiller.dev/dev-shell


Read more articles like this one...

List of article summaries

#nodejs

Running a NodeJS app with Postgres in Dokku

I have some side projects that don’t see much traffic so I run them on a 5$ Digital Ocean droplet running Dokku.

Dokku is an open source Heroku-like platform. It’s super easy to create and maintain multiple applications on a Dokku instance. It’s perfect for solo makers.

There are plugins for most of the common services you might want like PostgreSQL or Redis.

Here’s what we’re going to do

  1. A Brief overview of Dokku
  2. How to get a Dokku instance running
  3. Create a new Dokku application
  4. Add a database service
  5. Expose the database for debugging and testing (Optional)
  6. Add a domain to the application
  7. Add any configuration variables for your application
  8. Add SSL to the application
  9. Add the Dokku remote to your application
  10. Push your application to Dokku
  11. Maintaining your Dokku server
#nodejs

npmrc authentication for a private scoped organisation package

When you have to login to npm for multiple organisations it can be easier to use an .npmrc file that you move around rather than npm login command.

#nodejs

Semantic versioning javascript projects but skipping NPM publish

If you want to use semantic versioning and automate release versions using semantic-release for your front-end client application you probably don’t want to actually publish it to npm.

Here is how to use semantic-release while not releasing to npm.

#nodejs

Avoid these issues when using new ECMAScript modules in your Node.js application

ECMAScript modules are the official standard format to package JavaScript code for reuse in the future. Es6 modules now have full support in Node.js 12 and above so it’s time to start using them.

JavaScript developers and node libraries have typically used commonjs for modules up to now. If you’ve used typescript in the past few years you will be familiar with the module import syntax in you application. Instead of commonjs require("module") most typescript applications use some variation of import module from "module".

Typescript will then transpile this import syntax into commonjs require statements for you. This step is not necessary in modern Node.js applications. You can just use import module from "module" directly in your transpiled code.

If you use typescript you can change just change your tsconfig settings to output ECMAScript es6 modules and you will be good to go. If you don’t use typescript you might have to do some rewriting if you want to get your app updated.

Here are solutions to the issues that took me a bit of time and investigation to figure out when I was upgrading my Node.js application to use ECMAScript modules like configuring typescript, setting up jest, configuring the package.json correctly and more.

Comments