Managing dependency versions for your applications and modules can be a time consuming pain in the bum. Not only must you keep tabs on the release cycles of all of your dependencies, but you must then assess each release invidually, manually update the versions in all the consuming applications you own, and make any changes required to support these latest versions - every. single. time.
Luckily as software development practices and the dependency ecosystems we use have matured, we've seen the advent of several automation tools that allow us to simplify this burdensome process into something far more manageable.
Why Manage Dependencies At All?
I've already highlighted some of the pain points in keeping dependencies up-to-date, but I haven't really explained why you should even care to do so in the first place. When I suggest the need to keep dependencies up-to-date, I'm regularly met with the same refrain:
It works with the current version, so I won't even bother. I'll update when I need a new version.
This is an understable - albeit misguided - position for several reasons:
Practically speaking, by the time you find yourself needing functionality exposed in newer versions of any given dependency, you risk finding yourself several breaking versions behind. This all too common situation artificially increases the burden of incorporating newer versions by requiring several unrelated changes to the existing codebase - just to get access to a single version (which itself may not actually require any changes at all).
Another - more nebulous - concern is that of security. Any given version may be functionally adequate for the given state of your application, but security vulnerabilities are discovered and patched every day. By neglecting to keep your dependency tree up to date you put your application, and therefore your users, at risk. This is perhaps the most critical reason to keep your dependencies up to date.
Automatic Versioning
Hopefully by now you're convinced that keeping your dependencies up to date is an important task. However, we also know that it is by no means a trivial one. Keeping dependencies up-to-date could be a full-time job itself depending on the scope of your ownership. That said there are several tools we can leverage to make this process easier.
Tools
SemVer
Perhaps the most important concept to build upon is that of Semantic Versioning (or SemVer). Semantic Versioning is a practice that aims to imbue any given version of a code module with an explicit meaning that correlates to the code itself. When releasing new versions of modules that you maintain, its important to adhere to this standard so as to better communicate with those who consume your work.
In practice, Semantic Versioning manifests itself as three different release categories, each indicated by the various pieces of a version tag (you may be familiar with the syntax: v1.2.3
). Let's look at those in more depth:
Example:
v.1.2.3
Major
A Major version release is often the least common. This is because a major version change indicates that something, well, major has changed with the underlying code that necessitates consumers modify their consumption thereof. This is often referred to as a "Breaking" change, because without actual changes to the accomodate them, these changes will cause the consuming application to not work (be broken).
In the example above, 1
is the major version.
Minor
A Minor version release indicates that the module has changed in such a way that any consumer should be able to update without having to make any changes to their existing codebase. These minor versions, however, likely contain new features that one may wish to make use of in our applications.
In the example above, 2
is the minor version.
Patch
A Patch version release is the most common. Patches indicate changes to the module code that should be invisible to those consuming it. These sorts of changes include (but are not limited to): bugfixes, security vulnerability patches, and performance optimizations.
In the example above, 3
is the patch version.
It's worth acknowledging that the version definitions above are flexible. It may be impossible to address a given security vulnerability without introducing a breaking change to a codebase, and therefore a major version change may be required for what might intuitively be considered a patch. Having some awareness of how your code is used by others is a critical facet of maintaining shared modules.
Semantic Collaboration
Understanding what semantic versions mean is one thing, but having to make this decision every time you wish to release a version of your application can be a nightmare - especially if you're not the only developer on the project. In that scenario, how do you know what category is correct for releasing your code changes if that release will also include changes from others? This can lead to many wasted hours of effort simply communicating between developers who may not even inhabit the same time zone!
One excellent way to ensure that this communication can be done quickly and without coordinating disparate human beings is to use Semantic Commits. Semantic Commits aim to make Git commits convey meaning similar to the way that Semantic Versioning conveys meaning of releases. With semantic commits, each commit message is prefixed with a common keyword that indicates to other developers what that change includes. Examples of semantic commit prefixes include (but are not limited to):
feat
for commits that introduce new featuresfix
for commits that fix bugs or security vulnerabilitiesperf
commits introduce performance improvements
Semantic commit prefixes are entirely subjective. The goal is simply to foster better asynchronous communication between developers. Establishing a shared language with your collaborators is crucial. That said, you'll often find teams gravitate towards using the prefixes established by the Angular Project due to the network effect established by the various automation tools that build on them.
One such tool is Semantic Release. Semantic Release will automatically publish new releases of your code, and is typically executed on every merge to the master
branch. Semantic Release builds on Semantic Commits by using a codified set of prefixes to determine what semantic level of release to publish automatically.
Publishing a new release on every merge to the master
branch allows new versions to be concise in what they chance and reduces the amount of unpublished inventory. More frequent releases, however, also wind up creating far more releases than one may be used to. While this provides consumers far more control over what code changes to incorporate into their applications, it also increases the frequency with which that decision must be made. Wouldn't it be great if we could automate that as well?
Automatic Upgrades
As SemVer has rapidly taken over the world of open-source software development (thanks in no small part to the many practices outlined above), even more tools have been created that build on the assumption of SemVer to simplify consumption as well as maintenance of open source software. I'll outline two popular tools below: WhiteSource's Renovate and GitHub's Dependabot.
Renovate
Long the reigning champ of automated dependency management, Renovate (recently purchased by WhiteSource Software) is an application that runs on its own dedicated compute resources and scans source code repositories (think Github, GitLab, Bitbucket, etc.) that specify their dependencies in code (package.json
, pom.xml
, etc.) and then leverages public module repositories (npm, maven, etc.) to know when a dependency needs updated.
If you're using GitHub to host your application code, Renovate will open a Pull Request to your application whenever a new version of a dependency is released. This completely removes the burden of having to track dependencies from developers. Simply monitor you own codebase - you're already doing that, right? - for pull requests from Renovate's bot, and merge away! With some simple configuration, you can even configure GitHub + Renovate to automatically merge these pull requests without you having to even verify them yourselves.
Be sure to only automerge minor or patch releases! Major versions require other changes that require an understanding of your application that simply cannot be automated.
Dependabot
Perhaps less well known, Dependabot is an up-and-coming tool for automating updates to dependencies. After being acquired by GitHub, Dependabot has become far more prevalent within the GitHub ecosystem as a tool of first resort when approaching security. If you're using public GitHub, you'll likely have noticed the new "Security" tab on some of your code repositories; this is powered by Dependabot!
The fact that Dependabot's adoption into the GitHub suite of tools is maturing rapidly, its quickly becoming a qualified alternative to tools such as Renovate. With its security integrations being baked into the GitHub interface, and enterprise support coming soon, it's definitely worth giving it a shot if you're just getting started with automated dependency management or are still using Renovate.
A Word of Caution
As with any automation, the lack of manual oversight presents its own concerns. While systems and tools like SemVer aim to enforce a strict definition of dependencies that indicate consumption requirements, its entirely possible that erroneous release versions can occur - eg. it is entirely possible that a minor release version can include breaking changes. By automatically consuming this minor version without modifying our consumption, we risk breaking our application quickly and without warning.
The best way to mitigate this possibility is the same way we mitigate any other potentially destructive change to our applications: with a robust automated testing ecosystem. By automatically ensuring the integrity of our applications on every single change, we're able to confortably allow automation to assume more and more responsibilities so that we may focus on driving value for our business.