Upgrading Rails: The Dual-Boot Way at RailsConf 2023

This is a companion page to the Rails upgrade workshop at RailsConf 2023.
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.

0. Prior to coming to the workshop

Next Step

If you are attending the workshop in person, prior to your arrival, please complete step 1 below (installing the pre-requisites) and step 2 (setting up the outdated Rails app). This will reduce demand on the conference wifi and help ensure we get through the initial setup steps smoothly and efficiently during the workshop.

1. Pre-Requisites

Next Step

These are the requirements for our sample app (versions can be more recent):

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 our sample outdated Rails app

Previous Step Next Step Back to Top

We 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 rails-conf-2023
            

If you don’t already have Docker Desktop open you will need to open it.

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 with Apple Silicon, do the following - note this is only for Mac M1's and M2's. If you're not sure if your Mac is an Apple Silicon, in your Apple menu, open "About This Mac", and check the "Chip" value.

# For Apple Silicon Macs 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 an Apple Silicon 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. Address any current deprecation warnings

Previous Step Next Step Back to Top

During this step we only focus on deprecation warnings for the CURRENT Rails version.

Run the test suite to find deprecation warnings.


$ bundle exec rspec
# When the tests run we will see the following deprecation:

DEPRECATION WARNING: Rendering actions with '.' in the name is deprecated: restrooms/_restroom.html (called from switch_locale at /refugerestrooms/app/controllers/application_controller.rb:17)
            

This deprecation can be resolved by opening this file: app/views/restrooms/show.html.haml and removing ".html" from the partial on line 4:


# In app/views/restrooms/show.html.haml remove ".html" from the restrooms/restroom.html path
= render partial: 'restrooms/restroom', locals: { restroom: @restroom }
            

4. How outdated is our application?

Previous Step Next Step Back to Top

To 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.5: released Jan  1, 1980 (latest version, 6.2.1, released Jan  1, 1980)
high_voltage 3.0.0: released Apr 15, 2016 (latest version, 3.1.2, released May 20, 2019)
…
net-protocol 0.1.2: released Nov 15, 2022 (latest version, 0.2.1, released Dec  8, 2022)
rdoc 6.4.0: released Nov 15, 2022 (latest version, 6.5.0, released Dec  5, 2022)

0 gems are sourced from git
95 of the 163 gems are out-of-date (58%)
            

We now know that the dependencies are 58% out of date. Although outdated gems are not on their newest versions, they are still compatible with Rails 7.

`next_rails` also provides an interesting command that we could run to find known compatibility issues:


$ bundle_report compatibility --rails-version=7.0.4.3

=> Incompatible with Rails 7.0.4.3 (with new versions that are compatible):
These gems will need to be upgraded before upgrading to Rails 7.0.4.3.

activeadmin 2.10.1 - upgrade to 2.13.1

1 gems incompatible with Rails 7.0.4.3
            

Now we know there is one gem that will cause problems. We'll address this in Step 6 below.

5. Dual boot: setup

Previous Step Next Step Back to Top

We 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 do 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
            

6. Dual boot: usage

Previous Step Next Step Back to Top

Now we have all we need to start tweaking our `Gemfile` for the next version of Rails. Replace gem 'rails', '6.1.7' with the following:


if next?
  gem 'rails', '~> 7.0.4.3'
else
  gem 'rails', '6.1.7'
end
            

It's time to `bundle update` 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:
    activeadmin (~> 2.10.1) was resolved to 2.10.1, which depends on
      railties (>= 6.0, < 6.2)

    rails (= 7.0.4.3) was resolved to 7.0.4.3, which depends on
      railties (= 7.0.4.3)
            

Note with some older codebases errors may occur with the `next` command. If you encounter that when using next_rails in another project, you can try this alternative:

$ BUNDLE_GEMFILE=Gemfile.next bundle update rails

As you can see from the output, it looks like there's an issue with `activeadmin` when trying to update rails. To fix that we can use our new `next?` method:


# Gemfile

if next?
  gem ‘activeadmin‘
else
  gem ‘activeadmin', ‘~> 2.10.1’
end
            

And then run:

$ next bundle update rails activeadmin

7. Zeitwerk and class autoloading

Previous Step Next Step Back to Top

