This post describes the process of setting up Sorbet and Tapioca for the Jumpstart Pro Rails template.
Since Jumpstart Pro is a commercial product, I can’t share the full code, but if you’re a Jumpstart customer you can view the changes in this branch of my fork. I recommend viewing it as individual commits to understand it step-by-step. The order may not exactly match the blog post, but the end result should be the same.
For this post, I’ll assume you are starting from a freshly generated Jumpstart app. If you have already built your app on top of Jumpstart then it may take some more effort but the overall approach is the same.
I’ll demonstrate the process in incremental steps, so your app can continue to be deployed while type information is still being added. This follows Sorbet’s philosophy of Gradual Typing.
Start by creating a
sorbet branch for the Sorbet migration. This will be fairly short-lived, and only needed for the initial setup.
Setting up CI
Although Jumpstart Pro provides a GitHub Actions CI script, we’ll instead use setup-rails since it will detect if Sorbet is in use and run additional checks. We’ll also enable the
standard option, since that’s what Jumpstart uses instead of RuboCop.
on: [push, pull_request]
run-before-tests: sudo apt-get install -y -qq libvips
(I also had to disable parallel testing by commenting-out the
parallelize line in
test_helper.rb, as I found it was causing the tests to hang. I haven’t had a chance yet to look into the cause.)
Overall, the setup for Jumpstart Pro is not so different than for any other Rails app, but the optional dependencies complicate things a little: If there is code that references a gem that isn’t installed, then typechecking will fail, even if it’s within a
defined? check. To simplify things for this guide, we will open the Jumpstart configuration page and enable the following features:
- Payment Processor: Stripe
- Background Queue: Sidekiq
- Facebook Omniauth Provider
This will result in some additions to
Gemfile.lock which you should commit.
Next, we’ll add the
tapioca gems to the
bundle exec tapioca init.
init command can take a long time to run (10 minutes or more), and it may seem like it has frozen. Have patience!
You may be alarmed by the huge number of RBI files this creates, but you will very rarely need to interact with them.
After this is complete, we can commit everything. Let’s now run the typechecker:
$ bundle exec srb tc
We should see about 20 errors. It’s common to encounter errors when setting up Sorbet initially, usually due to known limitations in Sorbet or Tapioca.
In the case of Jumpstart, several are due potentially ambiguous definitions, which are easily fixed by using the full version of the definition. For example instead of:
class User::ImpersonatesController < Admin::ApplicationController
we need to write:
class ImpersonatesController < Admin::ApplicationController
Other errors are because some parts of the gem are not required by default. We need to add an entry to Tapioca’s
and then re-run
bundle exec tapioca gem administrate.
After this there should be only handful of remaining errors, which are due to the Sorbet limitation that “include must only contain constant literals”. We can work around this by marking those calls as unsafe:
At this point you can push to CI and everything should be green again. If you wish, you can merge the
sorbet branch into
main and continue the remaining work in other branches.
Clearing the TODO list.
Although we are no longer seeing any typechecking errors, some things are being ignored because they are listed in
todo.rbi which was generated by
sorbet init. We should aim to eliminate these before continuing.
We intentionally never manually edit
todo.rbi - we’ll make a change, and then regenerate it, to gradually reduce the number of entries.
Many of the remaining entries in
todo.rbi are due to optional gems, where they are conditionally referenced in an initializer. We could just delete those, but that would make it a little tricker pulling in changes from upstream in Jumpstart Pro. The approach I suggest is using Ruby’s
__END__ keywords. It indicates that the code in the file has ended, and so Ruby (and Sorbet) will ignore it:
Bugsnag.configure do |config|
config.api_key = Rails.application.credentials.dig(:bugsnag, :api_key)
Next, we have some entries in
todo.rbi that relate to the Devise and Noticed gems. For gems that make use of metaprogramming, we often need to give Sorbet some help by adding shims.
class Devise::OmniauthCallbacksController; end
class Devise::RegistrationsController; end
class Devise::SessionsController; end
class Noticed::NotificationChannel; end
Sidekiq::Web, we again need to add entries to
require.rb then regenerate the RBIs.
At this point, there should be no more entries in
todo.rbi and running
tapioca todo will delete it.
Although Jumpstart uses Standard rather than RuboCop, there are some useful cops in
rubocop-sorbet, so we will add that as a dependency.
.standard.yml, we’ll use Standard’s extend_config feature to reference a RuboCop Sorbet configuration file:
The config will look like this:
We’ll ignore the Dummy app used by the internal jumpstart gem, since that should be treated as a separate application.
We also we need to disable the
ConstantsFromStrings check for one file, due to Sorbet’s limitations for
With that done, we can now run
bundle exec standardrb --fix which will add a
typed: false entry to each file. This happens because rubocop-sorbet’s default configuration enables the
Sorbet/FalseSigil cop, which ensures all files are at a strictness of a least
On its own that doesn’t do anything, but it prepares the way so that we can use Spoom.
Spoom consists of several tools, one of which is the
bundle exec spoom bump
This helps us discover which files can be ‘bumped’ up a level of typing.
You should see that a large number are now marked as
# typed: true, without us having to do any work.
At this point, you can search for
# typed: false in
*.rb and you’ll see there are around 100 files remaining that aren’t yet typed. Resolving all those is outside the scope of this post, but you now have a strong starting point.
You’ll notice that we haven’t written any signatures yet. But even without those, we can start benefiting from Sorbet’s checks for things such as calls to non-existing methods, or unreachable code.