AndroidGitHub ActionsGoogle PlayCI/CDKotlin

GitHub ActionsでAndroidアプリをビルド・署名してGoogle Play Consoleに自動デプロイする

Sloth255
Sloth255
·5 min read·1,058 words

はじめに

Androidアプリのリリース作業、毎回手元の Android Studio から Generate Signed Bundle ボタンを押して、ビルドが通ったら Play Console を開いて AAB をドラッグ&ドロップして…という運用をしていませんか?

私もしばらくこの運用でしたが、リリース回数が増えてくると以下のような課題が出てきました。

  • 手元の環境差分でビルドが壊れる(JDK のバージョンや Gradle キャッシュ問題)
  • キーストアの管理が属人化する(担当者の Mac にしかない、みたいな状態)
  • リリース手順がドキュメントに依存し、ヒューマンエラーが発生しがち

これらをまるっと解決するために、GitHub Actions 上でビルド・署名・Google Play Console へのアップロードまでを自動化したので、その手順をまとめます。

全体像

最終的に組むパイプラインは次のような流れです。

flowchart TD
  A["git tag v1.0.0 push"]
  B["GitHub Actions 起動"]
  C["JDK セットアップと Gradle キャッシュ復元"]
  D["Keystore (= アップロード鍵) を Secrets から復元"]
  E["./gradlew bundleRelease で AAB をビルドして署名"]
  F["r0adkll/upload-google-play で内部テストへ配信"]
  G["Google が受け取り、アプリ署名鍵で再署名してユーザーへ"]

  A --> B --> C --> D --> E --> F --> G

「AAB」や「アップロード鍵」というキーワードは次のセクションで整理します。

事前に押さえておきたい 3 つの概念

実際の手順に入る前に、本記事で出てくるキーワードを整理しておきます。ここがふわっとしていると後の Play App Signing 周りで混乱しがちなので、軽く頭に入れておくとスムーズです。

APK と AAB の違い

APK (Android Package Kit) AAB (Android App Bundle)
中身 全 ABI ・全画面密度・全言語リソースを 1 ファイルにまとめたユニバーサルパッケージ 端末向けに最適化するのビルド成果物
ファイルサイズ 全部入りなので大きくなりがち Google Play 側で Split APK を生成して配信するため、ユーザーのダウンロードサイズが平均 15% 程度小さい
主な用途 直接配布 / 社内配布 / サードパーティストア Google Play Store への配信(2021 年 8 月以降の新規アプリは 必須)
Gradle タスク ./gradlew assembleRelease ./gradlew bundleRelease

ざっくり言うと 「APK = ユーザー端末にインストールできる完成形」「AAB = Google Play に提出する設計書」 のような関係です。Google Play は AAB を受け取り、ユーザーの端末に応じた APK を毎回組み立てて配信します。

本記事は Play Store 配信が目的なので AAB(bundleRelease)を使います。社内配布など別用途で APK が必要なケースでは assembleRelease で APK もビルド可能です。

アップロード鍵とアプリ署名鍵

Play App Signing では、署名鍵が 2 種類 存在します。

鍵の種類 誰が管理? 用途
アップロード鍵 (Upload Key) 開発者 AAB を Play Console にアップロードする時の署名
アプリ署名鍵 (App Signing Key) Google Play Store からユーザーに配信する最終 APK の署名

つまり Play Console は「正しいアップロード鍵で署名された AAB か?」を検証するために、開発者が自分で持っているアップロード鍵 = キーストアで署名する必要があるのです。Google が代わりにやってくれているのは、ユーザー配信時にアプリ署名鍵で再署名する部分だけ。

flowchart TD
  Dev["開発者"] -->|"アップロード鍵で署名"| AAB["AAB"]
  AAB --> Play["Play Console"]
  Play --> Verify["Google がアップロード鍵を検証"]
  Verify --> Resign["Google がアプリ署名鍵で再署名"]
  Resign --> User["ユーザー"]

Play App Signing になっても「自分でキーストアを作る必要がある」事実自体は変わらず、変わったのは「アップロード鍵を失くしても再発行できる」という安全性だけ、と捉えるとわかりやすいです。