A significant recent change in Rails is that Rails 7 requires the use of the Zeitwerk class autoloader, which requires following certain naming conventions for classes. Please see the Classic to Zeitwerk HOWTO Rails Guide for an overview of Rails' class autoloading and the changes that were introduced in Rails 6. The "Classic" autoloader was deprecated but still available in Rails 6 and 6.1. But for upgrading to Rails 7, we have to switch to Zeitwerk.


# Run the task to check Zeitwerk compatibility
$ next bin/rails zeitwerk:check
rails aborted!
NameError: uninitialized constant API

    mount API::Base => '/api'
          ^^^
Did you mean?  Api

# 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
Hold on, I am eager loading the application.
All is good!
            

8. Dual boot: Rails console and tests

Previous Step Next Step Back to Top

Now that we have bundled our project with both versions of Rails, and addressed the Zeitwerk issue, we can start the Rails console in both version of Rails, to make sure we don't have any start up errors.


$ bundle exec rails console
Loading development environment (Rails 6.1.7)
irb(main):001:0>
            

$ next bundle exec rails console
Loading development environment (Rails 7.0.4.3)
irb(main):001:0>
            

And we can run the test suite in both versions 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: Using legacy connection handling is deprecated. Please set
`legacy_connection_handling` to `false` in your application.

The new connection handling does not support `connection_handlers`
getter and setter.

Read more about how to migrate at: https://guides.rubyonrails.org/active_record_multiple_databases.html#migrate-to-the-new-connection-handling
 (called from <top (required)> at /refugerestrooms/config/environment.rb:5)
DEPRECATION WARNING: Non-URL-safe CSRF tokens are deprecated. Use 6.1 defaults or above. (called from <top (required)> at /refugerestrooms/config/environment.rb:5)

Randomized with seed 3565
................................................................

Finished in 1 minute 18.26 seconds (files took 17.22 seconds to load)
64 examples, 0 failures
            

There are no errors, but there are a couple of deprecation warnings. We'll investigate them in the next step.

Although this application doesn't have any test failures, any you find when upgrading your application should be logged as issues to address as part of the upgrade.

A good way to find the root cause of a test failure is 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 6.1 and Rails 7.0, for example.

9. Updating load_defaults

Previous Step Next Step Back to Top

A common source of deprecation warnings is forgetting to update `load_defaults` in `config/application.rb`. You can learn more about `load_defaults` in our What does load_defaults do? blog post.

You'll see in this application it's loading the defaults for Rails 5.0: config.load_defaults 5.0. This was likely an oversight when they upgraded from Rails 5. However, since we are dual booting, we can't just change it to 7.0, as this would break our current Rails 6.1 setup. We can handle this by going back to the `Gemfile` and defining a `$next_rails` global variable:

# 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 7.0
else
  # for a real-life situation, if this happened, you may want to stay at 5.0 at first
  # to avoid changing multiple things at once, which minimizes risk
  config.load_defaults 6.1
end

# Show that the tests still pass for Rails 6.1
$ bundle exec rspec spec

# Show that the tests pass with no deprecation warnings for Rails 7.0
$ next bundle exec rspec spec
            

10. Stay Current

Previous Step Next Step Back to Top

Making sure to address deprecation warnings in your current version of Rails will put you in a good position for upgrading to the next version of Rails. Treating deprecation warnings as exceptions will make sure we don't miss them:


# 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 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. However, if you prefer to not have deprecations cause exceptions, it's important to at least make sure that they are NOT silenced, like this: config.active_support.deprecation = :silence. Please see our Complete Guide for Deprecation Warnings in Rails for more information.

Another great resource is to add the bundler-audit gem as a dependency and make sure to run a check every time you run your test suite. You 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 every time 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'

if next?
  gem 'rails', github: 'https://github.com/rails/rails'
else
  gem 'rails', '~> 7.0.4.3'
end
            

You can find instructions on how to set this up in your CI server in our How to Dual Boot blog post (the post is for an upgrade to 6.0, but this approach works with any version).

At this point you should have the app running on Rails 7.0!

11. Questions?

Previous Step Next Step Back to Top

If you have any questions, you are welcome to reach out to us in the conference Slack or you can also find us online at: @toppa@mastodon.cloud and @fiona4661

Thanks for participating in our workshop! We hope you can apply all of this in your next Rails upgrade project! :)