Automate component library releases through commit conventions#

Release new versions of your code using tools like Lerna and Commitizen. By using commit conventions we can automate the versioning and get great looking changelogs.

Scenario: We need to release component libraries to npm because we want to consume them inside our Angular applications. We want to be able to release the components independently. Furthermore, we have some dependencies between the libraries.

Does this sound complicated? What if I tell you we want to use Semantic versioning and release multiple commits at once. And of course, we want professional looking changelogs like all the other cool projects. Does this sound too complex to you?

“Simple things should be simple, complex things should be possible.” — Alan Kay

In this article, I will show how to release new versions of your code using tools like Lerna and Commitizen. By using commit conventions, we can automate the versioning and get great looking changelogs.

changelog
code diff

The final code is on GitHub.

Definitions#

Before beginning, we need to go through some concepts and tools.

Semantic Versioning (SemVer)#

Following the Semantic Versioning spec helps other developers who depend on your code to understand the extent of changes in a given version and adjust their code if necessary.

Given a version number MAJOR.MINOR.PATCH, increment the:

  1. MAJOR version when you make incompatible API changes,
  2. MINOR version when you add functionality in a backward-compatible manner, and
  3. PATCH version when you make backward-compatible bug fixes.

Conventional Commits#

The Conventional Commits specification is a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history, which makes it easier to write automated tools. With these tools we can do things like:

  • Automatically generate change logs.
  • Automatically determine semantic version bumps

By better communicating the nature of changes we make it easier for people to contribute to your projects.

The commit message should be structured as follows:

<type>[optional scope]: <description>[optional body][optional footer]

A simple real-world example can look like this:

fix(button): jira-1234 fixed minor bug

Here we did a bug-fix for the button connected to jira-1234.

Commitizen#

Commitizen gives conventional commit messages as a global utility. I’ll use it to create git commit messages that can be analyzed to determine the next version. If installed globally, we can use git cz instead of git commit when committing code.

commitizen commit menu

Conventional Changelog#

Conventional changelog is a tool for generating a CHANGELOG.md from git metadata. This tool only works when we follow Conventional Commits rules.

Husky#

Something is needed to stop bad commits. By adding a git hook with Husky, we can run custom scripts on the commit before letting it through.

Commitlint#

After using Husky to capture the commit, we use commitlint to check that the commit is using the correct conventions.

Lerna#

Lerna is a tool for managing JavaScript projects with multiple packages. It allows you to manage your project using one of two modes:

  • fixed mode keeps all versions of packages at the same level
  • independent mode allows independent versions of each package

Prerequisites#

If you want to do more than read the article you need:

  1. node and npm installed on your computer
  2. git ready to run
  3. GitHub account
  4. A code editor like VS Code

Workspace with Libraries#

The goal is to have multiple libraries that can be versioned independently and handle dependencies. For this, I’m creating an empty workspace with two libraries.

Make sure you have Angular CLI installed globally with:

npm i @angular/cli -g

In global mode (i.e., with -g or --global appended to the command), it installs the current package context (i.e., the current working directory) as a global package.

To begin with, I’m going to create an empty workspace called semver-libs.

I don’t want to have an application and use the --create-application flag with the ng new command. Setting this to false creates an empty workspace with no initial app.

ng new semver-libs --create-application=false

With the application in place let’s create the libraries:

cd semver-libs
ng g lib button
ng g lib input

The lib command will create a new folder called projects containing the newly created libraries:

|- semver-libs
  |- projects
    |- button
    |- input

Commit to GitHub#

To be able to track the changes and see the changelog I need to set up a code repository. I’ll be using GitHub.

First, I create a repository with the same name as our project, semver-libs. Secondly, I commit all the code changes and push the code to GitHub by following the instructions given after creating the repository.

git commit -a -m "Initial commit"
git remote add origin https://github.com/melcor76/semver-libs.git
git push -u origin master

Now the repository is ready so let’s continue setting up the environment.

Setup Lerna#

Lerna is a CLI (command line interface) tool, so I install it globally.

npm i lerna -g

To integrate it to the project, to help manage the libraries, I can initialize the workspace by running lerna init, that creates a new Lerna repository. I’m using independent mode with -i to be able to increment package versions independently of each other.

lerna init -i

lerna init

The command added Lerna to devDependencies in package.json.

It also created the Lerna configuration file lerna.json in the root path. By default, Lerna points its packages to the packages folder. I need to make a few changes:

  1. Change packages to the projects folder.
  2. Add the publish command and set it to conventional commits.
  3. Add the version command commit message to be correct format.
  4. Delete the packages folder that Lerna created for us.
// lerna.json
{
  "packages": ["projects/*"],
  "version": "independent",
  "command": {
    "publish": {
      "conventionalCommits": true
    },
    "version": {
      "message": "chore(release): release"
    }
  }
}

Now Lerna is set up to read conventional commits.

It is important to set the commit message in the correct format or Husky will stop the commit. We could also do something like this to not push on version and set the commit message for publish instead:

"command": {
  "publish": {
    "conventionalCommits": true,
    "message": "chore(release): release"
  },
  "version": {
    "push": false
  }
}

You can read more on this in the docs.

Setup Commitizen#

