Rails Dual-boot + Dependabot = 💔?

davidwessman

David Wessman

Posted on December 13, 2020

Rails Dual-boot + Dependabot = 💔?

There are two concepts which greatly affected how I work with Rails development during the last couple of years, specifically with dependency management and preparing for upgrading to the next Rails version.

The first one is Dependabot which opens pull-requests to update one dependency at a time. This way of working felt natural at once and allowed me to be on top of dependency updates instead of being scared of it 👻.

The second one is dual-booting my Rails applications using separate lock-files, I was introduced to this concept by fastruby.io and their blog post Getting Ready for Rails 6.0: How to Dual Boot.

Unfortunately the two do not work well together out of the box 💔.

When using dual-booting of Rails, we specify the dependencies in the Gemfile like this:

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

if next?
  # Specific override for next
  gem 'rails', '~> 6.0'
  gem 'rubyzip', '~> 3' # Allow freer version
else
  gem 'rails', '~> 5.2'
  gem 'rubyzip', '> 2', '< 3' # Lock this to specific version compatible with Rails 5.2
end

gem 'webpacker', '~> 5.1.0' # Same version for Rails 5.2 and 6.0.
Enter fullscreen mode Exit fullscreen mode

This allows us to prepare working with higher versions of specific gems if needed.
The next?-block is used if the Gemfile we use is called Gemfile.next.
In order to use this we create a symbolic link, so our gemfiles in the project folder look like this:

Dec 13 13:07 Gemfile
Dec 13 12:57 Gemfile.lock
Dec 13 12:57 Gemfile.next -> Gemfile
Dec 13 13:08 Gemfile.next.lock
Enter fullscreen mode Exit fullscreen mode

This means Gemfile.next will refer to our usual Gemfile but next? will be true.

Problem

When Dependabot updates our dependencies it is configured to find Gemfile and Gemfile.lock and see if they need updating.

Lets say that webpacker releases a new version and needs to be updated to version 5.2, if we have a version requirement in the Gemfile that does not allow to update to the latest version, then Dependabot will update both Gemfile and Gemfile.lock.

Since Dependabot does not update Gemfile.next.lock it will get out of sync with our Gemfile:

# Gemfile - updated by Dependabot
gem 'webpacker', '~> 5.2.0'

# Gemfile.lock - updated by Dependabot
webpacker (5.2.0)

# Gemfile.next.lock - not updated
webpacker (5.1.0)
Enter fullscreen mode Exit fullscreen mode

This works in development, but in deployment using BUNDLE_DEPLOYMENT=1 we get an error:

% BUNDLE_DEPLOYMENT=1 BUNDLE_GEMFILE=Gemfile.next bundle
You are trying to install in deployment mode after changing
your Gemfile. Run `bundle install` elsewhere and add the
updated Gemfile.next.lock to version control.

If this is a development machine, remove the <project-path>/Gemfile.next freeze
by running `bundle install --no-deployment`.

The dependencies in your gemfile changed

You have added to the Gemfile:
* webpacker (~> 5.2)

You have deleted from the Gemfile:
* webpacker (~> 5.1)
Enter fullscreen mode Exit fullscreen mode

There have been discussions about using multiple Gemfiles on Dependabot, but the proposed solution did not work well with symbolic links in the Github API:

Solution

Instead of trying to configure Dependabot differently I wrote a Github Action to update the Gemfile.next.lock anytime Gemfile.lock is updated.

The steps in the Action are:

  1. Trigger on every pull-request.
  2. Check if Gemfile.lock was updated.
  3. Generate an access token using a Github App, based on this article.
  4. Checkout the code using the generated access token.
  5. Install ruby.
  6. Update dependencies using Bundler.
  7. Commit if there are any changes using EndBug/add-and-commit.

Here is a gist and it looks like this:

name: Update next

on:
  pull_request:
    paths:
      - "Gemfile.lock"

jobs:
  update:
    runs-on: ubuntu-20.04

    env:
      BUNDLE_GEMFILE: Gemfile.next

    steps:
      - name: Generate token
        id: generate_token
        uses: tibdex/github-app-token@v1
        with:
          app_id: ${{ secrets.APP_ID }}
          private_key: ${{ secrets.PRIVATE_KEY }}

      - uses: actions/checkout@v2
        with:
          token: ${{ steps.generate_token.outputs.token }}

      - name: Set up Ruby
        uses: ruby/setup-ruby@v1

      - name: Gems Cache
        id: gem-cache
        uses: actions/cache@v2
        with:
          path: vendor/bundle
          key: ${{ runner.os }}-gem-${{ hashFiles('Gemfile.next.lock') }}
          restore-keys: |
            ${{ runner.os }}-gem-

      - name: Update Gemfile.next
        run: |
          bundle update --minor --conservative

      - uses: EndBug/add-and-commit@v6
        with:
          add: Gemfile.next.lock
          message: 'Updated Gemfile.next.lock'
Enter fullscreen mode Exit fullscreen mode

The above action should be saved as .github/workflows/<workflow-name>.yml to get automatic updates.

Future work

  • The bundle strategy can probably be tweaked to something other than bundle update --minor --conservative.

Please get in touch with me on twitter @davidwessman if you have any questions or suggestions!

Updates

2020-12-13

  • When using a GITHUB_TOKEN, the added commit cannot trigger Github Actions again. This can be solved by using a Personal access token (PAT) as I learned from Github user airtower-luna (Thank you!) in this discussion.

2021-01-14

  • The initial setup used the default GITHUB_TOKEN from the Github Action, and it cannot be used to trigger another Github Action job.
  • Added a setup using an access token from a Github App, allowing new jobs to be triggered.
💖 💪 🙅 🚩
davidwessman
David Wessman

Posted on December 13, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related