How and Why to Measure Dependency Freshness in your Ruby Application

How and Why to Measure Dependency Freshness in your Ruby Application

Whether you are working in a legacy Ruby application, or a brand new application, measuring your dependency freshness score can be a positive indicator to understand whether you are staying current or gradually falling out of date.

Dependency freshness is defined as the difference between the currently used version of a dependency and the version that the system would ideally use.

In this article, I will discuss a couple of ways to keep track of how outdated or how fresh your dependencies really are.

Context

One of the core services that we offer our clients is a slow and steady approach to pay off their technical debt opens a new window .

Tech debt opens a new window sometimes shows up as a set of dependencies that haven’t been upgraded in years. Other times it shows up as a set of really complicated files, which are constantly changing and lacking a stable test suite.

As consultants, we want to constantly show the value that we provide, so showing the progress we have made upgrading outdated dependencies is a really important responsibility in our day to day.

How to Measure Dependency Freshness

We have considered two different ways of assessing the progress we make upgrading dependencies:

  • Measuring what percentage of your dependencies are out of date
  • Measuring how many years behind you are in terms of dependencies (Libyears opens a new window )

For this article, I will be using one of our open source projects, Skunk. Skunk opens a new window is a command line tool that helps you detect which files are constantly changing, really complicated, and lack tests opens a new window .

I will upgrade its minitest development dependency from 5.8.5 to 5.20.0. Then I will show what progress looks like using the two different approaches.

Measuring Out-of-date Percentage

For this section, I’m going to use another open source tool that we maintain: next_rails opens a new window

Despite its name, it works with Ruby and Rails projects. Basically any project that has a Gemfile.

When I run the bundler_report outdated command this is what I get:

$ bundle_report outdated
    minitest 5.8.5: released Sep 26, 2016 (latest version, 5.20.0, released Sep  6, 2023)
    simplecov-console 0.5.0: released May 24, 2019 (latest version, 0.9.1, released Feb  1, 2021)
    codecov 0.1.21: released Aug 26, 2020 (latest version, 0.6.0, released Aug 18, 2021)
    webmock 3.10.0: released Nov 12, 2020 (latest version, 3.19.1, released Aug 29, 2023)
    vcr 6.1.0: released Mar 13, 2022 (latest version, 6.2.0, released Jun 26, 2023)
    bundler 2.4.7: released Feb 15, 2023 (latest version, 2.4.22, released Nov  9, 2023)
    regexp_parser 2.8.2: released Oct 10, 2023 (latest version, 2.8.3, released Dec  4, 2023)
    rdoc 6.6.0: released Nov  6, 2023 (latest version, 6.6.1, released Dec  5, 2023)
    irb 1.9.1: released Nov 21, 2023 (latest version, 1.10.1, released Dec  5, 2023)
    json 2.7.0: released Dec  1, 2023 (latest version, 2.7.1, released Dec  5, 2023)

  0 gems are sourced from git
  10 of the 60 gems are out-of-date (17%)

In other words, my project’s dependencies are 83% up to date (or fresh).

Why Not bundle outdated?

Bundler includes a command called outdated which lists your dependencies in a concise table showing your application’s current version and the latest version available in Rubygems.

I love bundler and I’m thankful to its maintainers and contributors, but the output of bundle outdated is not that useful for quickly understanding the state of dependencies in a project. This is what it looks like:

$ bundle outdated
Fetching gem metadata from https://rubygems.org/........
Resolving dependencies...

Gem                Current  Latest  Requested  Groups
codecov            0.1.21   0.6.0   ~> 0.1.16  development
irb                1.9.1    1.10.1
json               2.7.0    2.7.1
minitest           5.8.5    5.20.0  ~> 5.8.4   development
rdoc               6.6.0    6.6.1
regexp_parser      2.8.2    2.8.3
simplecov-console  0.5.0    0.9.1   = 0.5.0    development
vcr                6.1.0    6.2.0   ~> 6.1.0   default
webmock            3.10.0   3.19.1  ~> 3.10.0  development

Sure, one by one you can go ahead and compare current and latest versions per dependency, but I usually work with legacy Rails applications that have 300+ dependencies, so it is quite time consuming to get insights from the output.

