GitHub ActionsでAndroidアプリのCIを構築する方法

## はじめに

GitHub Actionsを使い、Androidアプリのビルドと単体テスト、静的解析を行うCIを構築します。

## 本記事で書かないこと

- GitHub Actionsの概要や基本的な操作方法  
[私が以前書いた記事](https://qiita.com/uhooi/items/29664ecf0254eb637951) が参考になると思います

## 設定ファイルの紹介

設定ファイルの内容を上から順に紹介します。

### name

ワークフローの名前を設定します。

私は単純に「CI」としています。

```yaml:ci.yml
name: CI
```

### on

ワークフローを実行するトリガーを設定します。

私はGit-flowモデルで開発しており、以下のように設定しています。

```yaml:ci.yml
on:
  push:
    branches:
      - main
      - develop
    paths-ignore:
      - docs/**
      - README.md
      - LICENSE
  pull_request:
    branches:
      - develop
    paths-ignore:
      - docs/**
      - README.md
      - LICENSE
```

他の人からPull request（以下、「PR」と呼ぶ）を頂いたときにもCIが動くように、 `develop` ブランチへのPRをトリガーにしています。
PRのマージ時にもCIが動くように、 `develop` と `main` ブランチへのプッシュもトリガーにしています。
`feature/**` ブランチへのプッシュをトリガーにしていない理由は、 `develop` ブランチへPRを出したときにCIが二重に動作するのを防ぐためです。
早めにCIを動かしたい場合、「WIP:」を付けてPRを出します（GitHubではドラフト機能があるので、それを使うのもありです）。

`README.md` や `LICENSE` など、Androidアプリに直接関係ないファイルは、単体でコミットしてもCIが動かないようにしています。
（ `README.md` のみ更新してプッシュしてもCIが動くような気がします…。私の設定が誤っているのでしょうか？）

### jobs

今回はジョブを4つ用意して並行に実行しているので、順に紹介します。

#### build

ビルドを実行するジョブです。

基本的には『Androidテスト全書』を参考にしてGradleタスクを実行しているだけなので、詳細は省略します。

Gradleのキャッシュは、READMEの例をそのままコピペしています。
https://github.com/actions/cache/blob/master/examples.md#java---gradle

```yaml:ci.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    # チェックアウト
    - uses: actions/checkout@v2

    # JDKのセットアップ
    - name: set up JDK 11
      uses: actions/setup-java@v2
      with:
        distribution: 'zulu'
        java-version: '11'

    # Gradleのキャッシュ復元
    - uses: actions/cache@v2
      with:
        path: ~/.gradle/caches
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
        restore-keys: |
          ${{ runner.os }}-gradle-

    # 依存関係の出力
    - name: Displays the Android dependencies of the project
      run: ./gradlew androidDependencies

    # コンパイル
    - name: Run Compile
      run: ./gradlew assembleDebug
```

##### 注意

- JDKは11を使用している
  - 8を使いたい場合は `8` を指定する

#### test

単体テストを実行するジョブです。

`build` ジョブと同様、詳細は省略します。

```yaml:ci.yml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    # チェックアウト
    - uses: actions/checkout@v2

    # JDKのセットアップ
    - name: set up JDK 11
      uses: actions/setup-java@v2
      with:
        distribution: 'zulu'
        java-version: '11'

    # Gradleのキャッシュ復元
    - uses: actions/cache@v2
      with:
        path: ~/.gradle/caches
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
        restore-keys: |
          ${{ runner.os }}-gradle-

    # テスト
    - name: Test with Gradle
      run: ./gradlew test --stacktrace

    # コードカバレッジをJaCoCo形式で取得
    - name: Get code coverage for JaCoCo
      run: ./gradlew jacocoDebugTestReport

    # コードカバレッジをCodecovへアップロード
    - name: Upload code coverage to Codecov
      run: bash <(curl -s https://codecov.io/bash)
      env:
        CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

    # テスト結果とコードカバレッジのアップロード
    - name: Upload test results and code coverage Artifact
      uses: actions/upload-artifact@v2
      if: success() || failure()
      with:
        name: results
        path: |
          **/build/reports/tests/**/*
          **/build/reports/test-results/**/*
          **/build/reports/jacoco/**/*
        if-no-files-found: warn
        retention-days: 14
```

##### 注意

- `jacocoDebugTestReport` タスクは自作
  - JaCoCoは別記事で説明する予定
- CodeCovを使う場合は `CODECOV_TOKEN` をシークレットに追加する
  - CodeCovも別記事で説明する予定
- テスト結果は成否にかかわらず確認したいため、アーティファクトのアップロードステップで `if: success() || failure()` を指定している

#### lint

Android Lintを使って静的解析するジョブです。

`./gradlew lint` を実行して結果をアップロードしているだけなので、詳細は省略します。

```yaml:ci.yml
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
    # チェックアウト
    - uses: actions/checkout@v2

    # 静的解析
    - name: Run Inspection
      run: ./gradlew lint

    # 結果の表示
    - name: Show results on GitHub
      uses: yutailang0119/action-android-lint@v1
      with:
        xml_path: '**/build/reports/lint-results-*.xml'

    # アーティファクトへアップロード
    - name: Upload results Artifact
      uses: actions/upload-artifact@v2
      if: success() || failure()
      with:
        name: results
        path: |
          **/build/reports/lint-results-*.html
          **/build/reports/lint-results-*.xml
        if-no-files-found: error
        retention-days: 14
```

##### 注意

- 結果のファイル名は `lint-results.*` から `lint-results-{Build variant}.*` に変更された
  - おそらくGradle 7から
- `yutailang0119/action-android-lint` アクションでGitHub上のファイルに指摘を表示している

#### detekt

[detekt](https://github.com/detekt/detekt) を使って静的解析するジョブです。

`./gradlew detekt` を実行して結果をアップロードしているだけなので、詳細は省略します。

```yaml:ci.yml
jobs:
  detekt:
    runs-on: ubuntu-latest
    steps:
    # チェックアウト
    - uses: actions/checkout@v2

    # 静的解析
    - name: Lint with detekt
      run: ./gradlew detekt

    # アーティファクトへアップロード
    - name: Upload results Artifact
      uses: actions/upload-artifact@v2
      if: failure()
      with:
        name: results
        path: |
          **/build/reports/detekt/**/*
        if-no-files-found: error
        retention-days: 14
```

Android Lintにはない観点で静的解析するため、両方とも使うのがオススメです。

#### ktlint

[ktlint](https://github.com/pinterest/ktlint) を使って静的解析するジョブです。

`ktlint` を実行して結果をアップロードしているだけなので、詳細は省略します。

```yaml:ci.yml
jobs:
  ktlint:
    runs-on: ubuntu-latest
    steps:
    # チェックアウト
    - uses: actions/checkout@v2

    # インストール
    - name: Install ktlint
      run: |
        curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.42.1/ktlint
        chmod a+x ktlint
        sudo mv ktlint /usr/local/bin/

    # 静的解析
    - name: Lint with ktlint
      run: ktlint --reporter=checkstyle,output=build/ktlint-report.xml
      continue-on-error: true

    # 結果の表示
    - name: Show results on GitHub
      uses: yutailang0119/action-ktlint@v1
      with:
        xml_path: build/ktlint-report.xml

    # アーティファクトへアップロード
    - name: Upload results Artifact
      uses: actions/upload-artifact@v2
      if: failure()
      with:
        name: results
        path: |
          **/build/ktlint-report.xml
        if-no-files-found: error
        retention-days: 14
```

##### 注意

- `yutailang0119/action-ktlint` アクションでGitHub上のファイルに指摘を表示している

## 設定ファイルの全体図

最後に設定ファイルの全体図を載せます。

```yaml:ci.yml
name: CI

on:
  push:
    branches:
      - main
      - develop
    paths-ignore:
      - docs/**
      - README.md
      - LICENSE
  pull_request:
    branches:
      - develop
    paths-ignore:
      - docs/**
      - README.md
      - LICENSE

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    # チェックアウト
    - uses: actions/checkout@v2

    # JDKのセットアップ
    - name: set up JDK 11
      uses: actions/setup-java@v2
      with:
        distribution: 'zulu'
        java-version: '11'

    # Gradleのキャッシュ復元
    - uses: actions/cache@v2
      with:
        path: ~/.gradle/caches
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
        restore-keys: |
          ${{ runner.os }}-gradle-

    # 依存関係の出力
    - name: Displays the Android dependencies of the project
      run: ./gradlew androidDependencies

    # コンパイル
    - name: Run Compile
      run: ./gradlew assembleDebug

  test:
    runs-on: ubuntu-latest
    steps:
    # チェックアウト
    - uses: actions/checkout@v2

    # JDKのセットアップ
    - name: set up JDK 11
      uses: actions/setup-java@v2
      with:
        distribution: 'zulu'
        java-version: '11'

    # Gradleのキャッシュ復元
    - uses: actions/cache@v2
      with:
        path: ~/.gradle/caches
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
        restore-keys: |
          ${{ runner.os }}-gradle-

    # テスト
    - name: Test with Gradle
      run: ./gradlew test --stacktrace

    # コードカバレッジをJaCoCo形式で取得
    - name: Get code coverage for JaCoCo
      run: ./gradlew jacocoDebugTestReport

    # コードカバレッジをCodecovへアップロード
    - name: Upload code coverage to Codecov
      run: bash <(curl -s https://codecov.io/bash)
      env:
        CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

    # テスト結果とコードカバレッジのアップロード
    - name: Upload test results and code coverage Artifact
      uses: actions/upload-artifact@v2
      if: success() || failure()
      with:
        name: results
        path: |
          **/build/reports/tests/**/*
          **/build/reports/test-results/**/*
          **/build/reports/jacoco/**/*
        if-no-files-found: warn
        retention-days: 14

  lint:
    runs-on: ubuntu-latest
    steps:
    # チェックアウト
    - uses: actions/checkout@v2

    # 静的解析
    - name: Run Inspection
      run: ./gradlew lint

    # 結果の表示
    - name: Show results on GitHub
      uses: yutailang0119/action-android-lint@v1
      with:
        xml_path: '**/build/reports/lint-results-*.xml'

    # アーティファクトへアップロード
    - name: Upload results Artifact
      uses: actions/upload-artifact@v2
      if: success() || failure()
      with:
        name: results
        path: |
          **/build/reports/lint-results-*.html
          **/build/reports/lint-results-*.xml
        if-no-files-found: error
        retention-days: 14

  detekt:
    runs-on: ubuntu-latest
    steps:
    # チェックアウト
    - uses: actions/checkout@v2

    # 静的解析
    - name: Lint with detekt
      run: ./gradlew detekt

    # アーティファクトへアップロード
    - name: Upload results Artifact
      uses: actions/upload-artifact@v2
      if: failure()
      with:
        name: results
        path: |
          **/build/reports/detekt/**/*
        if-no-files-found: error
        retention-days: 14

  ktlint:
    runs-on: ubuntu-latest
    steps:
    # チェックアウト
    - uses: actions/checkout@v2

    # インストール
    - name: Install ktlint
      run: |
        curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.42.1/ktlint
        chmod a+x ktlint
        sudo mv ktlint /usr/local/bin/

    # 静的解析
    - name: Lint with ktlint
      run: ktlint --reporter=checkstyle,output=build/ktlint-report.xml
      continue-on-error: true

    # 結果の表示
    - name: Show results on GitHub
      uses: yutailang0119/action-ktlint@v1
      with:
        xml_path: build/ktlint-report.xml

    # アーティファクトへアップロード
    - name: Upload results Artifact
      uses: actions/upload-artifact@v2
      if: failure()
      with:
        name: results
        path: |
          **/build/ktlint-report.xml
        if-no-files-found: error
        retention-days: 14
```

シンプルなYAMLファイルなので、慣れれば読みやすいと思います。

## おわりに

GitHub Actionsで基本的なAndroidアプリのCIを回すことができました！

## 参考リンク

- [GitHub Actionsのワークフロー構文 - GitHub ヘルプ](https://help.github.com/ja/actions/reference/workflow-syntax-for-github-actions)
- [UhooiPicBook-Android/ci.yml at develop · uhooi/UhooiPicBook-Android](https://github.com/uhooi/UhooiPicBook-Android/blob/develop/.github/workflows/ci.yml)
- [Feature/setup jacoco by uhooi · Pull Request #105 · uhooi/UhooiPicBook-Android](https://github.com/uhooi/UhooiPicBook-Android/pull/105)
- [Bump Android Studio to Arctic Fox by uhooi · Pull Request #156 · uhooi/UhooiPicBook-Android](https://github.com/uhooi/UhooiPicBook-Android/pull/156)
- [Show lint report on CI by yutailang0119 · Pull Request #180 · uhooi/UhooiPicBook-Android](https://github.com/uhooi/UhooiPicBook-Android/pull/180)
- [Run ktlint on CI by yutailang0119 · Pull Request #182 · uhooi/UhooiPicBook-Android](https://github.com/uhooi/UhooiPicBook-Android/pull/182)