An illustrated guide to semantic versioning
A program’s version does not represent the state of the software but makes a statement about its API for the consumer.
Semantic versioning does not reflect the size of the update, but the changes in the software’s public API.
As part of breaking up monolithic applications at Fiverr, we’re sharing a lot of functionality between programs using various strategies to create independent, interchangeable pieces of software; Ruby Gems, Node Packages, Godeps, and so on. We will address them all as “modules” from here on.
Loose dependencies of software, matching to highest patch (
~) or even highest minor (
^), is very common, and spawning new images at different times could result in different versions of modules on different machines. This is desired behaviour. We want the ability to run the latest versions of modules with the latest performance improvements and vulnerability fixes, without the need to manually update the code.
Where applicable, we use semantic versioning to manage this. It is important to follow the rules of versioning to avoid breakage. Bad versioning can cause unexpected behaviour, from different bundle outputs causing different fingerprints across servers to operations straight-up breaking in a project we didn’t update. These instances can be the hardest to locate and debug because they do not involve direct code change.
Joe’s gonna help us illustrate the purpose of versioning.
This is where we meet Joe, in Joe’s first major release. Joe is a piece of software with an internal logic and an API, making his functionality available to his consumers (He can walk, talk, and throw a fist).
Patch updates are interchangeable, meaning consumers can upgrade or downgrade freely.
Content: Internal fix
Example: Bug fix, Performance improvement, environment or internal tweaks
Policy: Consumers should update their software without hesitation
We thought Joe’s a bit too slow on his feet. Research suggests sandals can really help Joe pick up the pace. This is an internal update, it does not change any of Joe’s behaviour or abilities, but improves on existing ones. We’ll update a patch so our consumers know they should update.
Minor updates are backwards compatible, meaning consumers can upgrade freely.
Content: Interface change with full backward compatibility
Example: New feature, Endpoint declared deprecated
Policy: Update your software to get some new features. Nothing will break
As we go along adventuring with Joe, we decide to add some functionality to Joe’s interface. We’ve added some weaponry. Now Joe can fight more fiercely and tackle more challenging endeavours.
Since we’ve added to Joe’s interface, we’re upgrading a minor version. Joe can do everything he used to, and then some!
We’re continuing to upgrade Joe with new features, but our tests suggest Joe could perform better using an axe instead of his sword. Because we want Joe’s users to keep updating without fear of old functionality breaking, we’ve decided to give him a small axe with which he can still jab, but we’re recommending users to start swinging, instead. This is called a deprecation strategy.
Major updates are non-compatible, meaning consumers can not upgrade without changing their software where applicable.
Content: Interface change breaking backward compatibility
Example: Change API endpoint name or signature, Remove an endpoint
Policy: Test your system extensively after updating.
Migration documents may be in order
Finally, we decide it’s time to move on to Joe’s big boy’s axe. To do that we need to free up Joe’s other hand, which means he can’t use his shield anymore. We’ve removed some of Joe’s functionality, which means this version is not fully backward compatible. It means users relying on Joe’s behaviour should update carefully, and replace instances where they use the shield if there are any. Their programs may break if they don’t test their usage. And while we’re breaking compatibility, we’re going to drop support of Jab, too.
It has come to our attention, that Joe has a vulnerability caused by his feather. It makes him too visible on the battlefield and we’ve decided to remove it.
Because this is a patch, our users know they can safely upgrade without rigorously testing their programs.
But the feather wasn’t introduced in this major release, it was introduced in an older version. If we want to keep supporting our user base, we should release a patch to our old versions as well.
This is where tagging comes in handy. You may have noticed, we’ve also published tags (named versions of our software), along the way and used our release numbers as tag names. Now we can easily go back and release a fix, then increment a patch on the old version.
Alpha (or Beta, etc.) versions are considered unstable and do not abide by versioning restrictions.
$ git tag -a 1.0.0-alpha.1 -m "Testing the new interface"
Release candidate means this version is under consideration for release.
$ git tag -a 2.1.0-rc.3 -m "Still performing some tests, but pretty sure this is the interface we'll be using"
Builds are usually internal software releases of working versions of the code.
It is considered that build differs in build metadata only. They usually accept either timestamp or subsequent numbers as a name. The convention is to append them using a plus sign:
Build versions do not have any precedence over the other.
When *not* to update
Occasionally, a maintainer or contributor only suggests readability or documentation fixes. Renaming a private variable or method; Adding or fixing documentation and code examples or some convention technicalities.
Since these changes are only meant for other maintainers, or to instruct how to better use the program, they do not affect the program’s operations and should not trigger a version update (not including build number extension of course).
Public API should not be considered stable under version 1.0.0
$ git tag -a 0.0.4 -m "removed half of the interface ¯\_(ツ)_/¯"