rails-local-ci
Backport of Rails 8.1's
ActiveSupport::ContinuousIntegrationfor Rails 5.2–7.x
Rails 8.1 introduced a standardized bin/ci script and ActiveSupport::ContinuousIntegration class (PR #54693) that runs the same CI steps locally and in the cloud. This gem ships that class for apps on Rails 5.2–7.x so you don't have to wait for an upgrade.
When you do upgrade to Rails 8.1, remove the gem — bin/ci and config/ci.rb stay unchanged because Rails 8.1 provides the class at the same load path.
Table of Contents
- Prerequisites
- Installation
- Usage
- DSL Reference
- Parallel Groups
- Fail-Fast Mode
- Commit Signoff
- CI Integration
- Running Tests
- Upgrading to Rails 8.1
- Compatibility
- How It Works
- License
Prerequisites
- Ruby >= 2.4
- Rails >= 5.2 and < 8.1
Installation
Add to your Gemfile:
gem "rails-local-ci"Install:
bundle installRun the generator:
rails generate rails_local_ci:installThe generator copies two files into your app:
-
bin/ci— the runner script (set to executable) -
config/ci.rb— your CI step definitions
Usage
Run all CI steps locally:
./bin/ciExpected output:
Continuous Integration
Running tests, style checks, and security audits
Setup
bin/setup --skip-server
✅ Setup passed in 2.31s
Style: Ruby
bin/rubocop
✅ Style: Ruby passed in 1.04s
Tests: Rails
bin/rails test
✅ Tests: Rails passed in 4.17s
✅ Continuous Integration passed in 8.34s
Define your steps 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?
heading "All CI checks passed!", "Your changes are ready for review"
else
failure "CI checks failed", "Fix the issues above before submitting your PR"
end
endCI.run sets ENV["CI"] = "true" for the duration of the run, which Rails uses to enable eager loading and disable verbose logging.
DSL Reference
All methods are available inside the CI.run do ... end block in config/ci.rb.
| Method | Description |
|---|---|
step "Title", "command" |
Run a shell command; records pass/fail and elapsed time |
step "Title", "cmd", "arg1", "arg2" |
Run with multiple args — passed directly to system, correctly shell-escaped |
group "Title" do ... end |
Group steps visually; runs sequentially |
group "Title", parallel: N do ... end |
Run up to N steps concurrently in threads |
success? |
Returns true if every step so far has passed |
heading "Title" |
Print a green banner |
heading "Title", "Subtitle" |
Print a green banner with a gray subtitle; accepts padding: false to suppress blank lines |
failure "Title", "Subtitle" |
Print a red error banner |
echo "Text", type: :success |
Print colorized text to the terminal |
Color types for echo and heading:
| Type | Color |
|---|---|
:banner |
Green (bold) |
:title |
Purple (bold) |
:subtitle |
Gray (bold) |
:error |
Red (bold) |
:success |
Green (bold) |
:progress |
Cyan (bold) |
CI.run accepts optional title and subtitle overrides:
CI.run "My App CI", "Linting, security, and tests" do
# ...
endParallel Groups
Run multiple independent steps concurrently using group:
CI.run do
step "Setup", "bin/setup --skip-server"
group "Checks", parallel: 3 do
step "Style: Ruby", "bin/rubocop"
step "Security: Brakeman", "bin/brakeman --quiet"
step "Security: Gems", "bin/bundler-audit"
end
step "Tests: Rails", "bin/rails test"
endparallel: N runs up to N steps in concurrent threads. Output is captured per-step (via PTY when available, Open3 otherwise) and displayed in declaration order after all steps complete — no interleaving. A live progress indicator shows which steps are running.
group without parallel: (or parallel: 1) runs steps sequentially, useful for visual grouping.
Nested groups: a group inside a parallel group runs its steps sequentially within that thread slot. Sub-groups cannot themselves be parallelized — attempting to set parallel: on a nested group raises ArgumentError.
Fail-Fast Mode
Stop at the first failing step instead of running all steps:
./bin/ci --fail-fast
./bin/ci -fCommit Signoff
After a successful local CI run, post a green commit status directly to your Git host to unblock PR merge — no cloud CI runner needed.
GitHub
Requires the gh CLI and the gh-signoff extension:
gh extension install basecamp/gh-signoffAdd to config/ci.rb:
if success?
step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
endBitbucket
Install bb-signoff:
curl -fsSL https://raw.githubusercontent.com/its-magdy/bb-signoff/main/bb-signoff \
-o /usr/local/bin/bb-signoff && chmod +x /usr/local/bin/bb-signoffCreate a Bitbucket repository access token with these scopes:
- Repositories: Read, Write, Admin
- Pull Requests: Read, Write
Store the token in ~/.bb-signoff or export it as BB_API_TOKEN.
Add to config/ci.rb:
if success?
step "Signoff: All systems go. Ready for merge and deploy.", "bb-signoff"
endCI Integration
GitHub Actions
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- run: bin/ciBitbucket Pipelines
pipelines:
default:
- step:
name: CI
script:
- bundle install
- bin/ciRunning Tests
bundle exec ruby test/continuous_integration_test.rbUpgrading to Rails 8.1
- Remove
gem "rails-local-ci"from yourGemfile - Run
bundle install
bin/ci and config/ci.rb require no changes. Rails 8.1 ships ActiveSupport::ContinuousIntegration at the same load path this gem uses.
Compatibility
| Rails | Ruby | Status |
|---|---|---|
| 7.2 | 3.1+ | Tested |
| 7.1 | 3.0+ | Compatible |
| 7.0 | 2.7+ | Compatible |
| 6.1 | 2.7+ | Compatible |
| 6.0 | 2.7+ | Compatible |
| 5.2 | 2.4+ | Compatible |
| 8.1+ | — | Use built-in — remove this gem |
How It Works
The gem places ActiveSupport::ContinuousIntegration at lib/active_support/continuous_integration.rb — the identical load path Rails 8.1 uses. The generated bin/ci does:
require_relative "../config/boot"
require "active_support/continuous_integration" # resolved by this gem, or Rails 8.1 built-in
CI = ActiveSupport::ContinuousIntegration
require_relative "../config/ci.rb"The generator copies only bin/ci and config/ci.rb into your app. The class itself lives in the gem.