Danger + ktlintでPull RequestのKotlinコードスタイルを自動チェックさせる (Travis CI編)

# TL;DR

- GitHubのPull Request (PR) を作成したときに、PRのDiffに含まれるKotlinコードのスタイルを自動的にチェックして、問題があればPRに自動的にコメントさせる方法の説明です
- 以下のツールを組み合わせて使っています
    - [ktlint](https://ktlint.github.io/): KotlinコードのLinter
    - [Danger](http://danger.systems/ruby/): コードレビューbotのフレームワーク
- サンプル: [Pull Request #3 · kafumi/android-danger-ktlint-sample](https://github.com/kafumi/android-danger-ktlint-sample/pull/3)
  [<img width="793" src="https://qiita-image-store.s3.amazonaws.com/0/80557/f7df4151-fe6e-aa11-ba5a-d8be3d16d3bd.png">](https://github.com/kafumi/android-danger-ktlint-sample/pull/3)

# 導入のきっかけ

- Android Studioでフォーマッターをかける (Command + Alt + L) ことをチーム内のルールにして、コードスタイルの統一を図っているのですが、ただ、そこは悲しい人の性、フォーマッターをかけ忘れたコードがときどきコミットされて、コードレビュー時に指摘されたりします
    - コードスタイルに対して神経質なレビュワーは、レビュー対象コードをチェックアウトして、Android Studioでフォーマットし、差分がでないことを確かめたりしてます (私です)
- コードスタイルなど、本質的でないことに対するレビューの労力は極力減らしたい！
- 最近よく目にする [Danger](http://danger.systems/ruby/) を使ってみたら、コードスタイルのチェックが自動化できて幸せになりました

# 仕組みの全体像

1. GitHubでPRを作成したとき、Travis CIのビルドが走る
1. Travis CIのビルドの中で、ktlintの実行、Dangerの実行を行う
    1. ktlintはGradleから実行する ([ktlint-gradle](https://github.com/JLLeitschuh/ktlint-gradle) プラグインを使う)
    1. ktlintが出力する [Checkstyle](http://checkstyle.sourceforge.net/) 形式のレポートを、Dangerが [danger-checkstyle_format](https://github.com/noboru-i/danger-checkstyle_format) プラグインを使って読み込む
    1. Dangerが、ktlintが報告した警告をGitHubのPRにコメントする

# Dangerとは

- http://danger.systems/ruby/
- コードレビューbotのフレームワーク
- Ruby製で、多くのCIシステム、コードホスティングサービスをサポートしている
- プラグイン方式をとっており、プラグインによって様々な機能を追加できる
    - この記事では説明しませんが、[Android Lint](https://developer.android.com/studio/write/lint.html) を実行するためのプラグイン [danger-android_lint](https://github.com/loadsmart/danger-android_lint) もあります
- 参考
    - [Android開発のコードレビューbotを乗り換えた話 - クックパッド開発者ブログ](http://techlife.cookpad.com/entry/2017/06/28/190000)
    - [コードレビューをより良くする Danger x Android](https://www.slideshare.net/tarappo/danger-x-android)

# ktlintとは

- https://ktlint.github.io/
- KotlinコードのLinter
- Lint結果を様々な形式で出力できる (プレーンテキスト, JSON, Checkstyle XMLなど)
- フォーマッターが組み込まれており、コードの自動フォーマットも可能
- 様々なインストール方法・実行方法のオプションがある
    - [`ktlint` コマンドをコマンドラインで実行する](https://github.com/shyiko/ktlint#installation)
    - [Mavenに組み込んで実行する](https://github.com/shyiko/ktlint#-with-maven)
    - [Gradleに組み込んで実行する](https://github.com/shyiko/ktlint#-with-gradle)
        - タスクを定義するのが割と面倒なので [ktlint-gradle](https://github.com/jlleitschuh/ktlint-gradle) プラグインや [kotlinter-gradle](https://github.com/jeremymailen/kotlinter-gradle) プラグインを使うのが吉

# 設定手順

## 環境

- 今回は、筆者が実際に使っている環境を例に説明します
    - CIシステム: [Travis CI](https://travis-ci.org/)
    - コードホスティングサービス: [GitHub](https://github.com/)
    - Publicレポジトリで説明していますが、Privateレポジトリでもほぼ同じ方法で設定できます 
        - Privateレポジトリの場合は、botアカウントがPrivateレポジトリにアクセスできるように、botアカウントの権限設定を少し変える必要があります (「GitHubアクセストークンの発行」のセクションで後述)
- DangerがサポートしているCIシステム、コードホスティングサービスなら、どの組み合わせでも設定可能です
    - Dangerがサポートしているシステムは、[Danger Webサイト](http://danger.systems/ruby/)の "WHAT MAGIC IS THIS" - "SUPPORTS" に列挙されています

## Travis CIの準備

まずは、GitHubでPull Requestが作成されたときに、[Travis CI](https://travis-ci.org/) でビルドを走らせる必要があります。

基本的には、[Androidプロジェクトビルド用の `.travis.yml` ファイルを作成して](https://docs.travis-ci.com/user/languages/android/)リポジトリにコミットしておき、Travis CIの設定画面で当該リポジトリに対するビルドを有効にすればOKです。`.travis.yml`は、典型的には次のようなファイルになります。

```yaml
# .travis.yml

language: android 
branches: 
  only: 
    - master 
android: 
  components: 
    - tools 
    - platform-tools 
    - build-tools-26.0.2 
    - android-26 
    - extra-android-m2repository 
script: 
  - ./gradlew assembleDebug 
before_cache: 
  - rm -f  $HOME/.gradle/caches/modules-2/modules-2.lock 
  - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 
cache: 
  directories: 
    - $HOME/.gradle/caches/ 
    - $HOME/.gradle/wrapper/ 
    - $HOME/.android/build-cache 
```

この `.travis.yml` には、まだ、Dangerの実行は含まれていません。Dangerの実行を追加する方法は、この後のセクションで説明します。

もし、Travis CIを使ったことがなく、上記のYAMLファイルが何を設定しているのかわからない場合は、Travis CIの次のDocumentページを参考にしてみてください。この3つを読めばだいたい分かるようになると思います。

- [Getting started - Travis CI](https://docs.travis-ci.com/user/getting-started/)
- [Customizing the Build - Travis CI](https://docs.travis-ci.com/user/customizing-the-build/)
- [Building an Android Project - Travis CI](https://docs.travis-ci.com/user/languages/android/)

## ktlintの設定

次に、[ktlint](https://ktlint.github.io/) の設定を行います。Androidプロジェクトに対してktlintを実行する場合、主に次の方法があります。

- コマンドライン
    - [`ktlint`コマンドをダウンロードして使う](https://github.com/shyiko/ktlint#installation)
- Gradle
    - [Gradleの依存関係にktlintを追加して、ktlintを実行するGradleタスクを自分で定義する](https://github.com/shyiko/ktlint#-with-gradle)
    - [ktlint-gradle](https://github.com/jlleitschuh/ktlint-gradle) プラグインや [kotlinter-gradle](https://github.com/jeremymailen/kotlinter-gradle) プラグインを使う
        - これらのプラグインは、ktlintを実行するGradleタスク定義を自動的に行なってくれます

Gradleで完結していると実行環境の準備が楽チンなので、Gradleを使う方法、特に、Gradleプラグインを使う方法がおすすめです。この記事では、[ktlint-gradle](https://github.com/jlleitschuh/ktlint-gradle) プラグインを使います。

### ktlint-gradleプラグインの導入

[JLLeitschuh/ktlint-gradle](https://github.com/jlleitschuh/ktlint-gradle) の [How to use](https://github.com/jlleitschuh/ktlint-gradle#how-to-use) に書かれているとおり、`ktlint-gradle` をGradleの依存先に追加し、`apply plugin` すれば、`ktlint-gradle` プラグインが導入できます。下の例では、2017年12月10日現在で最新の `2.3.0` を使用しています。プラグインの最新バージョンは、[Gradle Plugin Registry](https://plugins.gradle.org/plugin/org.jlleitschuh.gradle.ktlint) で確認できます。

```groovy
// build.gradle

buildscript {
    repositories {
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath "gradle.plugin.org.jlleitschuh.gradle:ktlint-gradle:2.3.0"
    }
}
```

```groovy
// app/build.gradle

apply plugin: "org.jlleitschuh.gradle.ktlint"
```

### ktlint-gradleプラグインの設定

次に、Dangerと組み合わせて使うために、[Configuration](https://github.com/jlleitschuh/ktlint-gradle#configuration) で説明されている方法を使って `ktlint-gradle` プラグインの設定を変更します。また、あわせて、[Android Kotlin Style Guide](https://developer.android.com/kotlin/style-guide) に準拠したLintを行うように設定します。

(**2017年12月10日編集**: `ktlint-gradle` プラグインのバージョンアップ2.1.1 → 2.3.0にあわせて、設定内容を変更しました)


```groovy
// app/build.gradle

ktlint {
    version = "0.14.0"
    android = true
    reporter = "checkstyle"
    ignoreFailures = true
}
```

- `version`: 利用するktlintのバージョンを指定します。`ktlint-gradle` プラグインの2.3.0は、デフォルトではktlintの [0.9.2](https://github.com/shyiko/ktlint/blob/master/CHANGELOG.md#092---2017-09-01) を使用するのですが、ktlint 0.9.2は [Android Kotlin Style Guide](https://developer.android.com/kotlin/style-guide) に準拠したLintをサポートしていません (ktlint [0.12.0](https://github.com/shyiko/ktlint/releases/tag/0.12.0) で追加された機能です)。[Android Kotlin Style Guide](https://developer.android.com/kotlin/style-guide) に準拠したLintを行いたい場合、0.12.0より新しいバージョンのktlintを使う必要があります。ここでは、2017年12月10日時点で最新の [0.14.0](https://github.com/shyiko/ktlint/releases/tag/0.14.0) を使うようにしています
- `android`: ktlintの `--android` オプションを有効にするかどうかを指定します。`--android` オプションが有効な場合、ktlintは [Android Kotlin Style Guide](https://github.com/shyiko/ktlint) に準拠したスタイルチェックを行うようになります
- `reporter`: レポートの出力形式を指定します。今回は、Dangerで扱えるようにCheckstyle形式の出力を得たいので、`checkstyle` を指定します
- `ignoreFailures`: 警告があってもビルドを継続するかどうかを指定します。`ktlint-gradle` は、デフォルト設定だと、ktlintからの指摘が1件でもあるとGradleタスクの結果を失敗として報告するようになっています (ビルドがエラーで止まります)。一般的に、コードスタイルに関する指摘があっただけでビルド失敗にしたくないと思うので、指摘があってもビルドを継続する (Gradleタスクは成功とする) ために、`true` を指定します

以上の設定を行なうと、Gradleに `ktlintCheck` と `ktlintFormat` というタスクが追加されます。

- `ktlintCheck`: ktlintによるKotlinコードのLintが実行される
    - Lintレポートは `app/build/reports/ktlint/ktlint-<sourceSet>.xml` に出力される
        - `main` ソースセットの場合、出力先は `app/build/reports/ktlint/ktlint-main.xml` となる
- `ktlintFormat`: ktlintによるKotlinコードの自動フォーマットが実行される
  - 本記事ではこちらのタスクは使っていません

## Dangerの設定

[Danger公式サイト](http://danger.systems/ruby/) の [Getting Set Up](http://danger.systems/guides/getting_started.html) に従って、Dangerを設定します。

なお、Gem `danger` をインストールした状態で `bundle exec danger init` を実行すると、テキストベースの設定ウィザードが実行できるのですが、[Getting Set Up](http://danger.systems/guides/getting_started.html) で説明されていることが表示されるだけなので、特に実行しなくて問題ありません (ただし、実行すると `Dangerfile` の雛形を作成してくれるという利点はあります)。

### `Gemfile` を作成する

DangerはRuby製であり、プラグインも含めた依存関係を `Gemfile` に定義し、[Bundler](http://bundler.io/) でインストールします。まずは、Androidアプリプロジェクトのルートディレクトリで `bundle init` を実行し、`Gemfile` の雛形を生成します。

```shell-session
$ bundle init
```

生成された `Gemfile` をエディターで開き、`#gem 'rails'` の部分を必要なGemで置き換えます。今回は、`danger` と `danger-checkstyle_format` の2つのGemを追加します。

```rb
# Gemfile

# frozen_string_literal: true
source "https://rubygems.org"

gem "danger"
gem "danger-checkstyle_format"
```

上記の編集が終わったら、`bundle init` を実行したのと同じディレクトリで `bundle install` を実行し、上記のGemをインストールします。

```shell-session
$ bundle install
```

### `Dangerfile` の作成

Dangerは、`Dangerfile` というファイルの内容にもとづいて検査を実行します。`Dangerfile` はRuby DSL形式のファイルで、DangerコアやDangerプラグインが提供するAPIを使って、実行内容を定義します。利用できるAPIや記述例は、以下のページで紹介されています。

- Dangerコアが提供する機能: [Danger - Reference](http://danger.systems/reference.html)
- Dangerプラグインが提供する機能: [Danger](http://danger.systems/ruby/) の "PLUGINS" セクション

今回は、ktlintの指摘を報告する他に、Pull Requestのタイトルに `[WIP]` が入っていたらコメントで警告する (WIP中なのがわかりやすくなる) ように設定してみます。`Gemfile` と同じディレクトリに、 以下の内容で `Dangerfile` というファイルを作成します。

```rb
# Ignore inline messages which lay outside a diff's range of PR
github.dismiss_out_of_range_messages

# Make it more obvious that a PR is a work in progress and shouldn't be merged yet
warn("PR is classed as Work in Progress") if github.pr_title.include? "[WIP]"

# ktlint
checkstyle_format.base_path = Dir.pwd
checkstyle_format.report 'app/build/reports/ktlint/ktlint-main.xml'
```

上記の定義の意味は次の通りです。

- `github.dismiss_out_of_range_messages` は、Pull RequestのDiff以外に対する指摘を無視する (PRに報告しないようにする) 設定です
- `warn` で始まる行は、Pull Requestのタイトルに `[WIP]` が入っているかどうかで警告を出すための設定です
- 最後の2行は、`danger-checkstyle_format` プラグインを使って、ktlintが出力したCheckstyle形式のレポートにもとづいてPRにコメントするための設定です
    - **2017年12月10日編集**: `ktlint-gradle` プラグインのバージョンアップ2.1.1 → 2.3.0にあわせて、レポートファイルのパスを変更しました

### GitHubアクセストークンの設定

DangerがPull Requestにコメントする際に使用するGitHubアカウント・アクセストークンを作成します。

#### GitHubアカウントの作成

Dangerが使うGitHubアカウントを作成します。自分のGitHubアカウントをDangerに使わせることもできますが、セキュリティのリスクを抑える (botアカウントなら権限を必要最小限にしぼっておける、アカウントの機密情報が漏洩したときにアカウント削除の手段がとりやすい、など) ために、別のアカウントを作成したほうがよいです。別アカウントだと、botによるコメントであることがわかりやすい、という利点もあります。

- Dangerの検査対象がPublicレポジトリである場合: botアカウントを当該レポジトリのCollaboratorに追加する必要はありません。アカウントを作成するだけでOKです
- Dangerの検査対象がPrivateレポジトリである場合: 作成したbotアカウントを当該レポジトリのCollaboratorや、レポジトリが属するOrganizationのメンバーに追加し、当該レポジトリへの "Write" 権限を与えてください

#### GitHubアクセストークンの発行

作成したbotアカウントでログインした状態で [Generate new token](https://github.com/settings/tokens/new) のページにアクセスし、アクセストークンを発行します。

- Dangerの検査対象がPublicレポジトリである場合: 権限 `public_repo` だけをトークンに付与します
- Dangerの検査対象がPrivateレポジトリである場合: `repo` スコープの権限全てをトークンに付与します

発行されたトークンはこの後、環境変数に設定するときに使うので、必ず控えておいてください。

#### アクセストークンの環境変数への設定

Dangerは、環境変数 `DANGER_GITHUB_API_TOKEN` からアクセストークンを取得します。

Travis CIにおいて、機密情報を含む環境変数を設定する方法は2つあります。

1. Travis CIの各リポジトリの設定ページ (`https://travis-ci.org/[user]/[repo]/settings`) で、環境変数を追加する
2. [`travis` コマンドで値を暗号化](https://docs.travis-ci.com/user/encryption-keys/)して、`.travis.yml` に環境変数として定義する

Dangerの [Getting Set Up](http://danger.systems/guides/getting_started.html) ページでは方法1が紹介されているのですが、Travis CIの設定情報を　`.travis.yml` に集約しておきたい、という思いがあり、個人的には方法2が好みです。ここでは、方法2を紹介します。

まず、`travis` コマンドをインストールします (まだインストールしていない場合)。

```shell-session
$ gem install travis
```

次に、検査対象レポジトリのルートディレクトリに行き、`travis encrypt` コマンドを実行します。もし、検査対象レポジトリがPrivateレポジトリの場合は、ここで `--pro` オプションをつけてください。

```shell-session
$ travis encrypt DANGER_GITHUB_API_TOKEN=[アクセストークン]
```

暗号化に成功すると、`secure:` から始まる長い行が出力されるので、それを `.travis.yml` の `env.global` 配列の要素として追加します。

```yaml
# .travis.yml

env:
  global:
    - secure: "R+epV1sw... 中略 .../A0sGJ8="
```

### Travis CIでのDangerの実行

これで、Travis CI実行環境で Danger を実行する準備が整いました。あとは、Travis CIのビルド中に、Dangerを実行するだけです。

まず、Dangerを実行する前に、必要なgemをインストールさせる必要があります。Travis CIの `install` フェーズで `bundle install` を実行します。

```yaml
# .travis.yml

install:
  - bundle install
```

あとは、`script` フェーズで、Gradleの `ktlintCheck` タスクを実行してktlintを実行した後、Dangerを実行すれば、ktlintが見つけた問題がPull Requestにコメントされます。

```yaml
# .travis.yml

script:
  - ./gradlew assembleDebug ktlintCheck
  - bundle exec danger
```

## サンプルプロジェクト

ここまでの設定を終えて、作成物をレポジトリにコミット・プッシュすれば完了です。

実際にひととおりの設定を行い、ktlintで問題を報告させてみたサンプルプロジェクトをGitHubにコミットしてありますので、良ければ参考にしてみてください。

- レポジトリ: [kafumi/android-danger-ktlint-sample](https://github.com/kafumi/android-danger-ktlint-sample)
    - Pull Requestの例: [Pull Request #3](https://github.com/kafumi/android-danger-ktlint-sample/pull/3)

# Appendix: 設定ファイルの例

今回、説明した設定ファイルは、サンプルプロジェクトのリポジトリ [kafumi/android-danger-ktlint-sample](https://github.com/kafumi/android-danger-ktlint-sample) にコミットしてあります。

- Gradle
    - [build.gradle](https://github.com/kafumi/android-danger-ktlint-sample/blob/master/build.gradle)
    - [app/build.gradle](https://github.com/kafumi/android-danger-ktlint-sample/blob/master/app/build.gradle)
- Travis
    - [.travis.yml](https://github.com/kafumi/android-danger-ktlint-sample/blob/master/.travis.yml)
- Danger
    - [Gemfile](https://github.com/kafumi/android-danger-ktlint-sample/blob/master/Gemfile)
    - [Dangerfile](https://github.com/kafumi/android-danger-ktlint-sample/blob/master/Dangerfile)