デバッグ署名と release 署名の使い分け

Android Studio で初めて Run ボタンを押した時、何の設定もしていないのに APK ができて端末で動いたのは、AGP (Android Gradle Plugin) が ~/.android/debug.keystore を自動生成してデバッグビルドに自動署名している からです。

デバッグ署名 release 署名
キーストア ~/.android/debug.keystore(AGP が自動生成) 自分で作成・管理(本記事の release.keystore)
パスワード android 固定 自分で設定
エイリアス androiddebugkey 固定 自分で設定
有効期限 30 年程度 自分で設定(本記事は約 27 年)
用途 開発中の動作確認 / ./gradlew assembleDebug Play Store 配信 / ./gradlew bundleRelease
Git 管理 不要(マシンごとにあれば OK) 厳重に保管(失くしても Play App Signing なら再発行可)

重要な性質が 2 つ あります。

  1. Play Console はデバッグ署名された AAB を絶対に受け付けない
    → なので CI では release.keystore を別途用意する必要がある
  2. デバッグ署名と release 署名は完全に独立している
    → CI 用に release 署名の設定を追加しても、ローカルの ./gradlew assembleDebug には一切影響しない

セクション 5・6 で signingConfigs.create("release") を条件分岐させているのは、まさにこの 2 つの性質を活かして「鍵を持っていない開発者でもデバッグビルドだけは普通に動く」状態を作るためです。

前提条件

  • Android アプリ(build.gradle または build.gradle.kts を使用)
  • GitHub リポジトリ
  • Google Play Console のデベロッパーアカウント
  • 既に Play Console 上に対象アプリの初回リリースが手動で済んでいること(API 経由の初回作成は不可)
  • 対象アプリで Play App Signing が有効になっていること

2021 年 8 月以降に作成された新規アプリは Play App Signing が自動的に有効化されるため、特別な操作は不要です。それ以前に作成された旧方式のアプリの場合は、まず Play App Signing への移行が必要になります(セクション 7-4 で触れます)。

1. アップロード鍵 (キーストア) を作成する

AAB の署名に使うキーストアをローカルで生成します。Play App Signing の構成では、このキーは「アップロード鍵」として機能します(=Play Console に AAB を提出するときの自分側の身分証)。ユーザーへの最終配信時の署名は Google が別途持つ「アプリ署名鍵」で行われるため、仮にこの鍵を失くしても Play Console からリセットを申請できます(詳細は 7-3)。

とはいえ日常運用上、紛失するとリセット完了までリリースが止まるので、安全に保管しておくに越したことはありません。

# macOS / Linux
keytool -genkey -v \
  -keystore release.keystore \
  -alias my-release-key \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000

Windows (PowerShell / コマンドプロンプト) ではバックスラッシュでの行継続が使えないため、1 行で実行するか、PowerShell ならバッククォート ` で改行します。

keytool -genkey -v `
  -keystore release.keystore `
  -alias my-release-key `
  -keyalg RSA `
  -keysize 2048 `
  -validity 10000

対話的にパスワード・氏名・組織名などを聞かれるので入力します。

2. キーストアを Base64 でエンコードする

GitHub Secrets はバイナリを直接保存できないため、Base64 化して文字列にします。ワークフロー側でデコード時に tr -d '\n\r' で改行を除去するため、出力に改行が含まれていても問題ありませんが、各 OS で改行なしで出力するのがより安全です。

macOS

base64 -i release.keystore -o release.keystore.base64
cat release.keystore.base64 | pbcopy

Linux

base64 -w 0 release.keystore > release.keystore.base64
cat release.keystore.base64 | xclip -selection clipboard

Windows (PowerShell)

[Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) `
  | Set-Clipboard

ファイルに書き出したい場合はこちら。

[Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) `
  | Out-File -Encoding ascii -NoNewline release.keystore.base64

Windows (コマンドプロンプト)

certutil でも可能ですが、ヘッダー・フッターと改行が付くので除去が必要です。

certutil -encode release.keystore release.keystore.base64

出力ファイルから -----BEGIN CERTIFICATE----- 行と -----END CERTIFICATE----- 行、および改行を取り除いて使います。素直に PowerShell を使うのがおすすめです。

