RubocopでPullRequestの変更箇所で問題があった場合のみCIを失敗させる
RubocopというRubyの文法チェッカがありますが、
これをCircleCI等のCIツールで実行してチェックしている人も多いと思います。
Rubocop:
https://github.com/bbatsov/rubocop
Rubocopに限らずですが、
このような静的チェックツールは、既存のリポジトリに追加しようとすると、
既存コードに対するエラーが存在しているため。
開発を一度止めて全体のリファクタリングとテストを行う、
除外指定をして既存コードを無視して適用するなどの対応が必要になり。
なかなか、導入に踏み切れないこともあると思います。
このエントリでは、
既存プロジェクトに対してスムーズに導入する方法として。
PullRequestの新規or修正箇所でエラーがあった場合のみ、
(既存コードは無視して)CIでエラーとなるようにする方法を提案します。
# ちなみにRubocopに限らず、どんなツールでも同じ方法が使えるはずです。
railsのプロジェクト作成から、順に流れを追って説明していきます。
プロジェクト作成とrubocop導入
rails new
したプロジェクトのGemfile
に、
以下のようにrubocopを追加、bundle install
します。
Gemfile抜粋
group :development do
gem 'rubocop'
end
gemのインストール
$ bundle install --path vendor/bundle
次にrubocopを実行します。
$ bundle exec rubocop
Inspecting 37 files
.CCCC.CC..CCC.C.CC..CCCC.CC..C..C...C
Offenses:
※途中省略※
db/seeds.rb:6:81: C: Metrics/LineLength: Line is too long. [81/80]
# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
^
37 files inspected, 52 offenses detected
たくさんエラーが検出されますが、一旦無視してgit commit & pushします。
$ git commit -a -m "add rubocop"
$ git push origin master
これでrubocopでエラーになる既存コードが存在するリポジトリが出来ました。
新規or修正箇所のエラー抽出スクリプト作成
ブランチを切って、修正を加えます(scaffoldで適当に)。
$ git checkout -b add_scaffold
$ bundle exec rails g scaffold note titile:string body:string
rubocopを実行してみると、(既存と併せて)76エラー検出しました。
$ bundle exec rubocop
Inspecting 47 files
.CCC.C.CC.C.CC...CCCCC.C.CC..CCCC.CC..C..C...CC
Offenses:
※途中省略※
db/migrate/20180129154129_create_notes.rb:1:1: C: Style/Documentation: Missing top-level class documentation comment.
class CreateNotes < ActiveRecord::Migration[5.1]
^^^^^
47 files inspected, 76 offenses detected
git add & commitします。
$ git add .
$ git commit -a -m "add scaffold"
次に新規or修正箇所のエラーのみ抽出するシェルスクリプトを作成します。
bin/diff_filter.sh
#!/bin/bash
BASE_REMOTE=origin
BASE_BRANCH=master
git fetch $BASE_REMOTE $BASE_BRANCH
git branch $BASE_BRANCH $BASE_REMOTE/$BASE_BRANCH
diff_list=()
commit_list=`git --no-pager log --no-merges $BASE_REMOTE/$BASE_BRANCH...HEAD | grep -e '^commit' | sed -e "s/^commit \(.\{8\}\).*/\1/"`
for f in `git --no-pager diff $BASE_REMOTE/$BASE_BRANCH...HEAD --name-only`; do
for c in $commit_list; do
diffs=`git --no-pager blame --show-name -s $f | grep $c | sed -e "s/^[^ ]* *\([^ ]*\) *\([0-9]*\)*).*$/\1:\2/"`
for ln in $diffs; do
diff_list+=( $ln )
done
done
done
err_count=0
while read -r ln; do
for m in ${diff_list[@]}; do
if [[ ${ln} =~ ^$m ]]; then
echo $ln
err_count=$((err_count+1))
break
fi
done
done < /dev/stdin
if [ $err_count -ne 0 ]; then
echo -e "\033[0;31mERROR FOUND, check above messages.\033[0;39m"
exit 1
fi
rubucopの出力をこのシェルスクリプトでフィルターして、
新規or修正部分のエラーに絞り込みます。
$ bundle exec rubocop | bash bin/diff_filter.sh
※途中省略※
test/controllers/notes_controller_test.rb:41:8: C: Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
db/migrate/20180129154129_create_notes.rb:1:1: C: Style/Documentation: Missing top-level class documentation comment.
ERROR FOUND, check above messages.
新規or修正箇所に対するCircleCIでのrubocop実行
後は、このシェルスクリプトをCircleCIで呼び出すだけです。
CircleCIのconfig.ymlを以下のように追加します。
.circleci/config.yml
version: 2
jobs:
build:
docker:
- image: circleci/ruby:2.4.1-node-browsers
working_directory: ~/repo
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "Gemfile.lock" }}
- v1-dependencies-
- run:
name: install dependencies
command: |
bundle install --jobs=4 --retry=3 --path vendor/bundle
- save_cache:
paths:
- ./vendor/bundle
key: v1-dependencies-{{ checksum "Gemfile.lock" }}
# Database setup
- run: bundle exec rake db:create
- run: bundle exec rake db:schema:load
# run rubocop
- run:
name: run rubocop
command: |
bundle exec rubocop | bash bin/diff_filter.sh
このファイルをGitHubにpushします。
$ git add .
$ git commit -a -m "add circleci"
$ git push origin add_scaffold
CircleCIを設定すると、以下の画面のようにCIが失敗します。
また、ここでエラーとなっている部分を修正すれば、
既存コードに問題があっても、CIは成功するようになります。
以上のような方法をとると、
既存プロジェクトに対しても、静的チェックツールが導入しやすくなります。
また、除外指定してしまうと、修正するタイミングを逃してしまうことも多いですが。
修正箇所に対してチェックすると、
改修のタイミングでリファクタリング出来るというメリットもあるかと思います。