GitHub Actionsを使ってAndroidアプリをFirebase App Distributionへ配信するまでのベストプラクティス

# はじめに
どうも、GitHub Actions大好きな[@tarumzu](https://twitter.com/tarumzu)です。
今回はGitHub ActionsでAndroidアプリを配信する際のベストプラクティスな記事がなかなか無いなと思ったので、無いなら作ろう！という試みです。
例としてdev環境とstg環境の２つをApp Distributionで配信するという流れをGitHub Actionsで作ります。

# コード

早速ですがコードの全文です。今回はタスク毎にJobを分けて対応しています。こちらのコードを順を追って説明していきます。

```yml:.github/workflows/publish.yml
name: publish
on:
  push:
    branches: # 今回はdevelopブランチをdev環境、release/〇〇〇ブランチをstg環境への配信としてます
      - 'develop'
      - 'release/**'
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: cache gradle
        uses: actions/cache@v2
        id: cache_gradle
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
          restore-keys: |
            ${{ runner.os }}-gradle-
        continue-on-error: true
      - name: set up JDK 1.8
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
      - name: restore debug.keystore
        run: |
          echo ${{ secrets.DEBUG_KEYSTORE }} | base64 --decode > ~/signingConfigs/debug.keystore
        shell: bash
      - name: Build with Gradle [DEV]
        if: github.ref == 'refs/heads/develop'
        run: ./gradlew :app:assembleDevDebug
      - name: Build with Gradle [STG]
        if: github.ref != 'refs/heads/develop'
        run: ./gradlew :app:assembleStgDebug
      - name: Upload apk
        uses: actions/upload-artifact@v2
        with:
          name: apk
          path: app/build/outputs/apk
          retention-days: 14
  publish:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Download apk
        uses: actions/download-artifact@v2
        with:
          name: apk
      - name: Publish Firebase App Distribution [DEV]
        if: github.ref == 'refs/heads/develop'
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{secrets.FIREBASE_DEV_APP_ID}}
          token: ${{secrets.FIREBASE_TOKEN}}
          file: dev/debug/app-dev-debug.apk
      - name: Publish Firebase App Distribution [STG]
        if: github.ref != 'refs/heads/develop'
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{secrets.FIREBASE_STG_APP_ID}}
          token: ${{secrets.FIREBASE_TOKEN}}
          groups: testers
          file: stg/debug/app-stg-debug.apk
  notice:
    if: always()
    needs: publish
    runs-on: ubuntu-latest
    steps:
      - uses: technote-space/workflow-conclusion-action@v1
      - uses: 8398a7/action-slack@v3
        with:
          status: custom
          fields: workflow,job,commit,repo,ref,author,took
          custom_payload: |
            {
              username: 'action-slack',
              icon_emoji: ':octocat:',
              attachments: [{
                color: '${{ env.WORKFLOW_CONCLUSION }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning',
                text: `${process.env.AS_WORKFLOW}\n${process.env.AS_JOB} (${process.env.AS_COMMIT}) of ${process.env.AS_REPO}@${process.env.AS_REF} by ${process.env.AS_AUTHOR} succeeded`,
              }]
            }
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # optional
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required
```

## buildジョブ

### caches

まずはコードのビルドを担当するbuildジョブです。
最初のstepで大事なのがactions/cache@v2 [^1] です。今回はビルドキャッシュである `~/.gradle/caches`と `~/.gradle/wrapper` の２つをキャッシュしてCI実行時間の削減をしてます。この２つで数百MBあるので結構大事。

[^1]: actions/cache@v2はキャッシュがあればキャッシュを復元します。また、ジョブの最後でここのpathで指定しているファイルをキャッシュしてくれます。

```yml
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2 # GitHubのcheckout
      - name: cache gradle
        uses: actions/cache@v2
        id: cache_gradle
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
          restore-keys: |
            ${{ runner.os }}-gradle-
        continue-on-error: true
```

### keystoreのリストア

続いて大事なのがkeystoreファイルのリストアです。GitHub ActionsではファイルをSecrets [^2] として指定できないためbase64でstringにして保存します。

[^2]: GitHubリポジトリ -> Settings ->  Secretsで文字列を変数として扱うことができます。

```bash:keystoreをbase64エンコードする方法
base64 ~/signingConfigs/debug.keystore
```

base64エンコードされた文字列をデコードする場合は下記のようにしてください。

```yml
      - name: set up JDK 1.8
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
      - name: restore debug.keystore
        run: |
          echo ${{ secrets.DEBUG_KEYSTORE }} | base64 --decode > ~/signingConfigs/debug.keystore
        shell: bash

```

### build

ここまで来たらようやくbuildです。
if文を使ってdev環境とstg環境でビルドコマンドを分けています。コマンドは皆様の環境のBuild Variantsによって異なるのでよしなに置き換えてください。

```yml
      - name: Build with Gradle [DEV]
        if: github.ref == 'refs/heads/develop'
        run: ./gradlew :app:assembleDevDebug
      - name: Build with Gradle [STG]
        if: github.ref != 'refs/heads/develop'
        run: ./gradlew :app:assembleStgDebug
```

### ジョブ間でapkファイルを受け渡しするためにアップロードする

ビルドが完了したらジョブ間でファイルを受け渡しするための設定を行いましょう。これをやらないとせっかく作成したAPKが台無しになってしまいます。
ここでは `app/build/outputs/apk` 以下のファイルを他のジョブに渡したいのでこのpathにあるディレクトリをアーティファクトとしてアップロードします。

```yml
      - name: Upload apk
        uses: actions/upload-artifact@v2
        with:
          name: apk
          path: app/build/outputs/apk
          retention-days: 14
```
※ 無料枠の場合、ストレージには制限がありアップロード上限が500MBまでとなっています。今回はアーティファクトを定期削除するためにretention-daysで14日で削除するように設定しています。


## publishジョブ

### needs

needsはジョブの依存関係が定義できます。下記の例だとbuildジョブのあとに実行するという意味になります。

```yml
  publish:
    needs: build
```

### 別のジョブでアップロードされたapkファイルをダウンロードする

ここで先程のbuildジョブでアップロードしたapkをダウンロードします。ファイル名はapkと定義しているのでここでは `apk` を指定するだけです。

```yml
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Download apk
        uses: actions/download-artifact@v2
        with:
          name: apk

```

続いて今回のゴールであるApp Distributionでの配信です。ここもdev環境、stg環境でsecrets変数を分けたかったのでif文を用いてます。今回は [wzieba/Firebase-Distribution-Github-Action@v1](https://github.com/wzieba/Firebase-Distribution-Github-Action)というアクションを利用させてもらってます。

```yml
      - name: Publish Firebase App Distribution [DEV]
        if: github.ref == 'refs/heads/develop'
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{secrets.FIREBASE_DEV_APP_ID}}
          token: ${{secrets.FIREBASE_TOKEN}}
          file: dev/debug/app-dev-debug.apk
      - name: Publish Firebase App Distribution [STG]
        if: github.ref != 'refs/heads/develop'
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{secrets.FIREBASE_STG_APP_ID}}
          token: ${{secrets.FIREBASE_TOKEN}}
          groups: testers
          file: stg/debug/app-stg-debug.apk
```

## noticeジョブ

配信できたら通知がほしい！ということで最後にslack通知です。通知には [8398a7/action-slack@v3](https://github.com/8398a7/action-slack)を利用しています。また、ジョブを複数に分けると各ジョブの結果を受け取るのが難しいのですが、そこは [technote-space/workflow-conclusion-action@v1](https://github.com/technote-space/workflow-conclusion-action)アクションを使うことですべてのジョブ実行結果のサマリーがenv.WORKFLOW_CONCLUSIONに保存されるようになるのでこちらを利用することで解決してます。

```yml
  notice:
    if: always() # 他のジョブが成功しても失敗しても実行させる
    needs: publish # publishジョブのあとに実行
    runs-on: ubuntu-latest
    steps:
      - uses: technote-space/workflow-conclusion-action@v1 # 他のジョブの成功、失敗を検知するために必要！
      - uses: 8398a7/action-slack@v3
        with:
          status: custom
          fields: workflow,job,commit,repo,ref,author,took
          custom_payload: |
            {
              username: 'action-slack',
              icon_emoji: ':octocat:',
              attachments: [{
                color: '${{ env.WORKFLOW_CONCLUSION }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning',
                text: `${process.env.AS_WORKFLOW}\n${process.env.AS_JOB} (${process.env.AS_COMMIT}) of ${process.env.AS_REPO}@${process.env.AS_REF} by ${process.env.AS_AUTHOR} succeeded`,
              }]
            }
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # optional
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required
```


# 最後に

最近よくGitHub Actionsを利用しているのですが、まだまだ利用しているAndroidエンジニアが少ないようで、ネット上での情報が不足していて対応に難儀することが多々あります。
この記事を機に利用してくれる人が増えてくれると嬉しいです！

# 参考
https://qiita.com/yokawasa/items/e8d2552b4dffebdc3328