Software versioning without thinking about it

Edit this page | 6 minute read

Solving the versioning problem
If you're building libraries, products or any other software system, versioning is usually a pretty big deal. It's the only way to determine what version of that library, product or system you're looking at. Before an organization settles on a versioning strategy, many discussions have been held on what constitutes a major release versus a minor release, how to version the component from a marketing perspective, and how to deal with bug fixes. In addition to that, if that software system involves a library or framework, or just component, then you'd be pretty interested to know when an update to that component involves breaking changes.

Fortunately, the open-source community has solved both of these problems for us. First, we have semantic versioning, which unambiguously defines how to update your version when you do hot fixes and patches, minor back-wards compatible improvements or breaking changes. They even define how you should post-fix your version numbers to denote pre-releases and build numbers. Assuming all decent software project are using Gitthese days, then the other problem is solved by following the GitFlowbranching strategy, an initiative by fellow countryman Vincent Driessen. GitFlow describes in detail on what branch you do your main development work, how you stabilize an upcoming release and how you track production releases.

So I'll assume for a minute you are going  to embrace the semantic versioning scheme as well as follow the branching strategy prescribed by GitFlow. Wouldn't it be cool if some kind of tool existed that used the names of the branches and the tags on master (representing the production releases) to generate the proper version numbers for you? Well, once again, the open-source community comes to the rescue. Jake Ginnivan and the guys from ParticularLabs, the company behind NServiceBus, have build just that and named it GitVersion. So let's see how that works.

Showing of the beauty of automatic versioning
First, we need to install GitVersion, which is pretty easy through Chocolatey.

> choco install gitversion.portable

This allows you to run it from the command-line. You can also hook it into your build system by copying the single executable into source control or adding it as a NuGet package. It will detect build engines like TeamCity and AppVeyor and adopt its output. Now let's assume you have a new Git project with a first commit on the master branch:

> mkdir GitDemo
> git init
Initialized empty Git repository in D:/Tmp/GitDemo/.git/

> echo "hello" demo.txt
> git add .
> git commit -m "My first commit"
[master (root-commit) 2a34238] First commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 demo.txt

Now run the GitVersion command-line tool

> gitversion

This will result in the following JSON output:

{
  "Major":0,
  "Minor":1,
  "Patch":0,
  "PreReleaseTag":"",
  "PreReleaseTagWithDash":"",
  "BuildMetaData":0,
  "FullBuildMetaData":"0.Branch.master.Sha.2a34238911a4b47ec4b8794e27cd69c5857cb12a",
  "MajorMinorPatch":"0.1.0",
  "SemVer":"0.1.0",
  "LegacySemVer":"0.1.0",
  "LegacySemVerPadded":"0.1.0",
  "AssemblySemVer":"0.1.0.0",
  "FullSemVer":"0.1.0+0",
  "InformationalVersion":"0.1.0+0.Branch.master.Sha.2a34238911a4b47ec4b8794e27cd69c5857cb12a",
  "ClassicVersion":"0.1.0.0",
  "ClassicVersionWithTag":"0.1.0.0",
  "BranchName":"master",
  "Sha":"2a34238911a4b47ec4b8794e27cd69c5857cb12a",
  "NuGetVersionV2":"0.1.0",
  "NuGetVersion":"0.1.0"
}

So, by default your version numbering starts with 0.1.0. Notice the many variables targeted to specific uses. For instance, we store the InformationalVersion that includes the hash of the commit in the AssemblyInformationalVersion attribute of the assembly. Similarly, we use the NuGetVersion for our NuGet packages. Finally, our build numbers are mapped to the FullSemVer variable. That last part is pretty important, because traditional build numbers don't say much about the actual changes. With Git versioning, rebuilding a particular commit renders the exact same number. This creates a whole lot more tracability.

Let's follow Gitflow and continue development on the develop branch and run GitVersion.

> git checkout -b develop
> gitversion

Ignoring the remainder of the variables for now, this is the result:

  "FullSemVer":"0.2.0.0-unstable"

You'll notice two things. First, the minor version number is automatically updated. Second, the version number is post-fixed to make it pretty clear your working a development branch. Now let's add a couple of commits and see what happens.

> echo "test" > demo2.txt
> git add .
> git commit -m "another commit"
> gitversion

