GitHub Actionsを使って、Androidアプリのリリースを自動化する

# はじめに
最近、社内のアプリチームで、ストアへのアップロード手順を簡略するために
GitHub Actionsの導入を検討しています。
今回は、ビルドしたアプリを内部テストトラックに向けて配布するまでの手順を
ご紹介したいと思います。

# 手順

## サービスアカウントの作成
Google Play Consoleにアップロードするとき、
認証のためにGoogle Cloud Platform(GCP)のサービスアカウント情報が必要になります。
ここではサービスアカウントの作成についての手順を紹介します。
### Google Play ConsoleからGCPのプロジェクトページにアクセスする
![スクリーンショット 2023-10-02 16.17.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/636bafdc-7c70-a6ff-62b1-79fbcf024e2a.png)

### 追記: Google Play ConsoleからGCPの連携が不要になりました
記事公開前にGCPの連携が不要になっていました

![スクリーンショット 2023-11-20 10.17.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/3ca0f8ea-3a3c-a1a7-bf5a-25a7c8968c7a.png)

「Publishing APIの設定の詳細」のページに飛ぶと、
日本語だとまだGCPに関する記述があるのですが、
英語版の場合は新しい手順になっていました。
基本的にサービスアカウントの作成からPlay Consoleへの招待までは変わらないです。

https://developers.google.com/android-publisher/getting_started?hl=en

### サービスアカウントを作成する
<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/d544d8d4-f2b8-bae0-df25-c271b9f2e68c.png" />
<br/>
<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/15ea02b6-7cf0-d4d1-d7d5-ce7c40bb3913.png" />

アカウント名・アカウントIDを設定します。
<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/0f7b2131-92aa-1496-2db7-57c4adf9f181.png" />

※アカウントのロールは参照者以上の権限にするようにしてください。

## サービスアカウントの鍵情報を取得
サービスアカウントのキー情報を取得します。
キーを直接使用しない方法もあるのですが、
今回使用するAPIはサービスアカウントのキーが必要になるので、
こちらの方法を紹介します。
サービスアカウントキーを使わない方法は以下を参照してください

https://cloud.google.com/iam/docs/workload-identity-federation

### キー情報を取得
<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/fbf5e446-3d67-ceab-c235-fa6fb981559a.png" />
<br/>
<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/d00d12f3-5e7d-d2fd-d11d-4bbc648e308b.png" />

JSON形式でダウンロードします。
こちらのファイルは一度ダウンロードすると再取得できないため、
安全な場所に保管してください。

## Google Play Consoleにサービスアカウントの権限を追加
Google Play Consoleの「ユーザーと権限」から、
先ほど作成したサービスアカウントのメールアドレスを追加します。

![スクリーンショット 2023-10-02 16.57.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/64f1b83c-3221-3f04-8a5c-cf787f88409a.png)
追加したメールアドレス
![スクリーンショット 2023-10-02 16.59.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/a3448afd-5f25-415d-a22e-531ce94f1149.png)
上記のメールアドレスを新しいユーザーとして招待します。
![スクリーンショット 2023-10-02 17.00.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/e02e38a9-d3f3-8d4c-f9f3-5e316b75fb1a.png)
![スクリーンショット 2023-10-02 17.00.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/671aa371-c44c-bfea-5ea4-fd275f16dc18.png)
権限は上記のように設定しました。

## GitHub Workflowの設定
### シークレットの設定
GitHubリポジトリのSettingsタブページ内の、以下の赤枠部分を選択します。
<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/8a4eb333-81a3-b1a8-04d2-4a1d75da6a7c.png" />

各種シークレットを設定します。
今回は以下のキー名で設定しています。

- ENCODED_RELEASE_KEYSTORE
    - 署名用のKeystoreファイルをBase64エンコードしたもの
- RELEASE_KEYSTORE_STORE_PASSWORD
    - Keystoreファイルのパスワード
- RELEASE_KEYSTORE_ALIAS
    - Keystoreが持つキー名
- RELEASE_KEYSTORE_ALIAS_PASSWORD
    - 上記のキーのパスワード
- SERVICE_ACCOUNT_JSON
    - 先ほどダウンロードしたサービスアカウントのJSON形式のキー情報

### workflow設定ファイル
今回は、deploy_google_play_console.ymlという名前でファイルを追加しました。
プロジェクトの直下に以下のディレクトリ構成で配置します。
<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/04b868c3-11ea-b358-40db-dfefb395d599.png" />

今回、Google Play Storeへのアップロードは以下のActionを使用しました。
設定が簡単で使いやすいです。

https://github.com/r0adkll/upload-google-play

全体は以下の通りです
<details>
<summary>deploy_google_play_console.yml</summary>