クリップボードにコピーした文字列を、次のステップで GitHub Secrets に貼り付けます。

3. Google Play Console 用のサービスアカウントを作る

Play Console への自動アップロードには Google Cloud のサービスアカウントが必要です。

3-1. Google Play Android Developer API を有効にする

まず、GCP 側で API を有効にします。この手順を省くと後で 403 エラーで詰まるので最初に済ませておきましょう。

  1. Google Play Android Developer API にアクセス
  2. 有効にする をクリック

3-2. Google Cloud Console でサービスアカウントを作成する

  1. Google Cloud Console にアクセス
  2. プロジェクトを作成(or 既存のものを選択)
  3. IAM と管理 → サービスアカウント から新規作成。IAM ロールは付与しない(Play Console 側で権限を管理するため)
  4. 作成後、鍵を追加 → 新しい鍵を作成 → JSON を選択して JSON ファイルをダウンロード

3-3. Play Console 側で権限付与

  1. Play Console を開く
  2. ユーザーと権限 を開き、ユーザーを招待 をクリック
  3. 手順 3-2 で作成したサービスアカウントのメールアドレスを入力
  4. アプリの権限 タブで対象アプリを追加し、以下の権限を付与
    • テストトラックへのリリース — 内部テストトラックへの配信に必要
    • アプリ情報とリリース管理を表示 — 読み取り権限

4. GitHub Secrets に値を登録する

リポジトリの Settings → Secrets and variables → Actions から、以下を登録します。

Secret 名 中身
KEYSTORE_BASE64 手順 2 で取得した Base64 文字列
KEYSTORE_PASSWORD キーストアのパスワード
KEY_ALIAS 例: my-release-key
KEY_PASSWORD キーのパスワード
SERVICE_ACCOUNT_JSON 手順 3-2 でダウンロードした JSON の中身をそのまま貼り付け

5. build.gradle で署名設定を環境変数から読むようにする

CI では環境変数経由、ローカル開発では keystore.properties 経由で読み取れるようにし、さらにキーストアが無い環境でもデバッグビルドだけは普通に通る 形に組みます。

app/build.gradle.kts を以下のように編集します。

import java.util.Properties
import java.io.FileInputStream

// --- 署名情報の解決 ---
// 優先順位: 環境変数(CI) > keystore.properties(ローカル) > なし(デバッグのみビルド可)
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties().apply {
    if (keystorePropertiesFile.exists()) {
        load(FileInputStream(keystorePropertiesFile))
    }
}

fun resolveSigning(key: String, envKey: String): String? =
    System.getenv(envKey) ?: keystoreProperties.getProperty(key)

val releaseStoreFile = resolveSigning("storeFile", "KEYSTORE_FILE")
val releaseStorePassword = resolveSigning("storePassword", "KEYSTORE_PASSWORD")
val releaseKeyAlias = resolveSigning("keyAlias", "KEY_ALIAS")
val releaseKeyPassword = resolveSigning("keyPassword", "KEY_PASSWORD")

val hasReleaseSigning = listOf(
    releaseStoreFile, releaseStorePassword, releaseKeyAlias, releaseKeyPassword
).all { !it.isNullOrBlank() }

android {
    signingConfigs {
        if (hasReleaseSigning) {
            create("release") {
                storeFile = file(releaseStoreFile!!)
                storePassword = releaseStorePassword
                keyAlias = releaseKeyAlias
                keyPassword = releaseKeyPassword
            }
        }
    }

    buildTypes {
        getByName("debug") {
            // debug は applicationIdSuffix を付けて
            // release ビルドと端末上で共存できるようにしておく
            applicationIdSuffix = ".debug"
            versionNameSuffix = "-debug"
            // signingConfig は AGP が自動生成する debug.keystore がそのまま使われる
        }
        getByName("release") {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
            // 署名情報がある場合だけ release 用 signingConfig を割り当てる
            // (= 鍵を持たない開発者でも assembleDebug は通る)
            if (hasReleaseSigning) {
                signingConfig = signingConfigs.getByName("release")
            }
        }
    }
}

