One thing has always been a thorn in my side. Having to remember to bump the package version for my code releases.
For certain projects, this is an essential part of the release because it ensures that customers will receive the latest changes. Moreover, we are obligated to bump the package version whenever we make any change. And that’s fine, because other developers have to be sure that they will always have the same content when pulling the package with the same version, at different times. In short, if we create some kind of package that will be shared with others, versioning is not just a good practice but a necessity. However, manual bump of the package version could led to several common problems:
- When we forget to do it, it requires another pull request review just for that simple change. It may take time.
- Developers forget the bump, their peers forget to review, then we have released the “same version” of our application.
These problems continue to grow with every new developer coming to the project.
In this article, we will learn how we can automate package versioning and publishing with Commitizen and Lerna.
Terminology
Before we start, let’s go through some of the concepts and tools.
What is Sematic Versioning?
You have probably seen different kinds of versioning many times. If you have ever fiddled with files like package.json
, you must have seen versioning annotation like, for example, >=1.2.1.
Each package manager has its own flavor of versioning annotation, but all of them have one thing in common – Semantic Versioning or SemVer in short.
SemVer works by structuring each version identifier into three parts, MAJOR, MINOR, and PATCH, and then putting these together using the familiar “MAJOR.MINOR.PATCH” notation. Each of these parts is managed as a number and incremented according to the following rules:
- PATCH is incremented for bug fixes, or other changes that do not change the behavior of the API.
- MINOR is incremented for backward-compatible changes of the API, meaning that existing consumers can safely ignore such a version change.
- MAJOR is incremented for breaking changes, i.e. for changes that are not within the backwards compatibility scope. Existing consumers have to adapt to the new API, most likely customizing their code.
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 on top. With these tools we can do things like:
- Automatically generate changelogs.
- Automatically determine semantic version bumps.
The commit message should be structured as follows:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
A simple real-world example can look like this:
1fix(inventory): fixed table scrolling
Commitizen
Commitizen is a simple wizard which guides the developer during the commit message creation, gathering all required information for well-formed conventional commits.
Lerna
Lerna is a tool that optimizes the workflow around managing multi-package repositories with Git and npm. It allows us to manage our project using one of two modes: Fixed or Independent .
Project setup
We will use a simple create-react-app monorepo template with yarn workspaces . You can also use your own monorepo for which you want to introduce automatic package publishing.
Having that said, let’s start with cloning a cra-typescript-monorepo-template repository. Additionally, we have to install our dependencies via yarn
command.
For easier tracking of changes, push this repo to your personal GitHub account, or any other source control platform.
Now that we have our repo ready, let’s see what our project structure looks like:
This project has a workspace called packages
that contains two packages: app
and shared
.
The shared
package is a dependency of an app
package. We can confirm that looking into the app’s package.json
:
Installing Lerna
Lerna is a CLI (command line interface), so let’s install it globally:
1npm install -g lerna
After successful installation, the next step is to initialize Lerna in the project. We will use independent mode with -i.
That way, Lerna will increment package versions independently of each other:
1lerna init -i
This creates a lerna.json
configuration file in the root of the project and adds Lerna to devDependencies in the package.json
.
In order to set up Lerna with yarn workspaces, we need to extend our lerna.json
, by adding yarn as our npmClient and specifying that we are using yarn workspaces.
By default, Lerna points its packages to the packages
folder, so we are good here:
Lerna provides the run
command which will run an npm script in each package that contains that script.
For instance, let’s say all of our packages follow the structure of the app
package:
and in each package.json
we have the test
npm script
then Lerna can execute each test
script with:
1lerna run test --stream
*— stream flag just provides output from the child processes
Let’s now add Lerna run commands in the root package.json
, for running, building, and testing our project.
Simply performing yarn test
, we should see the following output:
Installing Commitizen
It is possible to write commits in conventional style, but why bother? Commitizen can help us format commit messages with a series of prompts that are used to generate a commit message. Later, these commit messages will be analyzed to determine the next version.
First, let’s install the Commitizen CLI tools globally:
1npm install -g commitizen
Next, we need to choose an adapter to create the changelogs. The adapter tells us which template our contributors should follow. We will use the conventional changelog adapter:
1commitizen init cz-conventional-changelog -D -E
-D; –save-dev: Package will appear in devDependencies.
-E; –save-exact: Saved dependencies will be configured with an exact version rather than using npm’s default semver range operator.
In case you prefer npx instead of installing Commitizen:
1npx commitizen init cz-conventional-changelog -D -E
The above command does three things for us:
- 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 our package.json
Now we are set to run our first commits through Commitizen.
Once we have Commitizen installed, let’s also set Lerna to read conventional commits by additionally configuring the lerna.json
file:
- Add the publish command and set it to conventional commits.
- Add the version command commit message to be correct format.
Installing Verdaccio
In order to have somewhere to publish our packages and for better understanding, we need some npm registry.
Verdaccio is not the only tool for a private npm registry. I choose it because it is the easiest to set up.
Verdaccio is a simple, zero-config-required local private npm registry.
Setting up Verdaccio is simply about executing steps from the official documentation :
1npm install -g verdaccio 2npm set registry http://localhost:4873 3npm adduser --registry http://localhost:4873 4// run verdaccio 5verdaccio
If everything goes well, we should see an empty registry when we navigate to
http://localhost:4873
.
Publishing packages
After we successfully set up Lerna, Commitizen, and Verdaccio, it is time to publish our first package. But before we really publish something, let’s once more revisit our packages.
As we already mentioned, shared
package is a dependency of the app
. However, we want only shared
to be published. For Lerna to know which package to publish and which not, we have to scope our packages.
Scoping packages
Alright then, for shared packages, we will add a "private": false
field in the package.json file and for the app, we will set it to true
. That way only shared packages will be published to our private registry.
Just for a start, let’s commit the changes we made so far and publish the initial version of our shared package.
Simply, add the files to be committed:
1git add --all
Commit with git cz
and answer the questions.
After that, execute:
1lerna publish
When run, this command calls lerna version behind the scenes. A few seconds later, our console output should inform us that we have successfully published the shared
package:
Our personal instance of Verdaccio should look like this:
Dependencies
Let’s now make a small change to the Text.tsx
component and change the html tag h1 to h2. Commit with git cz
and answer the questions.
We are ready to publish these changes. Since we used Conventional Commits convention, we do not have to worry about modifying CHANGELOG.md
and figure out the proper version of new releases. Simply run:
1lerna publish
Here we can see that even though we did not make any changes to the app, it had its version patched because it depends on the shared package.
The new version of the shared package should reflect in Verdaccio as well.
That’s it! If we check the commits on GitHub, we can see that it added a Publish commit where it increased the version of both, app and shared packages, to 0.1.2
.
Let’s also have a look at the generated CHANGELOG files:
Installing Commitlint
We have Lerna to take care of the package versions. However, we also need to make sure that all commits have the correct format. Therefore, we will use commitlint .
Commitlint will help us adhere to a commit convention. Supporting npm-installed configurations, it makes sharing of commit conventions easy.
Let’s install commitlint with the conventional format, and configure it:
yarn add @commitlint/{cli,config-conventional} -D -W
echo "module.exports = {extends: ['@commitlint/config-conventional']};" > commitlint.config.js
Alternatively, the configuration can be defined in .commitlintrc.js, .commitlintrc.json, or .commitlintrc.yml file or a commitlint field in package.json.
Installing Husky
Husky is a tool that allows us to easily wrangle Git hooks and run the scripts we want at those stages.
To install Husky, execute:
1yarn add husky -D -W
Once the package is installed, we will add Git hooks directly into our package.json via the husky.hooks
field:
Using commit-msg gives us exactly what we want:
- It is executed whenever a new commit is created.
- Passing Husky’s HUSKY_GIT_PARAMS to commitlint via the -E|–env flag directs it to the relevant edit file. -e would default to .git/COMMIT_EDITMSG.
If we now try a git commit
with a message that is not well formatted …
1git commit -m "install commitlint and husky"
… Husky and commitlint will stop us with the following error:
However, if we commit with Commitizen …
1git cz
… voilà, commit passed successfully! 🙂
Conclusion
For anyone working on a fairly large project, having a code versioned according to these standards can definitely make life easier. First, there is a history to go through. Second, if the commits are self-explanatory, there is also a good documentation for free. Last but not least, we no longer have to remember to bump the package version for our code releases.
The final project can be found on GitHub .
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog author
Dragan Jakovljevic
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.