FlutterとSupabaseでgoogle認証【CI/CDパイプラインを用いたApp Distributionへのapkアップロードに挑戦】 Part2

# 概要
こちらの記事の続きです。

https://qiita.com/eno49conan/items/6f2b055eeca0f0594d00

ボリュームがあるので、2回に分けさせていただきました。今回はその第2回です。

- OAuthクライアントID作成、Supabaseの設定、apkファイルをAppDistributionへアップロード
- **apkファイルアップロードを手動ではなく、Github Actions経由に切り替え（今回はこちら）**

## パイプライン内で必要な処理
早速ワークフローのファイルの話をします。
githubActionsで利用するyamlファイルなので、`.github/workflows`フォルダの下にyamlファイルを作成します。

以下内容をパイプラインに含める必要がありました。

- Flutterの環境構築
- プロジェクトで使用するパッケージのインストール
- `google-services.json`の復元
- `release.keystore`の復元
- `key.properties`の作成
- `.env`（環境変数ファイル）の作成
- build runnerの実行（環境変数の読み込み）
- apkファイルのビルド
- `actions/upload-artifact@v4`を用いたapkファイルのアップロード
- アップロードしたファイルをApp Distributionにデプロイ

これらをyamlファイルで記述していきますが、
その前に、必要になるシークレットの登録から進めます。

## Github Actionsのシークレット設定

:::note warn
**シークレット情報の扱い**
重要な情報も含まれているので、実際に試す際には、値の扱いには注意してください。
:::

これまでシークレットを使う機会はありましたが、
ここまで数が多かったのは初めてだったかもしれません。

#### 設定方法
対象リポジトリのSettingから設定に進みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/4637d15d-21db-8dbd-8bfa-93dcd2e00c2b.png)

#### Google認証関連
- `GOOGLE_CLIENT_ID`：OAuth 2.0 クライアント ID、Androidの情報
- `GOOGLE_WEB_SERVER_CLIENT_ID`：OAuth 2.0 クライアント IDのウェブ アプリケーションの情報
- `GOOGLE_SERVICES_JSON`：Base64 エンコードした`google-services.json`の情報
- `KEYSTORE_PASSWORD`：keytoolコマンドでkeystoreファイルを作成したときのパスワード
- `KEY_ALIAS`：keytoolコマンドでkeystoreファイルを作成したときのエイリアス
- `KEY_PASSWORD`：keytoolコマンドでkeystoreファイルを作成したときのパスワード
- `RELEASE_KEYSTORE`：Base64 エンコードした`release.keystore`の情報
#### Supabase関連
- `SUPABASE_URL`：SupabaseプロジェクトのProject URL
- `SUPABASE_ANON_KEY`：SupabaseプロジェクトのProject API Keys（AnonKey）
#### App Distribution関連
- `CREDENTIAL_FILE_CONTENT`：FirebaseサービスアカウントのcredentialのJSON文字列
- `FIREBASE_APP_ID`：App Distributionの設定画面からまたは、`google-services.json`から取得できるID（以下画像の赤枠で囲った箇所）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/0855a093-8041-6d70-2f95-7bbfa0d65cf9.png)

#### Base64デコード
コマンドは非常にシンプルですが、ちょうどGoogle Cloudで資料があったので、合わせて添付いたします。
```shell
base64 android/app/google-services.json
```

#### サービスアカウント作成
AppDistributionにapkファイルをデプロイするには、権限がなければできません。

アカウント名入力して、「作成して続行」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/c2168a10-1225-4f7b-0e54-c31a839b1866.png)

「ロールを選択」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/b7ded1a6-244c-5c60-bb7d-b26bb349c1d6.png)

フィルタに、`roles/firebaseappdistro.admin`と入力すると、「Firebase アプリ配布管理者」と選択肢が表示されるはずですので、これを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/147ed77d-a689-5a07-1260-8e26958f516f.png)

対象サービスアカウントを選択して、鍵をダウンロードしてください。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/fad6e43e-24f1-11cc-c733-5edc5fc45331.png)

https://cloud.google.com/vertex-ai/generative-ai/docs/image/base64-encode?hl=ja

## yamlファイル作成

