Release management in Angular with Lerna
In this article we will learn about release management in Angular with Lerna.

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.
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:
- MAJOR version when you make incompatible API changes,
- MINOR version when you add functionality in a backward-compatible manner, and
- 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.
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:
- node and npm installed on your computer
- git ready to run
- GitHub account
- 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
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:
- Change packages to the
projects
folder. - Add the publish command and set it to conventional commits.
- Add the version command commit message to be correct format.
- 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 yourdevDependencies
.
-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:
- Installs the cz-conventional-changelog adapter npm module
- Saves it to package.json’s dependencies or devDependencies
- 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
.
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.
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.
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.
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.
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.
If we click on Publish, we can see that it updated the version in package.json
and updated CHANGELOG.md
with the changes.
And if we open the file, we see how it will look on the web when we publish changes to the libraries.
We can see that I fixed a bug and we get a link to the changes.
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
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.
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:
The same thing if I try it in VS Code:
That’s what I wanted, so that’s great. Let’s see if I can commit with git cz.
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...
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.