FlutterのiOSアプリをFirebaseAppDistributionにアップロードするワークフローをGitHubActionsで構築する(非AutoSigning)
はじめに
最近開発している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><none></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
エラーが解決できずに移行できないでいる)。