A Guide to Upgrading Gems and Dependencies in Ruby
Managing Your Gemfile
In the 18th century, the British Royal Navy faced a significant problem: shipworms. These marine bivalves would burrow into the wooden hulls of ships, weakening the timber until the vessels were barely seaworthy. The solution, eventually, was copper sheathing — a preventative measure that required regular maintenance and replacement, but prevented catastrophic structural failures at sea.
Similarly, development teams often treat dependency updates as a chore to be deferred until a major Ruby or Rails upgrade forces the issue. Software dependencies, though not literally eaten by worms, suffer from a similar rot over time. Security vulnerabilities are discovered, performance bugs linger, and the gap between your application’s code and the modern ecosystem widens. This approach transforms a routine maintenance task into a complex, high-risk project. A neglected Gemfile can make your application unstable and severely hinder future modernization efforts.
In this guide, we will look at practical approaches for upgrading Ruby gems that prioritize long-term maintainability over quick fixes.
Understanding Available Updates
Before we get into modifying our application’s state, we should understand what updates are actually available. Bundler provides a command specifically for this purpose: bundle outdated.
Usage
bundle outdated [GEM ...] [options]
In its most basic form, we can run the command without any arguments:
$ bundle outdated
This will output a list of all gems in your Gemfile.lock that have newer versions available in the remote repository. It’s worth noting that bundle outdated is a read-only operation; it contacts the gem sources to check for updates but does not modify your Gemfile.lock or install any new code. We can verify that this is a read-only process by checking our repository state:
$ git status
On branch main
nothing to commit, working tree clean
As we can see, running bundle outdated does not trigger any changes to our files.
We can refine this output using the --strict flag:
$ bundle outdated --strict
The --strict flag restricts the output to gems that can be updated within the version constraints already defined in your Gemfile. You might see output resembling this:
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...
Outdated gems included in the bundle:
* devise (newest 4.9.2, installed 4.8.1, requested ~> 4.8) in groups "default"
* pg (newest 1.5.3, installed 1.4.6, requested ~> 1.4) in groups "default"
You also may notice that Bundler shows not only the newest and currently installed versions, but also the version requested in your Gemfile. This is significant because it helps you identify which updates are safely within your defined constraints.
By removing the --strict flag, Bundler will display all available updates, including major version bumps that would require modifying the version requirements directly in your Gemfile.
The Incremental Update Approach
Before modifying dependencies, it is wise to ensure your tests pass and that the latest “known good” version of your Gemfile.lock is committed to source control. This provides a safe rollback point if an update introduces a regression.
When faced with a long list of outdated gems, one may wonder: why not update them all at once?
The temptation to run bundle update without any arguments is strong. That command, however, updates everything in your Gemfile.lock to the latest allowed versions simultaneously. The immediate effect is a fully updated Gemfile.lock; the broader consequence, though, is that if your test suite fails after a global update, determining which specific gem update introduced the regression becomes a tedious exercise in isolation.
Instead, we should update gems individually or in tightly related logical groups.
Usage
bundle update [GEM ...] [options]
We can update a single gem like this:
$ bundle update devise
Or, for related infrastructure gems, we can pass multiple names:
$ bundle update pg redis sidekiq
This incremental approach provides a clear path to identifying the source of any issues.
Reviewing Changes
For major gem updates, you should always review the maintainer’s CHANGELOG.md or release notes on GitHub. Strictly speaking, semantic versioning suggests that breaking changes only occur in major version bumps; in practice, however, unexpected behavior can sometimes slip into minor releases. Look specifically for notes detailing “Breaking Changes” or deprecations.
Verification and Commit Strategy
After each individual gem update, we can inspect the changes to understand exactly what Bundler did. Let’s look at the difference in our lockfile:
$ git diff Gemfile.lock
diff --git a/Gemfile.lock b/Gemfile.lock
index e3b0c44..b08c690 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -40,7 +40,7 @@ GEM
orm_adapter (~> 0.1)
railties (>= 4.1.0)
responders
- warden (~> 1.2.3)
+ warden (~> 1.2.9)
@@ -102,7 +102,7 @@ DEPENDENCIES
- devise (~> 4.8.1)
+ devise (~> 4.9.2)
You also may notice that not only did the devise version change, but Bundler also updated warden, which is a dependency of devise. This illustrates an important point: when you update a gem, its dependencies may also be updated if the new version requires it.
Next, verify the changes using your project’s automated test suite:
$ bundle exec rspec
If the tests pass, commit the updated Gemfile.lock immediately with a descriptive message:
$ git add Gemfile.lock
$ git commit -m "Update devise to 4.9.2"
This ensures that if a regression is discovered later, you have a straightforward path to revert the exact dependency change that caused it.
Dealing with Dependency Conflicts
When upgrading a gem, you may eventually encounter a resolution conflict. This typically happens because another gem in your bundle depends on an older version of the gem you are trying to update.
By default, when you ask Bundler to update a specific gem, it will also attempt to update that gem’s dependencies. To restrict this behavior and mitigate conflicts, we can use the --conservative flag:
$ bundle update devise --conservative
This tells Bundler to aggressively prefer the versions already in your Gemfile.lock for all other gems, only updating the requested gem and leaving its dependencies at their current versions if those versions satisfy the new requirements.
If Bundler still cannot resolve the dependencies, you will need to examine your Gemfile.lock to trace the dependency tree and determine which gem is holding back your desired update. For example, if you attempt to update rails but have an older version of rspec-rails that strictly requires an older Rails version, Bundler will refuse to proceed. The solution, in that case, is to update both gems simultaneously:
$ bundle update rails rspec-rails
Automation Options
There are two major approaches to managing gem updates: manual maintenance and automated dependency updates. Depending on your team size and testing infrastructure, one approach may be more useful than the other.
The first approach is manual updating, which we have discussed so far. This provides the most control and forces developers to actively review the changelogs of the dependencies they are upgrading. This is most useful, in my experience, for smaller projects or for major framework upgrades.
The second approach is utilizing automated tools like Dependabot or Renovate. These tools automatically scan your Gemfile.lock and open Pull Requests when new gem versions are released. This allows your team to review and merge updates continuously — incorporating the testing and verification steps we discussed above — rather than waiting for technical debt to accumulate.
Generally speaking, the automated approach is my preferred method for long-term maintenance of production applications, provided your test suite is comprehensive enough to catch regressions automatically. Of course, automation does not eliminate the need for review; it merely shifts the burden from discovering updates to evaluating them.
Sponsored by Durable Programming
Need help maintaining or upgrading your Ruby on Rails application? Durable Programming specializes in keeping Rails apps secure, performant, and up-to-date.
Hire Durable Programming