Semantic Versioning – Cool Concept, buuuut…

What is it?

If you are a software developer, you may have heard of Semantic Versioning, as outlined here: http://semver.org/

What this is, is a set of rules for versioning stuff with public APIs (usually library components) in order to minimise Dependency Hell. Dependency Hell is something rather common that you encounter with package managers, where libraries that depende upon others which may depend upon others (and so on) each require different versions of their dependencies.

Semantic Versioning attempts to fix this by stating the following rules (there are more, but this is the important part):

1) Three version segments, X.Y.Z.

2) X is the MAJOR version – means that an breaking change has been introduced.

3) Y is the MINOR version – means that new but compatible functionality has been introduce, without this being a breaking change

4) Z is the PATCH version – means that this version only contains bug fixes; no new functionality, no new version.

 

Why is this important? Imagine that library A has a dependency on libary B which is of version 2.0.1. Just so it happens, the developer of library B releases a new version, but library A isn’t updated yet. Can you replace the older version of library B with its newer one?

Semantic Version says “yes”, as long as the MAJOR version hasn’t changed. So you can say that your library will be compatible with all versions of library B that have a major version of 2, or 2.x.x! Awesome concept and very helpful as it’s been proven in NuGet so far.

 

It’s great, right?

It is. But there are 2 issues here that you may have spotted however which aim to ruin my day:

There is no way to indicate a “generation” for the project,

That’s applicable when for example you do a total redesign/rewrite or when you add enough new features to warrant a new version. The difference between version let’s say… 15.0.1 and 16.0.0 might be a simple extra parameter on a public method while the difference from version 16.0.0 to 17.0.0 might be a complete rewrite!

Now, there are some solutions for this project none of which I like much.

The first is to use large version spaces between large releases, kinda like what Microsoft does with its build numbers on Windows. For example you can mark your first “generation” as v1.0.0, your second as 10.0.0 or even 100.0.0, the third as 20.0.0 or 200.0.0 and so on.

This is too uncommon in the software world, where even continuously updated applications such as Firefox don’t easily reach such large numbers. By the way did I mention that not everyone uses Semantic Versioning? Thankfully that’s an issue that slowly diminishes as more and more developers start using SemVer, but you need to be careful when using such dependencies (jQuery is a component that doesn’t currently use SemVer but plans to) when you declare compatibility with a dependency’s versions.

The second is to simply have generations as different projects. So “AwesomeLibrary” should be a different project from “AwesomeLibrary 2”. Well, this is a better solution, but… you still don’t have any semantic information on it. If I’m working on a project where I want to make sure that all libraries are up to day and I am willing to factor in any breaking changes that may exist, I cannot do that with the automagical NuGet method. I need to manually look for any projects using a similar name and manually upgrade to the new generation.

What I’d do here is to have a four part version number, where the parts would be identified as Generation.Major.Minor.Patch; updating the first two indicates a breaking change,

A better (and more compatible) solution would be to keep the three part version number by keeping the previous four-part scheme then ditching the patch number. What this gives you is the ability to indicate a generation and a major version and only have one version for non breaking changes.

Yes, you lose information between new functionality and patch, but be pragmatic here, most consumers of your library only care whether 1) The new version is a totally new/rewrite compared to the old one and 2) if introducing this new version in their dependencies will break compatibility. It’s the approach I use, though you still have to inform any consumers of your component(s) that they cannot depend on both the minor and the patch versions staying the same, but only the patch one doing so.

 

There is no way to maintain older versions and introduce breaking bug fixes.

That’s a more serious issue. Imagine that a client of yours insists on using an older version of your library, let’s say v14.0.0. But in the meantime you’re working on v24.0.0. Since tbe client is always right and your attempts to force them to upgrade failed, you keep an eye on his version of your library every now and then, just to be sure that no (mostly security) issues arise.

… And then one day you fix an issue on your latest version which is also exhibited on the client’s version. Worse than that, it’s a breaking change, let’s say an extra parameter in a method (or whatever). The client is willing to do the upgrade and fix it, but now you have a problem. You cannot properly upgrade the version number now!

Admittedly this is an issue that may exist with any kind of versioning system, but the problem here lies with the “semantic” part of Semantic Versioning. You cannot introduce a new major version because you used that number for something else, but you cannot simply increment the patch number or even the minor version number because you have a breaking change in your hands.

How do you solve this?

One solution is to upgrade your major versions by a number greater than one. Eg. from version 1 go to version 5, then go to version 10 and then 15 for every major release, like in the previous issue (and I’m aware of the spacing between versions 1 and 5). This gives you breathing room for (admittedly rare) cases like this, but it’s not very pretty.

The other solution is to simply make an exception and perhaps marking it with an alphanumeric version identifier next to the number version number (supported by SemVer), like 14.0.1-breaking or so. I don’t like this solution for projects where I have to maintain older versions of libraries because I cannot guarantee that this won’t be a very rare exception, especially when you have to backport fixes. I am probably willing to go this way for projects where I don’t have to support older versions of (few cases, usually not public ones).

 

How do you do it then?

What I like doing is to use the Generation.Major.Minor schema, which is compatible with SemVer by mapping directly onto Major.Minor.Patch – but I have to clearly state in my READMEs that one can only set a dependency for the least significant version only.

The second is that upgrade the Major version by 2 – similar to how some projects use odd version numbers to indicate unstable releases, but very different in nature. This gives me exactly one chance to introduce a breaking change on an older library, but practically, I’ve only needed one so far. You can choose a better distribution depending on your needs.

 

Use Semantic Versioning, it’s cool.

Leave a Reply

Your email address will not be published. Required fields are marked *