Semantic Versioning 101
Semantic Versioning 2.0.0 (semver.org) is a robust and elementary standard that encapsulates a wealth of information about the software you’re publishing or consuming.
Open source veterans know and understand the importance of this standard. If you’ve run a project in long-term maintenance mode, you come to realize its power one way or another. Still, enthusiastic, fast-moving dev teams like to find ways around this standard. I’ve seen more than a few engineers decide to invent their own ideas around major, minor, and patch increments. Their rationale is rooted in aesthetics or their own release schedule.
A key principle
Aside from the concise and complete information at semver.org, it is critical to understand:
Semantic versioning is for your consumers. It’s not for your release schedule or your marketing plan.
This illustrates the single, largest misuse of semantic versioning. Developers and product managers really want to wait for the big X.Y release, despite the fact that they have been introducing breaking changes now for months and incrementing patches.
Note: It’s a different thing to plan breaking changes around a release schedule. That is good project management. I am talking about teams that break functionality and increment a patch because the breaking change doesn’t “impact too many consumers” or “is not that consequential.”
To follow are some examples that illustrate some tricky versioning situations you will encounter.
Bullet #1 in the summary from semver.org: MAJOR version when you make incompatible API changes.
Here are three examples of module changes. Only one of these is a major increment:
A new public method has been added to the module’s API.
For the sake of argument, we’ll assume a top >> bottom no breaking changes introduced for this public method. This is a canonical definition of a minor version increment. I understand the pressure from the communication’s side. If you make huge, non-breaking changes, it sounds much better to advertise version 2.0.0 than 1.3.0. On behalf of your consumers, it’s much better to know there is a safe upgrade path. This is especially true for applications that are in maintenance mode.
A default argument for an existing API method has been changed from true to false.
This is a tiny change. In contrast to the last example, your gut instinct probably does not want to call attention to a tiny change with a major version increment. As a mental test, anything that could break the ongoing use cases for existing consumers should be a major version increment. Don’t fall into the trap of sneaking this one into a patch or minor version.
An issue from the last minor release was fixed: the file creation method was throwing null-pointer errors.
This is a typical example of a patch increment. New functionality created a regression on existing functionality. We’ll discuss patch versions at more length in the section below.
Bullet #2 in the summary from semver.org: MINOR version when you add functionality in a backwards-compatible manner.
A new external configuration builder is available to simplify API settings.
New functionality, no breaking changes. I had envisioned this as a method or constructor that helps the consumer build a complex configuration. That configuration is then passed in an entirely backwards-compatible way. Assuming you have met that compatibility bar, this is a good minor version.
The configuration process for the API has been simplified, several key properties have been renamed.
Renamed, changed, streamlined >> All words that foreshadow a possible major increment. This is obviously the case for public methods. There are exceptions when you have only refactored internal functionality. Configuration properties are public. This is not a backwards-compatible change. This is a major increment.
Several pieces of internal functionality have been optimized to improve API performance.
Semver.org says that “substantial new functionality or improvements within private code” can be the cause of a minor version increment. They leave decision making up to the developer. I would not be afraid to use minor version increments to signal performance improvements. I always want my consumers to feel a need to upgrade to the latest patch. They should want to upgrade to the latest minor version.
Bullet #3 in the summary from semver.org: PATCH version when you make backwards-compatible bug fixes.
Fix an issue that has existed for several minor release cycles: .csv files are written with .txt extensions.
This is not a regression. It is entirely possible that some consumers have worked this public behavior into their workflow (see xkcd). While it is a bug from the perspective of your project plan. This is a case for a major increment. I recommend bundling a change like this into a release along with other ‘necessary’ breaks of functionality.
Fix a typo in a configuration property introduced by the latest major release.
This is a really nasty one. By the strict definition of the standard, this is a major release. Imagine, you could go from 2.0.0 >> 3.0.0 overnight because of some sloppy typing and review. Still, a project with proper tests in place should catch things like this. That’s a separate topic, but this example should serve as a warning.
Fix for a recent issue that accidentally translates English to Spanish when French is requested.
Something was working as expected until now. Minor or major increment comes along, now French consumers are seeing Spanish. This is a good example of a patch. No consumer chooses this kind of errant behavior. It’s not new functionality (unless you never supported French to begin with). This is a good case for a patch.
Go out and version, in peace
This week, I was working on a throwaway script and needed a logger that could write JSON to a file. I grabbed the popular winston library via npm. The latest 3.2.1 library was automatically installed. I copied over an existing setup script for my logger from a project that was 2–3 years old.
The old project had been locked into the 2.x.x version. My logger construction broke. No mind, I downgraded my dependency to 2.x.x and kept working on my throwaway script.
I read no release notes. I did not go to Stack Overflow. I did not go poking through the code. The properly applied, semantic version told me everything I needed to know.
I added a task on my @Trello board to upgrade my logger in the future (and why not make it better, etc.). For now, I can get back to work, in peace.