Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

reviewdog x perlcritic x Jenkins で最高の GitHub レビューライフ

ミラティブのサーバーサイドエンジニア、ハトネコエです!

今日は、GitHub の自動レビューとして reviewdog を導入した話をします。

1. 動機

すでに CTO の夏さんによって、Perl 用の linter である perlcritic が導入されていました。
そして、テストが走る際に perlcritic のチェックもおこなわれ、
指摘箇所があればテストが落ちるようになっていました。

まずは緩めの設定で導入したけれど、perlcritic のチェックをもっと厳しくしたい!
だけど厳しくすると、すでに存在するコード(つまり、プルリクで変更していない部分)が原因で
テストが落ちるようになってしまいます。

残念ながら perlcritic には autofix の機能も無いようで、
設定を変更した後は、人力での修正が終わるまでテストは落ち続けてしまいます。

これでは開発に支障が出るので、
テストでのチェックはやめて、プルリクで変更した部分にだけ指摘がなされる手段が求められました。

2. Danger か reviewdog か

最初に話題に上がったのは Danger でした。

f:id:nekonenene:20190706005348p:plain
CTOの夏さんとの会話(in 私のtimes)

Danger は自動レビューの手段として良いツールです。

Android, iOS, Ruby, JavaScript といった Danger のプラグインが存在するコードベースであれば良い選択でしょう。
しかし今回は Perl。Danger のプラグインは存在しません。
作ってもいいですが、あまり時間をかけたくもありません。

そこで見つけたのが reviewdog です。

reviewdogによるGoのコードレビュー - DeNA Testing Blog

こちらのブログがきっかけになりました。ありがとうございます。

3. reviewdog を選んだ理由

3-1. 比較

両者を比べると以下の違いがありました。

