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.