このコードのポイントは以下の通りです。

  • signingConfigs.create("release") をガードしているため、署名情報が無い環境では release コンフィグ自体が作られない
  • debug ビルドタイプは AGP 自動生成の debug.keystore がそのまま使われ、release 署名設定の有無に一切影響を受けない
  • 環境変数を最優先にしているので、CI 上では keystore.properties を置く必要がない

6. ローカル開発を壊さないための追加設定

「CI のために build.gradle を書き換えたら、ローカルでデバッグビルドが通らなくなった」というのは Android × CI でありがちな事故です。これを防ぐため、local.properties 以外も含めて以下を整備します。

6-1. keystore.properties を作る(ローカル専用)

local.properties は本来 SDK パスを書くファイルで、ここに署名情報を混ぜると見通しが悪くなります。署名情報専用に別ファイル keystore.properties を切る のが Android 公式ドキュメントでも推奨されている形です。

プロジェクトのルート(app/ ではなくその一つ上)に keystore.properties を作成します。

storeFile=/Users/yourname/keys/release.keystore
storePassword=your-store-password
keyAlias=my-release-key
keyPassword=your-key-password

このファイルは Git に 絶対に入れない ので .gitignore に追加します(後述)。

6-2. .gitignore に追加すべきもの

意外と見落としがちなのが .gitignore の整備です。Android Studio 生成の .gitignore には署名関連の項目が入っていないので、明示的に追加します。

# 署名関連 - 絶対にコミットしない
*.keystore
*.jks
keystore.properties
release.keystore.base64

# サービスアカウント JSON
*-service-account*.json
play-publisher.json

# google-github-actions/auth が生成する一時認証ファイル (WIF 使用時)
gha-creds-*.json

# Android Studio が作るが念のため
local.properties

local.properties は Android Studio がデフォルトで gitignore に入れてくれていますが、古いプロジェクトだと抜けていることもあるので確認しておきましょう。

6-3. applicationIdSuffix で release / debug を共存させる

上の build.gradle.kts

debug {
    applicationIdSuffix = ".debug"
}

を入れているのは、端末に release 版(Play Store からインストールしたもの)とローカルでビルドした debug 版を同時に入れられる ようにするためです。これがないと、Play Store で配信中のアプリを使っているユーザー(=自分)が、開発機にデバッグビルドを入れた瞬間に上書きしてしまう事故が起きます。

CI に直接関係ない話に見えますが、リリース自動化を始めると Play Store 配信版を端末に入れたまま開発する機会が増えるので、合わせて入れておくのを強くおすすめします。

6-4. ~/.gradle/gradle.properties を使う選択肢

もう一つの選択肢として、ホームディレクトリの Gradle プロパティ に書いておく方法があります。

# macOS/Linux: ~/.gradle/gradle.properties
# Windows:     %USERPROFILE%\.gradle\gradle.properties

MYAPP_KEYSTORE_FILE=/Users/yourname/keys/release.keystore
MYAPP_KEYSTORE_PASSWORD=xxxx
MYAPP_KEY_ALIAS=my-release-key
MYAPP_KEY_PASSWORD=xxxx

build.gradle.kts 側では findProperty("MYAPP_KEYSTORE_FILE") as String? のように読み取ります。

メリットは プロジェクトディレクトリに秘密情報を一切置かない こと。デメリットは新しい開発者にセットアップ手順を伝えるのが少し手間なこと。個人開発なら ~/.gradle/gradle.properties 派、チーム開発なら keystore.properties.gitignore で守る派 くらいの使い分けが私のおすすめです。

6-5. Android Studio で署名なしの release ビルドを試したいとき

./gradlew assembleDebug はそのまま通るのですが、./gradlew assembleRelease署名なしで通したいケース(リリースビルドの ProGuard 設定確認など)もあります。

その場合は、hasReleaseSigning が false のとき release ビルドタイプから signingConfig を外す挙動になっているので、assembleRelease を実行すると 未署名の APK がそのまま生成されます(端末にインストールはできませんが、サイズや mapping.txt の確認は可能)。

7. Play App Signing 運用上の補足

