CircleCI 2.0でAndroidのアプリをビルドしつつ、コードレビューBOTも動かす

## 動機

以前 [Androidのコードを自動で解析し、GitHubのpull requestにコメントする](http://qiita.com/noboru_i/items/2f30296db1c8a6dfbd9b) という記事で、CircleCI 1.0にて [Saddler](https://github.com/packsaddle/ruby-saddler) を利用して自動レビューを行いました。

ただ、CircleCI 2.0が正式にリリースされ、 「[CircleCI 2.0を使うようにするだけで、こんなに速くなるとは夢にも思わなかった！ | Tokyo Otaku Mode Blog](http://blog.otakumode.com/2017/06/09/cicle_ci_2/)」という記事もあり、CircleCI 2.0を使ってみようと思いました。

うまくいきはしましたが、いくつかの壁があったので、そのあたりをまとめようかと思います。

## TL;DR

https://github.com/noboru-i/SlideViewer/pull/72 のように変更したら、 https://circleci.com/workflow-run/fbc9c6bf-e7bd-4c8c-a2d0-65c98b9f92be のようにビルド・コードレビューが成功しました。

（実際には、その後に https://github.com/noboru-i/SlideViewer/pull/74 も入れ、 https://circleci.com/workflow-run/ae5fe2f5-8b45-47ef-a372-9532a421862e にてdeploy jobも含めて成功しました。）

## いくつかの壁

### 壁１：Android SDKの再配布について

[CircleCI側で用意されたdockerイメージ](https://circleci.com/docs/2.0/language-android/)はありますが、Saddlerを実行するにはRubyの実行環境が必要となります。

そのため、用意されているdockerイメージに、Rubyを追加インストールしたdockerイメージを作ることを思いつきました。

ただ、[公開用DockerイメージにAndroid SDKを含めるのはライセンス違反という話 - Islands in the byte stream](http://gfx.hatenablog.com/entry/2016/06/21/112404) という話もあり、カスタムしたdockerイメージをpublicに公開するのは、やめたほうが良さそうです。
（gfxさんの「削除予定のイメージ」はまだ削除されてないのですが、実は状況が変わった。。。？）

そのため、privateイメージとして作成する必要がありそうです。

### 壁2：CircleCI 2.0でprivateイメージを使う

[Using Private Images](https://circleci.com/docs/2.0/private-images/) に記載があるのですが、publicなイメージを使うときとはやり方がかなり違っています。

カスタムイメージをDocker Hubにprivateとしてpushし、いろいろやってみたのですが、ビルドすら上手く行かなかったです。。。
（[このへん](https://github.com/noboru-i/SlideViewer/pull/71)でいろいろやってみました。。）
私のDocker力が足りないのも原因かと思います。。。

### 壁3:複数Dockerイメージを指定する

上記のやり方は一旦諦め、CircleCI側が用意してくれたDockerイメージを利用しつつ、Rubyのイメージも同時に起動し、Rubyの処理はそちらのコンテナにやってもらおうと思いました。

[Specifying Container Images](https://circleci.com/docs/2.0/executor-types/) などに複数imageを利用した例もありますが、データベースなどの外部からアクセスする例ばかりで、 `docker exec` のようなシェルの実行する例がなく、どのように処理させるかはわかりませんでした。。

やりたかったのは、下記のようなイメージ。

```yaml
version: 2

jobs:
  build:
    working_directory: ~/code
    docker:
      - image: circleci/android:api-26-alpha
      - image: circleci/ruby:2.4.1
    steps:
      - checkout
      - run:
          name: Check lint
          command: ./gradlew :app:check -x test
      - run:
          name: Saddler
          command: docker exec -it circleci/ruby:2.4.1 sh scripts/saddler.sh
```

なのですが、成果物をコンテナ間で渡したり、execする際のコンテナの指定がよくわからなかったので、試してすらいないです。。。

## 今回の解決策

workflows を利用し、Saddlerの実行のみrubyのコンテナを利用するようにしました。

下記のような3つのjobを定義し、workflowで順序などを制御しました。

- build: gradleのスクリプトを実行し、apkの作成、各種チェック結果のXML作成を行います。
- check: Saddlerを実行し、前のjobで作成したXMLを読み込み、GitHubにコメントを追加します。
- deploy: masterブランチのみ実行し、Releaseビルドのapkファイルをartifactsに保存します。

一つずつ見ていきます。

### defaults

と、いきなりjobではないものです。。

buildとdeployでは同一のdockerイメージを利用します。
そのため、 `defaults` として共通化しておきます。

```yaml
defaults: &defaults
  working_directory: ~/code
  docker:
    - image: circleci/android:api-26-alpha
  environment:
    JVM_OPTS: -Xmx3200m
```

おおよそ、CircleCIの公式 https://circleci.com/docs/2.0/language-android/ に記載があるとおりです。

### build

前述の `defaults` を利用し、gradleタスクの実行などを行っていきます。

```yaml
  build:
    <<: *defaults
    steps:
      - checkout
      - restore_cache:
          key: jars-{{ checksum "build.gradle" }}-{{ checksum  "app/build.gradle" }}
      - run:
          name: Download Dependencies
          command: ./gradlew androidDependencies
      - save_cache:
          paths:
            - ~/.gradle
          key: jars-{{ checksum "build.gradle" }}-{{ checksum  "app/build.gradle" }}
      - run:
          name: Decrypt google-services.json
          command: openssl aes-256-cbc -k $DECRYPT_KEY -d -in app/encrypted_google-services.json -out app/google-services.json -md md5
      - run:
          name: Check licenses
          command: ./gradlew :app:checkLicenses
      - run:
          name: Check lint
          command: ./gradlew :app:check -x test
      - run:
          name: Build apk
          command: ./gradlew :app:assembleDebug
      - store_artifacts:
          path: app/build/outputs/apk/app-debug.apk
          destination: app-debug.apk
      - store_test_results:
          path: app/build/test-results
      - persist_to_workspace:
          root: ~/code
          paths:
            - .
```

こちらも公式に記載のあるように、キャッシュ、gradleタスクの実行、必要なartifactsの保存などを行っています。

`Decrypt google-services.json` については、 [Openssl 1.1.0でもファイルを復号化する - Qiita](http://qiita.com/noboru_i/items/687387148468e16c059a) に記載していたように、「暗号化してGit管理しているファイル」を復号化しています。

`persist_to_workspace` は、次のcheck jobで成果物を利用するため、ワークスペースごと引き継ぎます。
参考： https://circleci.com/docs/2.0/workflows/#using-workspaces-to-share-data-among-jobs

### check

Rubyのコンテナを利用し、Saddlerを実行します。

```yaml
  check:
    working_directory: ~/code
    docker:
      - image: circleci/ruby:2.4.1
    steps:
      - attach_workspace:
          at: ~/code
      - restore_cache:
          key: saddler-gems-{{ checksum "Gemfile.lock" }}
      - run: bundle install --path vendor/bundle
      - run:
          name: Run saddler
          command: sh scripts/saddler.sh
          environment:
              CIRCLE_ARTIFACTS: "/tmp/circle_artifacts"
      - save_cache:
          paths:
            - vendor/bundle
          key: saddler-gems-{{ checksum "Gemfile.lock" }}
      - store_artifacts:
          path: "/tmp/circle_artifacts"
```

まず、 `build` の結果を利用するために `attach_workspace` を記載しておきます。

また、 `bundle install` を高速化するため、キャッシュの設定などもしておきます。
（ただ、今回に関しては6sec程度でインストールが完了してしまうため、あまり効果はありませんでした。。。）

その後、Saddlerの実行手順などが記載されているシェルスクリプトを実行します。
シェルスクリプトの中身については、 https://github.com/noboru-i/SlideViewer/blob/71fbbf93f1981e2fa7f40ddd0b8153931a771d8d/scripts/saddler.sh を直接御覧ください。
ほぼ、 [Androidのコードを自動で解析し、GitHubのpull requestにコメントする - Qiita](http://qiita.com/noboru_i/items/2f30296db1c8a6dfbd9b) で記載したものと同一ですが、gemのインストールやgradleタスクの実行は外に出してあります。

一応、利用したXMLファイルなどをartifactsに保存してあります。

### deploy

gradleタスクを実行し、release用のapkの作成などを行います。

```yaml
  deploy:
    <<: *defaults
    steps:
      - attach_workspace:
          at: ~/code
      - run:
          name: Build apk
          command: ./gradlew :app:assembleRelease
      - store_artifacts:
          path: app/build/outputs/apk/app-release.apk
          destination: app-release.apk
      - store_artifacts:
          path: app/build/outputs/mapping/release/dump.txt
          destination: dump.txt
```

上で説明していた部分と、ほとんど同じかと思います。

### workflows

build -> checkとシーケンシャルに実行していきます。
また、masterブランチの場合のみ、そのあとにdeploy jobも実行します。

YAMLファイルとしては、下記のようになります。

```yaml
workflows:
  version: 2
  build_and_deploy:
    jobs:
      - build
      - check:
          requires:
            - build
      - deploy:
          requires:
            - build
            - check
          filters:
            branches:
              only: master
```

## まとめ

上で説明したYAMLファイルの全体像としては、 https://github.com/noboru-i/SlideViewer/blob/71fbbf93f1981e2fa7f40ddd0b8153931a771d8d/.circleci/config.yml にあります。

今回は、workflowを利用して、Android用のイメージとRubyのイメージを行ったり来たりすることで、Rubyのスクリプトを利用した自動レビューを行いました。

まだ全然安定していないですし、以前はdangerを使っており今回はSaddlerを使っているので評価しづらいですが、ビルド時間に関しては劇的にはやくなりました。
（以前は10分以上かかっていたものが、4分程度で終わるようになった。）

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8708/8fd9a33f-e823-2616-8ee7-19396c866c30.png)

CircleCI 2.0で利用しているインフラの性能は、1.0のときより良さそうです。

## ちなみに

[dangerをCircleCI上で使ってAndroidのコードを精査する (FindBugs/Android Lint)](http://qiita.com/noboru_i/items/2ad7c07d8b4266c9eb5b) ではdangerに言及してましたが、上記で書いたようにRubyからgradleのタスクを呼ぶのがめんどうな印象だったので、今回は避けました。

各プラグインで、gradleタスクを実行せず、受け取ったxmlファイルのパスから指摘できるようになったら、再度考えようかと思います。