reviewdog Danger
スター数 (2019/07/13) 979 3573
コミット頻度 高い 高い
言語 Go Ruby
コメント 修正すべき行にコメント まとまった1つのコメント
(or 修正すべき行にコメント*1
ドキュメント README.md で充分わかる 情報が散乱していて読みにくい

ドキュメントの読みやすさは主観ですが、
Danger のドキュメントの読みにくさは伝わる……はず……!

reviewdog の方がスター数は少ないですが、
それを補うほど優れた点が複数ありました。

3-2. reviewdog は Golang

まず Go 言語であることに惹かれました。
というのも、最近は bundle install 時に起こるエラーに辟易していて、
できれば Gemfile の管理はしたくないな……と思っていたためです。
bundler のバージョンが v2 になってから、まだ落ち着いていないようにも思えますし。

Go はもう少し単純で、かつ v1.13 でよりモジュール管理が簡潔になる予定です
その上、シングルバイナリでアプリケーションを配布することが可能です。

reviewdog もバイナリを配布していますので、
使用者は Go のインストールをすることなく reviewdog を扱えます。

Danger の場合は ruby(&たいていは rbenv)のインストールが必要になりますが、それらが不要なわけです!
reviewdog のバイナリファイルをダウンロードしてくるだけでいいので、
Docker や Jenkins での実行準備がとても簡単におこなえます。

3-3. reviewdog の errorformat が最高!

もし Danger のプラグインを書くとするなら、GitHub コメントを作成するために、
以下のようにいい感じに lint エラーを整形してあげる必要があります。
https://github.com/loadsmart/danger-android_lint/blob/0.0.7/lib/android_lint/plugin.rb#L134-L157
これはなかなかに面倒です。

一方、reviewdog の場合、
errorformat が内蔵されていることにより、
lint エラーのメッセージ形式を渡してあげれば、あとはよしなに解釈して、修正すべき行にコメントを残してくれます。

reviewdog はとても簡単なのです!

perlcritic のような、Danger のプラグインがない linter を使っているのであれば、
reviewdog はとてもいい選択肢に思えます。

4. reviewdog を使ってみる

reviewdog の使い方については、前述したとおり本家の README.md で十分理解できますが、
ここでもいくらか書いておきます。

4-1. -reporter=github-pr-review

reporter オプションの選択肢は複数ありますが、GitHub を使っているのであれば
github-pr-review reporter を選ぶのが良いでしょう。

まず local reporter は、動作テスト用です。
local reporter の場合のみ、 diff オプションが必要なので注意しましょう。
linter でチェックしたうちの、 diff オプションで渡した部分の結果のみを出力します。

参考 : https://github.com/nekonenene/perlcritic_reviewdog

次に github-pr-check reporter ですが、これは見栄えとしてはいいのですが、
README にある通り、これを動作させるためのサーバーは作者の haya14busa さんのポケットマネーで動かしているので、安定した稼働は保証されていません。

また、アプリ連携によりリポジトリのコード閲覧権限を渡すので、
会社で扱う場合には良くない選択肢でしょう。

reviewdog のアプリ連携画面

というわけで、 github-pr-review reporter を使うことになります。

これの使用にあたっては、 https://github.com/settings/tokens から作成できる
GitHub の Personal access token が必要になります。

公開リポジトリなら public_repo のみにチェック、
非公開リポジトリなら大項目の repo の方にチェックを入れて Token を作成します。

f:id:nekonenene:20190713220213p:plain
公開リポジトリなら public_repo のみにチェック

reviewdog のコメントは、この Personal access token の持ち主からおこなわれる形になるので、
reviewdog 用のアカウントがあると見栄えは良いかもしれません。

reviewdog 用のアカウントを作らないと、自分のプルリクにコメントしまくってる人みたいに…

4-2. 必要な環境変数

-reporter=github-pr-review の使用にあたってはたくさんの環境変数が必要になります。

  • REVIEWDOG_GITHUB_API_TOKEN : 上記 4-1. で説明した Personal access token
  • CI_REPO_OWNER : リポジトリの管理ユーザー名(GitHub ID)
  • CI_REPO_NAME : リポジトリ名
  • CI_PULL_REQUEST : プルリクエスト番号
  • CI_COMMIT : 対象ブランチの最新コミットID( git rev-parse HEAD で取得可能)

サポートされているCI(2019/07/13 現在は Travis CI, Circle CI, GitLabCI)であれば Personal access token だけ渡してあげればいいのですが、
ローカルからの実行や Jenkins からの実行に関しては、これらを全て埋めてあげる必要があることに注意しましょう。

4-3. -efm=%f:%l:%c:%m

efm オプションは大事です。が、
perlcritic の場合は出力形式をいじれるので、どちらかと言えば perlcritic 側の出力形式をいじることが大事になるでしょう。
verbose オプションですね。

%f:%l:%c については、reviewdog がどこにコメントすればいいかを示します。
%m はコメントの内容ですので、ここが工夫のしどころです!

最終的には以下のようになりました。

perlcritic --profile .perlcriticrc --verbose '%f:%l:%c:**%m**, near <code>%r</code>.<br>(Ref: [%p](https://metacpan.org/pod/Perl::Critic::Policy::%p))\n' | reviewdog -efm=%f:%l:%c:%m -name=perlcritic -reporter=github-pr-review

Markdown を使いつつ、バッククォート ` はシェル側で解釈して処理されてしまうので、
HTML タグを使うことで避けています。

指摘内容のリファレンスに簡単に飛べるようにしたのは良い工夫だったと思います。

5. Jenkins で使う際の工夫

上の 4-2. で環境変数をたくさん埋めなければいけないと書きましたが、
Jenkins の場合はどうすればいいのかについて触れておきましょう。

5-1. REVIEWDOG_GITHUB_API_TOKEN

秘匿情報であるトークンをどう扱うか、
ここが頭を悩ませるところだと思います。

Jenkins の Credentials (認証情報)を使います。

<your_jenkins_host>/credentials/store/system/domain/_/newCredentials の URL から、
『Secret text』として API Token を追加します。

f:id:nekonenene:20190713223618p:plain
認証情報の追加

Jenkinsfile からは以下のように扱います。

withCredentials([string(credentialsId: "reviewdog", variable: "githubToken")]) {
  env.REVIEWDOG_GITHUB_API_TOKEN = githubToken
  def reviewdogResult = sh(
    // ※ブログ公開用にコマンドは実際と変えてあります
    script: "perlcritic --profile .perlcriticrc --verbose '%f:%l:%c:**%m**\n' | reviewdog -efm=%f:%l:%c:%m -name=perlcritic -reporter=github-pr-review",
    returnStdout: true
  ).trim()
  env.REVIEWDOG_GITHUB_API_TOKEN = "" // token が環境変数として残るのを避けるため、空文字を再代入(unset は効かない)
  echo "${reviewdogResult}"
}

withCredentials スコープ外では値がマスクされないため、
githubToken を代入した env.REVIEWDOG_GITHUB_API_TOKEN の値を
env.REVIEWDOG_GITHUB_API_TOKEN = "" で初期化しているのがポイントです。

f:id:nekonenene:20190713224534p:plain
Credentials の値がマスクされている様子

これをやらないと、この後ろで sh "env" なんかがあると、トークンの値が平文で表示されてしまいます。

5-2. CI_REPO_OWNER, CI_REPO_NAME

リポジトリオーナー、リポジトリ名が変わることはめったにないので直接書いても良かったのですが、
汎用性を持たせるため Jenkins の環境変数である JOB_NAME を活用しました。

def jobNameArr = env.JOB_NAME.split("/")
env.CI_REPO_OWNER = jobNameArr[0]
env.CI_REPO_NAME = jobNameArr[1]

5-3. CI_PULL_REQUEST

Jenkins の環境変数 CHANGE_ID がプルリクエストの ID ですので、それをそのまま使用しました。

env.CI_PULL_REQUEST = env.CHANGE_ID

なお、プルリクエストに紐付かない Jenkins ビルドでは env.CHANGE_ID の値は null になっています。

5-4. CI_COMMIT

以下のように取得しました。

env.CI_COMMIT = sh(returnStdout: true, script: "git rev-parse origin/${env.BRANCH_NAME}").trim()

単純に git rev-parse HEAD となっていないのには理由があります。

私達は Jenkins のビルド設定に関し、安全のために
『Discover pull requests from origin』の設定で
『Merging the pull request with the current target branch revision』を選んでいます。

f:id:nekonenene:20190713230653p:plain
Merging the pull request with the current target branch revision

これにより CI は、 checkout scm 内での作業として、
指定ブランチのリポジトリをクローンした直後、
プルリクの向き先(たいていは master)ブランチをマージします。

そのため、HEAD のコミットIDが、プルリク上の最新コミットIDと一致しなくなってしまうのです。
これを、 origin/${env.BRANCH_NAME} のコミットIDを見に行くことにより避けています。

プルリクの最新コミットIDと env.CI_COMMIT の値が一致しない場合、
422 Unprocessable Entity エラーが返ってきてしまうので注意しましょう。

ここはハマりポイントだと思います。

6. そして開発はさらなる高みへ

こうして reviewdog の導入を果たした私達は、
「自分の通った道をよりきれいに」のボーイスカウト精神を意識しながら、
より一貫性のあるコードへ既存コードを整えていくのでした。

最終的に perlcritic の severity を夏さんが 1 (一番厳しいやつ)にしたのは驚きました(笑)

整えていくぞ〜〜!!

というわけで、Perl を使いつつも、誰もが読みやすいコードを目指して日々精進している私達に加わりたい方、
大募集中です!! 一緒に輝きの向こう側を目指しましょう!

一部 Go 言語への移行を少しずつ進めている箇所もあるので、
Go 言語やアーキテクチャーについての知見をお持ちの方も大歓迎です!

ご応募はこちらから〜

www.wantedly.com

まだまだサーバーサイドのメンバーは少ないですので、
「設計も改善も任せろ〜〜!」という方、ぜひぜひご応募ください!!

*1:プラグインによりますが、たいていのプラグインは inline mode をオプションとしてサポートしています