Engineering

How to keep dependencies up to date

Have you ever struggled updating some dependency because you missed some major updates? Did you receive security alerts for vulnerable dependencies? Your app keeps crashing because of some library changes that is already hotfixed and released months ago? Even if you have had no such issues yet, you can prevent having them by implementing simple script running automatically.

Background

During our company meetings we share every-day insights related to the projects we work on. At some point, one of backend developers mentioned that "a few days ago we had a huge issue with one of our libraries, and we realized that it was caused by using an old, unsupported version of this library". That made me feel like - Hey! maybe there is a way to prevent such problems? I started thinking about a simple tool that will be running from time to time for all projects during their Continuous Integration process.

How

The most important question I asked myself was - how to find such dependency? Almost all dependencies used in any platform work in a similar way. There is always a dependency manager and a configuration file. And the surprising part is that most dependencies managers have this feature already implemented! The only thing you need to do is run an outdated command. Let's analyze the output for bundler, which is called an exit from dependency hell in Ruby world.

Fetching gem metadata from https://rubygems.org/........
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies.........................................................
Outdated gems included in the bundle:
 * activesupport (newest 5.2.3, installed 4.2.11.1)
 * claide (newest 1.0.3, installed 1.0.2)
 * danger-swiftlint (newest 0.23.0, installed 0.22.0) in groups "default"
 * dotenv (newest 2.7.5, installed 2.7.4)
 * emoji_regex (newest 2.0.0, installed 1.0.1)
 * excon (newest 0.66.0, installed 0.64.0)
 * fastlane (newest 2.128.1, installed 2.127.2) in groups "default"
 * fuzzy_match (newest 2.1.0, installed 2.0.4)
 * google-api-client (newest 0.30.8, installed 0.23.9)
 * google-cloud-storage (newest 1.19.0, installed 1.16.0)
 * googleauth (newest 0.9.0, installed 0.6.7)
 * highline (newest 2.0.2, installed 1.7.10)
 * i18n (newest 1.6.0, installed 0.9.5)
 * jwt (newest 2.2.1, installed 2.1.0)
 * multipart-post (newest 2.1.1, installed 2.0.0)
 * public_suffix (newest 4.0.0, installed 2.0.5)
 * rake (newest 12.3.3, installed 12.3.2)
 * rouge (newest 3.8.0, installed 2.0.7)
 * ruby-macho (newest 2.2.0, installed 1.4.0)
 * tzinfo (newest 2.0.0, installed 1.2.5)
 * xcode-install (newest 2.6.0, installed 2.5.0) in groups "default"
 * xcodeproj (newest 1.12.0, installed 1.11.0)

So it's pretty straightforward, we may even create some regular expressions, such as looking for newest / installed that prevents printing a few first lines of this output. Of course for other dependency managers the output is a little different but it's also very easy to support it.

Implementation

But then I realized that I would have to force everyone to set this process up by adding integration stages, running commands such as bundle outdated, pod outdated ... that we all know will not work. The proper way to do this would be to have a tool that does not require any action from our developers.

So the ideal approach would look like this:

  • run once per X days
  • scan for dependencies configuration files and then, if some of them exist, garther outdated commands outputs together
  • post the output to Slack

Since we use Gitlab CI in our company, running script on each integration is pretty straightforward, but if you do not use self-hosted CI, you can also easily connect via git hooks. The last difficult part was: how to trigger this once every X days? Actually, in our company, we have a few services for our internal use, in this case, it's a very simple Server Side Swift that is being asked during each integration if the project XXX requires check or not and, by default, the check interval is set to 30 days.

So we fire a simple request, like:

curl -v \
   -X POST \
   -H "Content-Type: application/json" \
   -H "x-api-secret: ..." \
   -d '{"project_id": 784, "check_interval": 30 }' \
   "https://appunite-dependencies-service.io/project_info/dependencies"

And get the response with information whether check is required or not:

{"check_required":true}

I've also figured I’d add a few extra variables to personalize this tool a little bit more in a specific project: OUTDATED_SLACK_MENTIONS - to mention specific people with outdated message OUTDATED_CHECK_INTERVAL - to allow custom check period instead of default 30 days OUTDATED_SLACK_HOOK - to change channel / project for messaging outdated dependencies to slack

Result

Below, you can find an example message posted to Slack for iOS project where dependencies managers are Carthage and Cocoapods:

This is a very short article, but I would like to inspire you to do the same in your company, without even presenting my own scripts for parsing the output for outdated commands for specific managers. The gain is huge, the developers’ setup for each individual project is not needed at all so you can be a hero of your company even today! Anyway, if you would like to get some more details, you can ask me on twitter