Measuring Libyears: How Many Years Behind are my Dependencies?

Libyears opens a new window is a measure of the age of your dependencies. The age is calculated by the current version of your application and release date, the latest version of the dependency and release date, and the delta between those two release dates.

Libyear can be used with any programming language even Python

(Source: github.com/nasirhjafri/libyear opens a new window )

For example: If I’m using Minitest v5.8.5 in my project and the latest stable release is v5.20.0, what’s the age of that dependency? It’s the number of days between the release of v5.8.5 and v5.20.0.

If you look at the release dates, then it turns out that my minitest dependency is 6.9 years old (2023-09-06 minus 2016-09-26)

If you do that for every single dependency in your Gemfile, you get a total amount that represents the libyears of your dependencies.

Fortunately Jared Beck created and maintains an open source tool called libyear-bundler which you can use to get this number in a few seconds: https://github.com/jaredbeck/libyear-bundler opens a new window

$ libyear-bundler Gemfile
                       codecov         0.1.21     2020-08-26          0.6.0     2021-08-18       1.0
                           irb          1.9.1     2023-11-21         1.10.1     2023-12-05       0.0
                          json          2.7.0     2023-12-01          2.7.1     2023-12-05       0.0
                      minitest          5.8.5     2016-09-26         5.20.0     2023-09-06       6.9
                          rdoc          6.6.0     2023-11-06          6.6.1     2023-12-05       0.1
                 regexp_parser          2.8.2     2023-10-10          2.8.3     2023-12-04       0.2
             simplecov-console          0.5.0     2019-05-24          0.9.1     2021-02-01       1.7
                           vcr          6.1.0     2022-03-13          6.2.0     2023-06-26       1.3
                       webmock         3.10.0     2020-11-12         3.19.1     2023-08-29       2.8
                          ruby          3.1.3                         3.2.2                      0.0
System is 14.0 libyears behind

This doesn’t mean that my project is 14 years behind. I’ve been updating it over the years, but I haven’t updated some of its dependencies and I’m missing the opportunity to upgrade them.

If you don’t care for this new metric, libyear-bundler also provides a summary of the releases and versions and how behind your project is:

$ libyear-bundler --all
                       codecov         0.1.21     2020-08-26          0.6.0     2021-08-18        25      [0, 5, 0]       1.0
                           irb          1.9.1     2023-11-21         1.10.1     2023-12-05         2      [0, 1, 0]       0.0
                          json          2.7.0     2023-12-01          2.7.1     2023-12-05         2      [0, 0, 1]       0.0
                      minitest          5.8.5     2016-09-26         5.20.0     2023-09-06        30     [0, 12, 0]       6.9
                          rdoc          6.6.0     2023-11-06          6.6.1     2023-12-05         1      [0, 0, 1]       0.1
                 regexp_parser          2.8.2     2023-10-10          2.8.3     2023-12-04         1      [0, 0, 1]       0.2
             simplecov-console          0.5.0     2019-05-24          0.9.1     2021-02-01         7      [0, 4, 0]       1.7
                           vcr          6.1.0     2022-03-13          6.2.0     2023-06-26         1      [0, 1, 0]       1.3
                       webmock         3.10.0     2020-11-12         3.19.1     2023-08-29        19      [0, 9, 0]       2.8
                          ruby          3.1.3                         3.2.2                        4      [0, 1, 0]       0.0
System is 14.0 libyears behind
Total releases behind: 92
Major, minor, patch versions behind: 0, 33, 3

Showing Progress

Now I’m going to go ahead and upgrade the dependency that is most out of date: minitest

Sometimes doing this can be as easy as bumping the version in my gemspec:

   spec.add_development_dependency "debug"
-  spec.add_development_dependency "minitest", "~> 5.8.4"
+  spec.add_development_dependency "minitest", "~> 5.20.0"
   spec.add_development_dependency "minitest-around", "~> 0.5.0"

After doing that, I can run my test suite and make sure that everything works. CI reports that everything works so I can ship this change to production. 🚀

Luckily I didn’t have to change anything in my test suite. Everything just works with Minitest v5.20.

Now that I have shipped that upgrade: How can I show the progress I’ve made to a non-technical stakeholder?