Flutterのインストールや、ビルドといった部分については、割愛します。
[こちら（github gist）](https://gist.github.com/eno-conan/219d6dd5cea28cbcb6f1317d57ab321f)からyamlファイル全体は確認いただけます。
（本記事最後にもリンクあり）

#### .gitignoreしていた3ファイルの復元
基本的にはシークレットに設定した値を取得して、ファイルに書き込みをします。`key.properties`以外はbase64のデコードが必要なので、その処理を挟んで復元しました。
- `google-services.json`の復元
- `release.keystore`の復元
- `key.properties`の作成
```yaml
  # google-services.jsonの内容復元
  - name: Load Google Service file
    run: |
      echo "${{ secrets.GOOGLE_SERVICES_JSON }}" | base64 -d > android/app/google-services.json
    
  # キーストアファイルの作成
  - name: Create Keystore
    run: |
      echo "${{ secrets.RELEASE_KEYSTORE }}" | base64 -d > android/app/release.keystore
  # key.propertiesファイルの作成
  - name: Create key.properties
    run: |
      echo "storePassword=${{ secrets.KEYSTORE_PASSWORD }}" >> android/key.properties
      echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties
      echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties
      echo "storeFile=release.keystore" >> android/key.properties
```

#### アップロードしたファイルをApp Distributionにデプロイ
以下のActionが提供されているので、それを利用してアップロードしました。

https://github.com/marketplace/actions/firebase-app-distribution

```yaml
- name: Deploy to Firebase App Distribution
  uses: wzieba/Firebase-Distribution-Github-Action@v1
  with:
    appId: ${{ secrets.FIREBASE_APP_ID }}
    serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }}
    groups: testers
    file: build/app/outputs/flutter-apk/app-arm64-v8a-release.apk      
```

yamlファイルの中の処理については以上です。
続いて、`android`フォルダの中で一部修正が必要なので、その話をしていきます。

## `android`フォルダの一部ファイルを修正
今回のGoogle認証以外の機能も導入しているので、色々変更箇所はありますが、直接関係する箇所のみ記載します。
プロジェクト作成時と比較して、追加した箇所がグリーンになっています。

#### `android/build.gradle`
- Google認証を行うための記載
- keystoreの情報読み込みのための設定
    - `local.properties`と`key.properties`の読み込みに関する設定
    - `signingConfig` > `release`：`key.properties`から読み込む情報を設定
    - `buildTypes` > `release`：`signingConfig`をsigningConfigs.releaseに変更
```diff_gradle
plugins {
    id "com.android.application"
    id "kotlin-android"
    // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+    id "com.google.gms.google-services"
    id "dev.flutter.flutter-gradle-plugin"
}

+ def localProperties = new Properties()
+ def localPropertiesFile = rootProject.file('local.properties')
+ if (localPropertiesFile.exists()) {
+     localPropertiesFile.withReader('UTF-8') { reader ->
+         localProperties.load(reader)
+     }
+ }
+ 
+ def keystoreProperties = new Properties()
+ def keystorePropertiesFile = rootProject.file('key.properties')
+ if (keystorePropertiesFile.exists()) {
+     keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
+ }
+ 
+ def flutterRoot = localProperties.getProperty('flutter.sdk')
+ if (flutterRoot == null) {
+     throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+ }

android {
    namespace = "com.enoconan.testingappv2"
    compileSdk = flutter.compileSdkVersion
    ndkVersion = flutter.ndkVersion

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8
    }

+     signingConfigs {
+         release {
+             keyAlias keystoreProperties['keyAlias']
+             keyPassword keystoreProperties['keyPassword']
+             storeFile file(keystoreProperties['storeFile'])
+             storePassword keystoreProperties['storePassword']
+         }
+     }

    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId = "com.enoconan.testingappv2"
        // You can update the following values to match your application needs.
        // For more information, see: https://flutter.dev/to/review-gradle-config.
        minSdk = 24
        targetSdk = flutter.targetSdkVersion
        // versionCode = flutter.versionCode
        // versionName = flutter.versionName
    }

    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
-           signingConfig = signingConfigs.debug
+           signingConfig signingConfigs.release
        }
    }
}

flutter {
    source = "../.."
}
```
#### `android/key.propeties`
`android/build.gradle`で記載したファイルの中身です。パスワードとエイリアスはサンプルですので、各自で設定した値に書き換えてください。
```properties
storeFile=release.keystore
storePassword=XXX
keyAlias=XXX_alias
keyPassword=XXX
```
#### `android/app/build.gradle`
Google認証が行うために、数か所記載を追加しています。
```diff_gradle
buildscript {
    repositories {
+       google()
        mavenCentral()
    }

    dependencies {
+        classpath 'com.google.gms:google-services:4.4.2'  // Google Services の追加
    }
}

+ plugins {
+    id 'com.google.gms.google-services' version '4.4.2' apply false
+ }

allprojects {
    repositories {
+       google()
        mavenCentral()
    }
}

rootProject.buildDir = "../build"
subprojects {
    project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
    project.evaluationDependsOn(":app")
}

tasks.register("clean", Delete) {
    delete rootProject.buildDir
}
```

## もうちょっと具体的になってほしいエラー文
初めてだったので当然ですが、認証に大きく関係する、`google-services.json`や`release.keystore`が適切に復元できるまでに、パイプラインを何度も実行しました。
（ローカルからアップロードしたapkファイルではgoogleログインできるのに、
CI/CDパイプラインを経由すると、googleログインができない！）

都度エラー文を確認しますが、中盤から表示され始めた以下エラーメッセージについて、申し訳ないが、パッとしない印象を持ってしまいました。また若干メッセージが変わるときもあり、困惑しました。

調べる中で、
**フィンガープリントに設定が上手くいっていない、と言い換えて作業しました。**

```
PlatformException(sign_in_failed, X2.d: 10: , null, null)
PlatformException(sign_in_failed, l3.b: 10: , null, null)
```
個人開発だからできることですが、
CI/CDパイプラインを通じてビルドされたapkファイルのSHA-1と、`google-services.json`
が想定通りか検証するコードも適宜追加しながら確認を行いました。

**こういう情報を見つける上で、ClaudeやchatGPTは大変助かります。**

```yaml
- name: Check APK signature
  run: |
    unzip -l build/app/outputs/flutter-apk/app-arm64-v8a-release.apk | grep "META-INF"
    sudo apt-get install -y apksigner
    apksigner verify --print-certs build/app/outputs/flutter-apk/app-arm64-v8a-release.apk | grep "SHA-1"

- name: Detailed google-services.json verification
  run: |
    echo "1. Checking package name:"
    jq '.client[0].client_info.android_client_info.package_name' android/app/google-services.json
  
    echo "2. Checking all OAuth clients:"
    jq '.client[0].oauth_client[]' android/app/google-services.json
  
    echo "3. Checking SHA-1 certificates:"
    jq '.client[0].oauth_client[] | select(.client_type == 1) | .android_info.certificate_hash' android/app/google-services.json    
```

## 最終的なyamlファイル

冒頭、「パイプライン内で必要な処理」で記載した処理を全て盛り込んだyamlファイルについては、
[こちら（github gist）](https://gist.github.com/eno-conan/219d6dd5cea28cbcb6f1317d57ab321f)で確認いただけます。

参考になる箇所がありましたら、ぜひご利用ください。

## 学びとなったこと・今後
FlutterでUIを作るのみであれば、これまでの経験で活かせる箇所が多かったです。
一方今回は、署名とCI/CDパイプラインがメインだったので、UI作りとは関係しない箇所もあり、そのあたりの学びが多かったです。

**SHA-1による署名に関する内容を少し理解できたことが、一番大きな収穫だったかと思います。**
CI/CDパイプライン経由でデプロイしたapkファイルを用いてGoogleログインができたときは、ガッツポーツが自然と出ました。

アプリをリリース可能な状態にするための技術・実現方法を理解するためにはそれなりに時間をかける必要があると感じました。
今回はApp Distributionへのデプロイでしたが、いずれはPlay Storeへのリリースもやってみたいと思っています。

ありがとうございました。

## 参考記事

https://atsum.in/android/app-distribution-github-actions/#google_vignette

https://github.com/wzieba/Firebase-Distribution-Github-Action/wiki/FIREBASE_TOKEN-migration

https://stackoverflow.com/questions/75427162/firebase-app-distribution-admin-role-is-missing

https://developer.android.com/tools/apksigner?hl=ja

https://zenn.dev/yass97/articles/2fd595b5432659