Gems & Dependencies Upgrade Guide
Learn how to manage Gemfile updates safely.
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.
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 simplest 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 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 [GEMNAME] [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, 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.
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 preferable 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.