In this article, you will learn why it’s important to version your project and how you can introduce versioning with Conventional Commits and standard-version. Let’s start with refreshing our knowledge about one of the most popular versioning conventions.

Semantic Versioning – quick recap

You have probably already seen some kind of versioning many times. If you have ever been fiddling around files like package.json you must have seen versioning annotation like for example >=2.3.1. Each package manager has its own flavor of versioning annotation, but all of them have one thing in common – Semantic Versioning or SemVer for short. 1.0.0-beta1 is a full example of a SemVer version. Translating it to the common language would go like this, it’s the second beta of the first major release. Template for such a version is MAJOR.MINOR.PATCH (e.g 0.2.13). Optionally, a version can have a pre-release flag like MAJOR.MINOR.PATCH-PRE. Let’s examine what of each version segment means:

  • PATCH – Whenever you make a backward-compatible bug fix, no new functionality is added.
  • MINOR – Whenever you add new functionality in a backward-compatible manner, old functionalities are preserved.
  • MAJOR – Whenever you make incompatible API changes known as “breaking change”. That means, that old part of your software will work differently and users of your software must accommodate to the new ways
  • PRE – It stands for pre-release. Using PRE segment is optional, but could be quite useful. If you are working on the next big release of your project, you may not get it stable at the first try. It will take at least a few iterations to get it right. You could use alpha ~> beta ~> rc (release candidate) methodology to tag your progress on the release. Each of that tags could be incremented, so from rc1 you can go to rc2. That will give you very granular control over your releases.

The simplest decision tree of how to increment a semantic version would look like this:

Increment semantic version decision tree

Changing version number should always have an incremental form. That’s why it’s called bumping. You cannot go from 1.1.14 to 1.0.15. That could be very confusing. You can only go up. So, from 1.1.14 you can go either 1.1.15, 1.2.0 or 2.0.0. Lastly, remember that PRE segment is meant to annotate versions before the final release. Version order 2.0.0 ~> 2.0.0-rc0 ~> 2.0.0-rc1 is invalid. More details about SemVer rules could be found in Semantic Versioning Specification .

Alright, that looks like a no-brainer, but why should I care about versioning anyway?

Why should I care about versioning?

I guess that for more experienced developers the answer to that question is pretty obvious, but if you have doubts I highly encourage you not to skip this chapter.

The core value of the versioning is assurance that given version of the software will always represent one and unique state of that software. That means two things. Firstly, for a given version everyone will get exactly the same piece of code. In other words, all copies of the software with the same version will be exactly the same. Secondly, if you upgrade from version A to B and afterward you decide to go back to A you will certainly get the exact same version of the code that you used before. These two properties of versioning, uniform code distribution, and release (represented with version) immutability are the foundations of modern software distribution and are directly guaranteed by the versioning.

The other equally valuable advantage of versioning provides users with information about what they might expect from the version change. If the software proprietor applies to SemVer rules, the utilizer will have no doubts if they can update the software safely. Imagine that you are using an external package to calculate currency exchange. You are currently using version 1.2.10 of that package. Although, there had already been many releases since that version, you were not keen to update it cause no bug fixes/features that considered you were introduced. Suddenly, version 2.2.0 is released and it supports a whole bunch of new currencies that you just need in your project. Now, you cannot just upgrade the package. The difference in major version means that a breaking change was done and the new version might cause an execution failure. Before upgrading, you need to investigate what the breaking changes are and how you need to accommodate them.

On a side note: If you are planning to create some piece of software that will be distributed among a broad audience, the good practice would be to add deprecation warnings in following minor versions before breaking anything and going major. All mature frameworks do that (Rails or React to name a few).

For certain projects, versioning is a must. If you ever published NPM package, then you must know that providing version is a requirement. Moreover, you are obligated to change version whenever you made any change. And it is OK, cause folks need to be sure that pulling package with the same version at different point of time will always have the same contents. In short, if you create some kind of package or lib that will be shared with others, versioning is not just good practice but a necessity. You must remember about incremental way of changing your version and stick to the SemVer rules during that process. Users of your software will be grateful for that.

GET /health_check

I know that many web developers hesitate to version their projects, but if you decide to version, it’s a good idea to be open about it. Participants of the project (clients, QA devs, your PM and many others) should be able to know, what is the current version of the product they are using. Sole health check endpoint is a subject for a different article, but I think it’s a good place to publish your version number. You could mount it under /health_check endpoint, but there is no strict convention for that and you could simply decide what works best for you.

Simplest /health_check response could look like this.

Once you have your health check endpoint running you can do many useful things with it. For example, add the version check after deploy to your integration scripts to verify if the deploy was really successful.

By the way, if you ever wonder how to get a version from package.json in your JS app. Here is a quick snippet:

Yeah, that is surprisingly easy, but let’s not forget that JSON format was designed to have native JavaScript support.

