okaryo.log

FlutterのiOSアプリをFirebaseAppDistributionにアップロードするワークフローをGitHubActionsで構築する(非AutoSigning) | okaryo.log

FlutterのiOSアプリをFirebaseAppDistributionにアップロードするワークフローをGitHubActionsで構築する(非AutoSigning)

    #Flutter#Firebase#AppDistribution#GitHub#GitHubActions#CI/CD

はじめに

最近開発しているFlutterアプリをiOS対応した。その際にFirebaseのAppDistributionを使って実機端末にアプリを配布してインストールし、動作確認できるようにした。このワークフローをGitHubActionsを使って組んだので残しておく。

ちなみにAndroid版はこちら。2年前の記事なので少し古いかも。

前提条件

この記事では以下のことについては解説しない。

  • iOSアプリの証明書の作成方法
  • Firebaseのセットアップ方法
  • GitHubActionsの構文

また、今回のiOSビルドはAutoSigningではなく、証明書やプロビジョニングファイルを用意する手動署名で行う。AutoSigningでのワークフローの記事は後日書く予定。

組んだワークフロー

最終的に組んだワークフローは以下のようになった。各ステップについては後ほど補足する。

# ./.github/workflows/deploy-to-app-distribution.yml
name: Deploy to App Distribution

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

jobs:
  staging:
    if: inputs.environment == 'staging'
    uses: ./.github/workflows/_deploy-to-app-distribution.yml
    with:
      environment: staging
      ios_bundle_id: com.example.ios.staging
      ios_provisioning_profile_name: profile-staging-adhoc
    secrets:
      FIREBASE_IOS_APP_ID: ${{ secrets.FIREBASE_IOS_APP_ID_STAGING }}
      APP_DISTRIBUTION_SERVICE_ACCOUNT_JSON: ${{ secrets.APP_DISTRIBUTION_SERVICE_ACCOUNT_JSON_STAGING }}
      IOS_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_CERTIFICATE_P12_BASE64 }}
      IOS_P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
      IOS_ADHOC_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_ADHOC_PROVISION_PROFILE_BASE64_STAGING }}
      RUNNER_KEYCHAIN_PASSWORD: ${{ secrets.RUNNER_KEYCHAIN_PASSWORD_STAGING }}

  production:
    if: inputs.environment == 'production'
    uses: ./.github/workflows/_deploy-to-app-distribution.yml
    with:
      environment: production
      ios_bundle_id: com.example.ios
      ios_provisioning_profile_name: profile-production-adhoc
    secrets:
      FIREBASE_IOS_APP_ID: ${{ secrets.FIREBASE_IOS_APP_ID_PRODUCTION }}
      APP_DISTRIBUTION_SERVICE_ACCOUNT_JSON: ${{ secrets.APP_DISTRIBUTION_SERVICE_ACCOUNT_JSON_PRODUCTION }}
      IOS_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_CERTIFICATE_P12_BASE64 }}
      IOS_P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
      IOS_ADHOC_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_ADHOC_PROVISION_PROFILE_BASE64_PRODUCTION }}
      RUNNER_KEYCHAIN_PASSWORD: ${{ secrets.RUNNER_KEYCHAIN_PASSWORD_PRODUCTION }}

# ./.github/workflows/_deploy-to-app-distribution.yml
name: _Deploy to App Distribution

