A Guide to Update Gems with bundle update

Tips and tricks to master bundle update by following incremental, controlled and safe steps

Gonzalo Rodriguez
Jul 19, 2018 · 6 min read

tl;dr— Synopsis: Updating Gems cheat sheet.

“selective focus photography of faceted red gemstone” by Joshua Fuller on Unsplash

Have you ever been part of a massive Rails app upgrade, with tons of gems not being updated for years?

Was that closer to a joyful experience or a nightmare?

Have you ever felt that you — or the team — could do better at keeping up-to-date with Gemfile‘s gem updates? Was that in part because of an unexisting process around it?

If some of this rings true to you, I think you’ll find what follows helpful.


Step 1 — Fix security vulnerabilities

Before doing any bulk updating of gems, focus on fixing any security vulnerability — if present — in any of your app’s included gems.

To identify them you can use RubySec’s bundler-audit utility:

$ gem install bundler-audit$ bundle audit --update

Say — for an example Rails app — it outputs:

Name: sprockets
Version: 3.7.1
Advisory: CVE-2018-3760
Criticality: Unknown
URL: https://groups.google.com/forum/#!topic/ruby-security-ann/2S9Pwz2i16k
Title: Path Traversal in Sprockets
Solution: upgrade to < 3.0.0, >= 2.12.5, < 4.0.0, >= 3.7.2, >= 4.0.0.beta8
Vulnerabilities found!

If you get “Vulnerabilities found”, you need to find the smallest version update — patch-level only, if possible — that fixes the vulnerability so that feature regressions are less likely.

From the example above, you can infer that the smallest possible version change that makes sprockets secure again is moving from 3.7.1 to 3.7.2 — given that one of the listed solutions is >= 3.7.2 and the current vulnerable version is 3.7.1.

In this case, you may use:

$ bundle update --patch --conservative <vulnerable-gem-name>

to attempt a patch-level only (--patch) update to the Gemfile.lock with as minimal effect on other gems as possible (--conservative).

What you’ll get from doing this very small update as a separate task — and probably separate pull request — is that it would end up having a higher probability of getting merged fast; being less risky, easier to code review, test and deploy to production.

You’ll make a potential vulnerability in your app go away quickly.

What if you mix more updates in one big pull request and it gets temporarily blocked? Maybe a regression in one of the app main user flows — which is being caused by a different gem update — slows down the whole security update. Would you want that?


Updating gems

Is this when we update everything by running $ bundle update and be done with it? Not exactly.

Do you feel you should be equally cautious with every gem update?

How much more “regression risk” do you have when updating rails compared to a “development” gem likeweb-console?

Probably updating rails version is much more risky for your app and your end-users.

Following the same reasoning as with security updates (Step 1), you can benefit from splitting updates here as well — given the fact we’ve identified different risk levels for different gem updates.

So, what’s the split?

A simple one can be “Non-production”–“Production” gems.¹ Less and more risky respectively.


Step 2 — Update “Non-production” gems

$ bundle update --conservative --group test development

Bundler’s --conservative option prevents updates in any “Production” gem that is also a dependency of a“Non-production” gem.

Note: Make sure you’re using bundler v1.16.4 or higher. There was a known issue when mixing --group and --conservative options in previous versions.

Don’t forget safety measures against feature regressions:

  • run automated test suite — i.e. $ bundle exec rake test or similar;
  • test manually — as much as needed.
$ git commit -am "Update non-production gems"

Step 3 — Update “Production” gems

If you are fearless you could use $ bundle update.

Else — a more cautious approach — would be to split bundle update in 3 separate sub-steps, with increasing risk levels.

$ bundle update --patch --strict$ git commit -am "Update gems to latest patch version"
$ bundle update --minor --strict
$ git commit -am "Update gems to latest minor version"
$ bundle update --major
$ git commit -am "Update gems to latest major version"

You may even decide to run only a subset of the above.

It’s up to you if you want to make separate pull requests for these or not. At least, you should have separate commits for each grouped update.

In case of a feature regression caused only by one of those commits, you could revert — or discard — it in isolation, instead of putting yourself in an “all-or-nothing” situation.

You’ll have a better chance of making progress

  • run automated test suite — i.e. $ bundle exec rake test or similar;
  • test manually — as much as needed;
  • Check each updated gem’s CHANGELOG — if available — for potential backward-incompatible changes.

Step 4— What about Gemfile version constraints?

What if your Gemfile has rails constrained to:

gem 'rails', '~> 5.1.6'

and 5.2.0 is already out?

Well, in that case none of the previous commands updated rails version for you.

Of course, that’s expected… and probably why you — or someone else — intentionally set a constraint for rails in the Gemfile in the first place.

Here is when bundler’s built-in command bundle outdated becomes handy.

$ bundle outdated --groupsFetching gem metadata from https://rubygems.org/.........
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Outdated gems included in the bundle:
===== Without group =====
* actioncable (newest 5.2.0, installed 5.1.6)
* actionmailer (newest 5.2.0, installed 5.1.6)
* actionpack (newest 5.2.0, installed 5.1.6)
* actionview (newest 5.2.0, installed 5.1.6)
* activejob (newest 5.2.0, installed 5.1.6)
* activemodel (newest 5.2.0, installed 5.1.6)
* activerecord (newest 5.2.0, installed 5.1.6)
* activesupport (newest 5.2.0, installed 5.1.6)
* arel (newest 9.0.0, installed 8.0.0)
* railties (newest 5.2.0, installed 5.1.6)
* websocket-driver (newest 0.7.0, installed 0.6.5)
===== Group default =====
* pg (newest 1.0.0, installed 0.21.0, requested ~> 0.18)
* rails (newest 5.2.0, installed 5.1.6, requested ~> 5.1.6)

So pg (postgresql adapter) is also constrained (to the 0.x series while 1.0.0 is already out).

This last step of the process is the riskiest.

Depending on your context, and which app you are updating, you may think it’s fine to go ahead and update each — or some —constrained gem by:

  1. Changing version constraint string in Gemfile entry
  2. $ bundle install
  3. Regression verification; including running automated test suite, manual testing and checking gem’s CHANGELOG for backward-incompatible changes
  4. $ git commit -am "Update <gem-name> to <new-version>"

Splitting is your friend here, again.

Someone from the team—maybe even you— using git bisect in the future, trying to find the cause of a feature regression, will be very thankful to notice that the offending commit is updating only one gem instead of many at the same time.

If you prefer to execute this 4th step as separate — more focused — tasks, you can create tickets/user-stories in your project/app backlog, so you — or someone from the team — can pick it up at a later time.


Conclusion

Start small.

Go to your app and try execute “Step 1”:

$ bundle audit --update

Any vulnerable gem needs an update? Yes? Update gem and “New pull request”. Win!

Then try to update “Non-production” gems with

$ bundle update --conservative --group test development

Do that — say, once a week — for a couple of weeks.

Build momentum…

After a couple of weeks you’ll be confident; your update process is running smooth. Team members are starting to like it; you can now slowly start adding the rest of the steps…

  • approaching less risky tasks first;
  • splitting updates in smaller chunks; and
  • running this process as frequently as possible.

Recap

For a short recap and future quick reference see Updating Gems cheat sheet.


[1] You may call them “Development”–“The rest” as well.

Cedarcode

Adding value from day one. Effective software development & expertise for your products and teams.

Gonzalo Rodriguez

Written by

Co-founder & Full-Stack Developer @ cedarcode - It's not about doing as much as you can, it's about doing more of what's important, meaningful and impactful.

Cedarcode

Cedarcode

Adding value from day one. Effective software development & expertise for your products and teams.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade