How to set up a continuous integration pipeline for a Rails dual boot
Developer at Planet Argon
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 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.
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!