Rails Dual-boot + Dependabot = 💔?
David Wessman
Posted on December 13, 2020
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.
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
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)
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)
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:
- Trigger on every pull-request.
- Check if
Gemfile.lock
was updated. - Generate an access token using a Github App, based on this article.
- Checkout the code using the generated access token.
- Install
ruby
. - Update dependencies using Bundler.
- 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'
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.
Posted on December 13, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.