Semantic versioning — as detailed by the Semantic Versioning specification — explains how to version and manage your dependencies without getting stuck with complicated upgrades. Briefly, this means:
Major (X.y.z): Incremented for backwards incompatible public API changes.
Minor (x.Y.z): Incremented for new, backwards compatible, public API enhancements/fixes.
Patch (x.y.Z): Incremented for small, backwards compatible, bug fixes.
The above is what I call strict semantic versioning — as implemented via the Versionaire gem — where you only use a version that consists of major, minor, and patch information. Unfortunately, the specification allows for additional information so we need to dive into why this is allowed in the specification while also highlighting why adhering to strict semantic versioning is better for you in the long run.
The problem with the semantic versioning specification is it’s too loose. Beyond what is presented above, you can have:
Pre-Releases: Allowed but not required. Denoted by appending a hyphen immediately after the patch version followed by a series of dots and/or alphanumeric characters. Examples:
Metadata: Allowed but not required. Denoted by appending a plus sign immediately after the patch or pre-release version followed by a series of dots and/or alphanumeric/hyphen characters. Examples:
Metadata doesn’t have any precedence but pre-releases do. This means you always strip metadata when calculating version precedence to get the following (listed in descending order):
1.0.0 1.0.0-rc.1 1.0.0-beta.11 1.0.0-beta.2 1.0.0-beta 1.0.0-alpha.beta 1.0.0-alpha.1 1.0.0-alpha
Lastly — and this is a common antipattern — nowhere in the specification does it say you can use
v as a prefix. Example:
v1.2.3. Please don’t do this because it’s redundant, wrecks havoc on sorting, and doesn’t adhere to the specification.
When you put all of the above together (including the prefix), you can end up with this monstrosity:
The above is not only ugly but unnecessarily complicated.
The solution to the above problem is to avoid using
v as a prefix, pre-releases, and metadata. You only need strict semantic versioning where you use major, minor, and patch information. Example (listed in descending order):
1.0.0 0.1.1 0.1.0 0.0.0
Sortable. Consistent. Elegant.
We don’t need to make versioning any more complex than it already is. To elaborate — and even with the simplicity of strict semantic versioning — you still need to keep up-to-date with your own projects dependencies, sub-dependencies, and versioning of your own code. None of this is hard with a weekly maintenence cadence but so many teams and projects fail to do that which is the bare minimum of a high functioning team. When you add pre-releases and metadata then you, unnecessarily, increase the complexity further. Again, yes, the specification says you may use pre-release and metadata versions and — with strict semantic versioning — I’m saying you can avoid that nonsense altogether! 😉
At this point you might be thinking:
"What if folks want to experiment with my upcoming release before official release?"
"What if I want to embed additional metadata in my version?"
We’ll tackle each question, in order listed, in the next sections.
Pre-releases are a poor person’s way of forcing the entire community to test a version of your code that you lack confidence in. If you are unsure about your work, there are multiple ways you can have folks kick the tires so-to-speak without using pre-release versions. For example, let the curious clone, import, and/or build from:
Your main branch.
Any feature branch.
A specific commit SHA.
A fully built download of your implementation.
Don’t forget, with the power of semantic versioning, you can quickly publish a new version with patches if people detect issues. Even better, with full coverage test suite, you increase confidence in releasing early and often. 🚀 That said, mistakes can happen so being able to quickly respond shows your support to the community you are building whether that be fellow engineers and/or your customers.
When it comes to metadata, Git already has a couple solutions: commit trailers and notes. I’ve written about commit trailers in the past in my Git Commit Anatomy article. Basically, all you need to do is add the appropriate metadata to the end of your commit message (i.e. trailer). Example:
With the above, we know that the commit is a patch so only the patch version will be bumped. That’s one example, but you could add additional metadata that is specific to the work you are doing, the project you are on, the deploy you’ll eventually make, and so forth. Due to each commit having additional metadata, it’s not hard to build reports out of this metadata since each trailer is a key/value pair. This rich source of information is a lot easier to parse than the highly compact and cryptic information tacked onto your version.
If commit trailers are not to you liking, you can also use Git Notes which allow you to add additional details to your tags. Since notes are mutable, this means you can make multiple updates to your notes if desired.
Should you need additional tooling that adheres to this workflow, you can find a few here:
Versionaire: A Ruby gem that provides version types you can use in your own Ruby code that adheres to strict semantic versioning.
Milestoner: A Ruby gem that helps you tag, release, and generate release notes using strict semantic versioning.
Janus: Slides to my open source and software service, currently in development, to tie all of these solutions into a single solution which you can host locally or pay for the service outright.
By thinking about your software versions, you can focus on simplicity and avoid complexity. You’ll save yourself time and reduce the strain on your user base. Enjoy the extra time saved. 😉