A practical take on GitFlow and Semantic Versioning
Or how I finally slayed my versioning indecision
Posted on March 28, 2016
I have always had a complicated relationship to versioning. At first glance, versioning is seemingly simple: it is just a number. Looking closer, a version number can carry a lot of meaning besides identifying a set of files and binaries.
For starters, every increase tells a story about the changes made since the last version, and how significant those changes were. Then we have the marketing aspect: sometimes the major and minor parts of the version number are part of your product name, which can lead to a lot of hesitation and uncertainty regarding when and how to bump the version number. Finally, there is the emotional aspect: are these new features really significant enough to warrant a version 2.0?
Nowadays, my preference is to handle marketing- and technical version numbers separately. This allows me to use names like MyAwesomeProduct2016 or MyAwesomeProduct365, and it takes away all the emotional ties to the real version number. As for the technical side, I like to use Semantic Versioning, especially together with GitFlow. That combination has gained a lot of popularity (although some people feel it is overly complicated), but just pick a system that serves your needs and suits your workflow. The important thing is to get a system in place and eliminate the versioning indecision.
Both GitFlow and Semantic Versioning have very detailed specifications; making it very clear how you should work with branches, what to name them and under what circumstances you should bump your version number. But when it comes to actual implementation details, they only give you a few recommendations in the form of examples. Not very helpful when one is trying to figure out what a "best practices" implementation of GitFlow/SemVer should look like.
Just a clarification – I do get the specs and the big picture. :-)
What I am contemplating are the little things: should I utilize a build number or not? Are there any other practical reasons for that besides AssemblyInfo versioning? Should the build number be reset or not when the version number is bumped? Lots of questions, and none of them very important. But as you all know, the devil is in the details... :-)
Since I am currently setting up a continuous deployment process for a Lancelog, a SaaS product I am working on, I wanted to get my versioning system as good as possible right from the outset.
What I needed was a couple of blog posts outlining what worked well for others and what didn't, but apparently my Googling capabilities sucks because I didn't find any. So – in the hope of helping someone else out, here's my take on the matter. And if you happen to be a black belt in this area, please feel free to correct me on any misconceptions.
While pouring through the specifications yet again, a lot of questions popped up in my head. As an example, consider SemVer's concepts of pre-release versions and build metadata: which scheme is best suited for what? Just because we have the ability to label a build as alpha.1 or beta.32, should we use it? What about build numbers? What format is most readable? The list of questions kept growing:
The first question, about when to change the version, is actually being answered in the GitFlow spec; it recommends that one defers this decision until the creation of a release branch. Perfectly reasonable, since we never know what might end up in a release. We might have planned to release a certain feature, just to be taken aback by a bug that needs to be fixed before the feature.
Say we're on version 1.2.0 in the master branch, a.k.a production. We are planning a new feature called X, so the next version should become 1.3.0.
Then the client reports a critical bug, forcing us to prepare a hotfix. We fix the bug and bump the version to 1.2.1. But, given the insights gained from fixing the bug, we now realize that we might need to break the public API to get X working the way we want. And if we do that, we should no longer bump to version 1.3.0. Instead, we should move to version 2.0.0.
Rule: version bumping occurs either when we branch out from develop to release, or when we branch out from master to a hotfix branch.
Next question: naming conventions in the develop branch. Since all changes in the other branches should be merged back into develop, I think the naming should reflect that develop is virtually always the latest version of the product. Hence, I use the convention a.b.c-wip.d, where wip stands for work-in-progress and d is the build number.
Rule: always make sure that the version number in the develop branch is in sync with the latest number in any hotfix or release branches.
In the example above, the develop branch was at 1.2.0-wip.123 when we created the hotfix branch hotfix/1.2.1. Following the last rule, when we merge back to the develop branch, it gets bumped to 1.2.1-wip.x.
This rule can get complicated when we are working on a hotfix- and a release branch simultaneously. Which SemVer number should propagate back to develop? In most cases, the release branch version number should trump the hotfix one. Especially since we probably want to merge hotfix changes back into the release branch before wrapping it up.
What about naming conventions in the release branches? I don't expect releases to stay in this branch for very long, so the full alpha/beta/rc1/rc2 life cycle seems like overkill. In the end, I decided to use the rc-prefix, which leads to the following notation: a.b.c-rc.d. Again, d is the build number.
Let's continue with the example. We decide to break the API, so we create a new release branch from develop called release/2.0.0. The first tag in this branch should then be 2.0.0-rc.x, where x is the build number. But what should x be in this case? This brings up the question about when the counter should be reset, if ever. Let's examine each alternative:
Alternative 1: Never reset the build counter
This approach guarantees uniqueness across all builds, but in my opinion, it looks ugly. Another peripheral concern is that .NET AssemblyInfo revision numbers are limited to a maximum value of 65535. Ok, probably not a real-world issue on a small project unless you are extremely trigger happy.
Alternative 2: Reset the build counter whenever the version number is bumped
Together with the SemVer version number, this also guarantees uniqueness. It also looks way nicer, since the build number seldom will reach beyond three digits.
However, it introduces another problem: to remember to reset the counter whenever you change the version number. Or, if we go for full automation, how to detect when the version number has changed.
Anyway, as of now, alternative 2 this is my preferred approach. I haven't yet figured out how to achieve full automation regarding the build numbers, but maybe Git hooks and TeamCity's REST API can be a solution.
In the end, the takeaway here is that you should pick a system that always produces unique version numbers, regardless what you will use them for.
Rule: ensure that each versioning tag is unique in the repo.
At first, I thought the build server was the perfect choice for this. But what if the team grows; maybe we want to give everyone the ability to bump the number, but we don't want everyone to have access to the build server? So I changed my mind. Until a better idea presents itself, I store the SemVer part of the version number in a simple text file on disk. It is easy enough to parse that file using Powershell and feed the version into TeamCity's build pipeline.
What about the build counter? Well, like I mentioned above, it would be nice with full automation – I just need to figure out the best way to accomplish that first. For now, it lives in TeamCity as a regular build counter, which I reset manually.
Back to the question about tagging the develop branch: is it really necessary? Well, I want the develop branch to be deployable at all times. But, to be pragmatic, sometimes bugs slip through. With a build process in place that runs all the tests and tags every successful build, that is no problem: every tag in the develop branch is a receipt that everything was a-ok at that point in time. Without the tags, there is no easy way of knowing where to start looking, since we wouldn't know which one of the commits that was the latest one that worked.
Just to really hammer it in, here's an example of how the repo might evolve over time using the above rules (using the alternative where the build counter is being reset):
|Project starts, master is empty and we are committing our first feature in develop||0.0.0-wip.1|
|Feature A is committed to develop from a feature branch||0.0.0-wip.2|
|Quick bug fix directly in develop||0.0.0-wip.3|
|Feature B is merged into develop from a feature branch||0.0.0-wip.4|
|Time for the first minor release! Branch out from develop to a new release branch called release/0.1.0 and reset the build counter||0.1.0-rc.1|
|We polish the release a bit and commit||0.1.0-rc.2|
|To keep develop in sync, we merge the release branch back, creating a merge commit||0.1.0-wip.3|
|In the meantime, another team member commits feature C in develop||0.1.0-wip.4|
|Time for production release: we merge release/0.1.0 into master and develop||0.1.0-release.5
|A small refactoring is committed to develop||0.1.0-wip.7|
|A critical bug is reported in production; create the hotfix branch hotfix/0.1.1||0.1.1-hotfix.1|
|We merge the updated version file back to develop to avoid duplicate tags (since the counter got reset)||0.1.1-wip.2|
|In the meantime, someone commits another refactoring in develop||0.1.1-wip.3|
|The hotfix is finalized||0.1.1-hotfix.4|
|We merge it back into master and develop, thus creating to additional merge commits||0.1.1-release.5
|The team then merge in another feature branch in develop||0.1.1-wip.7|
|Time for another minor release; we create the release branch release/0.2.0 from develop and reset the build counter||0.2.0-rc.1|
...and so on. You get the idea. As the example shows, I am not a fan of fast-forwarding; I think merge commits tell an important part of the development story. Also, I like to use a global build counter, like the Autoincrementer plugin for TeamCity.
To wrap things up – this is just a proposal implementation, and by no means "best practice". I just implemented it myself, so it will probably evolve over time. All feedback is appreciated! :-)
Join my mailing list and get notified whenever a new post is out.
Topics range from team development, to application development strategy, to productivity tactics.
I won't spam you, and you can unsubscribe at any time.
A welcome email is on the way – please check your spam folder if it doesn't show up.
And to ensure that you don't miss anything, add firstname.lastname@example.org to your trusted senders.