Conventional Commits

Conventional Commits Logo

Revision control and versioning are like bread and butter. I cannot imagine proper versioning without good RC (e.g Git). It turns out that versioning and smart usage of Git come handy. There is a set of rules regarding how you should commit your code called Conventional Commits that might bring you the great benefit. Why use Conventional Commits?

  • Automatically generating CHANGELOGs.
  • Automatically determining a semantic version bump (based on the types of commits landed).
  • Communicating the nature of changes to teammates, the public, and other stakeholders.
  • Triggering build and publish processes.
  • Making it easier for people to contribute to your projects, by allowing them to explore a more structured commit history.

Source: https://www.conventionalcommits.org/en/v1.0.0-beta.2/#why-use-conventional-commits

Looks pretty nice, doesn’t it? Let’s learn how to use Conventional Commits properly. The foundation of CC is the right commit message that has the following structure.

The specification will tell you more about those keywords.

Simple example of such a commit message might be:

If you want to be more verbose about your commit, you could include an optional body and footer that might be included in the CHANGELOG.

You might also specify a scope of the change by writing scope name in the parenthesis like :

I personally believe that scoped commits are great for Monorepos. For example, if you are using Lerna to manage your codebase, you could use a name of the package in my-lerna-repo/packages dir as a scope name. Scoping a commit is not a must, but might give you a better insight into the changes between consecutive versions.

Lastly, if you anticipate that your commit might break backward compatibility, you must add BREAKING CHANGE: text at the beginning of its optional body or footer section.
Doing so will have some serious implications, cause as SemVer rules, any breaking change will require releasing new major version.
You should always plan and notify ahead before realizing major version. Keep a considerable time span between your major versions. People might be struggling to keep up with breaking changes if you release a major version every second week. If you create a software that will be a dependency for other systems (say NPM package), I would suggest that the time span should be at least one year, but it may vary drastically. Let’s see, how “breaking change” commit might look like:

Such a commit message will cause a major version release and that will be information for the utilizers that they need to see the CHANGELOG and find what breaking changes were made. If they don’t accommodate to them, script execution will be a failure, since sort_by method does not exist anymore.

Commit types fix and feat are restricted and thoroughly documented in CC specification, but those are not the only ones that you can use. Among these, you can come up with your own types, like chore, docs and refactor. As long as the prefix list is short and consistent you are good to go. Personally, I just use chore. These additional commit types are for changes that do not alter behavior, therefore they will not bump the version number. So, if I made a change in the README, I would write chore: Add Usage section to the README. During the next release, this commit will be omitted in the CHANGELOG and will have no impact on the next version number.

Automate versioning with the standard version

Ok, now we know what the deal is about. Let’s use it in practice. Thankfully, there is a tool that will come in very handy. Say hello to the standard-version.

standard-version does the following:

  1. bumps the version in metadata files (package.json, composer.json, etc).
  2. uses conventional-changelog to update CHANGELOG.md
  3. commits package.json (et al.) and CHANGELOG.md
  4. tags a new release

Source: https://github.com/conventional-changelog/standard-version

Let’s start from the beginning and install standard-version. This short tutorial will assume that your project is using package.json and you have NPM installed on your machine. If your project does not contain package.json file, take a look at the end of this chapter, where I included guide for standalone usage of standard-version.

The command above will install the tool and add it to devDependencies in the package.json. Now, add bump script in the same file.

We are ready now for an initial release. Run the following command to create the first release. Note that --first-release will not bump the version number. The initial release will be marked with whatever version you have right now in the package.json. In my case, it’s 0.1.0.

So, a few things happened now. First of all, we have a brand new CHANGELOG file. It should look more or less like this:

Secondly, we have a new commit of the release.

Finally, there is a new tag v0.1.0.

To push the new commit with the tag you can use git push --follow-tags origin master. If you keep your remote repository in the GitHub, the presence of the new tag will create a new release. You will learn more about that in the next chapter where I will present my whole Git workflow that utilizes Conventional Commits and standard-version.

From now on, you can bump your version with npm run bump. And that’s it. You don’t need to specify what part of the version you want to bump. If you utilize Conventional Commits correctly, standard-version will figure out how version should be bumped. The goal of this post is not to present standard-version in depth. It has many other useful options. I recommend taking a peek at CLI usage of standard-version.

Not a JS developer?

You might have noticed that standard-version is pretty oriented in JavaScript ecosystem. So far it has support for package.json, bower.json, manifest.json and composer.json that is used by PHP (source). If you have any of that files, you can still use standard-version, just install it globally with NPM.

Now you can use it in the root of your project:

So far I have not encountered similar tools oriented on other languages. I really hope that with the increasing popularity of Conventional Commits other developer communities will come up with their own tooling. Maybe you heard about something interesting. If so, let us know in comments section.

Simple Git workflow

