Package Management - Versions

The start of almost every package journey starts with versions. This is how one distinguishes between different copies and hopefully makes sense of when to get the latest version verses only updating a few or not updating at all.

Series

This is going to be a series of posts, but I have no idea of how fast I'll be writing them out. I want to work out my ideas, maybe have a few conversations, and then start to move to more technical concepts.

The Semantic Dream

These days, almost every package system proclaims that they use semantic versions and write the infrastructure as such. I love semantic versions with a passion, including putting them in my novels and stories. They are relatively clear and concise, and easy for me to understand: breaking-things.adding-things.fixing-things.

The problem is, semantic versions are hard to manage when it comes to packages and dependencies, not to mention the business practices.

Romantic Versions

The biggest one are the ones who won't use it. One of the library I use during my day job is EasyQuery because they have a .NET and a JS library and give me a lot of flexibility in creating dynamic queries. They also are responsive to bug issues. What they don't do is follow semantic versions. They use romantic versions with every x.y version being a breaking change from the previous one. This applies to their code on NPM and NuGet.

It is easy to manage, but you have to know that you can't go from 7.0.2 to 7.2.0 without major work, though semantic version says that it should have been a safe change.

My day job product is also romantically versioned, so I deal with this on a quarterly basis.

Dependency Changes

Another example is SlimMessageBus.Host. In version 1.14.1, it requires “.netstandard2.0”. In 1.15.0, it is “.netstandard2.1”. While this seems like safe change, it was a minor version bump that made it impossible for us to update because our product is still bound to “.netstandard2.0” until we can get off WebForms.

Version Ranges

Since it is important, there are also a number of ways of communicating version ranges. Node uses the ^ for semantic versions and ~ for “only patch updates” (basically what EasyQuery uses).

NuGet uses a different system such as [1.0.0, 1.15.0) with the [] meaning inclusive and () meaning exclusive. This is my preferred system, mainly because it hearkens back to my computer science days and allows a mid-version break such as SlimMessageBus. In that regard, it is more flexible because ^ and ~ can translate directly into the verison range.

Being Explicit

I haven't gotten to dependencies yet, but the developer knows when something is safe update a package verses when it is not. EasyQuery knows that a patch is the breaking point, it would be better that they be able to communicate that as part of their metadata instead of requiring a customer support call. In effect, using the first two semantic version components (major and minor), EasyQuery 7.0.0 would be:

{
    version: "7.0.0",
    majorUpdate: "[7.0.0, 7.1.0)",
    minorUpdate: "[7.0.0, 7.1.1)", // The third number is their minor versions
}

On the other hand, SlimMessageBus would use (using an older version for illustrative purposes):

{
    version: "1.10.0",
    majorUpdate: "[1.10.0, 1.15.0)",
    minorUpdate: "[1.10.0, 1.11.0)",
}

I'm assuming that anything more precise is a patch and is safe to update. I'm also think Bakfu should lean heavily on well-defined undefined values. So if majorUpdate, then assume semantic version. If minorUpdate is missing, then use majorUpdate (which may be assumed).

Package Versions

The last bit is one that I've encountered a number of times while packaging books, but also while I was doing Debian packages. The version of the package should be independent of the content. Using the above example, if SlimMessageBus realized they had made a breaking change, they should be able to update the 1.14.0 version of their package to indicate that 1.15.0 is a major breaking change.

Today, their only choice would be to remove the 1.15.0 (and any later) version and create a 2.0.0 version. This can be a lot of work because most likely someone realized it after there were a number of updates already out in the field.

Instead, if the package version was decoupled from the content version, then it would be possible to retroactively update critical information such as dependencies and safe upgrade ranges. Using SlimMessageBus again:

{
    package: {
        version: "1.0.0",
    },
    content: {
        version: "1.10.0",
    },
}

After they realize the breaking change:

{
    package: {
        version: "2.0.0",
    },
    content: {
        version: "1.10.0",
        majorUpdate: "[1.10.0, 1.15.0)",
        minorUpdate: "[1.10.0, 1.11.0)",
    },
}

Metadata

Categories:

Tags: