動機
以前 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分程度で終わるようになった。)
CircleCI 2.0で利用しているインフラの性能は、1.0のときより良さそうです。
ちなみに
dangerをCircleCI上で使ってAndroidのコードを精査する (FindBugs/Android Lint) ではdangerに言及してましたが、上記で書いたようにRubyからgradleのタスクを呼ぶのがめんどうな印象だったので、今回は避けました。
各プラグインで、gradleタスクを実行せず、受け取ったxmlファイルのパスから指摘できるようになったら、再度考えようかと思います。