Recently, we went through an upgrade of all ruby dependencies in our Ruby on Rails project to their latest versions, including Rails itself. Regarding the Rails upgrade from 4.2 to 6.0, I already shared a few tweets but I also wanted to somehow visualize the gist of all these mostly technical and invisible changes in a simple chart that I could just show to the team.
Side note: we did this upgrade after about four years in which we prioritized deploying new features and onboarding new colleagues over maintenance so it was quite an effort. Drawing a lesson from that, we now have employed the lovely Dependabot service to help us with frequent, rolling updates so that we don’t have to deal with such legacy dependencies ever again!
In essence, upgrade means replacing some old code with a newer version but software versions tend to be a very abstract metric − one can’t possibly tell if an upgrade of a library from, say, version 3.2 to 5.1 is an important, long-awaited change or just some relatively minor update (and even the Semantic versioning system can’t help much here). Some libraries, such as the
client_side_validations gem, tend to release a new major version every few months whereas it would take many years for other ones to do the same.
If tracking dependency versions is a no-go, how about their age? Age seems to be a very simple metric that is useful in many ways − we can make a distribution of the age of dependencies in our project; we can easily show the outliers − very old (and possibly unmaintained) pieces of code; and we can simply visualize the progress, the renewal of our dependencies by charting their age before and after the upgrade.
Of course, age doesn’t tell us much about the upgrade effort nor about the risk of inviting new bugs but it’s better than nothing and it’s so simple that it will be understood by everybody.
With the help of bundler and the rubygems repository, it’s actually very easy to get the gems age. Bundler will tell us which gems we use in our project and their concrete versions. The Rubygems API is able to tell us info about that specific gem versions, including the date when they were built.
I made a ruby script
ruby_gems_age.rb that collects all this info and prints it in two levels of detail: a detailed list of all gems, their versions and age first and the yearly distribution of the gems last. The output typically looks like this (and should be easy to import as a CSV into your favorite spreadsheet or charting program):
$ ruby ruby_gems_age.rb actioncable,6.0.0.rc1,2019-04-24 actionmailbox,6.0.0.rc1,2019-04-24 actionmailer,6.0.0.rc1,2019-04-24 actionpack,6.0.0.rc1,2019-04-24 ... webpacker,3.5.5,2018-07-09 websocket-driver,0.7.1,2019-06-10 websocket-extensions,0.1.4,2019-06-10 will_paginate,3.1.7,2019-03-18 xpath,3.2.0,2018-10-15 zeitwerk,2.1.9,2019-07-16 Yearly summary: 2014,8,acts-as-dag ... 2015,6,coffee-script ... ... 2019,115,actioncable ...
Note that the script prints info about all gems in your project, not only those explicitly listed in your
Finally, to visualize an upgrade in your code base, you can simply check out the master branch from before and after the upgrade, then in each of the two revisions run
bundle to install the correct versions of all gems and run the
ruby_gems_age.rb script to get the age data. From all this you can now make two charts, similar to the following from our own project:
before the upgrade:
and after the upgrade:
The graphs make it immediately apparent that:
- most of the ”a few years old“ gems were successfully updated to their current / recent versions
- there are some very old gems still present in our code base - these are legacy dependencies that not easy to replace and are either very stable or not maintained any more (we even adopted some of them into our own clones and modified them for our needs)
- in between there are gems that are stable and OK to use but may get too stale for our needs in the future
- overall the curve seems ”healthy“ after the upgrade and should largely stay like this thanks to the new rolling updates.
You might say that it is too easy and a bit dull to visualize an upgrade like this after all the work is already done. But similar charts could be made to estimate the possible outcome from an upgrade even before it starts. You could make a chart of the current state of your project dependencies, then determine the newest versions of all your gems using the Rubygems API and make another chart (of the desired upgraded state) to compare and show to your manager the next time you'll discuss the necessity of upgrading your project. Obviously we didn’t do that but it shouldn’t be too hard to implement.
Update: actually, as kindly pointed out in the comments, a very similar thing has already been implemented, check out the libyear project!
So, what would your dependency age charts look like?