💬

なめらかに行うRubocopのメジャーバージョンアップ | Offers Tech Blog

2022/11/28に公開

はじめに

はじめまして、Offers を運営している overflow さんで業務委託として参画している teitei_tk と言います。

今回はタイトルにもありますが、RuboCop を動かし、同時にメジャーバージョンアップをおこなった話を書きます。

RuboCopの重要性について

Ruby に関わっている人について RuboCop の重要性を解くのは釈迦に説法かと思いますが、なぜ行ったかを書くと、Ruby のバージョン更新を行いました、そこで RuboCop の静的解析、不具合の検出。フォーマットの統一を行いたいというモチベーションがありました。他にも rubocop-railsrubocop-performance をはじめとした有益な extension を利用したい意図もありました。

前提条件

私は現在フルタイムで働いており、overflow さんのタスクは業務後の数時間しかできません。なので、エラーが出ているがえいやと一気に更新し後でよしなに行うということはできません。方針としてプロダクト開発に影響が出ないように行いました。具体的な話は以下に続きます。

止まっていたRuboCopを動かした話

私が参画した当時 rubocop.yml は存在するが、設定エラーで動いておらず CI にも組み込まれていない状態でした。これは当時少数で開発しておりそこまで手が回っていないという状態でした。

1. 設定エラーの修正

まず行ったことは設定エラーを修正することから始めました。幸いにも各 Cop の設定は既に定義されていたのでエラーになっている箇所を修正するだけで済みました。この段階ではCIには組み込みません。徐々にならしていきます。

2. テストファイルへの適用

次に行ったことはテストファイルへの適用でした。app/ 以下の警告数が膨大だったもありますが、自動修正があるとはいえ長大な PR を見てもらうのはレビュワーへの負荷が大きく、プロダクト開発の片手間で見てもらうには厳しいと考えました。

まずは数と影響範囲も少ない spec/ 以下のコードを修正しました。ここで有用な Cop を有効にしたりと準備を整えました。

3. app/ 以下への適用

テストファイルへの適用で準備を整えたので、いよいよ app/ 以下に適用をさせます。
が、規模が大きくここも一度にえいやとやるわけにもいきません。

そこで app/ 以下を一度に適用するのではなく、ディレクトリ単位に PR を作成することにしました。
理由としてはビッグバンリリースを避けるとともにレビュワーへの負担を減らす意図があります。
レビュワーは普段プロダクトの開発をおこなっており、そこに RuboCop のビッグバン PR のレビューをお願いするのは負担が大きすぎます。ディレクトリ単位にすることでその負担を減らすようにしました。app/models といったファイル数が大きいものに関してはディレクトリ単位でも規模が大きいので、さらに細かくしています。

4. CIへの組み込み

最後に CI へ組み込みを行いました。これで全てのファイルが RuboCop の対象になりました。が、まだ問題点があります。

メジャーバージョンアップを行う

CI へ組み込んだのは良いのですが、0.x 系を利用しており今後更新が見込めないこと、新しい Cop が利用できないという問題点があり 1 系にメジャーバージョンアップを行う必要がありました。

プロダクト開発を行いつつ 1 系に更新する必要があります。1 系に更新しつつ new cop を disable にするなど、いくつかやり方はあると思いますが今回は方針のようにプロダクト開発に影響が出ないことを意識し、プロダクト開発は RuboCop 0 系、CI では 0 系・1 系の両方を CI で動かすようにしました。

1. RuboCop 1系用のGemfileを用意する

当たり前ですが、1 つの Gemfile に複数の RuboCop のバージョンを利用することはできません。ここでは新たに Gemfile, Gemfile.lock をコピーし RuboCop 専用のファイルにすることで対応しました。
当時知りたかった知識ですが、Bundler では BUNDLE_GEMFILE という環境変数を利用することで Bundler が利用する Gemfile を指定できます。

https://ruby.studio-kingdom.com/bundler/bundle_config/#list_of_available_keys

さらに eval_gemfile File.expand_path(File.join(File.dirname(__FILE__), '../Gemfile')) と eval を行うことで Gemfile を読み込みつつ、別の RuboCop のバージョンを利用できます。

eval_gemfile File.expand_path(File.join(File.dirname(__FILE__), '../Gemfile'))

dependencies.delete_if {|dependency| [
  "rubocop",
  "rubocop-rails",
  "rubocop-performance",
  "rubocop-rspec",
].include?(dependency.name)}

gem('rubocop', tag: 'v1.37.1')
gem('rubocop-rails')
gem('rubocop-performance')
gem('rubocop-rspec')

https://qiita.com/yohm/items/d32f01ac5a8e1d612d92

2. RuboCop 1系用のdocker-compose.ymlを用意する

次に手元でテストをしやすくするため、CI で動かすためにコンテナ環境を構築しました。Offers では CI 用の compose ファイルが存在しているので、そちらをベースに作成しました。

3. GitHub Actionsに組み込む

docker compose コマンド 1 つで環境が立ち上がるようになったのでこれを CI に組み込みます。
ただ動かすだけでは全ての CI でエラーになります。そこで以前のようにディレクトリ単位に 1 系を適用させつつ、適用させていないディレクトリは無視するようにします。

Offers の開発では GitHub Actions を利用しているので path-ignore に記載しつつ、ディレクトリへの適用が終わり次第記述を削除することで、0 系、1 系を並列で動かすようにしました。

    paths-ignore:
      - "app/controllers/**"
      - "app/models/**"
      - "app/views/**"
      - "spec/**"

4. GemfileのRuboCopを、RuboCop 1系のバージョンへ更新する

終わりに Gemfile の各種 RuboCop のバージョンを 1 系に更新し、compose ファイル、Gemfile を削除して終了です。

最後に

おそらく他に良いやり方はあるのかもしれませんが、今回はプロダクトの開発を止めないことを重点に更新を行いました。何か参考になりましたら幸いです。

関連記事

https://zenn.dev/offers/articles/20220707-develop-issues-part1
https://zenn.dev/offers/articles/20221107-github_actions_frontend_build_test

Offers Tech Blog

Discussion