GithubActionsでAndroidアプリのDeploygate配布、PlayStoreアップロードを自動化する

Androidアプリは規模が大きくなるとビルドにも時間がかかる。特にビルド待ち時間はbranchを切り替えて作業するわけにもいかず、とても効率が悪いため自動化をして楽をしよう。

## CI/CDの実現方法
やり方はいくつか考えられ、それぞれメリットやデメリットがあるので要件や費用、運用の手間を考えて選択したいところ。

* 自前サーバーで運用
    * サーバー立ててJenkinsなどのApplicationをインストールして運用する
    * Macなどにインストールして社内ネットワーク上で使う
    * 物理サーバーだと電源とかマシン自体のメンテナンスもやる必要があって非効率
* CI/CD専用のサービスを利用する
    * CircleCIやBitriseなどのサービスを契約する
    * お金がかかるが、コンテナ上でビルドできる
    * 無料プランもあるが、制限はある
* VCSサービスに付属の機能を利用する
    * お金がかかる場合があるが、コンテナ上でビルドできる
    * 無料枠あり
    * Gitlab CI/CD Pipeline（使ったことない）
    * GithubActions(今回使う）

物理サーバーへの依存は極力さけたいのと、昨今盛り上がってきているGithubActionsですべて対応することにしてみた。

## GithubActions
https://github.co.jp/features/actions
2019年に正式に発表された、Githubで使えるCI/CDツール。
Github上から直接コードをビルド、テスト、デプロイできる。

CircleCIなどを使ったことがあるのであれば、比較的簡単に入門できると思われる。
git管理されているプロジェクトルートに

```
.github/workflows/hogehoge.yaml
```
を追加し、YAML形式で内容を記載して行く流れになる。
※「workflows」でないと正しく認識されないため注意(最後のsを忘れがち)

### workflowのトリガーイベント
workflowの[トリガー](https://docs.github.com/ja/actions/reference/events-that-trigger-workflows)にはいくつか種類があるので、最適なものを選択してYAMLに記載すると、適切なタイミングでworkflowを実行することができる。
詳しくは本家ドキュメントにて。

```yaml:hogehoge.yaml
on:
  push:
    branches: # 指定branchへのpush / mergeをトリガーとしてworkflowを起動。マージ後に自動デプロイしたい場合など
      - master
      - feature/hoge
  pull_request: # 指定branchへのpull request作成をトリガーとしてworkflowを起動。レビュー前にテスト実行や自動レビューを行いたい場合など
    branches:
      - master
jobs:
  buid:
    name: hoge...
```

### 新たなトリガー： workflow_dispatch
https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/
2020年7月、workflow_dispatchという新たなトリガーがアナウンスされ、workflowの手動実行が可能になった。
これにより、githubイベントによるトリガーだけではなく、必要なときにworkflowを起動することができる。

inputパラメーターも受け付けるため、外部から値を受け取り動的に挙動を変えることも可能。

```yaml:hogehoge.yaml
on:
  workflow_dispatch: # 手動トリガーとする場合

    inputs:
      some_param_1:
        required: true
        description: 必須パラメーター
      some_param:
        required: false
        description: オプショナルパラメーター
jobs:
  buid:
    name: hoge...
```
そうすると、このようにgithubのActionsタブに行くと、workflowをマニュアル実行できるボタンが出現する。
![スクリーンショット 2020-09-16 17.56.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29379/41909bae-4e61-16bb-549e-2495d5d3dca9.png)


## 今回やること
今回やることは大きく分けて３つ。
1. githubのイベントトリガーによるDeploygate自動配布
2. workflow_dispatchトリガーによるDeploygateマニュアル配布
3. workflow_dispatchトリガーによるPlayストアアップロード

## 1. githubのイベントトリガーによるDeploygate自動配布
用途としては、QAをクリアしリリース待機状態になっているビルドを常に最新に保つ。
確認したければ固定のDeploygate配布ページから確認すればいつでも最新のビルドを確認することが可能になる。
以下のようなYAMLを ```.github/workflows/``` 直下に作成。

```yaml:.github/workflows/hoge.yaml
name: Deploygate Auto Distribution
on:
  push:
    branches:
      - master # master branchへのpush / mergeをトリガーとしてworkflow開始
jobs:
  build:
    runs-on: ubuntu-latest
    # skip ciの場合は実行しない
    if: "!contains(github.event.head_commit.message, '[skip ci]')"
    steps:
      - uses: actions/checkout@v2 # branchチェックアウト
      - uses: actions/cache@v1 # gradle依存関係をキャッシュする（次回以降高速化のため）
        with:
          path: ~/.gradle/caches
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
          restore-keys: |
            ${{ runner.os }}-gradle-
      - name: setup JDK # JDKをセットアップする
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
      - name: sensitive
        shell: bash
        env:
          DECRYPT_PASS: ${{ secrets.DECRYPT_PASS }} // github secretsから復号化キーを取得
        run: |
          # opensslが1.1.1gのためMacでLibreSSL使っている場合はコマンドを変える必要があるかもしれない
          # 復号化コマンド
      - name: Build Debug // デバッグビルドの実行
        shell: bash
        run: |
         ./gradlew assembleDebug
      - name: Distribute Debug
      	env:
      	  DEP_API_KEY: ${{ secrets.DEP_API_KEY }} # 以下のcurlで利用するAPIキーやdistributionキーなどをsecretsからgithub 取得
        run: |
          # DeploygateのAPIリファレンスに従って、curl処理	
      - name: Notification on Success # workflow処理成功時の処理
        if: success()
        run: |
          # slackなどのチャットツールに通知するcurl
      - name: Notification on Failure # workflow処理失敗時の処理
        if: failure()
        run: |
          # slackなどのチャットツールに通知するcurl
```

ここでのポイントは

* ```if: "!contains(github.event.head_commit.message, '[skip ci]')"``` によりCIスキップを実現
    * CI処理が不要な場合にコミットメッセージに [skip ci] を入れるとworkflowがスキップされるようになる
* ```actions/cache@v1``` によりgradle依存関係をキャッシュ
    * 次回以降はキャッシュから読み出すため、場合によっては数分程度処理が短縮される
    * ただし、workflow_dispatchの場合はキャッシュサポートされていないため注意が必要
* センシティブなキーは直書きせずに、github secretsに登録
    * ```{{ secrets.HOGE }}``` により環境変数に読み出すようにする
* ```if: success()``` ```if: failure()``` により成功・失敗をSlack等に通知する


## 2. workflow_dispatchトリガーによるDeploygateマニュアル配布
用途としては、開発中branchでエンジニア以外のメンバーに一旦確認をしてもらいたいときなど、branchを指定してworkflowを手動実行する。
配布終了するとチャットツールに通知されるため、誰でもそのビルドを確認できるようになる。
workflow_dispatchを使う上での変更点は以下のみ

```yaml
name: Deploygate Manual　Distribution
on:
  workflow_dispatch:
    inputs:
      distribution_name:
        required: true
        description: deploygateの配信名
jobs:
  # 中略
  - name: Distribute Debug
    run: |
    	# Deploygate APIのパラメーターで distribution_name=${{github.event.inputs.distribution_name}} のようにinputからの文字列を指定
```

ここでのポイントは

* inputs を受け付けることで、動的にDeploygateの配布ページを作成するようにする
    * このように、workflow実行時に配信名を入力できるようになる
![スクリーンショット 2020-09-16 18.45.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29379/f048c0dc-2caf-0462-88dd-66e73efa4e16.png)

## 3. workflow_dispatchトリガーによるPlayストアアップロード
[Play Publishing API](https://developer.android.com/google/play/developer-api?hl=ja#publishing_api_overview)を使ってPlayストアにアップロードを行う。
なお、Github Actionsマーケットプレイスにすでに対応されたActionsが公開されているため、一番早いのはこの方法。
https://github.com/marketplace/actions/upload-android-release-to-play-store

今回は、将来的にいろいろと拡張しやすいのと、そこまで大変な作業ではないため自前で準備することにした。
Play Publishing APIはpythonのサンプルがあるため、[こちら](https://github.com/googleapis/google-api-python-client)を参考に。最新版のv３を利用。
しかし、サンプルのコード自体が古く、今ではremoveされているメソッドや、サービスアカウント認証が推奨ではないp12キーファイルによる認証となっているため、新しい環境でも動くように改変。

まずは以下のファイルを ```.github/workflows/scripts``` などのディレクトリに配置

```requirements.txt
# pipで依存関係を一括インストール用
google-api-python-client ~= 1.11.0
oauth2client ~= 4.1.3
```

```python:upload_aab.py
"""Uploads an aab to the internal track."""

import argparse
import sys
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
import httplib2
from oauth2client import client
from oauth2client.service_account import ServiceAccountCredentials


TRACK = 'internal'  # or can be 'alpha', beta', 'production' or 'rollout'

# workflow.yamlからの入力受付用
argparser = argparse.ArgumentParser(add_help=False)
argparser.add_argument('package_name',
                       default='com.examole.app',
                       help='The package name. Example: com.android.sample')
argparser.add_argument('aab_file',
                       nargs='?',
                       default='app-release.aab',
                       help='The path to the AAB file')
argparser.add_argument('service_account_json',
                       nargs='?',
                       help='The path to the key file of service account.')


def main(argv):
  scopes = ['https://www.googleapis.com/auth/androidpublisher']
  flags = argparser.parse_args()
  service_account_json = flags.service_account_json

  # サンプルではここがp12ファイルを利用していたため、サービスアカウントのjsonキーファイルで認証するように変更
  credentials = ServiceAccountCredentials.from_json_keyfile_name(service_account_json, scopes=scopes)
  http = httplib2.Http()
  http = credentials.authorize(http)

  service = build('androidpublisher', 'v3', http=http)
  package_name = flags.package_name
  aab_file = flags.aab_file

  try:
    edit_request = service.edits().insert(body={}, packageName=package_name)
    result = edit_request.execute()
    edit_id = result['id']

    print('Edit ID : "%s"' % edit_id)

    # aabのアップロード(apkとはアップロード方法が異なるため注意)
    media = MediaFileUpload(aab_file, mimetype='application/octet-stream', resumable=True)
    aab_response = service.edits().bundles().upload(
        editId=edit_id,
        packageName=package_name,
        media_body=media).execute()

    print('Version code %d has been uploaded' % aab_response['versionCode'])

    # Trackの更新(internal track)
    track_response = service.edits().tracks().update(
        editId=edit_id,
        track=TRACK,
        packageName=package_name,
        body={u'releases': [{
            u'name': u'アップロード時の文言を指定',
            u'versionCodes': [str(aab_response['versionCode'])],
            u'status': u'completed',
        }]}).execute()

    print('Track %s is set with releases: %s' % (
        track_response['track'], str(track_response['releases'])))

    # Transactionのcommit
    commit_request = service.edits().commit(
        editId=edit_id, packageName=package_name).execute()

    print('Edit "%s" has been committed' % (commit_request['id']))

  except client.AccessTokenRefreshError:
    print ('The credentials have been revoked or expired, please re-run the '
           'application to re-authorize')

if __name__ == '__main__':
  main(sys.argv)
```

続いてworkflow。Deploygateと同様な部分は記載割愛。

```yaml:store_upload.yaml
name: Upload To Play Store
on:
  workflow_dispatch:

jobs:
  build-and-upload-store:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          ref: ${{ github.event.inputs.branch_name }}
      # 中略
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
      - name: Build Release # リリース用ビルド
        shell: bash
        run: |
          ./gradlew bundleRelease
      - name: Install python dependencies # requirements.txtから依存一括インストール
        run: |
          python -m pip install --upgrade pip
          pip install -r ./.github/workflows/scripts/requirements.txt
      - name: Upload to play store # ストアの内部テストトラックへアップロード
        run: |
          python './.github/workflows/scripts/upload_aab.py' \
          'com.example.your.package' \
          'app/build/outputs/bundle/release/app-release.aab' \ # ここは環境によって変更
          './.github/workflows/account.json' # 必要に応じて暗号化したjsonを事前に復号化する
      # 成功失敗の通知
```

ここでのポイントは

* サービスアカウントの認証をjsonキーファイルを利用するように変更
    * ただし、github secretsはjsonなどの構造化ファイルを置けないため、暗号化してpushし、使う前に復号化する等の対策が必要
* apkではなくaabアップロードできるように少し変更
* 自前の実行ファイル（python)を準備して、workflowから直接呼び出し


# 最後に
今回はAndroidビルドによるサンプルだったが、それ以外でもCI/CDとしてかなり使えそうなため、どんどん使って行きたい。
毎回ローカルPCでの手動ビルドによる待ち時間やビルドミスの削減にもつながるため、効率化はかなりされたと思う。
python2でしか動作しないので早いとこpython3に移行しないと・・・