Flutterの初心者ベストプラクティス ~CI/CDパイプライン①~

## はじめに

今回は GithubActions+Firebase App Distribution で CI/CD パイプラインの構築とテスト配布を行っていきます。サンプルプログラムは[GitHub](https://github.com/kmatsushita1012/flutter_sample) で公開中です。(シリーズで随時更新していくのでたまにエラーがあるかも)

主な参考記事:[[Flutter]GitHub Actions で App Distribution にアプリをアップロードした](https://zenn.dev/shima999ba/articles/ae1fc477744e2a)

## 概要

Github にプッシュすると以下の処理を自動で行います
- 共通
    - コード解析(リント)
    - テスト
- OS別
  - iOSのビルド&テスト配布
  - Androidのビルド&テスト配布

これをまとめたのが以下の図。
![whole_workflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/c0646b0f-1777-87cf-e0bf-f95ca0eea17c.png)


## 環境変数の収集と設定 
### 必要な環境変数のキーの一覧

- Firebase
  - `FIREBASE_PROJ_DEV_NAME` : プロジェクト ID
  - `FIREBASE_AUTH_TOKEN` : Firebase のトークン
  - `FIREBASE_DEV_TOKEN` : Firebase の Json Token
  - `FIREBASE_DEV_IOS_ID` : Firebase のiOSアプリ ID
  - `FIREBASE_DEV_ANDROID_ID` : Firebase のAndroidアプリ ID
- iOS
  - `APPSTORE_CERT_BASE64` : 証明書(Base64 でエンコード)
  - `CERT_PASSWORD` : 証明書のパスワード
  - `MOBILEPROVISION_ADHOC_BASE64` : プロビジョニングプロファイル(Adhoc)
  - `KEYCHAIN_PASSWORD` : キーチェーンパスワード(任意の文字列で可)
- Android
  - `ANDROID_KEY_JKS` : キーストア(Base64でエンコード)
  - `ANDROID_STORE_PASSWORD` : キーストアファイルのパスワード
  - `ANDROID_ALIAS_PASSWORD` : キーストアファイルの ALIAS のパスワード
  - `ANDROID_KEY_ALIAS` : キーストアファイルの ALIAS 名

### 設定方法
1. 後述の手順に従って環境変数を収集する
1. GitHubの`Settings`タブのサイドバーから`Security/Secrets and variables/Actions`を選択
1. `New Repository Secret`から環境変数を登録

### Firebase
1. エディタ(VSCode等)の一斉置換機能で`com.example.{プロジェクト名}`を`com.{任意の文字列}.{プロジェクト名}`に置換
    - 私は屋号(開発用名義やチーム名)を`{任意の文字列}`にしている
    - Androidならスネークケース,iOSならキャメルケースで書くことに注意
1. [Firebase](https://console.firebase.google.com/u/0/?hl=ja&pli=1)にログイン
1. プロジェクトを作成する
    - 名前はローカルのプロジェクト名と同じで良い(単語頭文字は大文字にしても良い)
    - あとはデフォルトの設定で作成
    - 作成できたらプロジェクトの個別ページへ
1. Flutterを追加する
    ![firebase1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/6c06baab-aa54-478e-a152-3f7adde1217d.png)
1. 画面の指示に従ってステップ1&2を実行
    ![firebase2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/14644061-5ef6-4b41-9a25-740f3513d431.png)
    - ステップ1ですでに行なっているタスク(FlutterSDKのインストールやプロジェクトの作成)は省略可
    - ステップ2は必須
        ```shell-session
        dart pub global activate flutterfire_cli
        flutterfire configure --project=fluttersample-29ba1
        ```
1. `pubspec.yaml`に`firebase_core`を追加
    - バージョンは[リファレンス](https://pub.dev/packages/firebase_core)を参照(2025/2/13時点で3.11.0)
    ```pubspec.yaml
    dependencies:
        flutter:
            sdk: flutter
        #追加
        firebase_core: ^3.11.0
    ```
1. ログイントークンを取得
    ```shell-session
    firebase login:ci
    ```
    - 取得したトークンは`FIREBASE_AUTH_TOKEN`にセット
1. ステップ3は`main.dart`を編集
    ```main.dart
    import 'package:flutter/material.dart';
    //追加
    import 'firebase_options.dart';
    import 'package:firebase_core/firebase_core.dart';
    
    void main() async {
      runApp(const MyApp());
      //追加
      await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform,
      );
    }
    //省略
    ```
1. Firebaseのコンソールに戻りプロジェクトの設定/全般に移動
1. プロジェクトIDを取得
    - `FIREBASE_PROJ_DEV_NAME`にセット
![Firebase3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/8bcd70a3-49c1-4962-aaf9-45605a1a295d.png)
1. アプリIDをiOS,Androidのそれぞれのタブで取得
    - `FIREBASE_DEV_IOS_ID`/`FIREBASE_DEV_ANDROID_ID`にセット
    ![Firebase5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/6d5d9841-f673-44cb-811d-d7173f6162a9.png)
1. プロジェクトの設定/サービスアカウントに移動して秘密鍵を作成
    ![Firebase6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/6d884bac-6ecb-47ab-9f14-fabe97f666e9.png)
    - 取得したJSONファイルの中身を`FIREBASE_DEV_TOKEN`にセット
- AppDistributionを有効にする
    - Firebase のプロダクトと機能/実行からAppDistributionを選択し
- テスターのグループを作成してテスターのメアドを追加
    ![FAD_tester.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/74213b3f-28d2-4a64-b505-76706dd02703.png)
    - グループ名は後に使用する


### iOS

1. iOS のバージョンを14.0 に変更
    - Xcodeで`ios/Runnner.xcworkspace`を開き`Runner`を選択
    - `General/Minimum Developments`からバージョンを変更
![ios_version.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/b5205197-9422-66b6-9bf5-a97d24e89a37.png)
1. キーチェーンアクセスから証明書要求ファイル作成
    - Apple公式の[証明書署名リクエストを作成する](https://developer.apple.com/jp/help/account/create-certificates/create-a-certificate-signing-request/)を参考
    - 作成した`CertificateSigningRequest.certSigningRequest`はわかりやすいフォルダに保存
1. Apple Developerの[Certificates](https://developer.apple.com/account/resources/certificates/list) で証明書を作成する
    - 新規作成
    ![ios_cert1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/1367af6b-66b1-d78e-c1dd-e709e73d913d.png)

    - Apple Distributionn を選択
![ios_cert2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/319fc63f-712c-5fec-4f5f-ed7758702cbe.png)

    - 取得した証明書要求ファイル(`CertificateSigningRequest.certSigningRequest`)をセット
![ios_cert3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/093aecdb-a539-45c9-7e04-bd6f342823d7.png)
    - ダウンロードする
        - `distribution.cer`ファイルがダウンロードされるのでわかりやすいフォルダへ移動
    ![ios_cert4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/1efb37e5-dd2f-f407-58c2-8ec8ce9f1766.png)
1. `distribution.cer`を`.p12`で書き出す
    - ログイン->自分の証明書を選択し`.cer`ファイルをドラッグ&ドロップ
  ![スクリーンショット 2025-01-29 14.20.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/94b4499f-4e6d-f7dc-9414-a54b86fbf68a.png)
    - 右クリックで"~を書き出す"を選択
    - 書き出し先と名前を選択
    - パスワードを入力し`APPSTORE_CERT_BASE64`にセット
    - Base64へエンコードして`APPSTORE_CERT_BASE64`にセット
        - エンコードするコマンドは`base64 -i {証明書名}.p12 | pbcopy`
        - クリップボードにコピーされているのでそのままペースト
1. Apple Developer から AppID を作成([参考:アプリIDを登録する](https://developer.apple.com/jp/help/account/manage-identifiers/register-an-app-id/))
    - バンドル ID には`com.example.yourappname`を入力(`yourappname`は自分のアプリ名に置き換え)
    - [Apple Developer/Profile](https://developer.apple.com/account/resources/certificates/list) からプロファイルを作成 
    - 用途に Ad Hoc を選択
  ![ios_pro1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/7462cca8-bd7c-56db-26f3-9ce27a20ef74.png)
    - 作成した AppID を選択
  ![ios_pro2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/2314cdec-7f2c-3508-a92e-73c4ff26edb2.png)
    - 取得したCertificatesを選択
      - 必ずDistributionを選ぶ
      - 同日にCertificatesを複数作成するとここで判別不能になるので注意
      ![ios_pro3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/c12ce4c2-3587-6044-f237-cf4b7bcfaa9a.png)      
    - テスターに含めるデバイスを選択
  ![ios_pro4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/513af34b-f9cd-3cbb-c419-c55c9add9860.png)
    - 名前を入力して作成
    - こちらもbase64で書き出して`MOBILEPROVISION_ADHOC_BASE64`
        - `base64 -i {自分のProvisioningProfile}.mobileprovision | pbcopy`
1. Xcode `BuildSettings`->`Code Siging Identity`で`Certificates in Keychain`から`Apple Distribution`を選択
    ![ios_codesign.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/ca10d501-dec8-d446-40e0-099a17e1a4e7.png)
    画像では2つあるが普通は1つしか無いはず(なぜか私はチームIDが2つあった)
1. Xcode `Signing & Capablities`
    - `Automatic manage signing`のチェックを外す
    - `Provisioning File`で作成した Profile を選択
      - ない場合は`import file`からディレクトリを探す

    ![ios_sign&cap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/79898595-e702-0901-ce9b-3606a8fb3b84.png)
    - `Provisioning profile *** doesn't include signing certificate`と表示されたら証明書と Profile が一致してないので要確認
1. ExportOption.plistの作成
   - ちゃんと作成するなら[【Flutter】flutter build ipa でipaファイルが生成されない](https://zenn.dev/tsukatsuka1783/articles/flutter_build_ipa) を参考にExportOption.plistを作成して中身を`EXPORT_OPTIONS`にセットするのだが,先ほど作成した`.mobileprovision`ファイルが必要となるが形式が違うためエラーとなる
   - 以下にExportOptions.plistの形式を示すので{}を書き換えて`EXPORT_OPTIONS`にセット
        ```xml
        <!-- ExportOptions.plist -->
        <?xml version="1.0" encoding="UTF-8"?>
        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
        <plist version="1.0">
        <dict>
        	<key>compileBitcode</key>
        	<false/>
        	<key>destination</key>
        	<string>export</string>
        	<key>method</key>
        	<string>ad-hoc</string>
        	<key>provisioningProfiles</key>
        	<dict>
        		<key>{バンドルID}</key>
        		<string>{アプリ名}</string>
        	</dict>
        	<key>signingCertificate</key>
        	<string>Apple Distribution</string>
        	<key>signingStyle</key>
        	<string>manual</string>
        	<key>stripSwiftSymbols</key>
        	<true/>
        	<key>teamID</key>
        	<string>{チームID}</string>
        	<key>thinning</key>
        	<string>&lt;none&gt;</string>
        </dict>
        </plist>
        ```
        - チームIDはAppleDeveloperの右上の英数字
        ![teamid.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/b08548da-e42b-4212-a94d-d6856d6f0b8e.png)
        - 正式な作成方法は別記事で後日書きます

1. `KEYCHAIN_PASSWORD`は好きに決めてよし

これでiOSの下準備が完了

### Android
1. `./android`に移動
    ```shell-session
    cd android
    ```
1. 以下のコマンドでkeystoreを作成
    ```shell-session
    keytool -genkey -v -keystore ./key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
    キーストアのパスワードを入力してください:  
    新規パスワードを再入力してください: 
    識別名を入力します。サブコンポーネントを空のままにする場合はドット(.)を1つ入力し、中カッコ内のデフォルト値を使用する場合は[ENTER]を押します。
    姓名は何ですか。
      [Unknown]:  
    組織単位名は何ですか。
      [Unknown]:  
    組織名は何ですか。
      [Unknown]:  
    都市名または地域名は何ですか。
      [Unknown]:  
    都道府県名または州名は何ですか。
      [Unknown]:  
    この単位に該当する2文字の国コードは何ですか。
      [Unknown]:  
    CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknownでよろしいですか。
      [いいえ]:  y
    
    10,000日間有効な2,048ビットのRSAのキー・ペアと自己署名型証明書(SHA384withRSA)を生成しています
            ディレクトリ名: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown
    [./key.jksを格納中]
    ```
    
1. `./android`直下に`key.properties`を作成
    ```./android/key.properties
    storePassword=パスワード
    keyPassword=パスワード
    keyAlias=key
    storeFile=key.jks
    ```
    - パスワードはkeystoreを作成した時のパスワード
    - 上から3つは順に`ANDROID_STORE_PASSWORD`,`ANDROID_ALIAS_PASSWORD`,`ANDROID_KEY_ALIAS`にセット
    - `key.properties`自体はローカルでビルドする際に必要
    - 機密情報なので`.gitignore`に`android/key.properties`を追加
1. `key.jks` をbase64に書き出して`ANDROID_KEY_JKS`にセット
    - `base64 -i ./android/key.jks | pbcopy`
1. `./android/app/build.gradle`を編集
    ```build.gradle
    plugins {
        //省略
    }
    //追加
    def keystorePropertiesFile = rootProject.file("key.properties")

    android {
        //省略
        //追加ここから
        signingConfigs {
            release {
                if (keystorePropertiesFile.exists()) {
                    def keystoreProperties = new Properties()
                    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
                    keyAlias keystoreProperties['keyAlias']
                    keyPassword keystoreProperties['keyPassword']
                    storeFile file(keystoreProperties['storeFile'])
                    storePassword keystoreProperties['storePassword']
                }
            }
        }
        //追加ここまで
        //省略
    }
    ```
## 次回
次は実際にGitHubActionsで実行するワークフローのコードを書きます.
[Flutterの初心者ベストプラクティス ~CI/CDパイプライン②~](https://qiita.com/kmatsushita1012/items/e4634882ae70d5509aa9)

## 参考文献
- [[Flutter]GitHub Actions で App Distribution にアプリをアップロードした](https://zenn.dev/shima999ba/articles/ae1fc477744e2a)

- [iOSのp12証明書の作り方](https://faq.growthbeat.com/article/178-ios-p12)
- [【Flutter】CocoaPodsの minimum versionがどうとかのエラーがおきた
](https://zenn.dev/t_fukuyama/articles/9048d5f26befee)
- [CocoaPods could not find compatible versions for pod "FirebaseCoreExtension":を解決する](https://qiita.com/kokogento/items/6c0baf22c85a28db388c)
- [[!] CocoaPods did not set the base configuration of your project because your project already has a custom config set. の解決法](https://qiita.com/kokogento/items/c2979542a34610925e2d)
- [[Xcode] アプリをArchiveしようとして「No signing certificate “iOS Development” found」エラーになった場合の解決策](https://dev.classmethod.jp/articles/xcode-no-signing-certificate-ios-development-found-error/)