Rails Dual-boot + Dependabot = 💔?
Tags: rails, dependabot, dualboot, github
Published: 2020-12-13 18:26:48

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](https://dependabot.com/) 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](https://fastruby.io) and their blog post [Getting Ready for Rails 6.0: How to Dual Boot](https://www.fastruby.io/blog/upgrade-rails/dual-boot/dual-boot-with-rails-6-0-beta.html).

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

<!--more-->

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

```ruby
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.
```

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:

```bash
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
```
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`:
```rb
# 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)
```

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

```bash
% 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)
```

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:

- [dependabot/dependabot-core#2106](https://github.com/dependabot/dependabot-core/issues/2106)
- [dependabot/dependabot-core#2231](https://github.com/dependabot/dependabot-core/issues/2231)

## Solution

Instead of trying to configure **Dependabot** differently I wrote a [**Github Action**](https://github.com/features/actions) to update the `Gemfile.next.lock` anytime `Gemfile.lock` is updated.

The steps in the Action are:
1. Trigger on every pull-request.
1. Check if `Gemfile.lock` was updated.
1. Generate an access token using a Github App, based on [this article](https://dev.to/dtinth/authenticating-as-a-github-app-in-a-github-actions-workflow-27co).
1. Checkout the code using the generated access token.
1. Install `ruby`.
1. Update dependencies using [Bundler](https://bundler.io/).
1. Commit if there are any changes using [EndBug/add-and-commit](https://github.com/EndBug/add-and-commit).


[Here is a gist](https://gist.github.com/davidwessman/d7e2d4b15619802a595cc96a2f583927) and it looks like this:

```yml
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'
```

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](https://twitter.com/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](https://github.com/airtower-luna) (Thank you!) in this [discussion](https://github.community/t/commit-generated-in-one-workflow-does-not-trigger-pull-request-workflow/147696).


### 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.