'Don't break the build!' or 'The dangers of approximating version numbers in dependencies'

One of the first rules of collaborative code work my teachers taught to me was, "Don't break the buld!" which is something developers have likely heard at one point or another during their careers. It's a good piece of advice, albeit a little vague: is a successful compile good enough to satisfy 'Not breaking the build'? Today we get a lesson in specifics inspired by the story of bower and a library for making temporary files and directories breaking their tool without a single line of code changing in bower.

Code Expectations

When you build release a version of your software you should expect that it will compile and run the same way today as it did yesterday. But your code likely depends on one or more libraries/frameworks/what-have-you from other developers and what's worse than having your build break because you made a change is having it break because someone else made a change. An obvious solution is to use a specific version of that dependancy consistently and update only when necessary. This is why we use version numbers (and tag releases) so that we have a common point of reference when using and talking about code. But how different is version 0.2.0 from 0.2.1?

Enter Semver

Semver is a semantic versioning sytem which seeks to standardize the meaning of version numbers. Version numbers come in the form of major.minor.patch and you increment each as such:

  • MAJOR version when you make incompatible API changes
  • MINOR version when you add functionality in a backwards-compatible manner, and
  • PATCH version when you make backwards-compatible bug fixes.

This (hypothetically) should take the guesswork out of versioning and provide at the very least semblemance of a guarantee to those who would use your software. With this system you could target 1.X.Y or 1.2.Z versions of a library you depend on and have a reasonable expectation that for any value X, Y or Z your code should work. Node even allows for this type of version targeting with their package manager npm. You can target ^1.2.3 which will be valid for versions greater than 1.2.3 and less than 2.0.0. Or you can be safer and target ~1.2.3 which will be valid for versions greater than 1.2.3 and less than 1.3.0.

Problem Exists Between Chair And Keyboard

The main issue here is that you have to be confident that the maintainer of the software you depend on will adhere to this and that they're testing process is robust enough to catch anything that will break how you use it. If breaking changes are made in a patch bump you're effectively SOL if you rely on approximating versions. You can contact whomever maintains the software that broke and inform them - or help them fix if it's open source and you have the time - but who knows when they'll have time to get to it. A simple hotfix for this would be to narrowly target the exact version that was last working. This way the next time you (or your Continuous Integration platform) run npm install or update your tools you won't be surprised with errors.

A best practice to take away from this would be to only use exact versions for dependancies to provide consistency for your build and assurances to your users. All the fancy automated testing pipelines in the world mean nothing when their green badge on your github is only valid for the exact point in time that it was run.