LoginSignup
59
38

More than 3 years have passed since last update.

Github ActionsでマルチモジュールAndroidプロジェクトのCI環境を整えよう(ビルド/Slack通知/Danger/ktlint)

Last updated at Posted at 2020-04-12

はじめに

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上で完結するのも良いですよね。
またチームメンバーに上限が特段設けられてないのもありがたいです。

image.png

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にこんな感じで書いてます。

build.gradle(Project)
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版でもうまくいきました。

DangerFile
# 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も忘れず。

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画像を貼ってくれるので面白いです

image.png

Slackへ結果を通知する

こんな感じ

スクリーンショット 2020-04-12 18.47.40.png

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 }}
59
38
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
59
38