I’ve been using sorbet recently on a project. If you haven’t come across it before, sorbet is a type checker for ruby that allows you to gradually add type annotations to your codebase (sigs in sorbet terminology) and then provide feedback (either on the command-line or in your editor) when things don’t check out.
As well as the sigs you add to your source, sorbet supports rbi
files for describing your classes. In particular these are key for describing your dependencies. The recommended way is to use tapioca, a tool released by Shopify, for generating rbis. One of the things tapioca will do is autogenerate rbis for all of the gems you use. These autogenerated ones aren’t super smart (effectively just method name + number of arguments), but a lot better than nothing (there are also handwritten ones known as annotations - this is a small and nascent collection compared to typescript’s definitelytyped).
Of course, as your change or update your gems, the corresponding rbis need to be updated. If you run tapioca gem --verify
then it will tell you whether there are any out of date gem rbis. We’ve added this to our CI to check that they’re always in sync. So far, so good! However we also use dependabot to manage our dependencies, so now every dependabot gem update PR would fail on CI, until someone came along and ran tapioca gem
. This doesn’t take long (I wrote a shell alias to run bundler, run tapioca and then commit and push the results) but it’s an extra manual step and dependabot is all about removing manual steps.
In theory dependabot could do this itself, but it’s not on their roadmap. You can however write a github action to automate this. Here’s the github action in its full glory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
Let’s break it down. The first section is just when the action should run
1 2 3 |
|
This says that the action should runs when a pull request is opened or synchronized (this covers things like when you ask dependabot to rebase the PR).
The next section is about the permissions required:
1 2 3 |
|
Actions triggered by dependabot have different permissions than normal actions. In this case we need:
- permission to write the updated rbis to the repository (
contents: write
) - permission to read PRs (
pull-requests: read
)
Skipping irrelevant PRs
We don’t want this to run on non dependabot PRs or on other people’s commits to those PRs. At best this is just a waste of github action minutes. The first thing we do is the check right at the top of the job whether the actor is dependabot:
1
|
|
Then, as the first step in the action we use the dependabot/fetch-metadata
action to fetch futher information:
1 2 3 |
|
This action also validates more thoroughly that the pull request is from dependabot, specificlly that the first commit is signed by dependabot. Other than authenticity, the main bit of metadata we’re interested in is the ecosystem of the package(s) that have been updated - for example there’s no point continuing if this is a yarn package update.
All the subsequent steps have
if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'bundler' }}
so they won’t run if the dependabot PR wasn’t for a gem update (you could split this into 2 jobs to eliminate the repetition but that seems a little overkill.
Checking out the app
1 2 3 4 |
|
By default the checkout action checks out a merge commit of the pull request into the target branch, but since we want to update the pull request we checkout the pull request’s head instead, using the sha in the event metadata.
Setup the app
1 2 3 4 5 |
|
This just installs the requested version of ruby and runs bundler. If you have private dependencies that require github secrets, note that they have to be stored as dependabot secrets - since this workflow is triggered by dependabot, it plays by the same rules and does not have access to “normal” github secrets.
Run Tapioca
1 2 3 |
|
This is the easy bit! We just let tapioca do its thing.
Push to github
1 2 3 4 5 6 7 8 9 |
|
To be able to push out changes we first need to tell git who we are with git config
and where to push the changes with git remote set-url
. This uses the https transport because we can use the github token as the password. After committing the changes all we need to do is push.
Just running git push
isn’t enough here, because we’re on a detached head: the checkout action has done the equivalent of git checkout abcdef
so we need to tell git which remote branch to push to. Happily this is in the event payload as
${{ github.event.pull_request.head.ref }}
.
And there we are - back to gem updates being a one click option (once you’re happy with the changes made of course). It is a tiny bit wasteful that CI will run twice, once on dependabot’s original commit and again on the commit updating the rbis, but at least that’s only wasting elapsed time, not your time!