I think it’s the highest time to use all that knowledge and see how it works from end to end. I will present here a Git workflow that utilizes standard-version and Conventional Commits. I use something very similar in my everyday work and I perceive this approach as simple and efficient.
Let’s assume that we already have an existing application that has package.json file. Now, we are going to carry out a series of changes in our project, that will help us to understand how to use introduced techniques and tools.

Our objective is to deliver a user registration feature. Naturally, I will omit code implementation as we are going to focus on our Git activity.

As always before starting work on a new feature I want to branch out from the current master.

We have a new branch, so let’s get to work. The feature will require touching many areas of our project, thus we will probably create at least a few commits during the process of the implementation. Here is how the list of commits may look like:

As you can see, my commits are not very tedious and verbose. I actually think of it as a log of my changes, that will help me to track my changes. As you can see, I have one WIP (work-in-progress) commit, but that’s fine. This commits will be squashed later on, so I’m trying to come up with something that works for me. I have a habit of pushing my code before the end of the day, so WIP commits are acceptable for me if the only commit message contains a subject of the changed component. Another thing I like to do is to keep my commits as small as possible. Working on one feature can drag for many days and I want to have an easy way to see my changes from previous days. Imagine that I want to remind myself how I implemented routing for user registration. If I kept all changes together and unorganized finding, the diff in routing file might be difficult and time-consuming, but since I have the commit just for routing I can easily check my new routes.

Ok, it seems like we are ready to create a new Pull Request. Let’s go to the GitHub and create one.

GitHub Pull Request

The most important thing to observe is the PR title. I’ve used Conventional Commits prefix and that’s the only place it should be used. After I will squash and merge my PR to the master, the PR title will appear as merge commit message and based on that message standard-version will create a new version release.

A few moments passed by and our pull request got approved by our coworkers. It’s the highest time to release it. Let’s go to the GitHub one more time and merge our PR using “Squash and merge” button.

Squash and merge

Once you click on the squash button, a new window will appear, where you can enter optional commit body. Note that in the title of the PR additional (#2) string appeared. That’s the GitHub reference to your PR. The hash number means an ID of your pull request. You can remove it, but I find it helpful. For now, let it be.

Squash message

The merge was done, don’t forget to remove feature branch. Now, let’s see how does our squash look like in the Git log.

You can see only one new commit. There is no merge commit, all previous commits are gone as well. Everything is packed in the one squashed commit. After our PR found its place on master branch we are ready to release a new version. That’s deadly simple. The only thing we need to do is to run standard-version and push new version (don’t forget about --follow-tags).

A few things happened now:

  1. we bumped version from 0.1.0 to 0.2.0;
  2. we have a new entry in the changelog;
  3. we have a new tag v0.2.0.

This is how my CHANGELOG looks like now:

If we pushed our changes with --follow-tags then the new tag should appear on the GitHub as a new release:

The new release

And that’s it. The whole workflow is complete now. You can use the exact same workflow for fixes or breaking changes. Just remember about correct Conventional Commit prefix.

Always squash commits

Conventional Commits advocates to always squash your commits and I understand that you might be a little bit defensive about that. Surely, you have done so many great commits while working on a feature. You spend so much time thinking about right commit message and now you are about to destroy all that hard work of yours. To be honest, I wasn’t a huge fan of that idea too, but after a short while, I don’t see a reason to go back. There are probably many discussions about why squashing commits is good (or bad), but I would like to point out a few reasons that come from my own experience.

  1. Squash commits result in much cleaner Git history. As presented in this short tutorial squashed PR condense all your Git activity in one meaningful commit.
  2. Some may say, that squashing commits means losing information for the previous commit messages. I highly argue with that. When squashing a few commits you need to think of a message that will describe all changes you conceived thus will be general. This generality will provide you a context later on when someone else will research your code by checking Git history. For example, commit message Add SMTP package does not give me any extra information, cause you can clearly see that when looking at commit changes. But if the package was added in the commit with message “feat: User registration” you will probably figure out that SMTP support was required cause maybe we send users an email after we are registered and that’s at least something.
  3. Lastly, we avoid all nasty commits like infamous WIP commits. On the other hand, we don’t have to think that much about correct commit message, cause all of them will be squashed at the end of the day. Any commit message that works for us will be just fine. Isn’t that nice?

Continuous Delivery

Versioning and Continuous Delivery (CD) might not go well together if you do not apply correct configuration. If your CD is triggered every time you push something to master, then utilizing the presented Git workflow may be problematic. The simplest solution is to change your CD configuration to be triggered only for release commits. That way you will avoid unwanted CD triggering.


Versioning is an open subject and we are always willing to hear about your approach. Do not hesitate to leave your feedback in the comments section below 🙂

maciej.komorowski

Senior Ruby on Rails developer at Codete. An enthusiast of new technologies and processes. He spends his free time in the garage, creates furniture and DIY. If there is still some free time, he plays football.