Rails 8.1のLocal CI(bin/ci)入門|プッシュ前にローカルでCIを一発で回す

従来のCIとRails 8.1のLocal CI(bin/ci)の流れを比較した図。従来はプッシュ後にGitHub ActionsのCIを待ち、RuboCopで赤になって再プッシュする往復が発生。Local CIは手元でbin/ciを実行し全部緑にしてからプッシュするので一発で通過する。
  • このエントリーをはてなブックマークに追加

スポンサーリンク

コードを書いてプッシュ、GitHub Actions の CI が回るのをコーヒーを飲みながら待つ。数分後に赤くなって、「あ、RuboCop 引っかかってた」と直して再プッシュ……。この「CI 待ち → 赤 → 出直し」のループ、地味に時間を溶かしていませんか?

Rails 8.1 で追加された Local CI(bin/ci)は、まさにこの悩みへの答えです。テスト・Lint・セキュリティチェックといった「CI で回している一式」を、プッシュ前に手元でまとめて実行できます。この記事では bin/ci と config/ci.rb の使い方を、完成イメージ → 設定の中身 → 既存プロジェクトへの導入 → GitHub Actions との併用、の順で見ていきます。

まず結論:bin/ci 一発で「CI と同じチェック」が手元で回る

細かい説明は後回しにして、先に完成形を見てもらいます。Rails 8.1 のアプリで bin/ci を実行すると、こんな出力が出ます。

$ bin/ci

  ✔  Setup
  ✔  Style: Ruby
  ✔  Security: Gem audit
  ✔  Security: Brakeman code analysis
  ✔  Tests: Rails

  All checks passed 🎉

この「何をチェックするか」を決めているのが config/ci.rb というファイルです。つまり、config/ci.rb が「CI が通る=何が OK か」の定義そのもので、それをローカルでもクラウドでも同じように走らせられる、というのが Local CI の考え方です。

従来のCIとRails 8.1のLocal CI(bin/ci)の流れを比較した図。従来はプッシュ後にGitHub ActionsのCIを待ち、RuboCopで赤になって再プッシュする往復が発生。Local CIは手元でbin/ciを実行し全部緑にしてからプッシュするので一発で通過する。
プッシュしてから待つのではなく、手元で先に潰す。これが Local CI のいちばんの狙い

GitHub Actions の結果を待たずに、プッシュ前に同じチェックを手元で潰せるので、「CI が赤くなって出直し」の往復が減ります。レビュー依頼の前に自分でゲートをくぐれる、と言い換えてもいいでしょう。

Local CI とは(bin/ci と config/ci.rb)

Rails 8.1 の Local CI は、次の2つがセットになった仕組みです。

  • config/ci.rb:実行するチェック(ステップ)を並べる定義ファイル。CI の「正」はここ1か所に集約される
  • bin/ci:config/ci.rb を読んで各ステップを順に実行するランナー

bin/ci は実行時に ENV["CI"] を true にセットして走ります。これにより、eager loading(アプリ起動時に全クラスをまとめて読み込むこと。読み込み漏れのエラーを早期に炙り出せる)が有効になるなど「CI 相当の環境」でチェックが動きます。手元の development とは少し違う条件で回るので、CI サーバーでだけ落ちる、といった食い違いも見つけやすくなります。

背景には「開発マシンが速くなり、小〜中規模のアプリならローカルでテスト一式を数分で回しきれる」という状況の変化があります。Rails 8.1 は、この現実に合わせて Local CI を標準機能として用意した、というわけです。

config/ci.rb の中身を読む

Rails 8.1 で新規アプリを作ると、config/ci.rb はだいたい次のような内容で生成されます。読みやすいように少し省略していますが、雰囲気はこのままです。

# config/ci.rb
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"
end

やっていることは、見た目のとおりシンプルです。CI.run do ... end の中に step を並べているだけ。各ステップが何をチェックしているかを表にすると、こうなります。

