RailsAgency
← All articles

Ruby on Rails

Running Rails CI Locally with bin/ci in Rails 8.1

By Daniel Rice · · 4 min read

Cloud CI made sense when laptops were slow and test suites were a shared, precious resource. A current machine with a pile of cores can run a serious Rails suite faster than a hosted runner spends starting. Rails 8.1 meets that fact with a small DSL. You describe the steps in config/ci.rb and run them with bin/ci.

The Rails team used their own numbers as the argument. The HEY suite, more than 30,000 assertions, used to take over ten minutes in the cloud once you counted image builds and coordination. The same suite ran in 1 minute 23 seconds on a Framework Desktop and 2 minutes 22 seconds on an M4 Max. For a small or mid-size app, the cloud runner is often the slow part.

The file you actually commit

config/ci.rb is a list of named steps. Each step is a label and a command. bin/ci runs them in order and stops you from shipping when one fails.

CI.run do
  step "Setup", "bin/setup --skip-server"
  step "Style: Ruby", "bin/rubocop"

  step "Security: Gem audit", "bin/bundler-audit"
  step "Security: Importmap vulnerability audit", "bin/importmap audit"
  step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
  step "Tests: Rails", "bin/rails test"
  step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant"

  if success?
    step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
  else
    failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
  end
end

Read it as the checklist a careful reviewer already wanted. Setup, so the run is not "works on my machine" in the sneaky sense. Style. Three security checks: known-bad gems, known-bad importmap packages, and Brakeman over the app. Then the test suite, then a seed replant so a broken seed file cannot hide until production. The signoff at the end is optional. It needs the GitHub CLI and the basecamp/gh-signoff extension, and it exists so a pull request cannot merge on a run that failed.

You can delete steps you do not use. An app with no importmap should not fail the importmap audit. An app that still has a JavaScript test suite should add a step for it. The DSL is not a religion. It is a list.

When local CI is the right default

Local CI fits when three things are true.

The suite finishes while you are still in the branch. If bin/ci takes twenty minutes, people will stop running it, and you are back to hoping the cloud is red. If it takes two, it becomes the thing you run before you push.

The checks are the same on every machine. bin/setup --skip-server is in the file for that reason. A step that assumes a global chromedriver installed by hand is not a CI step, it is a folk tale. Anything the step needs has to be installed by the repo.

You trust the person pushing. Local CI does not prove the code ran on a clean machine you control. It proves the author ran the checklist. For a team of one to a handful, that is usually the expensive part you were missing. A hostile contributor, or a compliance rule that demands a recorded run on your hardware, still wants a hosted runner. Rails is not telling those teams to turn GitHub Actions off. It is telling the other teams they can stop waiting on it.

What we keep in the cloud anyway

Some steps are a bad fit for a laptop even when the tests are fast.

Browsers and system packages drift. A Capybara suite that passes because your Chrome happens to be current will fail on a teammate's machine for a reason that is not the diff. Those steps can stay hosted, or they can stay in config/ci.rb only if setup installs the browser.

Secrets. bin/ci runs on a developer machine, with that machine's environment. A step that needs a production-like API key does not belong there. The security steps in the default file are deliberately local: bundler-audit, importmap audit, Brakeman. They read the repo, not your vault.

The signoff is the bridge. The tests ran locally, and gh signoff records that a passing run happened, so the pull request has a mark that is not just the author's word. If you do not want that integration, drop the if success? block. The rest of the file still works.

A practical first week

Generate or add config/ci.rb, delete the steps that do not apply, and time bin/ci once. If it is under a few minutes, make it the pre-push habit and keep a hosted workflow only for what cannot run locally. If it is slow, the slowness is now a number you can see on your own machine, which is a better place to fix it than a queue of cloud logs.

We set this up for Rails apps that have outgrown "run the tests you remember." Rails Agency will wire bin/ci to the checks that actually matter for your app, and drop the ones that only exist because a template included them.

FAQ

Does bin/ci replace GitHub Actions?
For many small teams, yes for the default checks. Browser suites, secret-dependent jobs, and compliance rules that require a recorded run on your own hardware can stay in the cloud.

Dealing with this in your own Rails app?

Book a free 30-minute call. You'll talk directly to a senior Rails engineer — no sales team, no hand-offs.