on:
  workflow_call:
    inputs:
      environment:
        type: string
        description: Name of target environment
        required: true
      ios_bundle_id:
        type: string
        description: Bundle ID for iOS App
        required: true
      ios_provisioning_profile_name:
        type: string
        description: Provisioning Profile name for iOS App
        required: true
    secrets:
      FIREBASE_IOS_APP_ID:
        description: iOS App ID on Firebase
        required: true
      APP_DISTRIBUTION_SERVICE_ACCOUNT_JSON:
        description: Service Account to Deploy to App Distribution
        required: true
      IOS_CERTIFICATE_P12_BASE64:
        description: Base64 encoded certificate p12 file
        required: true
      IOS_P12_PASSWORD:
        description: Password for p12 file
        required: true
      IOS_ADHOC_PROVISION_PROFILE_BASE64:
        description: Base64 encoded provisioning profile file
        required: true
      RUNNER_KEYCHAIN_PASSWORD:
        description: Password for GitHubActions runner keychain
        required: true

  build-ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Set up Flutter
        uses: subosito/flutter-action@v2
        with:
          channel: stable
          flutter-version-file: pubspec.yaml
          cache: true
      - name: Set up cider
        run: flutter pub global activate cider
      - name: Set up build number
        run: |
          VERSION=$(cider version)
          BUILD_NUMBER=$(git rev-list HEAD --count)
          cider version $VERSION+$BUILD_NUMBER
      - name: Install the Apple certificate and provisioning profile
        env:
          IOS_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_CERTIFICATE_P12_BASE64 }}
          IOS_P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
          IOS_ADHOC_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_ADHOC_PROVISION_PROFILE_BASE64 }}
          RUNNER_KEYCHAIN_PASSWORD: ${{ secrets.RUNNER_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 "$IOS_CERTIFICATE_P12_BASE64" | base64 --decode -o $CERTIFICATE_PATH
          echo -n "$IOS_ADHOC_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH

          # create temporary keychain
          security create-keychain -p "$RUNNER_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
          security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
          security unlock-keychain -p "$RUNNER_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH

          # import certificate to keychain
          security import $CERTIFICATE_PATH -P "$IOS_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
          security set-key-partition-list -S apple-tool:,apple: -k "$RUNNER_KEYCHAIN_PASSWORD" $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: Generate ExportOptions.plist
        run: |
          export EXPORT_METHOD=release-testing
          export EXPORT_DESTINATION=export
          export BUNDLE_ID=${{ inputs.ios_bundle_id }}
          export PROVISIONING_PROFILE=${{ inputs.ios_provisioning_profile }}
          envsubst < ios/Runner/ExportOptions-Template.plist > ios/Runner/ExportOptions.plist
      - name: Build ipa
        run: |
          flutter build ipa
            --release \
            --flavor ${{ inputs.environment }} \
            --dart-define=FLAVOR=${{ inputs.environment }} \
            --export-options-plist=ios/Runner/ExportOptions.plist
      - uses: actions/upload-artifact@v2
        with:
          name: app-${{ inputs.environment }}-release.ipa
          path: build/ios/ipa/app-${{ inputs.environment }}-release.ipa
      - 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

  distribute-ios:
    needs: build-ios
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v2
        with:
          name: app-${{ inputs.environment }}-release.ipa
      - name: Upload artifact to Firebase App Distribution
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{ secrets.FIREBASE_IOS_APP_ID }}
          serviceCredentialsFileContent: ${{ secrets.APP_DISTRIBUTION_SERVICE_ACCOUNT_JSON }}
          groups: ios_testers
          file: app-${{ inputs.environment }}-release.ipa

各ステップの詳細

ワークフローの起動

ワークフローはworkflow_dispatchを使って手動で行うようにしているが、この辺りは用途に合わせてブランチのマージ時にするなど変更してほしい。

ここでアップロード先をstaging/productionと環境を選択できるようにしておき、各環境で使用する変数やシークレットをReusable workflowに渡している。同一のワークフローで複数の環境に対応する方法については以下を参照されたい。

実行環境

さて、ここからはReusable workflowの中のジョブに入る。

まず、iOSのビルドを行う実行環境だが、もちろんruns-on: macos-latest。これはLinux環境の10倍のGitHubActionsクレジットを消費するので注意されたい。ビルドに10分かかれば100分のクレジットを消費してしまう。起動回数を減らしたり、セルフホステッドランナーを選択するなど良い感じに調整していただきたい。

actions/checkout@v4

よく使うチェックアウトだが、今回はfetch-depth: 0を引数に渡している。後のジョブでコミット数をビルド番号として使うため、全てのコミット履歴を取ってこれるようにしている。

Flutterとビルド番号の設定

Flutterのセットアップとciderというパッケージを使ってバージョンとビルド番号の設定を行っている。

- name: Set up Flutter
  uses: subosito/flutter-action@v2
  with:
    channel: stable
    flutter-version-file: pubspec.yaml
    cache: true
- name: Set up cider
  run: flutter pub global activate cider
- name: Set up build number
  run: |
    VERSION=$(cider version)
    BUILD_NUMBER=$(git rev-list HEAD --count)
    cider version $VERSION+$BUILD_NUMBER

ビルド番号の取得はコミット数から取ってきており、get rev-list HEAD --countで取得している。詳細は以下の記事を参照してほしい。

証明書とプロビジョニングプロファイルの設定

ここでは、GitHubActionsのランナー上に証明書とプロビジョニングプロファイルをインストールしている。

- name: Install the Apple certificate and provisioning profile
  env:
    IOS_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_CERTIFICATE_P12_BASE64 }}
    IOS_P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
    IOS_ADHOC_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_ADHOC_PROVISION_PROFILE_BASE64 }}
    RUNNER_KEYCHAIN_PASSWORD: ${{ secrets.RUNNER_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 "$IOS_CERTIFICATE_P12_BASE64" | base64 --decode -o $CERTIFICATE_PATH
    echo -n "$IOS_ADHOC_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH

    # create temporary keychain
    security create-keychain -p "$RUNNER_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
    security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
    security unlock-keychain -p "$RUNNER_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH

    # import certificate to keychain
    security import $CERTIFICATE_PATH -P "$IOS_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
    security set-key-partition-list -S apple-tool:,apple: -k "$RUNNER_KEYCHAIN_PASSWORD" $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

証明書とプロビジョニングプロファイルはそれぞれBase64化したものをシークレットとして設定しておく。また、$RUNNER_KEYCHAIN_PASSWORDはランナー上のキーチェーンで使用するパスワードなので自由に設定して問題ない。

より詳細はGitHubActionsの公式ドキュメントを参照されたい。

ExportOptions.plistの作成

ExportOptions.plistはiOSアプリをエクスポートする際の設定を記述するファイル。あらかじめExportOptions-Template.plistのようなテンプレートを用意しておき、envsubstを使って環境ごと/配信方法ごとの設定をワークフロー内で設定できるようにしておいた。

- name: Generate ExportOptions.plist
  run: |
    export EXPORT_METHOD=release-testing
    export EXPORT_DESTINATION=export
    export BUNDLE_ID=${{ inputs.ios_bundle_id }}
    export PROVISIONING_PROFILE=${{ inputs.ios_provisioning_profile }}
    envsubst < ios/Runner/ExportOptions-Template.plist > ios/Runner/ExportOptions.plist

作成しておいたExportOptions-Template.plistが以下。ExportOptions.plistの各設定についてはxcodebuild --helpで確認することができる。

<?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_DESTINATION}</string>
	<key>method</key>
	<string>${EXPORT_METHOD}</string>
	<key>provisioningProfiles</key>
	<dict>
		<key>${BUNDLE_ID}</key>
		<string>${PROVISIONING_PROFILE}</string>
	</dict>
	<key>signingCertificate</key>
	<string>iOS Distribution</string>
	<key>signingStyle</key>
	<string>manual</string>
	<key>stripSwiftSymbols</key>
	<true/>
	<key>teamID</key>
	<string>YOUR_TEAM_ID</string>
	<key>thinning</key>
	<string>&lt;none&gt;</string>
</dict>
</plist>

ipaのビルド

そしてやっとiOSアプリのビルドだ。build iosではなく、build ipaなので注意。自分はこれで時間を溶かした。

- name: Build ipa
  run: |
    flutter build ipa
      --release \
      --flavor ${{ inputs.environment }} \
      --dart-define=FLAVOR=${{ inputs.environment }} \
      --export-options-plist=ios/Runner/ExportOptions.plist

アーティファクトのアップロード

ビルド成果物であるipaをアーティファクトして次のジョブに渡すため、actions/upload-artifactを使用している。アーティファクトを使用せずにそのままAppDistributionへのアップロードジョブを実行したいところだが、ジョブに使用するwzieba/Firebase-Distribution-Github-ActionがLinuxしかサポートしていないため、そのまま実行するとContainer action is only supported on Linuxというエラーが出てしまう。

ちなみに自分はInfo.plistのCFBundleNameを設定してapp-${{ 環境名 }}-release.ipaという名前でipaが作成されるように設定している。

- uses: actions/upload-artifact@v2
  with:
    name: app-${{ inputs.environment }}-release.ipa
    path: build/ios/ipa/app-${{ inputs.environment }}-release.ipa

キーチェーンとプロビジョニングプロファイルのクリーンアップ

ワークフローをセルフホステッドランナー上で実行する場合は、キーチェーンとプロビジョニングプロファイルが残ってしまう可能性があるため、確実に削除するようにしておく。

- 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

ipaをアップロード

いよいよ最後のジョブ。build-iosが終わったタイミングで、アーティファクトをダウンロードし、AppDistributionへipaをアップロードする。前述したようにwzieba/Firebase-Distribution-Github-ActionはLinuxしかサポートしていないのでruns-on: ubuntu-latest上で実行する。

distribute-ios:
  needs: build-ios
  runs-on: ubuntu-latest
  steps:
    - uses: actions/download-artifact@v2
      with:
        name: app-${{ inputs.environment }}-release.ipa
    - name: Upload artifact to Firebase App Distribution
      uses: wzieba/Firebase-Distribution-Github-Action@v1
      with:
        appId: ${{ secrets.FIREBASE_IOS_APP_ID }}
        serviceCredentialsFileContent: ${{ secrets.APP_DISTRIBUTION_SERVICE_ACCOUNT_JSON }}
        groups: ios_testers
        file: app-${{ inputs.environment }}-release.ipa

おわり

Androidとは違ってiOSは証明書周りで苦戦するかと思ったが、GitHubActionsを含めてドキュメントがしっかり整備されているのでワークフローを構築しやすかった。

とはいえ、macosの実行環境は無料枠分のクレジットをあっという間に食い潰すし、証明書やプロビジョニングプロファイルの管理など厄介な点は多い。自分の場合はワークフローの実行トリガーをworkflow_dispatchを使って手動にすることで実行回数を減らす対応をしたが、それでもクレジットはどんどん減っていくのでセルフホステッドランナーへの移行を検討している。

また、今回は手動署名の方法を取ったが、証明書やプロビジョニングプロファイルの管理から解放されるAutoSigningの方法が最近は主流となっている。早いところそちらの方にも移行したい(CFBundleIdentifier Collisionエラーが解決できずに移行できないでいる)。


関連記事
最新記事
プロモーション

This site uses Google Analytics.