Dual Boot with Rails'

Dual Boot with Rails' "main" Branch

You just finished upgrading your app to the latest Rails version and you made the decision to never stay behind and always be ready to upgrade as soon as possible… But how can you do that?

In this article we’ll explain how the Dual-Boot technique opens a new window can be used to test the app against Rails’ main branch opens a new window , to catch problems and warnings before a new version is released.

Dual Boot with main

The Dual Boot technique consists on using different sets of gems on demand: the set of gems that will be used by Bundler opens a new window by default (in this case using the current version of Rails), and an alternative set of gems that we can tell Bundler to use (in this case using the next version of Rails).

Usually, we dual boot between 2 releases of Rails:

def next?
  File.basename(__FILE__) == "Gemfile.next"
end

if next?
  gem "rails", "~> 7.1.0"
else
  gem "rails", "~> 7.0.0"
end

But we can also dual boot pointing to a Git source (GitHub in this case) and a specific branch:

def next?
  File.basename(__FILE__) == "Gemfile.next"
end

if next?
  gem "rails", github: "rails/rails", branch: "main"
else
  gem "rails", "~> 7.1.0"
end

Now, with this setup, the Gemfile.lock file will list the Rails gems in the GEM section:

GEM
  remote: https://rubygems.org/
  specs:
    actioncable (7.1.2)
    ...

And the Gemfile.next.lock file will list the gem in a GIT section:

GIT
  remote: https://github.com/rails/rails.git
  revision: 82e33e44bf3d8bcdc2a4ea40b6039472f0e2b297
  branch: main
  spec:
    actioncable (7.2.0.alpha)

Test Against the Latest Commit

With the new setup, we can run the application using the Rails source at the commit specified in the revision metadata. This is fine but it’s a hardcoded commit hash… we want to keep that updated to test against the latest commit because the Rails codebase changes very quickly.

Update rails Locally

To update the lock file, we have to run BUNDLE_GEMFILE=Gemfile.next bundle update rails before running the tests. This command will fetch the code from the latest commit of the Rails repository.

Then we can commit the updated Gemfile.next.lock and run the test suite as usual.

One alternative to not having to remember that manual step is to create a script that first updates the rails gem and then runs the tests.

Update rails on CI

Having to remember to update the lock file locally every time for all PRs is not sustainable and can lead to different branches conflicting with changes in the same line of the lockfile pointing to different commits.

To avoid that, another option we have is to update rails in CI.

In our CI configuration, before the step that runs our test in the next version of Rails, we can add a call to bundle update rails.

  env:
    BUNDLE_GEMFILE: Gemfile.next
  ...
  steps:
    ...
    - name: Update Rails main
      run: bundle update rails
    - name: Run tests
      run: rails test

This will ensure that, whenever we push code, it is tested against the latest commit at that time.

Note that this can make your CI runs more brittle: if a new change in Rails makes one of your dependencies incompatible, you may not be able to solve the issue until the dependency is updated. Your CI/CD setup should not be blocked by failures running the tests in the next version of Rails.

With this setup, we avoid having to update to the latest commit in every branch, but it’s still a good idea to make new PRs updating the revision in the repository, so all engineers get regular updates.

Update rails with an Automation

The third approach we can take is to have an automation (a GitHub action, for example) that updates our repository without our intervention. It could be executed after every merge into the main branch, after a set number of days, or any criteria that makes sense for the project.

In this case, the rails gem won’t always be at the latest commit for every PR and nobody has to remember to update it manually, but it will be updated.

If you want to be even safer, you can make the automation create a PR that updates the rails gem instead of commiting directly to the main branch of the repo, giving you time to fix tests and merge it only when ready. Then, if an update of Rails introduces breaking changes, it will be isolated in a PR and won’t break the tests running in the main branch.

Conclusion

Keeping up to date with Rails’ main branch is easy to set up but can be hard to maintain because the Rails codebase changes very quickly, and in some cases some changes can be reverted. We wouldn’t recommend this approach in all cases because of this difficulty.

The benefit of this is that, by the time a new Rails version is released, our code will be already tested against it, and we can upgrade with fewer issues. Another benefit of this approach is that it allows us to encounter the problems one by one as they appear, since the scope of changes that we have to analyze to find a solution is reduced, instead of having to deal with all the changes done between releases (and problems) at the same time.

Are you running an outdated version of Rails and need to get up to date? Let us do the heavy lifting for you! opens a new window

Get the book