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

## はじめに

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

前編ではGitHub Actionsでのワークフローの実行に必要な環境変数の収集とセットを行いました。後編では実際にワークフローを書いてパイプラインを動かします。

## ワークフローの作成
実際にGitHub Actionsで実行するワークフローのコードを書きます。コードはこの後載せていますが各行の解説は[参考記事](https://zenn.dev/shima999ba/articles/ae1fc477744e2a)を見てください。参考記事からの変更点と個別で設定が必要な箇所はコメントアウトで示してあります。
### ファイル作成
以下のフォルダ構成を構築する。`.github`と`workflow`も新規作成。
   ```
   flutter_sample
   ├── .github
   │   └── workflow
   │       ├── build_deploy_stg.yaml
   │       ├── analyze_test.yaml
   │       ├── ios_build_stg.yaml
   │       └── android_build_stg.yaml
   ```
    
### build_deploy_stg.yaml
全体を統括する役目を果たします。
```build_deploy_stg.yaml
name: "build & deploy stg app"
on:
  push:
jobs:
  analyze_and_test:
    uses: ./.github/workflows/analyze_test.yaml
    secrets:
      FIREBASE_PROJ_DEV_NAME: ${{ secrets.FIREBASE_PROJ_DEV_NAME }}
      FIREBASE_AUTH_TOKEN: ${{ secrets.FIREBASE_AUTH_TOKEN }}

  android_build_stg:
    needs: [analyze_and_test]
    uses: ./.github/workflows/android_build_stg.yaml
    secrets:
      FIREBASE_PROJ_DEV_NAME: ${{ secrets.FIREBASE_PROJ_DEV_NAME }}
      FIREBASE_AUTH_TOKEN: ${{ secrets.FIREBASE_AUTH_TOKEN }}
      ANDROID_KEY_JKS: ${{ secrets.ANDROID_KEY_JKS }}
      ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
      ANDROID_ALIAS_PASSWORD: ${{ secrets.ANDROID_ALIAS_PASSWORD }}
      ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
      FIREBASE_DEV_ANDROID_ID: ${{ secrets.FIREBASE_DEV_ANDROID_ID }}
      FIREBASE_DEV_TOKEN: ${{ secrets.FIREBASE_DEV_TOKEN }}

  ios_build_stg:
    needs: [analyze_and_test]
    uses: ./.github/workflows/ios_build_stg.yaml
    secrets:
      APPSTORE_CERT_BASE64: ${{ secrets.APPSTORE_CERT_BASE64 }}
      APPSTORE_CERT_PASSWORD: ${{ secrets.APPSTORE_CERT_PASSWORD }}
      MOBILEPROVISION_ADHOC_BASE64: ${{ secrets.MOBILEPROVISION_ADHOC_BASE64 }}
      KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
      FIREBASE_PROJ_DEV_NAME: ${{ secrets.FIREBASE_PROJ_DEV_NAME }}
      FIREBASE_AUTH_TOKEN: ${{ secrets.FIREBASE_AUTH_TOKEN }}
      FIREBASE_DEV_IOS_ID: ${{secrets.FIREBASE_DEV_IOS_ID}}
      FIREBASE_DEV_TOKEN: ${{secrets.FIREBASE_DEV_TOKEN}}
      EXPORT_OPTIONS: ${{ secrets.EXPORT_OPTIONS_STG }}
```
### analyze_test.yaml
コード解析とテストを行います。テストの書き方については別記事で書きます。
```analyze_test.yaml
name: analyze & test
on:
  workflow_call:
    secrets:
      FIREBASE_PROJ_DEV_NAME:
        description: プロジェクトID
        required: true
      FIREBASE_AUTH_TOKEN:
        description: Firebaseのトークン情報
        required: true
jobs:
  analyze:
    runs-on: ubuntu-latest
    steps:
      - name: setup repository
        uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: "temurin"
          java-version: "21"
      - name: setup flutter
        uses: subosito/flutter-action@v2
        with:
          channel: stable
          # 現在のFlutterのバージョン flutter doctor や flutter --versionで確認
          flutter-version: "3.27.0"
      - name: check version
        run: flutter --version
      - name: Setup packages
        run: |
          flutter pub get
          dart run build_runner build -d
      - name: Setup Firebase Project
        env:
          FIREBASE_PROJ_DEV_NAME: ${{ secrets.FIREBASE_PROJ_DEV_NAME }}
          FIREBASE_AUTH_TOKEN: ${{ secrets.FIREBASE_AUTH_TOKEN }}
        run: |
          curl -sL https://firebase.tools | bash
          dart pub global activate flutterfire_cli
          flutterfire configure -p $FIREBASE_PROJ_DEV_NAME -y --platforms "ios, android" -i "com.exmple.example-app" -a "com.example.example_app" -m "com.example.example-app"  -w "1:XXX:web:YYY" -x "1:XXX:web:YYY" -t $FIREBASE_AUTH_TOKEN -f > null
      - name: run analyze
        run: flutter analyze
      - name: run test
        run: flutter test
```
### ios_build_stg.yaml
iOSのビルドとアップロードを行います。
```ios_build_stg.yaml
name: analyze & test
on:
  workflow_call:
    secrets:
      APPSTORE_CERT_BASE64:
        description: 証明書Base64
        required: true
      APPSTORE_CERT_PASSWORD:
        description: 証明書のパスワード
        required: true
      MOBILEPROVISION_ADHOC_BASE64:
        description: プロビジョニングプロファイル(Adhoc)
        required: true
      KEYCHAIN_PASSWORD:
        description: 一時的なKeychainのパスワード
        required: true
      FIREBASE_PROJ_DEV_NAME:
        description: プロジェクトID
        required: true
      FIREBASE_AUTH_TOKEN:
        description: Firebaseのトークン
        required: true
      FIREBASE_DEV_IOS_ID:
        description: FirebaseのiOS ID
        required: true
      FIREBASE_DEV_TOKEN:
        description: FirebaseのJson Token
        required: true
      EXPORT_OPTIONS:
        description: Export Options
        required: true
jobs:
  build:
    runs-on: macos-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Install the Apple certificate and provisioning profile
        env:
          BUILD_CERTIFICATE_BASE64: ${{ secrets.APPSTORE_CERT_BASE64 }}
          P12_PASSWORD: ${{ secrets.APPSTORE_CERT_PASSWORD }}
          BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.MOBILEPROVISION_ADHOC_BASE64 }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
        run: |
          # create variables
          CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
          PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
          # import certificate and provisioning profile from secrets
          echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH
          echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode --output $PP_PATH
          # create temporary keychain
          security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
          security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
          # import certificate to keychain
          security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
          security list-keychain -d user -s $KEYCHAIN_PATH
          # apply provisioning profile
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
      - name: Flutter get
        uses: subosito/flutter-action@v1
        with:
          channel: stable
          # 現在のFlutterのバージョン
          flutter-version: "3.27.0"
      - name: Setup packages
        run: |
          flutter pub get
          dart run build_runner build -d
      - name: Setup Firebase Project
        env:
          FIREBASE_PROJ_DEV_NAME: ${{ secrets.FIREBASE_PROJ_DEV_NAME }}
          FIREBASE_AUTH_TOKEN: ${{ secrets.FIREBASE_AUTH_TOKEN }}
        run: |
          curl -sL https://firebase.tools | bash
          dart pub global activate flutterfire_cli
          flutterfire configure -p $FIREBASE_PROJ_DEV_NAME -y --platforms "ios, android" -i "com.studiomk.flutterSample" -a "com.studiomk.flutter_sample" -m "com.example.example-app"  -w "1:XXX:web:YYY" -x "1:XXX:web:YYY" -t $FIREBASE_AUTH_TOKEN -f > null
      - name: Building IPA
        env:
          EXPORT_OPTIONS: ${{ secrets.EXPORT_OPTIONS }}
        run: |
          echo $EXPORT_OPTIONS > ios/Runner/ExportOptions.plist
          flutter build ipa --release --export-options-plist=ios/Runner/ExportOptions.plist
      - name: collect ipa artifacts
        #v2からv4にアップデートされていた
        uses: actions/upload-artifact@v4
        with:
          name: release-ipa
          path: build/ios/ipa/*.ipa
          if-no-files-found: error
      - name: Clean up keychain and provisioning profile
        if: ${{ always() }}
        run: |
          security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
          rm ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision
  release:
    name: Release ipa to Firebase
    needs: [build]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Get release-ipa from artifacts
        #v2からv4にアップデートされていた
        uses: actions/download-artifact@v4
        with:
          name: release-ipa
      - name: Upload artifact to Firebase App Distribution
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{secrets.FIREBASE_DEV_IOS_ID}}
          serviceCredentialsFileContent: ${{secrets.FIREBASE_DEV_TOKEN}}
          #テスターグループ名(個別)
          groups: sample
          #プロジェクト名.ipa(個別)
          file: flutter_sample.ipa
          releaseNotes: $GITHUB_REF_NAME
```


### android_build_stg.yaml
Androidのビルドとアップロードを行います。
```android_build_stg.yaml
name: "[DEV] Build and Publish Android"
on:
  workflow_call:
    secrets:
      FIREBASE_PROJ_DEV_NAME:
        description: プロジェクトID
        required: true
      FIREBASE_AUTH_TOKEN:
        description: Firebaseのトークン情報
        required: true
      ANDROID_KEY_JKS:
        description: キーストアファイル
        required: true
      ANDROID_STORE_PASSWORD:
        description: キーストアファイルのパスワード
        required: true
      ANDROID_ALIAS_PASSWORD:
        description: キーストアファイルのALIASのパスワード
        required: true
      ANDROID_KEY_ALIAS:
        description: キーストアファイルのALIAS名
        required: true
      FIREBASE_DEV_ANDROID_ID:
        description: Firebase上のAndroid ID
        required: true
      FIREBASE_DEV_TOKEN:
        description: FirebaseのJsonの認証情報
        required: true
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: setup repository
        uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          # Javaのバージョンは17を推奨される
          distribution: "temurin"
          java-version: "17"
      - name: setup flutter
        uses: subosito/flutter-action@v2
        with:
          channel: stable
          #現在のFlutterのバージョン
          flutter-version: "3.27.0"
      - name: check version
        run: flutter --version
      - name: Setup packages
        run: |
          flutter pub get
          dart run build_runner build -d
      - name: Setup Firebase Project
        env:
          FIREBASE_PROJ_DEV_NAME: ${{ secrets.FIREBASE_PROJ_DEV_NAME }}
          FIREBASE_AUTH_TOKEN: ${{ secrets.FIREBASE_AUTH_TOKEN }}
        run: |
          curl -sL https://firebase.tools | bash
          dart pub global activate flutterfire_cli
          flutterfire configure -p $FIREBASE_PROJ_DEV_NAME -y --platforms "ios, android" -i "com.studiomk.flutterSample" -a "com.studiomk.flutter_sample" -m "com.example.example-app"  -w "1:XXX:web:YYY" -x "1:XXX:web:YYY" -t $FIREBASE_AUTH_TOKEN -f > null
      - name: Create key.properties
        run: |
          echo ${{ secrets.ANDROID_KEY_JKS }} | base64 -d > android/app/release.jks
          echo 'storeFile=release.jks' > android/key.properties
          echo 'storePassword=${{ secrets.ANDROID_STORE_PASSWORD }}' >> android/key.properties
          echo 'keyPassword=${{ secrets.ANDROID_ALIAS_PASSWORD }}' >> android/key.properties
          echo 'keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}' >> android/key.properties
        # --build-numberを消すとうまくいった
      - name: Build APK
        run: flutter build apk --release
      - name: Upload apk to Firebase App Distribution
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{secrets.FIREBASE_DEV_ANDROID_ID}}
          serviceCredentialsFileContent: ${{secrets.FIREBASE_DEV_TOKEN}}
          #テスターグループ名(個別)
          groups: sample
          file: ./build/app/outputs/apk/release/app-release.apk
          releaseNotes: $GITHUB_REF_NAME
```

## 実行
では実際にパイプラインを動かしてみましょう。
```shell-session
git add .
$ git commit -m "任意のメッセージ"
$ git push origin master
```
### GitHub Actions
リポジトリの`Actions`に新しいワークフローが追加されているはずです。
![workflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/51844a06-6a8d-42a4-a567-6665bd2b958a.png)
単純なコードのエラーは`analyze_and_test`で、ビルド関連のエラーは`ios_build_stg`や`android_build_stg`でキャッチされます。

### Firebase App Distribution
次にFirebaseのコンソールにアクセスし、App Distributionを開きましょう。リリースの欄に追加されているはずです。
![FAD_release_iOS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/fa422d30-7611-42f4-a704-718e31f259db.png)
### テストのダウンロード方法
1. App Distributionの招待リンクタブから招待リンクを発行
    ![InviteLink.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/b520ddaa-840b-4c85-9265-0605c2450563.png)
1. リンクを開きスマホのメアドを登録
1. スマホから招待メールを開く
![Mail.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3994994/ea736a01-f321-4ce7-96f2-03235f45c24b.jpeg)
1. Get startedボタンを押すとインストールが開始
1. ホーム画面からアプリを開く

## まとめ
これでFlutterのプロジェクトにCI/CDパイプラインとFirebase App Distributionによるテスト配布が導入できました。ご指摘、ご質問あればコメントにてお寄せください。
[前編](https://qiita.com/kmatsushita1012/items/7ebbf507ba6133b706ae)

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