A few years ago I standardised on a variation of SemVer 2.0 for versioning projects.

The semantic meaning that SemVer version numbers could provide was useful for the management of internal dependencies across projects and team members, but, the primary goal was really just to settle on a consistent version number format to use across multiple CI/CD systems.

SemVer in the era of Continuous Delivery

These days I'm the sole developer on many of my projects, and I'm no longer working on as many shared libraries or needing to manage cross-project or cross-team dependancies, at least most of the time.  My current software projects are primarily complete, full-stack web applications, with a sprinkling of desktop/mobile apps here and there.

Where possible, I'm getting closer to a true Continuous Delivery approach with my projects, and this means that there are many more software versions being released and deployed, with each version typically containing a much smaller set of changes than they used to.

The concept of semantically labelling a release as being a major, minor or patch version doesn't fit quite so well when there is a new version being deployed every day–sometimes multiple times per day–and quite often with the tiniest of changes.

I realised that I was never manually bumping the major version, because when you're deploying on such a regular basis, there is never really a release that stands out as being worthy of that major version bump.  On top of this, these releases weren't depended upon by upstream projects, so the semantic meaning of version bumps became somewhat irrelevant anyway.

So, my need for using semantic versioning for most of my projects has decreased, but as I increase the pace and automation of deployments, I do still have a need for reliable and consistent versioning, with an emphasis on full automation.

Speaking of automation; my previous SemVer implementation would require occasional manual steps.  It was mostly automated, but it was a manual decision on when to bump the major version, and sometimes the minor version too (depending on the level of automation for the specific project).  This manual bump would often be done by updating a Version text file in the root of the project repository with something like v3.1.0, which would then be read as the "current base version" by a custom versioning script at build time.  The script would then take that base version and automatically increment the Patch and Minor portions for the current build.

I wanted my new versioning approach to be 100% automated, requiring no thinking or manual steps at all.

Moving to calendar-based versioning

I decided to move to calendar based versioning for my full-stack projects, taking inspiration from CalVer. It's not a novel approach.  It's popular with plenty of organisations these days, and its popularity has been growing as the general trend continues moving away from installable software and in the direction of centrally hosted–and continuously updated–SaaS applications.  This CalVer approach ditches the major/minor/patch form of versioning in favour of a primarily date based format (albeit sometimes with some of those older semantic versioning concepts mixed in).

My old SemVer 2.0 format

A typical SemVer 2.0 version for one of my projects used to be something like 4.5.2+337.97e7a6d which can be shortened to 4.5.2 when the build metadata portion is removed.

4.5.2 is obviously Major, Minor, Patch.  With Major generally always been manually bumped, and Minor + Patch numbers being incremented somewhat automatically.

One thing I was fond of with SemVer 2.0, was not so much the semantic side of things, but the allowances for various bits of metadata.  Everything after the + symbol is metadata, and as per the SemVer 2.0 spec, is ignored from a semantic and order-of-precedence point of view, but is particularly useful when referring to release versions across multiple systems as part of a CI pipeline.  The 337 in my example above is a unique, always incrementing, build number (generated from the GitHub Actions Run ID for example).  This build number is then followed by the shortened Git commit hash.

This version format would be used for builds from my main / master branches.  For feature branches, I would generally append a prerelease tag before the metadata portion.  Sometimes this would be a shortened version of the branch name for example: 4.5.2-featureabc+337.97e7a6d.

The new, backwards compatible CalVer format

Given the format of my old version numbers, I was looking for a way to switch to a CalVer inspired versioning format, but with the aim of maintaining a similar structure (eg. exact same number of elements and format of build data) to maintain compatibility with some of my existing CI tooling.

The format I decided on is YYYY.RELEASE.CHANGES{-PRERELEASE}+BUILD.COMMIT which has the same number of components as my previous version numbers, but switches out the Major portion with the current year, the Minor with an automatically generated sequential release number for the current year, and the "Patch" portion is now the count of commits in this release since the last released version.

So as an example 2022.23.9+283.3a55123 would indicate the 23rd release in the year 2022, containing 9 Git commits since the last release, with a unique build number of 283 and a Git head commit hash of 3a55123.

Figure 1 The components of my new versioning scheme

