こんにちは、技術本部 Eight Engineering Unit でエンジニアをやっている茂木です。
普段はSansan株式会社で正社員としてフルコミットしつつ、情報系の大学に通っています。
今回は Reviewdog x RuboCop を使って、ローカル環境で Lint をかける機構を Eight に導入した話をしていきます。
目次
背景
Eight では、コードの品質を保ち、バグや不具合を未然に防ぐために静的解析ツール RuboCop を導入しています。Pull Request が作成されると、GitHub Actions を用いた CI が自動的に実行され、RuboCop による静的解析が行われます。違反があった箇所にはコメントが付けられ、開発者はそれをもとに修正を行うことができます。
ただし、開発の初期には RuboCop が導入されていなかったこともあり、それ以前のコードには多くの違反が含まれています。これら全てを一度に修正するのは非常に困難なため、CI に Reviewdog を導入しています。Reviewdog を使用すると、コードの差分に対してのみ Lint をかけたり、脆弱性診断をしたりすることができます。Eight でもこの機能を利用して、ファイル内の変更箇所に含まれるエラーのみに対して警告が出るように設定しています。
しかし、ローカル環境では Reviewdog を導入していないため、ファイル全体に対して RuboCop の静的解析を行うしかありません。これにより、変更箇所に含まれる警告を見落としてしまった場合、CI でエラーが発生し、修正してから CI を回し直さなければなりません。
そこで、この問題を解決するために、ローカル環境でも変更箇所のみに対して警告を出す仕組みを導入することにしました。これにより、開発者はローカル環境での開発段階で違反箇所を確認し、修正することができるようになります。
要件方針
Eight はモノレポ構成を取り入れており、フロントエンドとバックエンドの両方を含んでいます。フロントエンドでは、既に Husky を使用した pre-commit フックが導入されていました。バックエンドにおいても Husky の利用を検討しましたが、「トップディレクトリに package.json を追加したくない」、「バックエンド専任のエンジニアが Yarn や Node.js に依存せずに開発できるようにしたい」という思いがありました。そのため、最終的には独自のシェルスクリプトを作成し、これを使ってフロントエンドとバックエンドの両環境で pre-commit フックを実行するようにしました。
実装
Git フックは以下のような流れで行われます。
git commit ↓ [確認] front/ 以下に変更はあるか? │ ├─ はい → [実行] frontend hooks を呼ぶ │ └─ いいえ → [確認] ruby関連ファイルに差分はあるか? │ ├─ はい → [実行] backend hooks を呼ぶ │ └─ いいえ → コミットする
これにより、開発者が行った変更に合わせて、適切なフックを実行してくれるようになります。
このフックを定義したシェルスクリプトのファイルは以下の通りです。
#!/bin/sh set -eo pipefail frontend_hook_run=false backend_hook_run=false function call_frontend_hooks { if [ "$frontend_hook_run" = false ] ; then echo "Running frontend hooks..." front/script/hooks/pre-commit frontend_hook_run=true fi } function call_backend_hooks { if [ "$backend_hook_run" = false ] ; then echo "Running backend hooks..." .gitconfig/serverside_hooks/pre-commit backend_hook_run=true fi } git diff --cached --quiet -- front || call_frontend_hooks git diff --cached --quiet -- '*.rb' '*.rake' '*.ru' '*.jbuilder' Gemfile Guardfile Capfile .irbrc || call_backend_hooks
上記のシェルスクリプトでは、git diff
で差分を確認し、その中に '*.rb' '*.rake' '*.ru' '
など Ruby や Ruby on Rails に関係するファイルがあれば、バックエンドのフックを呼び出します。
また front/
以下に変更があれば、フロントエンドのフックを呼び出します。
コミットした際に複数ファイルあった場合は、そのファイル数分実行されてしまうため、実行後 hoge_hook_run
フラグを true
にすることで重複実行を防いでいます。
次にフックの内部の挙動を紹介しますが、フロントエンドのフックの内容は今回のスコープ外のため割愛させていただきます。
バックエンドのフックは以下のように設定をしています。
#!/bin/sh set -eu # localにreveiwdogが存在しない場合, rubocopを実行せずに終了する if ! which reviewdog &> /dev/null then echo "reviewdog could not be found. Please install it first." exit 1 fi echo 'Running rubocop with reviewdog 🐶 ...' git diff --cached --name-only \ | xargs bundle exec rubocop --force-exclusion \ | reviewdog -f=rubocop -diff='git diff --cached' -filter-mode=diff_context -fail-on-error
Reviewdog のコマンドについて簡単に説明します。
| reviewdog -f=rubocop -diff='git diff --cached' -filter-mode=diff_context -fail-on-error
:-f=rubocop
は、入力フォーマットとしてRuboCopを指定します。-diff='git diff --cached'
は、どの差分をReviewdogがチェックすべきかを指定します。-filter-mode=diff_context
は、diffのコンテキストに基づいてフィルタリングするモードを指定します。-fail-on-error
は、エラーがあった場合にコマンドが失敗終了するようにします。
導入後の効果
Eight では新たに自作のシェルスクリプトによる Git フックの仕組みを導入しました。この変更によって、Pull Request 上での指摘が大幅に減少し、開発プロセスの速度と品質が向上しました。最初は実行速度についていくつか懸念がありましたが、実際には特に遅延は発生していません。
実行速度を実際に計測してみても、コミットにかかる時間は1.5秒ほどでした。
git com -m 'test' 0.58s user 0.46s system 65% cpu 1.575 total
導入後の開発体験について現場社員にヒアリングした意見を一部紹介します。
「RuboCop の指摘が CI での Reviewdog の指摘と一致しているので、VSCode では気づかずにコミットしてしまい、CI でエラーが出て修正し、CI を再度最初から回すという煩わしさが完全になくなりました!」
「この仕組みを導入しても速度や使い心地が損なわれることはありませんでした!」
このように、実際のユーザー体験からも、この新しい仕組みの導入がポジティブな影響を与えていることが分かります。
まとめ
pre-commit フックでの RuboCop チェックを導入したことで、開発者体験を向上させることができました。 こういった社内向けツールは直接フィードバックを聞くことができるので、モチベーションにも繋がりますね。
今後は RuboCop 導入以前のコードに含まれる違反にも対応し、最終的に RuboCop の違反を0にしていきたいと思います!