It’s possible to write the commits in conventional style, but I will use Commitizen to create git commit messages that can be analyzed to determine the next version.

First, install the Commitizen CLI tools globally:

npm i commitizen -g

Next, we need to choose an adapter to create the changelogs. The adapter tells which template our contributors should follow. Let’s use the conventional changelog adapter.

commitizen init cz-conventional-changelog -D -E

-D, --save-dev: Package will appear in your devDependencies.
-E, --save-exact: Saved dependencies will be configured with an exact version rather than using npm’s default semver range operator.

Or if you prefer npx over installing Commitizen:

npx commitizen init cz-conventional-changelog -D -E

The above command does three things for you:

  1. Installs the cz-conventional-changelog adapter npm module
  2. Saves it to package.json’s dependencies or devDependencies
  3. Adds the config.commitizen key to the root of your package.json
"config": {
  "commitizen": {
    "path": "./node_modules/cz-conventional-changelog"
  }
}

Now you are all set to run your commits through Commitizen with git cz.

commitizen menu

You can commit and push these changes to GitHub before we try some conventional commits.

Conventional Commits#

Now we make a small change to button.component.ts and capitalize “Button works!” Then add the file to be committed.

git add --all

Commit with git cz and answer the questions.

commitizen questions

If I now run lerna version, it will look for conventional commits and figure out that, since I chose the change type refactor, the version bump needed for the button is patch.

lerna version

If I instead run lerna publish it will run the same process but also ask if I want to publish the packages. If I answer yes it will try to publish to npm. But since I have not yet set this up, it will not work.

lerna publish

If we check the commits on GitHub, we can see that it added a Publish commit where it increased the version of the button to 0.0.3.

publish commit

Change Log#

Now we have a process to version our commits automatically. But it would also be great to have a changelog that picks up all the changes. By adding a flag to Lerna, we can get that.

When running with --conventional-commits, lerna version will use the Conventional Commits Specification to determine the version bump and generate CHANGELOG.md files.

A reminder of SemVer version bumps: MAJOR.MINOR.PATCH

Examples of how the versions are decided:

  • Fix/refactor = patch version bump
  • Feature = minor version bump
  • Breaking changes = major version bump

Let’s make a few changes to try out this magic.

git add -A
git cz
lerna version --conventional-commits

If we check under commits in GitHub, we see that we got one commit for the change (fix) and one for the Publish.

github commits

If we click on Publish, we can see that it updated the version in package.json and updated CHANGELOG.md with the changes.

changelog diff

And if we open the file, we see how it will look on the web when we publish changes to the libraries.

changelog

We can see that I fixed a bug and we get a link to the changes.

code diff

Not too bad! One could even say this looks professional!

Dependencies#

Let’s create a dependency between the libraries and see what Lerna does with it. We can use the add command to add button as a dependency to input.

lerna add button --scope=input

The button was added to the dependencies in the package.json of input. Let’s not worry too much about the install errors but instead see what happens when the button is updated.

First, I’ll commit and push the dependency change and then make a small change to the button again before I commit and run Lerna.

git add -A
git cz
lerna version --conventional-commits

lerna version changes

And here we can see that even though I didn’t make any changes to input, it had its version patched because it now depends on the button. And since button got its version bumped up, then input automatically got the version updated in its package.json.

version bump

In other words, Lerna is now taking care of the dependencies for us.

Setup Husky#

Now that I use Lerna to calculate the versions of the packages I need to make sure that all commits have the correct format. To get a git hook to the commit command, we can use Husky. And as the documentation says we should only add Husky to the root package.json in a multi-package repository.

Let’s install Husky to the devDependencies.

npm i husky -D

Git hooks can get parameters via command-line arguments, and Husky makes them accessible via HUSKY_GIT_PARAMS. Husky’s commit-msg hook can be used to lint commits before they are created.

"husky": {
  "hooks": {
    "commit-msg": "echo $HUSKY_GIT_PARAMS"
  }
}

Husky gives us the needed hook, but another package is needed to lint the commit message.

Commitlint#

To lint the commit messages I use commitlint. Let’s install it with the conventional format.

npm i @commitlint/cli -D
npm i @commitlint/config-conventional -D

Now the Husky hooks can be added in package.json:

"husky": {
  "hooks": {
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
  }
}

And lastly, tell which rules are used by again adding to package.json:

"commitlint": {
  "extends": [
    "@commitlint/config-conventional"
  ]
}

If I now try a git commit with a message that is not correctly formatted:

git commit -a -m "Add Husky and commitlint"

-a, — all Tells the command to automatically stage files that have been modified and deleted, but new files you have not told Git about are not affected.

I get stopped by husky and commitlint:

commitlint error

The same thing if I try it in VS Code:

vs code error

That’s what I wanted, so that’s great. Let’s see if I can commit with git cz.

commitizen commit

Success! New feature added with the correct commit format.

Conclusion#

By using Lerna together with a few other tools, we can improve the release process. And by using Conventional commits and Semantic versioning we can get proper documentation of our changes that benefits not only us but all who depend upon our libraries.

Go forth now and publish your packages...

like a boss

The final code is on GitHub.

Thanks to Kristiyan Serafimov who put this excellent tech stack together in React. I only copied it to Angular and wrote about it.

Resources#