Rails Gem Upgrading Tips and Strategies
What are the best-practices for upgrading gems to newer versions? What sort of tips and techniques can save time and headaches?
I built this guide based on my real-world experiences over years of gem migrations, including a recent upgrade to Rails 4.1, RSpec 3.0, and Twitter Bootstrap 3.2. There are some more specific examples of errors you might encounter at this article on the Rails on Maui blog: Specific Issues Upgrading Gems to Rails 4.1, RSpec 3, and Twitter Bootstrap 3.2.
Why Update?
Here's my favorite reasons for keeping gems relatively current:
- If you work on several projects, keeping the gems and ruby version consistent makes your coding more productive as you don't have to keep adjusting for which version is which. Web searches tend to find relatively recent versions first. It's relatively annoying to be yak shaving issues that turn out to be "oh, that doesn't work in that older version of Rails".
- Recent versions of gems will have fixes for bugs and security issues, in addition to new features. With popular open source projects, new bugs are quickly discovered and fixed.
- Updates are much easier if you stay relatively current. I.e., it's much easier to update from Rails 4.0 to Rails 4.1 than to go from Rails 3.0 to Rails 4.1.
That being said, recent versions can have new bugs, so it's best to avoid versions that are unreleased or that haven't aged at least a few weeks.
Some Gems Will Be Way More Difficult to Update
Large libraries, like Rails, RSpec, Twitter Bootstrap, etc. are going to take more elbow grease to update. Typically if a major version number is updating, like Rails 3.x to 4.x and RSpec 2.x to 3.x, that's going to require lots of code changes. Semantic versioning also comes into play. Going from Rails 3.x to Rails 4.x is more difficult than Rails 4.0 to Rails 4.1. There's a similar story with RSpec 2.x to 2.99, compared to going to RSpec 3.0.
Techniques for Smoother Gem Upgrades
Locking Gem Versions
Unless you have a good reason, don't lock a gem to a specific version as that makes updating gems more difficult. In general, consider only locking the major Rails gems, such as rails, RSpec, and bootstrap-sass, as these are the ones that will likely have more involved upgrades.
Don't Upgrade Major Libraries Too Soon
3 Reasons to wait a bit before gem updates:
- Dependencies among gem libraries are not yet resolved. I had
tried upgrading to RSpec 3 and Rails 4.1 a couple months ago, but it
was apparent that I had to fix to many other gems to get them to
work with
RSpec 3
. Thus, I retreated back to RSpec 2.99 for a while. Now, as of August, 2014, the gem ecosystem was ripe to move to RSpec 3.0. So unless you have a good reason, it's best to wait maybe a couple of months after major upgrades are released before migrating. - Bugs may be lurking in changed code. If you wait a bit, the early adopters will find the bugs, saving you time and frustration. The more popular a gem, the faster it will be put to rigorous use.
- *Security*/ problems may have been introduced. This is pretty much a special case of bugs, except that this a possibility of a malicious security change. If you wait a bit, hopefully somebody else will discover the issue first.
Don't Use Guard, Zeus, Spring, Spork, Etc. When Upgrading
Tools that speed up Rails like Zeus and Spring are awesome productivity
enhancers, except when upgrading gems. I found that they sometimes
correctly reloaded new versions of gems. That means massive frustration
when they are not picking up the gems you actually have specified. The
corollary to this is to run your tests using plain rspec
rather than
the recommended ways for speeding up testing, such as the
parallel_tests
gem..
It's not necessary to introduce the added complexity of the test
accelerators when doing major library updates. Once you've updated your
gems, then try out your favorite techniques for speeding up running
tests. I've learned the hard way on this one. The pgr
and pgk
scripts below are awesome for ensuring that pre-loaders are NOT
running.
pgr() {
for x in spring rails phantomjs zeus; do
pgrep -fl $x;
done
}
pgk() {
for x in spring rails phantomjs zeus; do
pkill -fl $x;
done
}
Tests: Try to Keep and Immediately Get Tests Passing
There are a lot of discussions about the value or lack of for an emphasis on Test-Driven Development (TDD). However, one thing that's indisputable is that having a large library of tests is absolutely helpful for upgrading your gems.
Naturally, it's an iterative process to get tests passing when updating gems. First, make sure your tests suite is passing.
You can try updating the gems one by one until you get a test failure. Then the issue becomes one of figuring out which related gems you might want to update to fix the test failure.
If you don't have good tests coverage, a great place to start is with integration tests that do the basics of your app. At least you'll be able to quickly verify a good chunk of your app can at least navigate the "happy path" as you iterate updating your gems.
Alternate Big or Baby Steps
If you've updated gems recently, sometimes you can run bundle update
and everything works great. Recently, that strategy failed miserably
when I tried going from Rails 4.0 with RSpec 2.2 to Rails 4.1 and RSpec
3. An eariler attempt shortly after the releases of Rails 4.1 and RSpec
3 clearly showed that many dependent gems would have to get updated. A
few months later, I still had many issues with trying to update too much
at once.
When this happens, take small steps and kept tests passing. I.e., don't
do a bundle update
without specifying which gems to update. You might
update 60 gems at once! And then when tests fail, you won't be able to
easily decipher which dependency is the problem. Specify which gems to
update by running the command:
bundle update gem1 gem2 etc
Then after updating a few gems, run rspec
and verify your tests pass.
Then commit your changes. Consider putting a summary of how many tests pass and how long it takes. The length of time is useful in case some change greatly increases test run time. Or if you notice run time or the number of tests dramatically decrease. Plus, this ensures you ran the test before committing!
On a related note, you can see which gems are outdated with this
command: bundle outdated
.
Try bundle update
Remember I told you not to do a bundle update
? Once you're getting
closer to finishing your gem updates, all big gems are updated, and all
tests are passing, and deprecation warnings are addressed, then it's
time to run bundle update
and then run rspec
to see if your tests
pass. If you don't have adequate tests, then be ready to do some
adequate manual testing. Even if you have lots of tests, you still need
to do manual testing if you upgrade a UI library such as
sass-bootstrap
. Besides testings, check the bundler output or the diff
of your Gemfile.lock
to see what got updated.
Troubleshooting Gem Upgrades
Read Error Messages Carefully and then search Google and Github Issues
Too often Ruby developers will blindly copy-paste their error messages into a Google search without really reading the console output carefully. This can actually waste more time, since thinking about the problem for a moment can often give you a solution without Google, or you'll write a better search query. If you don't find what you need on Google and you have an idea what gem is causing issues, the next place to search is the issues page for the gem's Github repository.
Remember to do these 2 types of searches rather than spending too much time inserting print statements or launching the debugger! If you don't get any search hits, then typically you have some problem in app customizations (see below).
Visit the Gem Repository on Github
Some essential places to look at when upgrading gems are:
- README.md file (shown on the main page of repository). Some projects might have a NEWS.md or CHANGELOG.md file.
- The Github issues list for a gem (and search here)
- The Github commit history for a gem, sometimes switching branches.
Errors or deprecation messages can come from compatibility issues among your gems. The RSpec 3 upgrade had many such issues. If you're having an upgrade issue, then a concise, detailed post of a new issue typically results in a very quick response.
Try an RC Version on RubyGems
Sometimes the fix you need has already been released to RubyGems in an
RC version (RC means Release Candidate). bundle update <gem>
seems to
not pick RC versions. You have to specify these manually. I search for
gems on RubyGems so often that I created a Chrome search shortcut.
Here's an example of an RC version gem that I'm currently using:
gem 'simple_form', '>= 3.1.0.rc2'
Try a Github Gem Version Rather Than a RubyGems Version
Sometimes what you need has not been shared with RubyGems, yet the issue has received commits on Github. In that case, you can use the Github version of a gem. This might be on a specific branch of a gem, or even another user's fork of a gem.
For example, this image shows that the last version bump of the gon
gem is behind several commits.
If you needed those commits post gem release, here's an example of the syntax to specify the very-latest version of a gem (the tip of the master branch):
gem 'gon', github: "gazay/gon", branch: "master"
Sometimes what you need is something less than the most current version, or a specific branch, or a fork of the gem.
Consider Forking a Gem
Sometimes you need to fork a gem for some changes. If you've never done this, it's a very worthwhile thing to try out, and it's easy! For example, if you had wanted to update to rspec 3 sooner than later and didn't want to see tons of deprecation messages, then your only option was to fork the gems that had the deprecated syntax. Once you've verified the validity of your changes, consider submitting a pull request. Here's an example of a fork and commit of the zeus-paralleltests gem that loosened a gem dependency.
You should typically prefer a rubygems version of a gem rather than a github version. Thus, after some months, you should try to remove any previously necessary github references in your Gemfile.
Order of Gems in your Gemfile Can Matter
I ran into a case where including rspec-instafail before rspec resulted
in zeus failing due to rspec-instafail
failing to recognize that I was
using rspec 3. Simply placing rspec-instafail
after loading rspec
in
the Gemfile fixed that issue.
I had a clue that was the issue due to this stack dump. Note how the
bundler is loading rspec-instafail, and when I looked at the source
code, I could see why file rspec_2.rb
was being loaded (2nd line of
the below stack dump)
zeus test ✹ ✚ ✭ [15:37:26]
/Users/justin/.rvm/gems/ruby-2.1.2@bpos/gems/rspec-core-3.0.3/lib/rspec/core/formatters/progress_formatter.rb:1:in `<top (required)>': uninitialized constant RSpec::Support (NameError)
from /Users/justin/.rvm/gems/ruby-2.1.2@bpos/gems/rspec-instafail-0.2.5/lib/rspec/instafail/rspec_2.rb:1:in `<top (required)>'
from /Users/justin/.rvm/gems/ruby-2.1.2@bpos/gems/rspec-instafail-0.2.5/lib/rspec/instafail.rb:11:in `<module:RSpec>'
from /Users/justin/.rvm/gems/ruby-2.1.2@bpos/gems/rspec-instafail-0.2.5/lib/rspec/instafail.rb:1:in `<top (required)>'
from /Users/justin/.rvm/gems/ruby-2.1.2@global/gems/bundler-1.6.2/lib/bundler/runtime.rb:85:in `require'
from /Users/justin/.rvm/gems/ruby-2.1.2@global/gems/bundler-1.6.2/lib/bundler/runtime.rb:85:in `rescue in block in require'
from /Users/justin/.rvm/gems/ruby-2.1.2@global/gems/bundler-1.6.2/lib/bundler/runtime.rb:68:in `block in require'
from /Users/justin/.rvm/gems/ruby-2.1.2@global/gems/bundler-1.6.2/lib/bundler/runtime.rb:61:in `each'
from /Users/justin/.rvm/gems/ruby-2.1.2@global/gems/bundler-1.6.2/lib/bundler/runtime.rb:61:in `require'
from /Users/justin/.rvm/gems/ruby-2.1.2@global/gems/bundler-1.6.2/lib/bundler.rb:132:in `require'
from /Users/justin/.rvm/gems/ruby-2.1.2@bpos/gems/zeus-0.13.3/lib/zeus/rails.rb:162:in `test_environment'
from /Users/justin/.rvm/gems/ruby-2.1.2@bpos/gems/zeus-0.13.3/lib/zeus.rb:166:in `run_action'
...
from -e:1:in `<main>'
Evaluate Customizations
In general, when doing relatively major gem upgrades, you really need to evaluate customizations to these places. Typically, deprecation messages will tell you which customizations to remove or alter. Sometimes, you've monkey patched some gem to work around some issue, and this would be the place where you'd do that (and forget that you did it!).
- Any initializers in the
config/initializers
directory. Review each file there. - Any customizations in your environment files in the
config/environments
directory, such astest.rb
,development.rb
. - Any customizations for running specs: a.
spec/spec_helper.rb
b. Each file in thespec/support
directory.
Example of Next Steps when Upgrading a Gem
Here's an example of where updating related gems help.
bundle update capybara
fixed the following error
--------------------------------------------------------------------------------
Capybara::RSpecMatchers::HaveText implements a legacy RSpec matcher
protocol. For the current protocol you should expose the failure messages
via the `failure_message` and `failure_message_when_negated` methods.
--------------------------------------------------------------------------------
The final error I got was this one, from cancan
.
Deprecation Warnings:
`failure_message_for_should_not` is deprecated. Use `failure_message_when_negated` instead. Called from /Users/justin/.rvm/gems/ruby-2.1.2@bpos/gems/cancan-1.6.10/lib/cancan/matchers.rb:11:in `block in <top (required)>'.
`failure_message_for_should` is deprecated. Use `failure_message` instead. Called from /Users/justin/.rvm/gems/ruby-2.1.2@bpos/gems/cancan-1.6.10/lib/cancan/matchers.rb:7:in `block in <top (required)>'.
A quick google search reveals that cancancan
fixes the issue:
Once I got all tests passing, I tried to update to Rails 4.1, but ran into this issue:
bundle update rails ✹ ✭ [20:31:38]
Fetching source index from https://rubygems.org/
Resolving dependencies........................
Bundler could not find compatible versions for gem "activemodel":
In Gemfile:
simple_form (>= 0) ruby depends on
activemodel (< 4.1, >= 4.0.0) ruby
rails (~> 4.1) ruby depends on
activemodel (4.1.0)
I verify I'm on the current maximum GA version of simpleform, but I find that there's an RC version, so I specify that in the gemfile. It's important to note that "bundle update" will tend not to pull in RC versions of gems, which you sometimes need after major libraries are upgraded.
In Gemfile
gem 'rails', '~> 4.1'
gem 'simple_form', '>= 3.1.0.rc2'
> bundle update rails simple_form
Using rails 4.1.4 (was 4.0.8)
Installing simple_form 3.1.0.rc2 (was 3.0.1)
Your bundle is updated!
After the 4.1 upgrade, I addressed a number of deprecation warnings.
DEPRECATION WARNING: Implicit join references were removed with Rails 4.1.Make sure to remove this configuration because it does nothing. (called from block in tsort_each at /Users/justin/.rvm/rubies/ruby-2.1.2/lib/ruby/2.1.0/tsort.rb:226)
config.active_record.disable_implicit_join_references = true
Then I got this warning with a full stack dump.
Warning: you should require 'minitest/autorun' instead.
Warning: or add 'gem "minitest"' before 'require "minitest/autorun"'
From:
/Users/justin/.rvm/gems/ruby-2.1.2@bpos/gems/activesupport-4.1.4/lib/active_support/dependencies.rb:247:in `require'
The stack dump was useless, but the search for error message on Google
found this
indicating that the issue had something to do with shoulda-matchers
. A
check of my gem version revealed that my gem version was not current.
> bundle update shoulda-matchers
Installing shoulda-matchers 2.6.2 (was 2.5.0)
And that fixed that issue!
Thanks to Mike Perham, Ed Roman, Ben Ward, and Greg Lazarev for reviewing drafts of this article.
Please let me know if this article helped you or if I missed anything!
Aloha,
Justin