Adding Sorbet and Tapioca to a Jumpstart Pro app
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.
Preparation
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.
name: Verify
on: [push, pull_request]
jobs:
verify:
uses: setup-rails/setup-rails/.github/workflows/verify.yml@main
with:
run-before-tests: sudo apt-get install -y -qq libvips
standard: true
(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.)
Basic Setup
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
- ActsAsTenant
- Facebook Omniauth Provider
This will result in some additions to Gemfile.lock
which you should commit.
Next, we’ll add the sorbet-static-and-runtime
and tapioca
gems to the Gemfile
, run bundle
then bundle exec tapioca init
.
The 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:
module Admin
class User::ImpersonatesController < Admin::ApplicationController
end
end
we need to write:
module Admin
module User
class ImpersonatesController < Admin::ApplicationController
end
end
end
Other errors are because some parts of the gem are not required by default. We need to add an entry to Tapioca’s require.rb
:
# sorbet/tapioca/require.rb
require "administrate/base_dashboard"
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:
T.unsafe(self).include Engine.routes.url_helpers
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:
__END__
Bugsnag.configure do |config|
config.api_key = Rails.application.credentials.dig(:bugsnag, :api_key)
end
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.
# sorbet/shims.rbi
class Devise::OmniauthCallbacksController; end
class Devise::RegistrationsController; end
class Devise::SessionsController; end
class Noticed::NotificationChannel; end
For Minitest::Mock
and Sidekiq::Web
, we again need to add entries to require.rb
then regenerate the RBIs.
# sorbet/tapioca/require.rb
require "minitest/mock"
require "sidekiq/web"
At this point, there should be no more entries in todo.rbi
and running tapioca todo
will delete it.
Standard
Although Jumpstart uses Standard rather than RuboCop, there are some useful cops in rubocop-sorbet
, so we will add that as a dependency.
Then in .standard.yml
, we’ll use Standard’s extend_config feature to reference a RuboCop Sorbet configuration file:
# .standard.yml
extend_config:
- .rubocop_sorbet.yml
The config will look like this:
# .rubocop_sorbet.yml
require: rubocop-sorbet
AllCops:
NewCops: disable
Exclude:
- lib/jumpstart/test/dummy/**/*
Sorbet/ConstantsFromStrings:
Exclude:
- 'app/models/connected_account.rb'
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 const_get
.
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 false
.
On its own that doesn’t do anything, but it prepares the way so that we can use Spoom.
Spoom
Spoom consists of several tools, one of which is the bump
command:
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.
Next Steps
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.