DEV Community

Cover image for rails-local-ci: Essential Local CI for Older Rails Apps
Mohamed Magdy Omar
Mohamed Magdy Omar

Posted on

rails-local-ci: Essential Local CI for Older Rails Apps

Rails 8.1 shipped a genuinely useful feature: a standardized bin/ci script backed by ActiveSupport::ContinuousIntegration. Run your full test suite, linter, and security checks locally before pushing. No cloud bill. No waiting in a CI queue. Just your laptop doing what it was built for.

The catch? I'm on Rails 7. And I use Bitbucket, not GitHub.

So bin/ci wasn't available to me yet. And Basecamp's gh-signoff — the companion tool that posts a green commit status to your host after local CI passes — only works with GitHub. Bitbucket users don't get a version of that either.

I built both myself. This post explains what each tool does, how they connect, and why the upgrade path for rails-local-ci is the whole point of the project.

TL;DR: rails-local-ci brings Rails 8.1's bin/ci workflow to Rails 5.2–7.x. bb-signoff posts green commit statuses to Bitbucket after local tests pass. Use them together for a full local CI -> signoff -> merge workflow. Remove the gem when you hit Rails 8.1 — nothing else changes.


What Is Rails Local CI — And Why Doesn't It Work on Rails 7?

Rails 8.1's local CI pattern is elegant. You define your steps in config/ci.rb, run ./bin/ci, and you're done. It sets ENV["CI"]=true, handles colorized output, supports parallel steps, and stops early on failure if you want. It's exactly what a lot of teams have been cobbling together with shell scripts for years.

But if your app runs Rails 5.2 through 7.x, that class doesn't exist. And even if it did, posting a green commit status back to Bitbucket Cloud still requires hitting the REST API directly. There's no official tooling for it.

These two problems led to two projects.


How Does rails-local-ci Backport the bin/ci Workflow?

rails-local-ci is a Ruby gem that backports ActiveSupport::ContinuousIntegration to Rails 5.2 through 7.x. Not a reimplementation with its own API. The actual class, placed at the identical load path Rails 8.1 uses.

That last part matters. Here's why.

When you're ready to upgrade to Rails 8.1, you remove the gem from your Gemfile and run bundle install. That's it. You don't touch bin/ci. You don't touch config/ci.rb. Rails 8.1 ships the class at active_support/continuous_integration, which is the same path the gem uses. Your files just keep working.

Installing rails-local-ci

Add it to your Gemfile:

gem "rails-local-ci"
Enter fullscreen mode Exit fullscreen mode

Then run:

bundle install
rails generate rails_local_ci:install
Enter fullscreen mode Exit fullscreen mode

The generator creates two files: bin/ci (the runner) and config/ci.rb (your step definitions).

What config/ci.rb looks like

# Run using bin/ci

CI.run do
  step "Setup", "bin/setup --skip-server"

  # Run independent checks in parallel for faster feedback:
  #
  #   group "Checks", parallel: 3 do
  #     step "Style: Ruby",        "bin/rubocop"          if File.exist?("bin/rubocop")
  #     step "Security: Brakeman", "bin/brakeman --quiet"  if File.exist?("bin/brakeman")
  #     step "Security: Gems",     "bin/bundler-audit"     if File.exist?("bin/bundler-audit")
  #   end

  step "Style: Ruby",                      "bin/rubocop"        if File.exist?("bin/rubocop")
  step "Security: Gem audit",              "bin/bundler-audit"  if File.exist?("bin/bundler-audit")
  step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" if File.exist?("bin/brakeman")
  step "Tests: Rails",                     "bin/rails test"
end
Enter fullscreen mode Exit fullscreen mode

Then run it:

./bin/ci
Enter fullscreen mode Exit fullscreen mode

Output is colorized, each step shows pass/fail and elapsed time, and the whole run sets ENV["CI"]="true" so Rails behaves the same way it does in your cloud CI pipeline.

You can also pass --fail-fast (or -f) to stop at the first failing step instead of running everything.


bb-signoff: How Do Bitbucket Users Get a Signoff Tool?

Running tests locally is only half the story. The other half is communicating to your team: "I ran this. It passed. The PR is ready."

