Semantic Release with Lerna and Conventional Commits

Help yourself and your teammates to follow the standards

Published: 2018-06-16

In this tutorial, I will show you how to configure Lerna with Conventional Commits to achieve automatic Semantic Release based only on the history of commits. But the truth is: there is not so much to configure (only one flag in Lerna config), mostly it's only a sticking to the Conventional Commits rules.

When may you need these settings?

  • when you use --independent flag in Lerna
  • when you are lazy (or you don't believe in your and teammates ability to determine proper release version)

In Lerna docs just at the beginning, they mention this problem:
Independent mode allows you to more specifically update versions for each package and makes sense for a group of components. Combining this mode with something like semantic-release would make it less painful.

There can be a lot of new concepts here, so I prepared a glossary of difficult concepts:

Conventional Commits
Opinionated convention of the standardized commit message. This convention comes from original AngularJS commit rules. It's not tight strictly to AngularJS rules, but for sake of simplicity, you can think of this as a synonym for AngularJS commit convention since it is the most popular option.

Conventional Changelog
Tool for generating a CHANGELOG.md from git metadata. This tool works only when you follow a Conventional Commits rules.

Semantic Release
Tool for generating version number, git tag, Conventional Changelog, release commit, pushing changes to the origin and publishing npm package. There is a lot of responsibility in one package, but don't be afraid of it's build in Lerna when you specify a flag "conventionalCommits": true. This tool works only when you follow a Conventional Commits rules.

Commitlint
Tool for helping you and your teammates to follow a Conventional Commits standards. It's not mandatory but it's nice to have. Other options are to use a cz-cli with a cz-conventional-changelog config.

Table of Contents

  1. Setup project (clone a repo)
  2. Setup Verdaccio
  3. Publish initial package versions
  4. Configure commitlint
  5. Write and publish changes

Setup project (clone a repo)

This tutorial is based on reggi/lerna-tutorial final configuration, so I highly recommend you to become familiar with this configuration, exceptionally when you're new to Lerna.
In the beginning, clone starter repo lerna-conventional-commits-example

git clone git@github.com:Everettss/lerna-conventional-commits-example.git

Remove local tags, since this example contains a final solution.

git tag -d $(git tag -l)

After that setup your own origin.

This starter kit follows mostly original reggi/lerna-tutorial except for following configurations:

After cloning a repo run:

npm install
npx lerna bootstrap
# or if you have lerna installed globally
lerna bootstrap
Now your project structure should look like this:
.
├── lerna.json
├── package.json
└── packages
    ├── alpha
    │   ├── index.js
    │   └── package.json
    ├── beta
    │   ├── index.js
    │   └── package.json
    └── usage
        ├── index.js
        ├── node_modules
        │   └── @my-scope
        │       ├── alpha (symlink)
        │       │   ├── index.js
        │       │   └── package.json
        │       └── beta (symlink)
        │           ├── index.js
        │           └── package.json
        └── package.json
lerna.json
{
  "lerna": "2.11.0",
  "version": "independent",
  "command": {
    "publish": {
      "conventionalCommits": true
    }
  }
}
packages/alpha/index.js
module.exports = 'alpa';
packages/alpha/package.json
{
  "name": "@my-scope/alpha",
  "version": "1.0.0",
  "publishConfig": {
    "registry": "http://localhost:4873"
  }
}
packages/usage/index.js
const alpha = require('@my-scope/alpha');
const beta = require('@my-scope/beta');
console.log(alpha + ' ' + beta);
packages/usage/package.json
{
  "name": "@my-scope/usage",
  "version": "1.0.0",
  "dependencies": {
    "@my-scope/alpha": "^1.0.0",
    "@my-scope/beta": "^1.0.0"
  },
  "publishConfig": {
    "registry": "http://localhost:4873"
  }
}

Setup Verdaccio

Verdaccio is not the only one tool for private npm registry. I choose it because it's the easiest to set up. If you are familiar with private registry you can skip this chapter, or if you don't bother to publish to public npm registry you can remove "publishConfig" from packages

Setting up Verdaccio it's simply about doing steps from official docs

npm install --global verdaccio
npm set registry http://localhost:4873
npm adduser --registry http://localhost:4873
verdaccio

If everything goes well you should see empty registry when you enter http://localhost:4873

Publish initial package versions (1.0.0)

Just for a start, we are going to populate Verdaccio with initial versions of packages. For simplicity, all of the packages have version 1.0.0

npx lerna publish --repo-version=1.0.0

Your console output should inform you that you successfully published all packages:

#

Now your personal instance of Verdaccio should look like this:

Configure commitlint

Before we write any changes to our packages it's nice to configure commitlint, it will help you stick to the standards. In the project root add packages (if you clone lerna-conventional-commits-example it's already configured for you):

npm install @commitlint/cli @commitlint/config-conventional husky --save-dev

And in the root package.json configure linter like this:

{
  "name": "lerna-conventional-commits-example",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "commitmsg": "commitlint -e $GIT_PARAMS"
  },
  "devDependencies": {
    "@commitlint/cli": "^7.0.0",
    "@commitlint/config-conventional": "^7.0.1",
    "husky": "^0.14.3",
    "lerna": "^2.11.0"
  },
  "commitlint": {
    "extends": [
      "@commitlint/config-conventional"
    ]
  }
}

Write and publish changes

Now when everything is setup we can start adding new features to our packages!
Our goal is to change message from alpha beta to the classic Hello world!. Since this task is "very demanding" I'm going to pretend real development cycle of it.

The first task will be to fix nasty typo in alpha package:

git commit -am "fix: typo in message"

Awesome, now it's time to develop real feature:

git commit -am "feat: part of classic Hello world"

Oh... "hello" should be capitalized... (let's pretend that --amend flag doesn't exist for sake of this examples)

git commit -am "fix: upper-case message"

OK, it's enough. Now we are ready to publish this changes! Since we used Conventional Commits convention we don't have to worry about modifying CHANGELOG.md for each package and figure out proper version of new releases. Just run:

npx lerna publish
#

Package alpha bump minor version because since the last release was made one feature and two fixes. Package beta bumps minor version because it contains one feature commit. usage was calculated only as a fix version relese because we didn't precise any commit releated to it but package fersion of alpha and beta was updated in usage/package.json.

You can find final repo version here.

Take a look at the generated CHANGELOG for alpha package:

Our goal was a print Hello world! but for now, our packages prints Hello world. Give it a try and make changes to beta packages and see what version will be your new release.

If you ever try to do this kind of package version management you know how much overhead is it. As I mentioned at the beginning, this is mostly done only by one flag in Lerna config!