ステップ実行コマンド役割
Setupbin/setup --skip-server依存関係のインストール・DB準備(サーバーは起動しない)
Style: Rubybin/rubocopコードスタイル(Lint)のチェック
Security: Gem auditbin/bundler-auditgem に既知の脆弱性がないか
Security: Importmapbin/importmap auditimportmap 経由の JS に脆弱性がないか
Security: Brakemanbin/brakeman ...Rails 特有のセキュリティ問題を静的解析
Tests: Railsbin/rails testテストスイート
Tests: Seedsbin/rails db:seed:replantseeds が壊れていないか(投入し直して確認)

ポイントは、ステップの実体はただのシェルコマンドだということ。RuboCop も Brakeman も、普段ターミナルで打つコマンドをそのまま並べているだけです。だから「うちのプロジェクト独自のチェックを足したい」というのも、step を1行追加するだけで済みます。

ここで出てくる書き方(DSL)は、覚えることが少ないのも魅力です。よく使うものは次の5つだけ。

書き方役割
CI.run do ... endCI 全体の定義ブロック。この中にステップを並べる
step "名前", "コマンド"1つのチェックを追加する(実体はシェルコマンド)
group "名前", parallel: N do中のステップを最大 N 個まで並列実行する
success?ここまでの全ステップが通ったかを判定する
failure "見出し", "説明"失敗時にメッセージを出す

これだけ押さえれば、あとは step を足したり並列にしたりの組み合わせです。順に見ていきましょう。

step の書き方

step の引数は「表示名」+「コマンドと引数」です。第1引数がログに出るステップ名、残りが実行するコマンドになります。

# 1つの文字列でまとめて渡す
step "Tests: Rails", "bin/rails test"

# コマンドと引数を分けて渡す(シェルエスケープされる)
step "Tests: All", "bin/rails", "test", "test:system"

引数を分けて渡すと、スペースを含む値なども安全にエスケープしてくれます。動くコマンドでありさえすれば何でも登録できるので、たとえば step "Types", "bin/rails runner ..." のように、自作のチェックスクリプトを差し込むこともできます。

並列実行で速くする(group)

ステップは上から順に実行されますが、お互いに独立したチェックは並列で走らせて時短できます。group に parallel: を渡すと、その中のステップを同時に実行します。

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

  group "Checks", parallel: 2 do
    step "Style: Ruby", "bin/rubocop"
    step "Security: Brakeman", "bin/brakeman --quiet"
    step "Tests: Rails", "bin/rails test"
  end
end

RuboCop・Brakeman・テストは互いに影響しないので、まとめて並列にすると全体時間が縮みます。出力は宣言した順に整理して表示されるので、並列にしてもログがぐちゃぐちゃに混ざることはありません。Rails 8.1 の新規アプリのデフォルトでも、チェック系はこの group でまとめられています。

成功・失敗で処理を分ける(success?)

全ステップが通ったかどうかは success? で判定できます。これを使うと、「全部緑なら承認(signoff)まで済ませる」「1つでも落ちたら分かりやすくメッセージを出す」といった分岐が書けます。

CI.run do
  # ... 各ステップ ...

  if success?
    step "Signoff", "gh signoff"
  else
    failure "CI failed", "直してから再実行してください"
  end
end

デフォルトの config/ci.rb にも、この success? を使って「全チェック通過なら gh signoff でコミットにOKを付ける」例がコメントで入っています(gh CLI の拡張が必要なので、最初はコメントアウトされています)。まずはここまで踏み込まなくても、テストと Lint を並べるだけで十分に役立ちます。

既存プロジェクトに bin/ci を入れる

「うちは Rails 8.0 以前から動かしてる」「8.1 に上げたけど bin/ci が無い」という場合は、アップデートタスクで取り込めます。

$ bin/rails app:update

差分を確認する対話が出るので、config/ci.rb と bin/ci を受け入れる(Y を選ぶ)と一式が入ります。既存の設定ファイルまで上書きしてしまわないよう、差分は1つずつ中身を見てから受け入れるのが安全です。

