The dual boot strategy has been a popular upgrade approach since GitHub and Shopify used it to move their Rails apps from older versions of Rails to running on Rails Edge. Shopify demonstrated this strategy in their Rails 5 upgrade, and GitHub similarly explained their strategy while upgrading from Rails 3.2 to 5.2 in 2018.

One of the benefits of a dual boot is that you are able to upgrade incrementally, and your entire development team doesn’t need to stop building new features. A separate, long-lived upgrade branch isn’t required either. Each part of the upgrade can be treated like a small unit of work, based off the master branch. It is isolated from running on your servers unless you tell it to do so.

At Planet Argon, an agency specializing in Rails support, we use this method of updating and would like to share our technique through this tutorial. It will be demonstrated with Bike Index, an open-source Rails application. Bike Index is a bike registration tool that helps reunite missing and/or stolen bikes with their owners. We had two reasons that we were excited to use this service as an example: the company is also based in Portland, and our studio has a handful of bike commuters, myself included.

In this tutorial, you’ll learn how to incorporate a dual boot in your application using the ten_years_rails gem and reconfigure an existing CircleCI config to support dual booted CI runs.

When your application’s automated unit and integration tests cover a large percentage of your codebase, you can measure your upgrade’s progress by the number of passing tests on the upgraded side of the dual boot. Many apps use continuous integration (CI) tools to run their test suite, and this walkthrough will integrate the dual boot process into your CI pipeline to quickly determine whether a new change affected the current build or the upgraded build.

For a high-level overview, here’s a PR with the changes made to get Bike Index’s CI pipeline running a dual boot process.

Prerequisites

To follow along with this tutorial, you will need the following:

  • A Ruby on Rails application with a test suite
  • An existing CircleCI configuration for the application

Let’s get started!

Updating your Gemfile

First, we’ll install the ten_years_rails gem to set up our dual boot. Using this gem requires Ruby 2.3 or above. Add the following to your Gemfile:

gem 'ten_years_rails'

The gem includes a ‘next’ script that allows you to run next before your commands to test them in the upgraded environment. Initialize the gem by running next --init. This will create a symlinked Gemfile named Gemfile.next.

Bundle our gems

Now let’s make sure we can bundle our gems.

ten_years_rails works by alternating between two different Gemfiles: the standard Gemfile that already exists in your application, and the symlinked Gemfile.next. What goes into the Gemfile.next bundle is based on the next? method.

The key gem for our purposes is for upgrading Rails. To specify the version to put into Gemfile.next, write a conditional like this:

if next?
  gem 'rails', '~> 6.0', '>= 6.0.2.1'
else
  gem 'rails', '5.2.4'
end

Any gems installed under the if statement will be installed in the symlinked Gemfile.next, and can be verified by checking the Gemfile.next.lock version. The gems in the else statement will be installed in the Gemfile.lock file.

Then run:

next bundle install

If you run into troubles with the next command, you can also pass the BUNDLE_GEMFILE=Gemfile.next environment variable to specify which gem file should be used in your current context. There may be other dependencies that need to get upgraded, too.

We’re not going to get into this too deeply, but one tip I’ve learned is that there may not be as many gems that need upgrades as you may think. Don’t be intimidated by this bundler output. Look for the gem that has a version that doesn’t match what you’re working on, and create a conditional for that version. Wrap their installation commands in next? conditionals.

An initial test could be to install the gem without a version constraint and let Bundler pick the version for you.

if next?
  gem 'devise'
else
  gem 'devise', '~> 3.5', '>= 3.5.10'
end

Set up your CI pipeline

Once your bundle successfully installs, we’re ready to get the CI pipeline up and running. We could also work on tests, but at this point, it’s not necessary. We just want the pipeline to run on both versions of Rails in two separate builds. It’s okay if RSpec fails miserably for now.

The strategy is to use workflows to add a new job to run the rails next CI using Gemfile.next.

Duplicate your existing build job, and name the duplicate something that indicates it’s for the dual boot. We chose build-rails-next.

In the build-rails-next job, you’ll add a key/value pair under the ENVIRONMENT key for BUDNLE_GEMFILE and specify Gemfile.next.

  "build-rails-next":
    working_directory: ~/bikeindex/bike_index
    parallelism: 2
    environment:
      RAILS_ENV: test
      NODE_ENV: test
      RACK_ENV: test
      BUNDLE_GEMFILE: Gemfile.next

Then, in the commands that save and cache the bundled gems, change the file to Gemfile.next.lock

      - type: cache-restore
        name: "Ruby dependencies: cache restore"
        key: cache-{{ .Environment.CACHE_VERSION }}-gems-{{ checksum "Gemfile.next.lock" }}

      - run:
          name: "Ruby dependencies: install"
          command: |
            set -x
            bundle check --path=~/.cache/bundle && use_local="--local"
            bundle install --deployment --frozen --path=~/.cache/bundle $use_local

      - type: cache-save
        name: "Ruby dependencies: cache save"
        key: cache-{{ .Environment.CACHE_VERSION }}-gems-{{ checksum "Gemfile.next.lock" }}
        paths:
          - ~/.cache/bundle

Finally, call the new build job in the workflows section at the bottom of your config:

workflows:
  version: 2
  commit:
    jobs:
      - build
      - "build-rails-next"

You can test your changes using [CircleCI’s CLI]/docs/2.0/local-cli/) by running circleci local execute. However, if you’re not seeing the new build job, you may need to push your changes to get it to work. Here’s a support article from CircleCI on dealing with this.

I tried using the local execute method to test my changes and ran into errors, so I pushed them up in a new commit and went to CircleCI’s website to check out the changes.

You should see two build jobs on CircleCI, one for your regular build and one for build-rails-next.

To confirm everything is working, inspect the gems installed on each build to make sure they’re the correct ones. In our example, build installs actionview 5.2 and build-rails-next installs actionview 6.

Your Rails 5 build, uninterrupted by the dual boot changes, should beautifully pass, and your Rails 6 build will gloriously fail.

Conclusion

That’s it! Now you get to embark on the Rails app upgrade journey. Keep fixing specs, refactoring code, consulting your CI pipeline, and leveraging the next? method to keep your current code running and incorporate the upgrade code into the process. Rails upgrades can be difficult, but utilizing the dual boot method helps by providing some structure.

Happy upgrading!


Kayla Reopelle is a Developer at Planet Argon. Her favorite parts about web development are refactoring legacy code to make it more maintainable and making coding concepts accessible to non-developers. Outside of work, you might bump into her on a long bike ride on one of Portland’s trails.