This will result in the last digit representing the number of commits since we branched of from master.

"FullSemVer":"0.2.0.1-unstable"

So let's work on a little feature through a feature branch.

> git checkout -b feature1
> echo "test2" > demo2.txt
> git add .
> git commit -m "another commit"

Running Gitversion again will give you:

"FullSemVer":"0.2.0-Feature.1+1"

And again it's crystal clear what version you're working on. To simulate working on some feature, I'll assume a couple of more commits without repeating myself here. Running GitVersion after those changes results in:

"FullSemVer":"0.2.0-Feature.1+4"

Now it's time to integrate those changes back into the developbranch.

> git checkout develop
Switched to branch 'develop'

> git merge feature1
Updating eb79902..8c374d1
Fast-forward
 demo.txt  | Bin 0 -> 16 bytes
 demo3.txt | Bin 0 -> 14 bytes
 demo4.txt | Bin 0 -> 14 bytes
 3 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 demo3.txt
 create mode 100644 demo4.txt

> gitversion
"FullSemVer":"0.2.0.5-unstable"

So, whatever you do, the version is always in sync with the changes without any manual tasks.

Now suppose principal development has completed and it's time to stabilize the code base. Let's make that explicit by starting a release branch.

> git checkout -b release-1.0.0
Switched to a new branch 'release-1.0.0'

> gitversion
"FullSemVer":"1.0.0-beta.1+0"

So it seems release branches are not for shipping release versions. Instead, they are used to ship beta versions of your system. Again, the +0 is used to denote the number of commits since the last time you shipped a beta package. Consequently, if you do ship such a package, you're supposed to tag that commit with something like 1.0.0-beta.1. When you do, and you add any additional commits to that branch, the following happens.

> git tag 1.0.0-beta.1

> ..make some changes

> git commit -m "some more changes"
[release-1.0.0 76682ac] some more changes
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 demo5.txt

> gitversion
"FullSemVer":"1.0.0-beta.2+1"

As long as you're stabalizing a system or shipping intermediate versions for acceptance testing or similar situations, stay on the release branch. By doing so, you can support both the upcoming version as well as the current production version. Now, when you're ready to go into production it's time to merge to master.

> git checkout master
Switched to branch 'master'

> git merge release-1.0.0
Updating 2a34238..76682ac
Fast-forward
 demo.txt  | Bin 0 -> 16 bytes
 demo2.txt | Bin 0 -> 14 bytes
 demo3.txt | Bin 0 -> 14 bytes
 demo4.txt | Bin 0 -> 14 bytes
 demo5.txt | Bin 0 -> 14 bytes
 5 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 demo2.txt
 create mode 100644 demo3.txt
 create mode 100644 demo4.txt
 create mode 100644 demo5.txt

What's important to remember is that you should tag the commit you ship, so in this case we're going to tag the merge result with the final release number. Let's see what GitVersion will do with that.

> git tag 1.0.0
> gitversion
"FullSemVer":"1.0.0"

This marks the end of the development cycle. Any hot-fixes should end up on master as well, and will have their last digit incremented, e.g. 1.0.1, 1.0.2. Any successive development should continue on develop again, so as a last step, let's switch back to that branch and see what happens.

> git checkout develop
Switched to branch 'develop'

> gitversion
"FullSemVer":"1.1.0.0-unstable"

Nice isn't it? GitVersion understands the concept of that tag on masterand will assume you'll continue with the next minor version on the develop branch. If you really want to continue with a different version, there are waysto make that happen. And I've been just showing you the most common flow. I highly recommend checking out the examples in the GitVersion wiki.

In summary
In short, you just need to remember a couple of things:

  • Development happens on the develop branch
  • Stabilizing upcoming releases and shipping beta packages happens from release- branches
  • The master branch tracks production releases and hot-fixes.
  • Anytime you ship something, you must tag that commit. No need for tracking releases anywhere else.


    So what happens if you don't have regular releases and your project needs to deliver continuously? Well, the guys at Github had the same questions and came up with an alternative to GitFlow called GitHubFlow. Fortunately, GitVersion supports this flow out-of-box as well.

    So what branching strategy do you use? And how do you track your releases? Let me know by commenting below or tweeting me at @ddoomen.

    Leave a Comment