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 waysPRE
– It stands for pre-release. UsingPRE
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 usealpha
~>beta
~>rc
(release candidate) methodology to tag your progress on the release. Each of that tags could be incremented, so fromrc1
you can go torc2
. 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:

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.
1 2 3 |
{ "version": "1.0.0" } |
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:
1 |
import { version } from "./package.json"; |
Yeah, that is surprisingly easy, but let’s not forget that JSON format was designed to have native JavaScript support.
Conventional Commits

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.
1 2 3 4 5 |
[optional scope]: [optional body] [optional footer] |
The specification will tell you more about those keywords.
Simple example of such a commit message might be:
1 |
feat: Search products by EAN code |
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.
1 2 3 4 5 |
fix: Sorting products by a price Sorting was not working properly cause prices were compared as string values instead of compared as floats. Resolves TICKET-201 |
You might also specify a scope of the change by writing scope name in the parenthesis like :
1 |
fix(ui): Display top bar correctly on iPhone X |
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:
1 2 3 |
feat: Rename 'sort_by' function to `sortBy` `BREAKING CHANGE:` Rename 'sort_by' function to the camel case form in order to be consistent with the rest of the API |
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:
- bumps the version in metadata files (package.json, composer.json, etc).
- uses conventional-changelog to update CHANGELOG.md
- commits package.json (et al.) and CHANGELOG.md
- 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
.
1 |
npm i --save-dev 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.
1 2 3 4 5 6 7 |
{ // ... // omitted part of the package.json "scripts": { "bump": "standard-version" } } |
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
.
1 |
npm run bump -- --first-release |
So, a few things happened now. First of all, we have a brand new CHANGELOG file. It should look more or less like this:
1 2 3 4 5 6 7 |
# Change Log All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. <a name="0.1.0"></a> # 0.1.0 (2019-01-06) |
Secondly, we have a new commit of the release.
1 2 3 4 5 6 7 8 9 |
git log --name-status HEAD^..HEAD commit de1f8fb385d9512dcda66d38f61e754c7ad5fca4 (HEAD -> master, tag: v0.1.0) Author: Maciej Komorowski <mckomo@gmail.com> Date: Sun Jan 6 21:40:01 2019 +0100 chore(release): 0.1.0 A CHANGELOG.md |
Finally, there is a new tag v0.1.0
.
1 2 3 |
git 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
.
1 2 3 4 5 6 7 8 9 10 11 |
git push --follow-tags origin master Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 8 threads Compressing objects: 100% (4/4), done. Writing objects: 100% (4/4), 604 bytes | 604.00 KiB/s, done. Total 4 (delta 1), reused 0 (delta 0) remote: Resolving deltas: 100% (1/1), completed with 1 local object. To github.com:mckomo/my-app.git c49bc43..de1f8fb master -> master * [new tag] v0.1.0 -> v0.1.0 |
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.
1 |
npm install --global standard-version |
Now you can use it in the root of your project:
1 |
standard-version [optional_args] |
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
.
1 2 |
git checkout master && git pull git checkout -b feat-user-registration |
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:
1 2 3 4 5 6 7 8 9 |
git log --pretty=format:"%h %s" 497dbcd3e UI for login and registration page 8f6bd245b Working on Account model af6ece777 Fishing up Account model + tests 19883ac37 Create session controller for authentication 41bc6067c Add routing for registration 612f07009 Add SMTP package 0c9423678 Mail users after successful registration |
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.
1 2 3 4 5 6 7 8 9 10 11 12 |
git diff 41bc6067c^! diff --git a/app/routing.ts b/app/routing.ts index 19883ac37..41bc6067c 100644 --- a/app/routing.ts +++ b/app/routing.ts @@ -1,2 +1,5 @@ import home from 'controllers/home' +import session from 'controllers/session' app.use('/', home); +app.use('/account', session); |
Ok, it seems like we are ready to create a new Pull Request. Let’s go to the GitHub and create one.

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.

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.

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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
git checkout master && git pull git log commit 1e26dcfb7b29e60dd594f73b5e620a2862839c10 (HEAD -> master, origin/master, origin/HEAD) Author: Mckomo <mckomo@gmail.com> Date: Sun Jan 13 13:48:56 2019 +0100 feat: User registration (#2) Add user registration functionality. The registration form is available for guest under "/account/register" path. commit de1f8fb385d9512dcda66d38f61e754c7ad5fca4 (tag: v0.1.0) Author: Maciej Komorowski <mckomo@gmail.com> Date: Sun Jan 6 21:40:01 2019 +0100 chore(release): 0.1.0 |
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
).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
npm run bump > my-app@0.1.0 bump /Users/mckomo/Projects/my-app > standard-version ✔ bumping version in package.json from 0.1.0 to 0.2.0 ✔ bumping version in package-lock.json from 0.1.0 to 0.2.0 ✔ outputting changes to CHANGELOG.md ✔ committing package-lock.json and package.json and CHANGELOG.md ✔ tagging release v0.2.0 ℹ Run `git push --follow-tags origin master` to publish git push --follow-tags Enumerating objects: 10, done. Counting objects: 100% (10/10), done. Delta compression using up to 4 threads Compressing objects: 100% (6/6), done. Writing objects: 100% (6/6), 104.81 KiB | 701.00 KiB/s, done. Total 6 (delta 2), reused 0 (delta 0) remote: Resolving deltas: 100% (2/2), completed with 2 local objects. To github.com:mckomo/my-app.git 1e26dcf..d9bf988 master -> master * [new tag] v0.2.0 -> v0.2.0 |
A few things happened now:
- we bumped version from
0.1.0
to0.2.0
; - we have a new entry in the changelog;
- we have a new tag
v0.2.0
.
This is how my CHANGELOG looks like now:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# Change Log All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. <a name="0.2.0"></a> # [0.2.0](https://github.com/mckomo/my-app/compare/v0.1.0...v0.2.0) (2019-01-13) ## Features - User registration ([#2](https://github.com/mckomo/my-app/issues/2)) ([1e26dcf](https://github.com/mckomo/my-app/commit/1e26dcf)) <a name="0.1.0"></a> # 0.1.0 (2019-01-06) |
If we pushed our changes with --follow-tags
then the new tag should appear on the GitHub as a 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.
- 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.
- 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. - 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 🙂