Version numbers
When releasing software, we need to also assign unique identifiers to each state that gets promoted to an artifact that others can use. A version number, if you will. But, how many types of these are there, and how can we generalize them?
Historically, MIT’s Incompatible Timesharing System (ITS) introduced file
version numbers to keep track of file changes. This evolved into using the
revision number from the version control system as the version number of the
released software. In elder days, this was SVN (there were VCS systems before
that too), as one system that used numbers for each revision. But, with the
migration to git, Mercurial, and the like, where each code version is,
ultimately, a hash, the system of tying the version number to the VCS snapshot
started having problems: which one is newer, the release from commit abcdef
or the one from commit c00fe3?
This is one of the reasons why new versioning schemes arose. Using CI/CD build
numbers is an alternative attempt to still restore just one number per
version, to still keep the ordering between two releases. It is easy to see
that version #42 (from the 42nd run of the CI/CD system) is newer than
version #10.
But, using just one number also has the issue that it does not provide any signal between two consecutive releases. Are the changes significant or is there just a minimal improvement, a bug fix, something that can be updated instantly?
Which is why some other versioning schemes started using 2 numbers. This is the start of major / minor split, where a bump in a major number signalled significant changes (such as architectural redesigns, significant breaking changes), whereas a minor bump could be used for all other changes. This is a scheme used by Linux before 2.6, where, to add more semantic meaning, an old/even versioning scheme got created. Unstable, pre-release, development versions of the kernel would use odd minor numbers, whereas the stable ones would use the even ones.
But, the granularity obtained by using 2 numbers turned out to not be enough.
Especially in modern development where we have multiple releases. Similarly,
the need to support multiple releases across different branches in the same
release train, signalled that just two numbers are not enough. Nevertheless,
the legacy of this scheme is still present. For example, if you look at the
shared libraries in your system, you fill find that libfoo.so is a symlink
to libfoo.so.4 (4 being the major number), which is then linked to the real
library, from the, say, 4.2, release. Typically, the linker will look for only
the matching major number, and hope that the rest is matching / working
correctly enough.
Before moving to the schemes with 3 numbers, which are the majority of schemes
today, let’s consider the 4 numbers ones. My Chrome version right now is
143.0.7499.40, following a major.minor.build.patch
rule. Here, the build
is an identifier for the branch from which the build originates.
We’ll come back to the Chrome version later, but the main reason to jump to 4 numbers for the versions is Haskell’s Package Versioning Policy (PVP) which uses two components for the major number, one for the minor number and an optional number of additional components, usually one, for patches, etc. It is not very opinionated on when to bump the first major number versus when to bump the second one, giving more power to each package maintainer and taking away from anyone using this package and wanting to understand if the upgrade is safe. Furthermore, the fact that 3 components are mandatory and the others are optional, mean that PVP can also be confused with the schemes that use 3 numbers, the schemes that are now the popular ones.
Something that has captured the entire world, being adopted almost everywhere,
is Semantic Versioning (SemVer). It established the
concepts of major.minor.patch across the entire software industry. It
established the convention that a 0 major number means unstable, development,
library which will have breaking changes. It is easy to understand and
multiple ecosystems / languages only support this scheme right now.
More like a joke, but turning out to be a quite nice description of SemVer, we
have Pride Versioning. Here, the version number is
the triplet proud.default.shame where the first number is increased with
every release that the owner is proud of (that is, every release of major new
features, big changes, etc.) while the last one is bumped when a new release
is made to fix some issues (things that are minimal, embarrassing to add to
release notes, maybe). There is a clear matching between these numbers and the
SemVer ones, so developers can adopt PrideVer while still looking like they
use SemVer to the outside world.
This actually led to a new scheme:
Effort Based Versioning (EffVer). Here,
the version numbers are bumped in a way that signals how much work is needed
by your users when they upgrade the package. The first number in the triplet
increases when users have to do a significant amount of work to migrate,
whereas the last one increases when the upgrade could be done automatically,
with no changes. This schemes also comes with nice benefits that if the author
of a package discovers that users actually need to do more work than expected,
then they could perform a new release at the same commit but with bumping a
higher priority number (e.g., releasing 3.2.0 at the same commit as 3.1.4
when it is discovered that the bump to 3.1.4 requires some work for users).
Since this scheme also matches SemVer, it is easy for projects to migrate to
this. To be honest, I would like to see more and more projects migrate to
this.
Finally, Calendar Versioning (CalVer) is another 3 numbers scheme. It has the benefit that you know from the version numbers when the package got released, but there is no semantic to distinguish the impact of changes between two consecutive releases.
There is a potential to merge two of the schemes. This is usually taken advantage of. Given that SemVer and other schemes also support additional tags at the end of the triples of numbers, it is possible to do this merging in a mostly compatible way. For example, PEP 2026 proposes that after the release of “\(\pi\)-thon” (tongue in check for Python 3.14), the new releases will be following a CalVer-inspired scheme: the next release would be Python 3.26, released in 2026.
Before going into formalizing these numbers, let’s also look at some other packages that have interesting versioning schemes. TeX version numbers are increasing approximations of \(\pi\) (adding a new digit at the end of the current version, pinning to \(\pi\) after the death of Knuth, when TeX will become the final release, all bugs being elevated to features). Similarly, Metafont tends to \(e\).
Looking back at all these versions, we see that we can formalize everything by requiring that a version number is a collection of integers, with potential tags appended (to signal pre-releases, alpha and beta versions, etc.) In all cases, increasing a number means a newer release and increases of one of the numbers (e.g., the major number) could reset the subsequent numbers (i.e., the minor and patch) to 0, or could keep them (like in the Chrome version). Ordering the releases can be done by ordering the vectors of numbers, in lexicographic order, with special care being done for the tags.
data Version = V
{ numbers :: [Int]
, tags :: Maybe [String]
}The fact that we can impose a strict ordering relation between most of these versioning schemes is what lies at the basis of all dependency resolution system in all package managers. I plan to write more about these in a future article.
But, before finishing off, let’s go back to the Chrome version number. It turns out that users don’t really need to care about it, except maybe when reporting bugs. This is because Chrome is one of the systems that updates continuously, behind the scenes. This is good for security, although is not a scheme that could be taken by most other packages, given the dependency chains. More, in another article, too. I am again late with this article, spilling over to the next day.
Comments:
There are 0 comments (add more):