Upgrading Rails: The Dual-Boot Way at RailsConf 2022
This is a companion page to the Rails upgrade workshop at RailsConf 2022.
Here you will find a series of hands-on exercises to get started.
The main goal for this workshop is to learn a proven technique for upgrading Rails applications. For all exercises you will use our sample application.
1. Pre-Requisites
Next StepThese are the requirements for our sample app:
- - Git (Installation Steps)
- - Docker (Installation Steps)
- - Docker Compose (Installation Steps)
To make sure `git` is installed, please run this in your terminal:
git --version
git version 2.30.1 (Apple Git-130)
To make sure `docker` is installed, please run this in your terminal:
docker --version
Docker version 20.10.8, build 3967b7d
To make sure `docker-compose` is installed, please run this in your terminal:
docker-compose --version
docker-compose version 1.29.2, build 5becea4c
If you don't have Docker, docker-compose, or git installed, please take a moment to install them.
2. Set up Sample Outdated Rails App
Previous Step Next Step Back to TopWe will work with this sample application: Refuge Restrooms -- an open source application that indexes and maps safe restroom locations for trans, intersex, and gender nonconforming individuals.
You can clone the repository with git:
git clone git@github.com:fastruby/refugerestrooms.git
cd refugerestrooms
git checkout start-exercise-1
Next we'll set up the application in your local environment and start a bash session. However, what you need to do depends on the type of computer you have.
If you have a Mac M1 laptop, do the following - note this is only for Mac M1's. If you're not sure if your Mac is an M1, in your Apple menu, open "About This Mac", and check the "Chip" value.
# For Mac M1 only!
docker-compose -f docker-compose.yml -f docker-compose.mac-m1.yml up --no-build
# Then Ctrl-C when it's done and do:
docker-compose -f docker-compose.yml -f docker-compose.mac-m1.yml run web /bin/bash
If you do not have a Mac M1 laptop, do the following:
docker-compose run web /bin/bash
For the rest of the workshop, make sure you are always running the steps inside the docker container's bash.
3. How outdated is our application?
Previous Step Next Step Back to TopTo get an idea of how outdated our application really is we can use the next_rails gem.
group :development, :test do
gem 'next_rails'
# ...
end
Then we will need to install it:
$ bundle install
Let's check it out! It should give us a good idea about the shape of our dependencies:
$ bundle_report outdated
puma 5.6.2: released Jan 1, 1980 (latest version, 5.6.4, released Jan 1, 1980)
formtastic_i18n 0.6.0: released Mar 10, 2016 (latest version, 0.7.0, released May 23, 2021)
…
rails 5.2.6.3: released Mar 8, 2022 (latest version, 7.0.3, released May 9, 2022)
bundler 2.1.4: released Mar 29, 2022 (latest version, 2.3.13, released May 4, 2022)
0 gems are sourced from git
105 of the 163 gems are out-of-date (64%)
We now know that the dependencies are 64% out of date.
`next_rails` also provides an interesting command that we could run to find known compatibility issues:
$ bundle_report compatibility --rails-version=6.0.5
=> Incompatible with Rails 6.0.5 (with new versions that are compatible):
These gems will need to be upgraded before upgrading to Rails 6.0.5.
dotenv-rails 2.2.2 - upgrade to 2.7.6
1 gems incompatible with Rails 6.0.5
Now we know there will be two gems that will cause problems. We can add these gems to our TODO list.
4. Dual Boot: Setup
Previous Step Next Step Back to TopWe are going to use a helper tool for dual booting: `next_rails`.
$ next --init
This command created a symlink called `Gemfile.next` and added a helper method to the top of the `Gemfile`.
Now we can open our `Gemfile` and verify that we have a `next?` method defined in it. In the odd chance it didn't work, we can try doing it manually like this:
$ ln -s Gemfile Gemfile.next
Then we can add this method to the top of your `Gemfile`:
def next?
File.basename(__FILE__) == Gemfile.next
end
5. Dual Boot: Usage
Previous Step Next Step Back to TopNow we have all we need to start tweaking our `Gemfile` for the next version of Rails:
if next?
gem 'rails', '~> 6.0'
else
gem 'rails', '5.2.6.3'
end
It's time to `bundle install` using the next version. We can use the `next` command like this:
$ next bundle update rails
...
Bundler could not find compatible versions for gem "railties":
In Gemfile.next:
dotenv-rails (~> 2.2.1) was resolved to 2.2.2, which depends on
railties (>= 3.2, < 6.0)
rails (~> 6.0.5) was resolved to 6.0.5, which depends on
railties (= 6.0.5)
If that doesn't work, you can try with:
$ BUNDLE_GEMFILE=Gemfile.next bundle update rails
As you can see from the output, looks like there's an issue with the dotenv-rails when trying to update rails. To fix that we can use our new `next?` method:
# Gemfile
if next?
gem ‘dotenv-rails‘
else
gem ‘dotenv-rails', ‘~> 2.2.1’
end
And then run:
$ next bundle update rails dotenv-rails
6. Dual Boot: rails console and tests
Previous Step Next Step Back to TopNow that we have bundled our project with both versions of Rails, we can start the rails console to check if we have any errors.
$ bundle exec rails console
Loading development environment (Rails 5.2.6.3)
irb(main):001:0>
$ next bundle exec rails console
Loading development environment (Rails 6.0.5)
irb(main):001:0>
In the same way we can start running the test suite to identify possible issues.
$ bundle exec rspec
.........................................................
Finished in 58.22 seconds (files took 2.49 seconds to load)
64 examples, 0 failures
$ next bundle exec rspec
DEPRECATION WARNING: Class level methods will no longer inherit scoping from `current` in Rails 6.1. To continue using the scoped relation, pass it into the block directly. To instead access the full set of models, as Rails 6.1 will, use `Restroom.default_scoped`. (called from block in at /refugerestrooms/app/models/restroom.rb:47)
.........................................................
Finished in 35.61 seconds (files took 3.04 seconds to load)
64 examples, 0 failures
Note that the deprecation warning that we see doesn't need to be addressed now, but on the next version jump (Rails 6.0 to 6.1).
Also, this application doesn't have any test failure, but it's worth mentioning that every failure we find should become a new story in our roadmap.
A good way to find the root cause of a test failure would be to check the changes between Rails versions. You can do that in the official guides or our guides.
Another useful resource is RailsDiff, where we can see what changed between Rails 5.2 and Rails 6.0, for example.
See the Bonus section below for more about deprecation warnings.
7. Class Autoloading & Dual Booting
Previous Step Next Step Back to TopPlease see the Classic to Zeitwerk HOWTO Rails Guide for an overview of Rails' class autoloading and the changes introduced in Rails 6.
Let's update the application to use Zeitwerk with Rails 6, while still using the Classic autoloader for Rails 5.2. We'll start with some experimenting.
# In config/application.rb change the config defaults to Rails 6.0:
config.load_defaults 6.0
# Now run the tests for Rails 6
# You should see an error, after the deprecation warning:
$ next bundle exec rspec spec/models/
# Add this to config/application.rb and run the tests again:
config.autoloader = :classic
# The tests should pass again now:
$ next bundle exec rspec spec/models/
# Change the autoloader to Zeitwerk
config.autoloader = :zeitwerk
# Run the task to check Zeitwerk compatibility,
# which will return the same error we saw in the tests:
$ next bin/rails zeitwerk:check
# Let's fix it! Add this to config/initializers/inflections.rb
# For the explanation see:
# https://guides.rubyonrails.org/classic_to_zeitwerk_howto.html#acronyms
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym "API"
end
# The Zeitwerk check should pass now:
$ next bin/rails zeitwerk:check
# Show that the tests pass again for Rails 6:
$ next bundle exec rspec spec/models/
Now we're ready to take what we've learned to set up dual booting for Rails 5.2 and Rails 6.0.
# Let’s define a $next_rails global var in the Gemfile:
def next?
$next_rails = File.basename(__FILE__) == "Gemfile.next"
end
# Update config/application.rb
if $next_rails
config.load_defaults 6.0
config.autoloader = :zeitwerk
else
config.load_defaults 5.2
config.autoloader = :classic # optional line
end
# Show that the tests pass for Rails 5.2
$ bundle exec rspec spec
# Show that the tests pass for Rails 6.0
$ next bundle exec rspec spec
8. Stay Current
Previous Step Next Step Back to TopDeprecation warnings in the latest versions of Rails have been quite helpful in guiding us to the next version of Rails. We should treat all new deprecation warnings as exceptions:
# config/environments/test.rb
Rails.application.configure do
# Raise on deprecation notices
config.active_support.deprecation = :raise
end
That way we can turn _noise_ into many _signals_. Deprecation warnings can easily be ignored when they're just a message in `test.log`. They can't be easily ignored if they break your test suite.
Another great resource is to add bundler-audit gem as a dependency and make sure to run a check every time you run your test suite. We can do this by tweaking your `Rakefile`:
# Rakefile
require File.expand_path('../config/application', __FILE__)
require 'rake'
Rails.application.load_tasks
task default: %i[
bundle:audit brakeman:check
spec spec:javascripts cucumber
]
Now everytime you run `bundle exec rake`, it will not only run your test suite but also run `bundle:audit` which does this:
namespace :bundle do
desc "Audit bundle for any known vulnerabilities"
task :audit do
unless system "bundle-audit check --update"
exit 1
end
end
end
end
Finally you can keep up with the latest version by using the Rails `main` branch:
# Gemfile
source 'https://rubygems.org'
git_source(:github) do |repo_name|
repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
"https://github.com/#{repo_name}.git"
end
if next?
gem 'rails', github: 'rails/rails'
else
gem 'rails', '~> 7.0.3'
end
You can find instructions on how to set this up in your CI server over here: https://fastruby.io/blog/upgrade-rails/dual-boot/dual-boot-with-rails-6-0-beta.html
At this point you should have the sample app running on Rails 6.0 (or even Rails 6.1!).
9. Questions?
Previous Step Next Step Back to Top
If you have any questions, you are welcome to reach out to us in Slack or sending us a message at FastRuby.io.
You can also find us on Twitter at:
@etagwerker,
@mtoppa and
@lubc32
10. Thank you!
Previous Step Next Step Back to TopYou are wonderful! Thanks for participating in our workshop. We hope you can apply all of this in your next Rails upgrade project! :)
11. Bonus: Deprecation Warnings
Previous Step Back to TopTracking deprecation warnings
next_rails has a handy feature to track deprecation warnings when running tests. You can configure it like this:
# rails_helper.rb / spec_helper.rb
RSpec.configure do |config|
# Tracker deprecation messages in each file
if ENV["DEPRECATION_TRACKER"]
DeprecationTracker.track_rspec(
config,
shitlist_path: "spec/support/deprecation_warning.shitlist.json",
mode: ENV["DEPRECATION_TRACKER"],
transform_message: -> (message) { message.gsub("#{Rails.root}/", "") }
)
end
end
And run it like this:
$ DEPRECATION_TRACKER=save next rspec
This will generate a file where you can better see all your deprecation warnings.
Fixing the deprecation warnings
The first deprecation warning you'll see is this one:
DEPRECATION WARNING: Single arity template handlers are deprecated. Template handlers must
now accept two parameters, the view object and the source for the view object.
Change:
>> Coffee::Rails::TemplateHandler.call(template)
To:
>> Coffee::Rails::TemplateHandler.call(template, source)
(called from <top (required)&rt; at /refugerestrooms/config/environment.rb:5)
This can be addressed by updating the coffee-rails
gem (the mention of Coffee::Rails
in the error is a hint to try updating the gem):
if next?
gem 'coffee-rails', '~> 5.0'
else
gem 'coffee-rails', '~> 4.2'
end
And then run next bundle update coffee-rails
The second deprecation warning you'll see is this one:
DEPRECATION WARNING: Class level methods will no longer inherit scoping from `current` in Rails 6.1. To continue using the scoped relation, pass it into the block directly. To instead access the full set of models, as Rails 6.1 will, use `Restroom.default_scoped`. (called from block in <class:Restroom> at /refugerestrooms/app/models/restroom.rb:47)
It tells us the line of code to look at, which take us to:
scope :current, lambda {
Restroom.where('id IN (SELECT MAX(id) FROM restrooms WHERE approved GROUP BY edit_id)')
}
Since this code is in the Restroom
model, there is no need to specify the name of the model here. So the following change will address the warning:
scope :current, lambda {
self.where('id IN (SELECT MAX(id) FROM restrooms WHERE approved GROUP BY edit_id)')
}