Android
CircleCI

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

動機

以前 Androidのコードを自動で解析し、GitHubのpull requestにコメントする という記事で、CircleCI 1.0にて Saddler を利用して自動レビューを行いました。

ただ、CircleCI 2.0が正式にリリースされ、 「CircleCI 2.0を使うようにするだけで、こんなに速くなるとは夢にも思わなかった! | Tokyo Otaku Mode Blog」という記事もあり、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も含めて成功しました。)

いくつかの壁

壁1:Android SDKの再配布について

CircleCI側で用意されたdockerイメージはありますが、Saddlerを実行するにはRubyの実行環境が必要となります。

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

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

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

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

Using Private Images に記載があるのですが、publicなイメージを使うときとはやり方がかなり違っています。

カスタムイメージをDocker Hubにprivateとしてpushし、いろいろやってみたのですが、ビルドすら上手く行かなかったです。。。
このへんでいろいろやってみました。。)
私のDocker力が足りないのも原因かと思います。。。

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

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

Specifying Container Images などに複数imageを利用した例もありますが、データベースなどの外部からアクセスする例ばかりで、 docker exec のようなシェルの実行する例がなく、どのように処理させるかはわかりませんでした。。

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

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 として共通化しておきます。

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タスクの実行などを行っていきます。

  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 に記載していたように、「暗号化してGit管理しているファイル」を復号化しています。

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

check

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

  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 で記載したものと同一ですが、gemのインストールやgradleタスクの実行は外に出してあります。

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

deploy

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

  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ファイルとしては、下記のようになります。

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

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

ちなみに

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

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