ここまでの手順 1〜6 で、実は Play App Signing を使った構成がもう完成しています。手順 1 で作成した release.keystore は、Play App Signing の文脈ではアップロード鍵として機能します。

このセクションでは、運用していく上で知っておくと便利な補足を整理します。

7-1. 初回アップロード時に何が起きるのか

新規アプリの場合、初回 AAB を Play Console にアップロードした時点で:

  • その AAB に使われた証明書が「アップロード鍵証明書」として自動登録される
  • Google 側で新しい「アプリ署名鍵」が自動生成される
  • Play App Signing が有効化される

つまり追加の Play Console 設定は不要で、本記事の手順をそのまま実行すれば Play App Signing 構成が成立します。

flowchart TD
  CI["CI"] -->|"release.keystore (= アップロード鍵) で署名"| AAB["AAB"]
  AAB --> Play["Play Console"]
  Play --> Verify["Google がアップロード鍵証明書を検証"]
  Verify --> Resign["Google がアプリ署名鍵で再署名"]
  Resign --> Device["ユーザー端末"]

7-2. もう一段安全にする: アップロード鍵を専用に作る

新規アプリでも、Play Console から「アップロード鍵を別途登録する」構成を選べます。

  • 初回アップロード時に Play Console で「Google が生成したアプリ署名鍵を使用」を選ぶ
  • 同じ画面で アップロード鍵証明書を別途アップロード

この場合、release.keystore に入れるのは「完全にアップロード専用の鍵」になります。

通常パターン(7-1) 鍵を分ける運用(7-2)
release.keystore の中身 アップロード鍵 = 初回 AAB を署名した鍵 純粋なアップロード鍵
万が一漏洩した場合 リセット申請で復旧可能 リセット申請で復旧可能
セキュリティ感 十分 より厳格

CI 側の手順は どちらのパターンでも同じ です。release.keystore をどう作るかが違うだけ、と捉えれば OK です。

7-3. アップロード鍵を紛失したらどうするか

Play App Signing 最大のメリットがここです。Play Console からリセット申請ができます

  1. 新しいキーストアをローカルで作成

  2. 証明書を PEM 形式でエクスポート

    keytool -export -rfc \
      -keystore release.keystore \
      -alias my-release-key \
      -file upload_certificate.pem
    
  3. Play Console → 設定 → アプリの整合性 → アプリ署名 → アップロード鍵をリセットするようリクエスト から証明書をアップロード

  4. Google サポートが確認(通常 1〜2 営業日)

  5. 承認後、新しい鍵で署名した AAB を受け付けてくれるようになる

旧方式では「鍵を失くしたら同じパッケージ名で永遠にアップデート不可」という詰みがあったので、この救済措置だけでも Play App Signing を使う価値があります

7-4. 既存(旧方式)アプリを移行する場合

2021 年 8 月以前に作成され、まだ Play App Signing に移行していないアプリの場合は、現在使っている署名鍵そのものを Google に預ける 工程が必要です。

  1. Play Console → 該当アプリ → 設定 → アプリの整合性 → アプリ署名

  2. Google が用意した PEPK (Play Encrypt Private Key) ツールをダウンロード

  3. PEPK ツールで現在のキーストアを暗号化された形にエクスポート

    java -jar pepk.jar \
      --keystore=existing-release.keystore \
      --alias=my-release-key \
      --output=encrypted-key.zip \
      --include-cert \
      --rsa-aes-encryption \
      --encryption-key-path=public-key.pem
    
  4. 出力ファイル (encrypted-key.zip) を Play Console にアップロード

  5. 移行後、必要に応じて新しいアップロード鍵を作成・登録(既存鍵をそのままアップロード鍵として継続使用も可能)

public-key.pem は Play Console の移行画面に表示される公開鍵を保存したものです。

移行後は本記事の手順 1〜6 がそのまま使えるようになります。

8. GitHub Actions ワークフローを書く

いよいよ本題のワークフローです。.github/workflows/release.yml を作成します。

