はじめに
2019年ごろにBeta版が出たGithub Actions、気になってはいたもののまだ触ったことがなかったので今回Androidリポジトリで動くCIをGithub Actionsで構築してみました。
これまでCIはBitriseしか使ったことがなかったのですが、無料版だと1回のビルド時間が10分までだったり、チームメンバーが合計3人までしか使用できないので、これを機にGithub Actionsに乗り換えることにしました。
マルチモジュールで気をつけるところはDanger周りくらいで、基本的にはシングルモジュールと変わらないかと思います。
まだまだ改善できそうなところは多いので、是非教えていただければ幸いです。
※こちらは趣味で一緒に開発しているメンバー向けの解説記事でもあります。
Github Actions基礎知識
サービス概要
- 20jobまで並列実行できる
- 1度のビルド時間のリミットは6時間(Bitriseの36倍ですねw)
この時点でもうBitriseから乗り移ろうと決めたのですが、開発をする上で欠かせないGithub上で完結するのも良いですよね。
またチームメンバーに上限が特段設けられてないのもありがたいです。
publicなら無料。最高ですね。
詳しくは 公式サイトをご覧ください。
この記事で出てくる構文
Github Actionsのワークフロー構文に山ほどありますが、今回使用している構文のみピックアップして軽く紹介します。
name:
その名の通り、ワークフローやjobに名前がつけられます。
on:
workflowをトリガーするGithubのイベントの名前。
on: push
のように1つのイベントでも、
on: [push, pull_request]
のように2つでも指定できる。
指定できるイベントはワークフローをトリガーするイベントに詳しく記載されています。
jobs:
1つのワークworkflowの実行は複数のjobから成るため、その集合を表します。
jobs:
setup_environment:
name: setup
build_gradle:
needs: setup_environment
name: build
のように書くと、 setup
や build
はjobのid、nameは下図のように実際に表示されるjob名となります。
また、 needs
で依存関係を指定することもできます。
runs-on:
ジョブを実行するマシンの種類を指定します。
runs-on: ubuntu-latest
uses:
jobで実行されるアクションを示します。
簡単に言うと、誰かがつくったGithub Actionsのプラグインをここで指定することで使用できるようになるイメージです。
with:
を添えることで必要な入力を指定することもできます。
- uses: actions/checkout@v2
この例だと、
{user名}/{action名}@{version}
なので、
actions公式が出している、ブランチを切り替えてくれるversion2のアクションということですね。
run:
OSのシェルを利用してCLIで動くコマンドを実行できます。
- name: Build Gradle
run: ./gradlew assemble
また、複数行のコマンドを打つときは
- name: Clean & Build Gradle
run: |
./gradlew clean
./gradlew assemble
のように書くことができます。
その他
あとは env
で環境変数を指定できたり、 if
でjobの実行を条件分岐したりするのですが、
量が多いため割愛します。
やってみる
まずは動かしてみたい。
ビルド
Github Actionsのテンプレを使えば、簡単にAndroid他のworkflowを構築することができます。
Andrdoiのテンプレはこんな感じです。
name: Android CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build with Gradle
run: ./gradlew build
master branchに対するPRとpushがトリガーでworkflowが走り、
pushしたbranchへcheckout -> JDK1.8のsetup(これがないと./gradlewコマンドが使えない) -> Gradle Buildの順番でstepが実行されていますね。
./gradlew build
のところは適宜 assembleDebugとか testDebugUnitTest等で置き換えたりしてもokです。
実際、初期のリポジトリであればこのymlを .github/workflows
配下におけば動きます。
しかし、今回はFirebaseを一部使用していたため、.gitignoreでgit管理から外している google-services.json
を生成してあげる必要がありました。
API_KEYなど、センシティブな情報があるためです。
google-services.jsonファイルを生成する
こちらの記事を参考に、Base64化した google-services.json
をデコードするやり方でやってみます。
こちらの記事を丸写しするだけでは動かないので、少し補足させていただきます。
Base64化
google-services.json
があるプロジェクトで、
cat /Users/{user_name}/AndroidStudioProjects/{project_name}/app/google-services.json | base64
を実行し、base64化しましょう。
{user_name}, {project_name}は適宜補完してください。
Base64をGithubのSecretsにセキュアな環境変数として登録する
上記の記事参照です。
ymlの編集
- name: Generate google-services.json
env:
GOOGLE_SERVICES_BASE64: ${{ secrets.GOOGLE_SERVICES_BASE64 }}
run: echo $GOOGLE_SERVICES_BASE64 | base64 --decode --ignore-garbage > $GITHUB_WORKSPACE/app/google-services.json
こんな感じです。
ここで注意しないといけないのが、出力先が ./app/google-services.json
だと動かないです。
実際に動かしてみればわかるのですが、 assembleでの app:processDebugGoogleServices
では //home/runner/work/{project_name}/{project_name}/app/google-services.json
を見に行っています。
Githubのデフォルトの環境変数で
$GITHUB_WORKSPACE
(//home/runner/work/{project_name}/{project_name})が用意されているのでこちらを使いましょう。
ktlint, Android Lintを回してDangerでPRにコメントをつける
ktlint, Android Lint, Dangerの詳しい解説は省きますが、自分の設定や気をつけること等はさらっと書いていこうと思います。
ktlint
ktlintは、Kotlinのコード規約に沿っているかをチェックしてくれたり、フォーマットしてくれるツールです。
こちらも色々調べると出てくると思うので詳しい説明は省きます。
設定的には、自分はprojectレベルの build.gradleにこんな感じで書いてます。
allprojects {
repositories {
google()
jcenter()
maven { url "https://jitpack.io" }
}
configurations {
ktlint
}
dependencies {
ktlint Dep.Ktlint.ktlint
}
// ここ、タスク名をktlintにするとdependenciesと競合して動かなかった。
task ktlintMain(type: JavaExec, group: "verification") {
description = "Check Kotlin code style."
classpath = configurations.ktlint
main = "com.pinterest.ktlint.Main"
args "src/**/*.kt", "--android", "--color", "--reporter=plain",
"--reporter=checkstyle,output=${buildDir}/reports/ktlint-results.xml"
// lintエラーが起きた時に続行するか。続行しないと落ちる
ignoreExitValue true
}
task ktlintFormat(type: JavaExec, group: "formatting") {
description = "Fix Kotlin code style deviations."
classpath = configurations.ktlint
main = "com.pinterest.ktlint.Main"
args "-F", "src/**/*.kt", !"src/**/*Test.kt"
ignoreExitValue true
}
}
Android Lint
lint チェックでコードを改善する
の通り、Android Studioに標準整備されているlintです。
上記のktlintのような設定は不要で、
./gradlew lint
で動きます。
Danger
Dangerは、上記のktlintやAndroid Lintの結果をみて、PRで自動コメントをつけてくれるツールです。
これにより、コード規約違反や乱れを人力で指摘する手間が省け、健全なコードを保てます。
今回CI環境を整えるにあたって、Dangerの設定が1番骨が折れました。
- マルチモジュール対応
- bundleが見つからないエラー
シングルモジュールのDangerは
Android開発のコードレビューbotを乗り換えた話や、
Android の開発環境へ Danger を導入するメモ [GitHub x Bitrise 編]
を見ていただければわかると思います。
マルチモジュール対応
マルチモジュールのプロジェクトでdanger-android_lintを使う
にとても助けられました。同じ要領でktlintもできます。
シングルモジュールの場合、ktlintやAndroid Lintのレポートファイルは、app moduleの build/reports
配下に生成されます。
マルチモジュールの場合は各モジュールにレポートファイルが生成されます。
Android lintの結果をDangerでPRコメントするためには danger-android_lint を使用するのが一般出来なのですが、report fileを1つしか指定できないため、普通にやると1つのmoduleに対してしかコメントできません。
方法としては、2つあるそうです。
- 生成されたreport fileをマージして1つのファイルにする
- 予めgradle taskを実行して各report fileからDangerを実行する
上記の記事を参考に後者でやってみるとktlint版でもうまくいきました。
# ktlint
checkstyle_format.base_path = Dir.pwd
Dir["**/reports/ktlint-results.xml"].each do |file|
checkstyle_format.report file
end
# android lint
android_lint.skip_gradle_task = true
android_lint.filtering = true
Dir["*/build/reports/lint-results*.xml"].each do |file|
android_lint.report_file = file
android_lint.lint(inline_mode: true)
end
普通であれば
android_lint.gradle_task = "lintDebug"
のようにするのですが、
CIのstepで先にgradle taskを実行しておく感じです。
難しかった。
Gemfile
bundleでDangerを設定した場合はGemfileも忘れず。
gem "danger"
gem "danger-checkstyle_format"
gem "danger-android_lint"
ymlの編集
Github Actions (v2) でDanger + ktlintを実行させるを参考にしました。
bundleでDnagerを設定した場合は少し記事と異なるので補足です。
- name: run danger
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gem install bundler:1.17.2
bundle update
bundle install
bundle exec danger
Gemfile.lockの BUNDLED WITH
でバージョンが指定されていたため 1.17.2のインストールをいているのと、
bundle update
しないと octokit
の依存関係?とコンフリクトして動かなかったです。
Bundle周り全然わからん... めっちゃ大変だった。
ちなみに、Dangerは現状ではPRトリガーでしか動かず、 pushトリガーだと
Not a GitHubActions Pull Request - skipping danger run.
とスキップされてしまうのでご注意ください。
キャッシュ
- name: Cache Gems
uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-danger-${{ env.cache-name }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-danger-${{ env.cache-name }}-gems-
${{ runner.os }}-danger-${{ env.cache-name }}-
${{ runner.os }}-danger-
キャッシュを利用すると良いかもしれません。
おまけ
danger-lgtm
lintエラーがない場合にランダムでLGTM画像を貼ってくれるので面白いです
Slackへ結果を通知する
こんな感じ
Slack Notify - GitHub Marketplace
こちらのActionを使うとすごく簡単にSlackへの通知ができました。
基本的な使用法は上記を見ていただきたいのですが、
こちらも補足です。
失敗した場合と成功した場合でメッセージを変える
Github Actionsの構文で if:
があります。
falseの場合に以下のstepを実行しないため、
always()
, failure()
, success()
等の、workflowのステータスと組み合わせれば失敗した場合と成功した場合でメッセージや色を変えることができます。
commit messageを取得する
Slackへの通知に限ったことではないのですが、commit messageを取得して、どのcommitが失敗/成功したかを表示したかったので調べてみました。
Github ActionsではREST APIのレスポンスに近い、contextなるものが用意されています。
詳しくは公式をご覧ください。
これにより、
${{github.repository}}
のようにしていろんな情報が取れます。
pushイベントの場合
${{github.event.head_commit.message}}
でとれます。
pull_requestの場合
DangerはPRトリガーでしか現状動かないため、こちらの方法で取得します。
- name: get commit message
run: echo ::set-env name=commitmsg::$(git log --format=%B -n 1 ${{ github.event.after }})
並列にしてみる
これまで紹介してきたstepは大きく4つに分けれると思っています。
- JDKなどのセットアップ
- ビルド/テスト
- Lint, Dangerでコメント
- Slackへの通知
そして依存関係的には
①JDKなどのセットアップ
②ビルド/テスト
Lint, Dangerでコメント
③Slackへの通知
の順番ですかね。
詳細はyml全文を見ていただきたいのですが、
- 各jobでcheckoutしないといけない
- google-services.jsonはキャッシュ等に保存せず必要なjobで冗長的に生成する(センシティブな情報を含めるためキャッシュするのは推奨されていない)
を気をつければ簡単だと思います。
yml全文
name: Android CI
on: pull_request
jobs:
setup:
runs-on: ubuntu-latest
steps:
# usesでplugin的なものが使える
- uses: actions/checkout@v2
# ./gradlewを使えるようにする
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
# Gradleの依存関係をキャッシュ/リストアしてビルド時間を短くする
- name: Gradle Cache
uses: actions/cache@v1
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: |
${{ runner.os }}-gradle-
build:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# .gitignoreしているファイルを生成する
- name: Generate google-services.json
env:
GOOGLE_SERVICES_BASE64: ${{ secrets.GOOGLE_SERVICES_BASE64 }}
run: echo $GOOGLE_SERVICES_BASE64 | base64 --decode --ignore-garbage > $GITHUB_WORKSPACE/app/google-services.json
# Build
- name: Build with Gradle
run: ./gradlew assembleDebug
# Unit Test
- name: Unit Test
run: ./gradlew testDebugUnitTest
danger:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# .gitignoreしているファイルを生成する
- name: Generate google-services.json
env:
GOOGLE_SERVICES_BASE64: ${{ secrets.GOOGLE_SERVICES_BASE64 }}
run: echo $GOOGLE_SERVICES_BASE64 | base64 --decode --ignore-garbage > $GITHUB_WORKSPACE/app/google-services.json
- name: Setup ruby
uses: actions/setup-ruby@v1
with:
ruby-version: '2.6'
architecture: 'x64'
- name: run ktlintMain
run: ./gradlew ktlintMain
- name: run andoidLint
run: ./gradlew lintDebug
- name: Cache Gems
uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-danger-${{ env.cache-name }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-danger-${{ env.cache-name }}-gems-
${{ runner.os }}-danger-${{ env.cache-name }}-
${{ runner.os }}-danger-
- name: run danger
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gem install bundler:1.17.2
bundle update
bundle install
bundle exec danger
notification:
needs: [build, danger]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: get commit message
run: |
echo ::set-env name=commitmsg::$(git log --format=%B -n 1 ${{ github.event.after }})
# 失敗したときのSlackへの通知
- name: Slack Notification Failure
if: failure()
uses: rtCamp/action-slack-notify@v2.0.1
env:
SLACK_CHANNEL: notify-android
SLACK_ICON: https://github.com/actions.png?size=48
SLACK_COLOR: '#ff0000'
SLACK_TITLE: ':fire: Build Failure :fire:'
SLACK_MESSAGE: |
Build failure!
See more detail to check github.
commit -> `$commitmsg`
SLACK_USERNAME: Github Actions
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
# 成功した時のSlackへの通知
- name: Slack Notification Success
if: success()
uses: rtCamp/action-slack-notify@v2.0.1
env:
SLACK_CHANNEL: notify-android
SLACK_ICON: https://github.com/actions.png?size=48
SLACK_TITLE: ':rocket: Build Success :rocket:'
SLACK_MESSAGE: |
Build success! yattane!
commit -> `$commitmsg`
SLACK_USERNAME: Github Actions
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}