```yaml:deploy_google_play_console.yml
name: Deploy Google Play Console
on:
  # PRがmainにマージされ、クローズしたときに起動
  pull_request:
    branches:
      - main
    types: [closed]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      # 今回はJava 17でビルド
      - name: set up JDK 17
        uses: actions/setup-java@v1
        with:
          java-version: 17

      # キーストア設定
      # シークレットファイル配置用のディレクトリ作成
      - name: Create secrets directory
        run: |
          mkdir secrets
      # キーストアファイルをBase64からデコードしてファイルとして配置
      - name: Decode Keystore
        run: echo '${{ secrets.ENCODED_RELEASE_KEYSTORE }}' | base64 --decode > playstore.keystore
      # デコードしたキーストアファイルをsecretsディレクトリ下に配置
      - name: Copy Keystore
        run: |
          cp playstore.keystore secrets/playstore.keystore

      # aabファイルを生成
      - name: Generate AAB
        run: ./gradlew :app:bundleRelease
        # build.gradle.ktsから参照するための環境変数を設定
        env:
          KEYSTORE_ALIAS: ${{ secrets.RELEASE_KEYSTORE_ALIAS }}
          KEYSTORE_ALIAS_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_ALIAS_PASSWORD }}
          KEYSTORE_STORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_STORE_PASSWORD }}

      # シークレットに設定したサービスアカウントキーをファイルとして配置
      - name: Create service_account.json
        id: createServiceAccount
        run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json

      # Google Play Storeの内部テストトラックにアップロードする
      - name: Deploy to Play Store
        id: deploy
        uses: r0adkll/upload-google-play@v1.1.2
        with:
          serviceAccountJson: service_account.json
          packageName: your.app.packagename
          releaseFiles: app/build/outputs/bundle/release/app-release.aab
          track: internal
          # 未公開のアプリはstatusをdraftに指定する必要あり
          status: draft
```

</details>

### ビルド設定
今回は.ktsでビルドしています。
念の為ローカルでもビルドできるために、署名周りは以下のように設定しています。

```kotlin:build.gradle.kts
// 署名キー用のシークレットプロパティファイル(今回はローカルでのみ使用)
val secrets = readProperties(file("../secrets/secrets.properties"))
// リポジトリ上にsecrets.propertiesはアップしないので、読み込まなかったときはエラーログだけ出す
fun readProperties(propertiesFile: File) = Properties().apply {
    try {
        propertiesFile.inputStream().use { fis ->
            load(fis)
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

android {
    signingConfigs {
        create("release") {
            // secrets/secrets.propertiesが無ければ、環境変数を使用する
            // ローカルビルドの設定が不要であれば、System.getenvのみで可
            keyAlias = secrets["alias"] as? String ?: System.getenv("KEYSTORE_ALIAS")
            keyPassword = secrets["aliasPassword"] as? String ?: System.getenv("KEYSTORE_ALIAS_PASSWORD")
            storeFile = file("../secrets/playstore.keystore")
            storePassword = secrets["password"] as? String ?: System.getenv("KEYSTORE_STORE_PASSWORD")
        }
    }

    // ...

    buildTypes {
        release {
            // リリースビルド時に署名用のキーを設定
            signingConfig = signingConfigs.getByName("release")
            // ...
        }
    }
    // ...
}
```

Java 17でビルドするので、ターゲットバージョンも設定

```kotlin:build.gradle.kts
android {
    // ...
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = "17"
    }
    // ...
}
```

# 動かしてみる
<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/d179bf36-102a-9d11-5129-216cd5fecace.png" />

PRを作成して、mainにマージしてみます。

<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/83170411-0335-f7c3-0102-4c9195f4c4f1.png" />

無事Google Play Consoleにアップロードできました。
(ビルドが一発で通らず、だいぶPRを作り直しました…)

<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123957/847a2973-6eaf-df41-45a5-5582f6b5f354.png" />

Google Play Consoleにアップされました。
今回はストアにリリースしていないアプリなので、statusはdraft(未公開)になります。

# 今後の課題
- 複数人で作業する場合のGitHubの権限設定
- テスト・ビルドの結果の通知
- Firebase App Distribution向けの配布
- 現状は毎回フルビルドが走るので、変更が無い部分はキャッシュできるかどうか

# 参考にさせていただいた記事
https://techblog.zozo.com/entry/google-play-upload

サービスアカウントの作成・ビルド・デプロイまでまとまっており、とても参考になりました。

# おわりに
お疲れ様でした。

今回触ってみた感じだと、GitHub Actionsでアプリの自動リリースを行えると、
- ビルド後にアプリを配布し忘れる、といったようなケアレスミスを減らせる
- ローカルマシンでリリースビルドする必要がなくなるので、個々のマシンでリリース用のKeystore設定が不要になり、セキュリティが高くなる

などのメリットがありそうだと感じました。
今後、積極的に導入していきたいと思います。

# 運動通信社について
私たち運動通信社は、共に働きたい方を募集しています。
ご応募の際は、下記のリンクの採用情報をご覧ください。

https://sportsbull.jp/about/#recruit