name: Android Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Cache Gradle packages
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew

      - name: Decode Keystore
        run: |
          echo "${{ secrets.KEYSTORE_BASE64 }}" | tr -d '\n\r' | base64 --decode > ${{ github.workspace }}/release.keystore

      - name: Build Release AAB
        env:
          KEYSTORE_FILE: ${{ github.workspace }}/release.keystore
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
        run: ./gradlew bundleRelease --no-daemon

      - name: Upload AAB as artifact
        uses: actions/upload-artifact@v4
        with:
          name: release-aab
          path: app/build/outputs/bundle/release/app-release.aab

      - name: Deploy to Play Store (Internal Track)
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
          packageName: com.example.myapp
          releaseFiles: app/build/outputs/bundle/release/app-release.aab
          tracks: internal
          status: completed
          whatsNewDirectory: distribution/whatsnew

ポイント解説

  • タグトリガー (v*) で起動するようにし、誤って main push で配信されるのを防止
  • Gradle キャッシュ を効かせてビルド時間を短縮(初回 6 分 → 2 回目以降 2 分くらいになりました)
  • Artifact アップロード を入れておくと、Play Console アップロードに失敗しても AAB を Actions の画面から手動でダウンロードできるので保険になる
  • tracks: internal は、alpha / beta / production などに変更可能
  • whatsNewDirectoryリリースノート を言語別に置くディレクトリ。例えば distribution/whatsnew/whatsnew-ja-JP というファイル名で配置します

8-2. より安全な構成: Workload Identity Federation を使う

GitHub Secrets に長期有効な JSON キーを保存する代わりに、Workload Identity Federation (WIF) を使うと、GitHub Actions が一時的な認証情報を取得して Play Console にアクセスできます。JSON キーの漏洩リスクがなくなるため、本番運用では WIF が推奨です。

事前準備(GCP 側)

WIF の設定は一度だけ行えば以降は不要です。google-github-actions/auth の手順に沿って設定します。この方式では サービスアカウントの JSON キーを発行しないため、セクション 3-2 の手順 4(鍵の作成・ダウンロード)と、セクション 4 の SERVICE_ACCOUNT_JSON 登録は不要になります。

以下の gcloud コマンドで設定します。各プレースホルダは実際の値に置き換えてください。

プレースホルダ 説明
${PROJECT_ID} GCP プロジェクト ID
${GITHUB_ORG} GitHub の組織名またはユーザー名
${REPO} org/repo 形式のリポジトリ名
${SERVICE_ACCOUNT} 手順 3-2 で作成したサービスアカウント名(メール @ の左部分)
${WORKLOAD_IDENTITY_POOL_ID} 手順 2 で取得する Pool の完全 ID
# 1. Workload Identity Pool を作成
gcloud iam workload-identity-pools create "github" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --display-name="GitHub Actions Pool"

# 2. Pool の完全 ID を取得
gcloud iam workload-identity-pools describe "github" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --format="value(name)"
# 例: projects/123456789/locations/global/workloadIdentityPools/github

# 3. Workload Identity Provider を作成
#    --attribute-condition で「この組織からのトークンのみ Pool に入れる」と制限する
gcloud iam workload-identity-pools providers create-oidc "my-repo" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="github" \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
  --attribute-condition="assertion.repository_owner == '${GITHUB_ORG}'"

# 4. サービスアカウントに roles/iam.workloadIdentityUser を付与
#    principalSet の attribute.repository で「この特定リポジトリのみ」に絞る
#    ${WORKLOAD_IDENTITY_POOL_ID} = 手順 2 で取得した完全 ID
gcloud iam service-accounts add-iam-policy-binding \
  "${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --project="${PROJECT_ID}" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"

設定後、Provider の完全 ID(projects/.../providers/...)をメモしておきます。GitHub Actions YAML の workload_identity_provider に使います。

ワークフロー YAML(WIF 版)

リポジトリへの id-token: write 権限と google-github-actions/auth ステップを追加します。

name: Android Release

on:
  push:
    tags:
      - 'v*'