Basecamp's gh-signoff does exactly this for GitHub — it posts a commit status with a green check that you can make required before merging. But it calls the GitHub API. Bitbucket has an equivalent Bitbucket REST API. Nobody had built the tool.

bb-signoff is that tool. It's a single Bash script.

Installing bb-signoff

curl -fsSL https://raw.githubusercontent.com/its-magdy/bb-signoff/v1.0.0/bb-signoff \
  -o /usr/local/bin/bb-signoff && chmod +x /usr/local/bin/bb-signoff
Enter fullscreen mode Exit fullscreen mode

Create a Bitbucket repository access token (Repository: Read/Write/Admin, Pull Requests: Read/Write), then store it:

cat > ~/.bb-signoff <<EOF
BB_API_TOKEN=ATCTT3...
EOF
chmod 600 ~/.bb-signoff
Enter fullscreen mode Exit fullscreen mode

Using it

After your tests pass:

bb-signoff
Enter fullscreen mode Exit fullscreen mode

That posts a green commit status to Bitbucket for the current HEAD. You can also use named contexts to break signoff into separate checks:

bb-signoff tests lint security
Enter fullscreen mode Exit fullscreen mode

Which shows up in Bitbucket as three distinct statuses. If you want Bitbucket to warn you before merging when signoff hasn't passed, run:

bb-signoff install
Enter fullscreen mode Exit fullscreen mode

That adds a Bitbucket branch restriction requiring passing builds before merge. Remove it with bb-signoff uninstall. Check whether it's active with bb-signoff check.

One guard worth knowing: bb-signoff will refuse to post a status if you have uncommitted or unpushed changes. You can override with -f, but the default is intentional. The point is to certify a specific commit, not a working tree.


What Does the Full Local CI Workflow Look Like Together?

Here's what the combined setup looks like in config/ci.rb:

CI.run do
  step "Setup", "bin/setup --skip-server"
  step "Style: Ruby",                      "bin/rubocop"        if File.exist?("bin/rubocop")
  step "Security: Gem audit",              "bin/bundler-audit"  if File.exist?("bin/bundler-audit")
  step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" if File.exist?("bin/brakeman")
  step "Tests: Rails",                     "bin/rails test"

  if success?
    step "Signoff: All systems go. Ready for merge and deploy.", "bb-signoff"
  else
    failure "CI failed", "Fix the issues above before submitting your PR"
  end
end
Enter fullscreen mode Exit fullscreen mode

Run ./bin/ci. If everything passes, bb-signoff fires automatically. Your PR gets a green status. If you've run bb-signoff install on the branch, Bitbucket will warn you before merging if the signoff check hasn't passed — so it won't go unnoticed.

When all steps pass, the terminal prints each step in green with its elapsed time, then exits with code 0 — at which point bb-signoff runs and posts to Bitbucket. If any step fails, the run exits early and bb-signoff never fires, so your PR won't show a false green. After a successful signoff, the Bitbucket PR view shows a green build status indicator next to the commit SHA, the same way a cloud CI run would appear.

No cloud CI required. Your laptop is the CI.


Why Is the Upgrade Story the Whole Point of rails-local-ci?

I want to be direct about the design decision in rails-local-ci, because it's not obvious from the outside.

The gem places ActiveSupport::ContinuousIntegration at lib/active_support/continuous_integration.rb. Rails 8.1 ships the class at the same path. When require "active_support/continuous_integration" runs in bin/ci, Ruby's load path resolves it. On Rails 7, the gem wins. On Rails 8.1, the gem is gone and Rails itself wins.

Your bin/ci and config/ci.rb never change. The upgrade step is one line in your Gemfile.

That's the whole bet: write your CI config once, against a stable API, and don't rewrite it when you upgrade Rails. The gem is a bridge, not a permanent dependency.


Try It, Star It, Open Issues

Both projects are open source and MIT licensed.

If you're on older Rails and want the bin/ci workflow now, give rails-local-ci a try. If you're on Bitbucket and have been wishing gh-signoff existed for your platform, bb-signoff is ready.

Bug reports and PRs are welcome on both. If something doesn't work for your setup, open an issue.


Built by Magdy — a Software Engineer who got tired of waiting for Rails 8.1 and didn't have a signoff tool for Bitbucket. So he built both.

Top comments (0)