Github Actions と Firebase App Distribution と Slack で apk を自動でテスターに配布


GitHub Actions を使って Android の CI 環境を整えてみようと思います。
今回は PR が master にマージされたら、ビルドして Firebase App Distribution に apk をデプロイ (+ Slack でデプロイ完了を通知) するまでを目指します。


## Firebase の準備

まずは Firebase のアカウントを準備しましょう。

[ログイン - Google アカウント](https://console.firebase.google.com)


### Firebase のプロジェクトを作成

Firebase にプロジェクトを作成し、Android アプリを追加します。

![add_android_app](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308397/07d1175a-53dc-16c2-93fe-446d79570b75.png)


追加できたら、サイドメニューから 品質 -> App Distibution を選びます。
App Distribution を開始してテスターを追加してください。

アプリを配布するだけなら google-services.json は必要ありません。
FCM とか使う場合は組み込んでおきましょう。
ただし、google-services.json をパブリックリポジトリで使う場合は少し工夫がいるので後ほど説明します。

これで Firebase App Distribution で apk を配布する準備ができました。


### 作成したプロジェクトから アプリ ID を取得

GitHub Actions から App Distribution で配布するために、該当のアプリ ID を控えておきます。

![app_id](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308397/970b3b76-5bda-9523-96ad-116db0ac5980.png)


### Firebase のトークンを取得

以下のリファレンスを参考に CI サーバ用のアクセストークンを取得します。

[Firebase CLI リファレンス](https://firebase.google.com/docs/cli?hl=ja#cli-ci-systems)

まず、Firebase CLI をインストールします。
Mac の方は brew でインストールできます。

```
$ brew install firebase-cli
```

次に、ci サーバにログインするコマンドを入力します。

```
$ firebase login:ci
```

するとブラウザに連携されるので、Firebase Console にログインするアカウントを選択します。

以下の様な表示がされていればログイン成功です。Woohoo!😄

![Woohoo](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308397/a9911dc8-85b4-064f-12dd-99b3aaf14f3c.png)

コンソールに戻ってアクセストークンを控えておきましょう。

![access_token](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308397/bb05116c-041c-be78-7161-871d620e29a5.png)


Firebase 側の設定は以上になります。


## Actions で自動ビルドと apk の配布

続いて GitHub 上で自動ビルド -> App Distribution に配布する設定をします。


### GitHub の環境を設定

`Settings -> Secrets` から先程控えたアプリ ID とアクセストークンをアカウントに登録します。
後ほど説明しますが、セキュアな情報はここに登録して使います。

![secrets](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308397/8f9ece21-4b1a-2e4a-b6ee-f53712ae2535.png)


### workflow を追加

最後に Actions の workflow を定義します。

マーケットプレイスに色々 Action がリリースされており、
App Distibution へのデプロイ用 Action があるので今回はそれを使います。

ただしサンプルにはグループの指定が書いてあるので設定していない場合は、削除するかコンソールからグループを作成しておきましょう。

[Firebase App Distribution · Actions · GitHub Marketplace · GitHub](https://github.com/marketplace/actions/firebase-app-distribution)


今回は PR が master にマージされたら apk を配布する設定にしています。


```deploy.yml
name: Build & upload to Firebase App Distribution 

on:
  pull_request:
    types: [closed]
    branches:
      - master

jobs:
  build:

    runs-on: ubuntu-latest

    if: github.event.pull_request.merged == true

    steps:
      - uses: actions/checkout@v1
      - name: set up JDK 1.8
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
      - name: build release 
        run: ./gradlew assembleRelease
      - name: upload artifact to Firebase App Distribution
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{secrets.FIREBASE_APP_ID}}
          token: ${{secrets.FIREBASE_TOKEN}}
          file: app/build/outputs/apk/release/app-release-unsigned.apk
```

これで PR が master にマージされる度に App Distribution に apk がデプロイされます。

Console を確認すると以下の様にリリースされています。

![app_distribution](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308397/28e5f5da-782c-154a-74cc-60714cada4db.png)


#### workflow 内の各プロパティ

##### name

その Actions や各実行される処理の Description です。


##### on

workflow が実行されるトリガーを指定します。
types や branches で更に条件を絞ることができます。
今回は master に対する PR が close された時にトリガーされる様な指定になっています。


##### jobs

workflow で実行される処理です。
今回は build と言う ID のジョブが登録されています。


##### runs-on

必須パラメータ。ジョブを実行するマシンの種類。
GitHub にホストされているマシンや自分でホストしているマシン等が選べます。


##### if

処理を実行するための条件を指定します。
今回はマージされずにクローズされた PR でもトリガーされて配布されるのを防ぐために、マージされた時だけ処理される様にしています。


##### steps

ジョブが実行する処理の集まりです。
各ステップはプロセスが別です。


##### with

action で定義されているパラメータを指定します。
Firebase-Distribution-Github-Action ではアプリID、アクセストークン、 apk ファイルを指定して実行します。


### gradle-wrapper.jar が見つからない

ワクワクしながら一度 Actions を走らせてみたらビルドに失敗していました😂

![build_failed](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308397/16750c5b-15e3-0c33-2770-f9681ed5a44d.png)

失意の中さまよっていると下記の様な記事を見つけました。

[gradlewが謎のエラーで動かない件について - Qiita](https://qiita.com/pg_mot/items/bf02f4f37382696f3898)

何も考えずに Gibo で kotlin プロジェクトを gitignore にダンプすると jar ファイルが ignore されます。
それに引っかかって GitHub のリポジトリ上に `gradle/wrapper/gradle-wrapper.jar` が存在しないせいでビルドができなくなっていました。

.gitignore に以下を追加して gradle-wrapper.jar だけ ignore から除外する様にしておかないといけません。

```.gitignore
!gradle/wrapper/gradle-wrapper.jar
```

後は gradle-wrapper.jar を作成してコミットしましょう。
jar ファイルの作成は以下のコマンドでできます。

```
$ gradle wrap
```


### Secrets でセキュアな情報を管理

既存のプロジェクトに GitHub Actions を導入した時の話です。
SDK の認証情報を外部ファイルに切り出して管理し ignore していたため、
上の workflow のままだとこれまたリポジトリにファイルがないのでビルドに失敗します。

以下の様にファイルの内容を Base64 でエンコードして Secrets に保存し、
ビルドのステップの前にデコードして追加してあげるとビルドが通る様になりました。

[GithubActionsでgitignoreしたgoogle-services.jsonを読み込む - Qiita](https://qiita.com/sudo5in5k/items/5b6da5dbba3fc2514319)

追加の仕方が悪いのか平文で Secrets に登録すると改行とかがあると上手くいかなかったりしたので、エンコード -> デコードの手順が必要なのかなと思います。
ワンラインの内容なら特に問題はないかと。

google-services.json 等セキュアな情報を抱き込む必要がある場合は Secrests に登録すると安全に CI できます。


## ついでにデプロイされたら Slack に通知する様にしてみた

Firebase App Distribution は apk が新しく登録されたらテスターにメールが飛ぶのでそれでいいのかもしれませんが、
できれば Slack で通知されると見落としが減っていいなぁと思ってやってみました。
PR の URL も一緒に流せたらもっと素敵。

過去に Slack の Incoming Webhook をセットアップした事があるんですが、もう一度調べてみたところアップデートされており、
ペイロードの attachments はいつ消えてもおかしくないから blocks 使いなよって事らしいです。
ただ、左側のカラー付きサイドバーで引用っぽく表示するやり方 (color の指定) は今のところ attachments にしか無いみたいなのでどちらを使うかはメッセージのレイアウトによりますね。

Slack にポストする Actions もマーケットプレイスにたくさんありましたが、今回は

- 新しい Incoming Webhook に対応している
- レイアウトを自由に決めれる様に attachments, blocks 等 Slack のメッセージペイロード に対応している

を元にこちらの Actions を使わせていただきました。
ありがとうございます。

[Slack Incoming Webhook · Actions · GitHub Marketplace · GitHub](https://github.com/marketplace/actions/slack-incoming-webhook)

Webhook のセットアップの仕方は今回は割愛します。
Webhook URL を secrets に登録しておきましょう。

```deploy.yml
name: Build & upload to Firebase App Distribution 

on:
  pull_request:
    types: [closed]
    branches:
      - master

jobs:
  build:

    runs-on: ubuntu-latest

    if: github.event.pull_request.merged == true

    steps:
      - uses: actions/checkout@v1
      - name: set up JDK 1.8
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
      - name: build release 
        run: ./gradlew assembleRelease
      - name: upload artifact to Firebase App Distribution
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{secrets.FIREBASE_APP_ID}}
          token: ${{secrets.FIREBASE_TOKEN}}
          file: app/build/outputs/apk/release/app-release-unsigned.apk

      - name: post message to Slack
        if: success()
        uses: tokorom/action-slack-incoming-webhook@master
        env:
          INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
        with:
          text: "New app was deployed!!"
          blocks: |
            [
              {
                "type": "section",
                "text": {
                  "type": "mrkdwn",
                  "text": "New app was deployed!! :clap:"
                }
              },
              {
                "type": "divider"
              },
              {
                "type": "section",
                "text": {
                  "type": "mrkdwn",
                  "text": "*${{ github.event.pull_request.title }}*\nPushed by ${{ github.event.pull_request.user.login }}\n\n*URL*\n${{ github.event.pull_request.url }}"
                },
                "accessory": {
                  "type": "image",
                  "image_url": "${{ github.event.pull_request.user.avatar_url }}",
                  "alt_text": "User Icon"
                }
              },
              {
                "type": "actions",
                "elements": [
                  {
                    "type": "button",
                    "text": {
                      "type": "plain_text",
                      "text": "Join Test"
                    },
                    "url": "${{ secrets.APP_DISTRIBUTION_URL }}"
                  }
                ]
              },
              {
                "type": "divider"
              }
            ]
```

これで workflow が成功すると Slack にデプロイされた通知が飛んでくる様になります。

![slack_posted](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308397/1914dc5b-d8c0-cc2c-8999-8d595e75d741.png)


色々とパラメータが使えるみたいで好きな様にレイアウトやテキストがカスタマイズできそうでいいですね。
もっと素敵なメッセージにしたかった。デザイン力をください。。。

どういったパラメータが使えるかはドキュメントに記載してあります。

[GitHub Actions のコンテキストおよび式の構文 - GitHub Docs](https://docs.github.com/ja/actions/reference/context-and-expression-syntax-for-github-actions)

他のパラメータも知りたい場合は workflow の途中とかでダンプすると出てきます。

```
  steps:
    - name: Dump GitHub context
      env:
        GITHUB_CONTEXT: ${{ toJson(github) }}
      run: echo "$GITHUB_CONTEXT"
```


## Conclusion

こういう設定周りのファイルを弄るのは、手間暇かけた分楽できるのがダイレクトに感じられて楽しいです。

触ってみるととても分かりやすく、ドキュメントもしっかりしていました。
Enterprise でリリースされたらぜひプロジェクトで使ってみたいですね😄

今後は aab にシフトしていくらしいですけど、その時は DeployGate に流したほうがいいんでしょうかね🤔


## 参考

[GitHub Actionsのワークフロー構文 - GitHub Docs](https://docs.github.com/ja/actions/reference/workflow-syntax-for-github-actions)

[AndroidプロジェクトをGitHub ActionsでCIする - Qiita](https://qiita.com/tetsukay/items/f3b79743292bdecb1cd0)

[Github ActionsからSlackへ通知するのを良い感じにしたい \| Spinners Inc.](https://spinners.work/posts/github-actions-context/)