permissions:
  contents: read
  id-token: write          # WIF のトークン取得に必要

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Cache Gradle packages
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew

      - name: Decode Keystore
        run: |
          echo "${{ secrets.KEYSTORE_BASE64 }}" | tr -d '\n\r' | base64 --decode > ${{ github.workspace }}/release.keystore

      - name: Build Release AAB
        env:
          KEYSTORE_FILE: ${{ github.workspace }}/release.keystore
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
        run: ./gradlew bundleRelease --no-daemon

      - name: Upload AAB as artifact
        uses: actions/upload-artifact@v4
        with:
          name: release-aab
          path: app/build/outputs/bundle/release/app-release.aab

      - name: Authenticate to Google Cloud (WIF)
        uses: google-github-actions/auth@v3
        id: auth
        with:
          workload_identity_provider: projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID
          service_account: SERVICE_ACCOUNT@PROJECT_ID.iam.gserviceaccount.com

      - name: Deploy to Play Store (Internal Track)
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJson: ${{ steps.auth.outputs.credentials_file_path }}
          packageName: com.example.myapp
          releaseFiles: app/build/outputs/bundle/release/app-release.aab
          tracks: internal
          status: completed
          whatsNewDirectory: distribution/whatsnew

workload_identity_providerservice_account は WIF 設定後に GCP コンソールで確認できる値に置き換えてください。JSON キー版との違いは serviceAccountJsonPlainText の代わりに serviceAccountJson へ認証ファイルのパスを渡す点だけで、それ以外の手順は同じです。

9. リリースノート用ディレクトリを作る

リポジトリのルートに distribution/whatsnew/ を作り、以下のファイルを置きます。

distribution/whatsnew/
├── whatsnew-en-US
└── whatsnew-ja-JP

中身はそれぞれの言語でのリリースノート(500文字以内)です。

10. 動かしてみる

ここまで来たら、あとはタグを push するだけです。

git tag v1.0.0
git push origin v1.0.0

GitHub の Actions タブを開くとワークフローが走り始めます。成功すれば Play Console の 内部テスト に新しいリリースが追加されているはずです。

11. ハマりどころと対策

実際に運用してみて踏んだ落とし穴をいくつか共有します。

versionCode を上げ忘れる

Play Console は versionCode の重複を許しません。GitHub Actions の実行番号 (GITHUB_RUN_NUMBER) を使って自動で採番する仕組みを入れておくと事故が減ります。versionName はタグ名から v を除いた値を使います。

android {
    defaultConfig {
        // CI では GITHUB_RUN_NUMBER を versionCode に使う(ローカルは 1 にフォールバック)
        versionCode = (System.getenv("GITHUB_RUN_NUMBER")?.toInt() ?: 1) + 1000
        // CI では git tag (v1.0.0 → 1.0.0) を versionName に使う
        versionName = System.getenv("GITHUB_REF_NAME")?.removePrefix("v") ?: "1.0.0-local"
    }
}

サービスアカウント権限の反映待ち

権限を付与した直後に Actions を実行すると 403 The caller does not have permission で落ちることがあります。設定変更の反映には少し時間がかかるので、最初の一発目は気長に待ちましょう。

Base64 デコードで改行が混ざる

base64 コマンドの出力は OS によって挙動が違います。

  • macOS: 76 文字ごとに自動で改行が入る → -i / -o で完全な形にする
  • Linux: デフォルトで改行が入る → -w 0 を指定すると改行なし
  • Windows (certutil): ヘッダー・フッター・改行すべて入る → 後処理が必要、PowerShell の [Convert]::ToBase64String の方が安全

GitHub Secrets に貼り付ける前に、改行が混じっていないか必ず確認しましょう。CI 側でデコードする際に念のため tr -d '\n\r' をかましておくのが鉄板です。

- name: Decode Keystore
  run: |
    echo "${{ secrets.KEYSTORE_BASE64 }}" | tr -d '\n\r' | base64 --decode > release.keystore

おわりに

GitHub Actions を導入したことで、リリース作業が

「タグを切って push するだけ」

になり、リリース頻度を上げても作業負荷がほぼゼロで運用できるようになりました。

特に複数人で開発しているチームでは、「あの Mac じゃないとリリースできない」問題 が解消されるだけでも導入の価値があります。Android アプリ開発で同じ悩みを抱えている方はぜひ試してみてください。

参考リンク

Android / 署名まわり

Google Play Console

GitHub Actions

Gradle