If we consider the two different tools, we can summarize it as making your dependencies X% more up to date.

Using Out-of-date Percentage

I can use next_rails’s bundle_report command. Here is the output after the upgrade:

$ bundle_report outdated
    simplecov-console 0.5.0: released May 24, 2019 (latest version, 0.9.1, released Feb  1, 2021)
    codecov 0.1.21: released Aug 26, 2020 (latest version, 0.6.0, released Aug 18, 2021)
    webmock 3.10.0: released Nov 12, 2020 (latest version, 3.19.1, released Aug 29, 2023)
    vcr 6.1.0: released Mar 13, 2022 (latest version, 6.2.0, released Jun 26, 2023)
    bundler 2.4.7: released Feb 15, 2023 (latest version, 2.4.22, released Nov  9, 2023)
    regexp_parser 2.8.2: released Oct 10, 2023 (latest version, 2.8.3, released Dec  4, 2023)
    rdoc 6.6.0: released Nov  6, 2023 (latest version, 6.6.1, released Dec  5, 2023)
    irb 1.9.1: released Nov 21, 2023 (latest version, 1.10.1, released Dec  5, 2023)
    json 2.7.0: released Dec  1, 2023 (latest version, 2.7.1, released Dec  5, 2023)

  0 gems are sourced from git
  9 of the 60 gems are out-of-date (15%)

Now we could say that the project’s dependencies are 2% more up to date. My pull request took the out-of-date percentage from 17% to 15%. A smaller number is definitely better!

Using Libyears

Here is the output after the upgrade:

$ libyear-bundler --all
                       codecov         0.1.21     2020-08-26          0.6.0     2021-08-18        25      [0, 5, 0]       1.0
                           irb          1.9.1     2023-11-21         1.10.1     2023-12-05         2      [0, 1, 0]       0.0
                          json          2.7.0     2023-12-01          2.7.1     2023-12-05         2      [0, 0, 1]       0.0
                          rdoc          6.6.0     2023-11-06          6.6.1     2023-12-05         1      [0, 0, 1]       0.1
                 regexp_parser          2.8.2     2023-10-10          2.8.3     2023-12-04         1      [0, 0, 1]       0.2
             simplecov-console          0.5.0     2019-05-24          0.9.1     2021-02-01         7      [0, 4, 0]       1.7
                           vcr          6.1.0     2022-03-13          6.2.0     2023-06-26         1      [0, 1, 0]       1.3
                       webmock         3.10.0     2020-11-12         3.19.1     2023-08-29        19      [0, 9, 0]       2.8
                          ruby          3.1.3                         3.2.2                        4      [0, 1, 0]       0.0
System is 7.0 libyears behind
Total releases behind: 62
Major, minor, patch versions behind: 0, 21, 3

Now we could say that the project’s dependencies are 50% better. My pull request took the libyears metric from 14 to 7 years. That’s 7 libyears closer to up to date!

Why should I care?

While automated dependency management tools like Dependabot opens a new window can help, they won’t do much for major or minor releases without a concerted effort by your engineering team to constantly upgrade dependencies.

Systems using outdated dependencies are four times as likely to have security issues compared to systems that are up-to-date. There is a correlation between dependency freshness and dependencies with a reported vulnerability:

Correlation between Dependency Freshness and Vulnerabilities in Dependencies

(Source: Measuring Dependency Freshness in Software Systems opens a new window )

Conclusion

Both metrics can be useful for assessing the state of your dependencies and communicating progress. Personally, I like the additional information provided by libyears more than the out-of-date percentage, but I also see the value in combining both metrics to truly understand the state of my dependencies.

Having a libyears value of 0 years and having a set of dependencies that is 100% up-to-date does not guarantee that your dependencies are free of vulnerabilities. It does guarantee that all known vulnerabilities and CVEs in your dependencies have been addressed, which takes you one step closer to running a secure set of dependencies in production.

If you are interested in other ways to secure your application, you should read this article: 4 Essential Security Tools To Level Up Your Rails Security opens a new window

How does your current project do in terms of libyears and out-of-date percentage? Do you keep track of these metrics? I strongly recommend it!

Interested in paying off 1% of your tech debt every month? Let’s talk! opens a new window

Get the book