Given that my old SemVer major versions never got anywhere close to 2022, this format gave me a nice seamless switchover, where the order of precedence across version numbers in both schemes would still be maintained.  Plus, because version numbers still had a consistent number of elements, there was no update needed for any of my CI tooling to cater for this new format.  (For example, Octopus Deploy allows you to setup Regex expressions to parse version numbers and automatically apply different rules to your releases).

So as an example, the recently released versions for one of my projects might look like this – sorted in order of precedence (the most recent version first) – showing how the old and new formats will still sort correctly:

Figure 2 Order of precedence is preserved across version schemes

Walking the Talk

Since I now use this approach across all my projects, you can actually see it in action on this very website. If you scroll down, you'll see the current version number of this site in the footer of this (and every other) page. I also send it as a custom header X-App-Version.

Advantages

In some ways I find this new approach to be more "semantic" for my purposes than SemVer ever was.  It's nice to look at a version number and to immediately know  how many releases the project has had so far this year, and, although somewhat arbitrary, the CHANGES portion (number of commits since the last release) is handy as a quick measure of how large the change-set is in the current release–though admittedly this is not a particularly accurate metric.

I think it's better too from a stakeholder perspective, because the "freshness" of the software release is much more obvious to other parties who might have visibility of the version number, when it starts with the current year, and number of releases that year.  It feels more transparent.  Obviously this increased transparency could be a downside if you haven't deployed an update in a long time, (especially across year boundaries).  But this versioning approach is best paired with a continuously delivered project that gets small incremental updates every few days or weeks.

Tooling

I may write more in the future about how this works from a tooling perspective, but it's pretty simple.  I have a very simple and reusable custom GitHub Action (consisting primarily of a small Bash script) which generates these release numbers automatically as part of my builds.  When I deploy a release via Octopus Deploy, at the end of the deployment process ,Octopus calls back to the GitHub API to trigger a special "Tag Release" workflow, which in turn creates a GitHub Release entry and tags the commit with the released version number.

So, I don't use GitHub's release/deployment features to proactively trigger deployments–I prefer Octopus Deploy to drive that that side of things–but rather I retrospectively create and tag releases in GitHub to bring it up to date once a deployment has finished.

For the next build following a release, my versioning GitHub Action will notice the new tag and automatically increment the RELEASE portion of the newly generated version number as well as count the number of commits since this previous release tag to generate the new CHANGES portion.  When the next year rolls around, the RELEASE portion will automatically reset to 1 for the first release of that year and will start to increment again from there.

Potential Downsides

The main drawback to be aware of, if you're considering something similar to this version numbering scheme, is that if you rewrite the history of your release branch, the number of commits since the last tagged release of that branch may change (say if you've squashed a few commits for example), and the next build could potentially have a lower version number than the previous build prior to the rewrite (due to the CHANGES portion of the updated version number now having a lower value).  Crucially, this will never result in a version number being generated that's lower than your last deployed and tagged release - it just affects your unreleased builds prior and after the rewrite. In my case this hasn't proved to be an issue yet, since I rarely (and ideally, never) rewrite the history of my primary branches (eg. main or release), and if I do, it's almost always done prior to pushing up to GitHub in the first place.

If I'm rewriting history on my release branch, it's normally a sign that something has gone a bit wrong anyway!  So I can live with this downside for now, but there remains a possibility of some conflicting version numbers if you rewrite history on your release branch.

In conclusion... no thinking required!

With everything being 100% automated, I no longer need a file in my repositories to keep track of manually bumped portions of version numbers, and just don't have to think about versioning at all any more, while still having meaningful version numbers being created for my projects.

I find that often the setup of something like this can get quite involved and time consuming - moreso than you would have predicted when you're just getting started and beginning to pull on the thread.  You find yourself questioning whether it's really worth the pain of setting up all the scripts, API integrations and automation.  There's not a lot to it, but still, I now have a bash script, GitHub Action, API integration between GitHub and Octopus Deploy and some custom Octopus Deploy steps which drive all this.  Crucially though, they can now be used across all my projects with very little setup or customisation required.

The end result of this type of exercise, is almost always worth it, in my experience.  Especially when you solve the problem once, and then get to reap the rewards for months/years to come without needing to think much about it again... until the next iteration of course!