When setting up new software projects or changing existing processes, determining the right versioning strategy proved always to be a challenging procedure. Choosing the right branching strategy, making consensus with team members and last, but not least, enforcing and automating the process were some the hurdles that had to be taken.
Especially automating the strategy was always difficult. A manual step was always involved inside the repository to determine what the next version would be. And then only the happy flow is considered. How to deal with bug fixes? Does a magic property need to be changed every time when a deployment is wished? In one word, tedious!
What if some tooling capable of calculating the right human readable version number for us? This is where GitVersion comes in to play!
What is software versioning?
While hearing of the thought of explaining software versioning, your thought could be why? The definition initial sounds trivial and simple. When people start diving deeper into it, they usually draw the conclusion that it is not that simple at all.
With software versioning we try to come up with a way to uniquely identify our different phases of our software that we have delivered. When talking about a specific versioning, we use this number or text as a reference to our software in given time.
With the help of release notes or a changelog, a list of changes can be maintained and communicated with stakeholders. In this way we give ourselves, and the users of a given system, handlebars to identify quickly and without hassle in what progress has been made.
Which versioning schemes are there?
In the scope of versioning an application, there are couple of different schemes that can be chosen. This depends on what the chosen Continuous Integration software would support.
Common versioning schemes that people choose are:
- Build numbers: where an incremental number is used that is defined by the run of an automatic build pipeline
- Date and time: where the timestamp of a build is used as an unique timestamp to define a version
- Semantic version: shortened SemVer, where on the basis of creating major.minor.patch scheme a version is defined
There is a main disadvantage with the first two schemes; they aren’t descriptive. When comparing multiple versions that follow an incremented version, it’s hard to understand for a user if non breaking changes has been introduced in a new version. The intention of the new version can’t be deduced of the version number alone.
Semantic versioning offers a solution to be more descriptive in the version numbers. A semantic version number follows the structure MAJOR.MINOR.PATCH.
The different sections are numbers. We increment:
- the MAJOR part when we introduce incompatible/API breaking changes,
- the MINOR part when we add functionality in a backwards compatible manner, and
- the PATCH part when we make backwards compatible bug fixes.
The version number can be suffixed with an additional tag, for example 0.1.0-alpha. In this way, a version becomes more descriptive.
The semantic version strategy became the industry standard to version applications. The semantic version strategy has been written down in a well-defined specification. You are advised to read it through, since understanding the specification will help you further defining the right version number.
Integrating semantic versioning in your CI/CD processes
Okay, you have me. This semantic versioning looks awesome! But how are we going to implement this into our automated processes?
In the processes of teams developing software, the actual code gets stored inside a version control system. Git is one of the most popular systems that is being used by most of the biggest players in the software engineering world.
With the help of GitVersion, using git branches and your CI/CD pipelines, integration of automatic version number generation is possible. GitVersion is a Command Line Interface, shortened CLI, to generate these version numbers. GitVersion works well with existing Git branching strategies like GitFlow or GitHub Flow. Although using a standardised branching strategy is recommended, with GitVersion’s flexible configuration the tool can be set up according to fulfil the desired needs.
GitVersion provides seamless integration for Azure DevOps and GitHub Actions. If your CI / CD solution allows you to install custom CLI tools, you are good to go. There are walkthrough guides available for several build servers like Bamboo and Octopus Deploy. The only three requirements that GitVersion itself has, is mandatory usage of Git, a proper branching strategy setup, and being properly configured.
What are we going to setup for our demonstration?
In our example we are going to version a NPM package by using Azure DevOps. To create the NPM package we need the NPM CLI, which needs to be downloaded together with Node.js. The installers for Windows, Linux or MacOS can be found on the Node.js website.
We’ll use the GitVersion CLI to generate our GitVersion Configuration. The CLI can be easily installed through chocolatey or HomeBrew. For linux, GitVersion can be used with the help of Mono. A guide for this installation process can be found here.
Let’s assume we have already an empty Git repository in Azure DevOps. If not, you can follow this guide to create an empty repository.
Setting up the NPM package
For our sample NPM package, we’ll create a simple function that can used by other applications. First, a
package.json needs to be generated by using the NPM cli.
Let’s start creating our module by opening a terminal (MacOS/Linux) or a CMD (Windows) instance. Then execute the
npm init command inside the repository. This command guides you through the process to generate a
package.json file for you. When asked for the desired version, set the version to 0.0.1. This will be replaced later automatically inside our build pipeline.
After walking through this process, the desired
package.json file has been created. Open now your favourite IDE, create the
index.js file, and copy and paste the contents below:
The function above accepts a name argument. When invoked, it will return the text: “Hello, [name]!”.
Setting up the GitVersion Configuration
To be able to run the GitVersion tasks inside our pipelines, we will need to install the GitTools extension from the Visual Studio Marketplace. This extension can be found here. This extension will help us determining the right version and changes as well the build run number to the generated semantic version.
Let’s create our GitVersion configuration by opening a terminal (MacOS/Linux) or a CMD (Windows) instance, and type the
gitversion init command.
Inside this menu, we’ll choose option
2) Run getting started wizard. This wizard helps you with setting up GitVersion according to your branching strategy. For our example, the choice was made to follow the GitHub Flow branching strategy. When you are unsure, you can choose the
3) Unsure, tell me more option. You’ll get some questions to determine which settings fits you best.
After choosing your branching strategy choice, you will be asked on which default increment mode should be applied:
The possible options are
- following SemVer and only increment when a release has been tagged (continuous delivery mode). When choosing this option, numbers only get incremented when tags are added to commits. Builds that follow after a tagged build will have the same version number with an incremented number as label. Take as an example 1.0.1+3, which is the third build.
- increment based on branch config every commit (continuous deployment mode). With this mode, the version numbers will be incremented on every commit, with the addition that builds on develop branch will be suffixed with
-alphatag and builds on release branch with
- each merged branch against master will increment the version (mainline mode). In this mode, every merged commit (e.g. a Pull Request branch) will trigger an incrementation of the patch version.
Since we are creating a NPM package, the first option is not suitable for our situation. We want to release new versions of our app when we merge into the master branch. Package managers like NPM and NuGet don’t support the same versions with different labels. For this we will choose option number 3. At last, we choose the
save and quit option.
More about version incrementation can be read here.
When checking our repository, we will notice that a
GitVersion.yml file has been created. This is a YAML file that contains the GitVersion Configuration.
Inside this file several configuration options can be defined. Examples are which incrementation mode should be used per branch, or the several bump messages that will be used to increment manually the major, minor patch versions. More on this later.
Possible configuration options can be found on the GitVersion Configuration page.
Setting up our build pipeline
Now the most important part… building our build pipeline. Inside Azure DevOps the Azure Pipelines functionality needs to be used to integrate CI pipelines. Most of the tasks inside this pipeline will be specific to the needs of the application that needs to be built, but will have one task in common. That is the task to determine the version by using GitVersion.
Since we are building an NPM module, we will need the following tasks to be executed:
- Determining the Version by GitVersion,
- setting the version number right in the package.json, and
- publishing the module.
Like the GitVersion Configuration file, Azure DevOps Pipelines scripts are defined in YAML. Another option would be to use the classic form, where the tasks can be created by navigating through Azure DevOp’s Web GUI.
With keeping the three tasks above in mind, the resulting
azure-pipelines.yml file would look as following:
Let’s walk through the script and explain what exactly happens.
- Line 1–2: This trigger defines on which branches pushed commits will trigger the build pipeline.
- Line 4–5: A pool block describes which (build) agent pool to use. In this case, an Ubuntu Latest virtual machine is desired. Supported operating systems and their pre-installed software can be found here.
- Line 8–11: This is the GitVersion task used to determine the right version. When running this task, the embedded GitVersion CLI will calculate the right version number and makes the determined version number available through pipeline variables.
- Line 12–18: This is the task to replace programatically the version number in the
package.jsonfile. Due to best practices, an intermediate
GITVERSIONNUMBERgets defined and filled by the pipeline variable
$(GitVersion.SemVer). This variable contains the version number determined by the GitVersion Task.
- Line 19–24: This is the NPM publish task to publish the NPM module to a NPM registry. In our case, this is Azure Artifacts, part of our Azure DevOps Environment.
azure-pipelines.yml file in the root of our repository.
If you followed the guide correctly, you should end up with the following files in your directory:
Running the first build
Time to start the first build! Let’s commit and push our creation towards our Azure DevOps and see what happens in our build pipelines section.
Hmm, that looks empty. The created pipelines file sometimes doesn’t get picked up automatically by Azure DevOps.
Let’s add a pipeline by pressing Create Pipeline. Choose
Azure Repos Git and then the right repository to tell Azure Pipelines where the azure-pipelines is stored. Azure Pipelines should now pick up the earlier committed Pipelines file. Press run to start the first run manually.
After roughly twenty seconds, the build will finish successfully and the result will be visible in the overview. When looking in the last run column, the first version can be found as well. In this case this will be the default first version, 0.1.0. Since the build succeeded, inside Azure Artifacts the published package is added as well.
And indeed, the NPM package has been pushed towards the Azure Artifacts. The generated version number has been respected since the initial version number inside our
package.json file is set to 0.0.1.
Incrementing patch, minor and major versions
So, new versions can be generated. That’s awesome! Let’s test this by committing and pushing a small change to the repository. This immediately triggers a new build and finishes after around twenty seconds.
This new build automatically increments the patch version. But how about the minor and the major versions? How can these be incremented?
GitVersion offers multiple possibilities to increment these versions manually. These are:
- setting a
next-versionproperty in the GitVersion Configuration. This serves only as a base version,
- using the branch name. By setting the branch name to release-1.0.1 or release/1.0.1, GitVersion will use that version for all builds on that branch, with an incremented number as label,
- tagging the commit, GitVersion will use that tag as the version number,
- using the commit messages.
With the last one, adding
+semver: breaking or
+semver: major to the commit message will increment the major version. By using
+semver: feature or
+semver: minor inside the commit message, the minor version will be increased. And finally, with
+semver: patch or
+semver: fix in the commit message, the patch version will be incremented.
When committing the message “Big change +semver: major”, GitVersion indeed increases the major version and will use 1.0.0.
When checking Azure Artifacts, we can draw the conclusion that the determined version is indeed respected and used.
With semantic versions it’s possible to generate descriptive versions for your applications.
GitVersion offers an easy approach to integrate automatically generated semantic versions in your CI/CD pipelines. With the help of Git branching strategies and GitVersion Configuration, integration is easy and flexible. Major, minor and patch versions can be easily updated through different possibilities.