取り込んだあとは、生成された config/ci.rb のうち自分のプロジェクトに無いステップ(bin/rubocop を入れていないなら Style ステップ、など)を削るだけ。逆に bin/ にコマンドが無い場合は、bundle exec rubocop のように置き換えれば動きます。まずはテスト1本から始めて、慣れてきたら Lint・セキュリティチェックを足していく形でも十分です。

ちなみに、Rails 8 系で Solid Queue などを使って複数 DB 構成にしているなら、bin/setup や db:seed:replant が正しく動くよう database.yml の書き方もあわせて確認しておくと、CI のステップでつまずきにくくなります。

GitHub Actions は捨てなくていい(使い分けの考え方)

ここまで読むと「じゃあ GitHub Actions は要らない?」と思うかもしれませんが、答えは「捨てずに使い分ける」です。Local CI とクラウド CI は、役割が少し違います。

  • ローカルの bin/ci:プッシュ前に手元で回して、明らかな失敗を先に潰す。フィードバックが速い
  • GitHub Actions などのクラウド CI:クリーンな環境・複数の Ruby/DB バージョン・チーム全員のプッシュに対する最終ゲート

大事なのは、クラウド CI 側も config/ci.rb を「正」として同じチェックを走らせること。そうすれば「手元では通ったのに CI で落ちた(設定がズレていた)」が起きにくくなります。

config/ci.rbがCIの正であることを示す概念図。CI.run doにstepでSetup・RuboCop・Brakeman・Testsを定義し、手元ではbin/ci、GitHub Actionsではrun: bin/ciとして同じ定義を実行する。
チェックの定義は config/ci.rb の1か所。ローカルもクラウドも同じものを走らせる

GitHub Actions の設定は、極端にはこれだけまで縮められます。

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true
      - run: bin/ci

ワークフロー側に個別のステップをズラズラ書くのではなく、bin/ci を呼ぶだけ。チェックの内容を変えたいときは config/ci.rb を1か所直せば、ローカルとクラウドの両方に効きます。「CI の定義が YAML とローカルで二重管理」から解放されるのが、地味ながら大きなメリットです。

ハマりポイント

  • bin/ci が無い:8.1 に上げただけでは入らないことがある。bin/rails app:update で config/ci.rb と bin/ci を受け入れる
  • ステップのコマンドが実在しない:config/ci.rb は bin/rubocop のような bin スタブ前提。無ければ bundle exec ... に書き換えるか、そのステップを消す
  • ローカルだけ・CI だけで落ちる:bin/ci は CI=true で走るので eager loading などが有効。development で気づけなかった読み込みエラーがここで出ることがある(=それを早く見つけられる、とも言える)

まとめ

Rails 8.1 の Local CI(bin/ci / config/ci.rb)は、「CI で回している一式を、プッシュ前に手元で一発実行する」ためのシンプルな仕組みです。ポイントを整理します。

  • config/ci.rb に step を並べ、bin/ci で実行する。CI の「正」が1か所に集約される
  • ステップの実体はただのシェルコマンド。テスト・Lint・セキュリティチェックを自由に足せる
  • 独立したチェックは group ..., parallel: 2 で並列にして時短できる
  • GitHub Actions は捨てずに、同じ config/ci.rb を走らせる最終ゲートとして併用する

まずはテスト1本から config/ci.rb に書いて、bin/ci を叩いてみてください。「プッシュ前に自分でゲートをくぐる」習慣がつくと、レビューも CI も驚くほどスムーズになります。

ローカルで CI が緑になったら、次はデプロイです。Rails 8 × Kamal 2 で AWS にデプロイする手順で、テストを通したコードを本番へ届けるところまでつなげてみてください。CI のステップが赤くなって原因が分からないときは、エンジニアの「調べ方」の記事も役に立つはずです。

スポンサーリンク

  • このエントリーをはてなブックマークに追加