Flutterアプリ(iOS)へ、Cloud-managed certificatesで署名して、GitHub Actions経由で配信する
はじめに
GitHub Actionsは便利なCIサービスです。
GitHub Actions上でFlutterアプリ(iOS)へ署名する方法としては、次のような方法が挙げられるかと思います。
- GitHub Actionsでのsecretsへ証明書・プロビジョニングプロファイルを格納し、署名する方法 (GitHub Actions公式で紹介されている方法)
- fastlane matchを使って署名する方法
- Cloud-managed certificatesを使って署名する方法
チーム開発でCIを導入する場合、fastlane matchを使う方法がよく採用されるのではないかと思います。
ただ、個人的にはfastlane matchを使う運用で、次のような点が手間に感じることがありました。
- 一年に一度、手動で証明書に関して更新する必要がある
- 証明書・プロビジョニングプロファイルを管理するためのリポジトリを用意する必要がある
- (特に
username:
へ自身のApple IDを設定できない場合)端末(UDID)を新規に登録する際での運用が、煩雑になることがある - fastlane match自体に関する学習コストが発生する
一方で、Xcode13以降で利用可能な、Cloud-managed certificatesを使うと、上記でのような手間を削減できる部分があります。
Cloud-managed certificatesとは、Xcode13以降で利用可能となった、クラウド上で管理される証明書のことです。
Cloud-managed certificatesを使うと、署名はクラウドを使用して行われるため、ローカルで証明書を管理する必要がありません。
また、Cloud-managed certificatesを使うと、次のようなメリットがあります。
- 証明書の手動更新が不要になる
- プロビジョニングプロファイルを自動で管理してくれる
- 端末の登録がApple Developer Portalへの登録のみですむ
- CI上で証明書・プロビジョニングプロファイルをセットアップする手間が省ける
そのため、この記事ではCloud-managed certificatesを利用して、GitHub ActionsでFlutterアプリ(iOS)へ署名して、Firebase App Distributionへベータ配信する方法を解説します。
なお、この記事ではCloud-managed certificatesで署名する部分を中心に解説し、次についての解説は省略します。
- fastlane自体に対する解説
- Firebase App Distribution自体に関する解説
- GitHub Actions自体に対する解説
事前準備
まず、Cloud-managed certificatesを使い、GitHub Actions上でFlutterアプリ(iOS)へ署名する事前準備として、次を行います。
- Xcodeで、Cloud-managed certificatesを使うための設定
-
ExportOptions.plist
の作成 - App Store Connect API Keyの作成
-
Fastfile
の作成 (アプリをFirebase App Distributionで配信するため) -
Gemfile
の作成 (CI環境でfastlaneを使用するため)
1. Xcodeで、Cloud-managed certificatesを使うための設定
まず最初に、Xcode上で、Xcode > Preferences > Accounts
より、対象となるチーム(署名を行うためのチーム)でのアカウントでログインしていることを確認しておきます。
次に、TARGETS > Runner > Signing & Capabilities
を選択し、Automatically manage signing
にチェックを入れます。
上記で一旦、Xcode上での、Cloud-managed certificatesを使うための設定は完了です。
2. ExportOptions.plistの作成
次に、ExportOptions.plist
を作成します。
今回は、Firebase App Distributionでの配信を行うため、AdHoc用のExportOptions.plist
を作成します。
AdHoc用のExportOptions.plist
を作成するためには、次のコマンドで一度Flutterアプリをビルドします。
fvm flutter build ipa --release --export-method=ad-hoc
上記コマンドを実行すると、build/ios/ipa
下に、次のような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>method</key>
<string>ad-hoc</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string><Team ID></string>
<key>thinning</key>
<string><none></string>
</dict>
</plist>
作成したExportOptions.plist
は、ios/export_options
等に配置しておきます。
また、この段階でApple Developer Portalにログインし、Certificates, Identifiers & Profiles > Certificates
より、iOS Distribution
の証明書が作成されていることを確認しておきます。
なお、Certificates, Identifiers & Profiles > Provisioning Profiles
を確認しても、AdHoc用のプロビジョニングプロファイルは作成されていません。
ただ、(正常に.ipa
をビルドできており、以降のステップでも問題なくビルドできることから)おそらく内部的にはプロビジョニングプロファイルを作成してくれているようで、特に問題はありません。
(また、Certificates, Identifiers & Profiles > Devices
へ登録した端末(UDID)は、自動でプロビジョニングプロファイルに含めてくれるようです。)
3. App Store Connect API Keyの作成
次に、App Store Connect API Key(以降、APIキー)を作成します。
App Store Connect API とは、その名の通り、App Store Connectの機能をAPI経由で利用するためのものです。
App Store Connect API Keyを作成するためには、次の手順で作業を行います。
まず、App Store Connectにログインし、User And Access > Keys
を選択します。
はじめて作成する場合は、画面上に「Request Access」というボタンが表示されているので、これをタップします。
タップすると、次での内容を確認する旨のダイアログが表示されるので、内容を確認し、同意する場合はチェックを入れて、Submitをタップします。
(以下は、DeepLによる翻訳)
App Store Connect APIへのアクセス権を申請する
App Store Connect APIは、お客様のチーム内での開発、テスト、レポート作成のみを目的としており、以下のようなお客様自身の内部ワークフローの主要部分を自動化することができます:
TestFlight。アプリのベータ版ビルド、テスター、グループの管理。
ユーザーとアクセス。ユーザーのチームへの招待の送信、ユーザー権限の調整、ユーザーの削除。
レポーティング。お客様のアプリの売上および財務レポートのダウンロード。
お客様は、このApp Store Connect APIを使用して、第三者へのサービス提供やその他の用途に使用することはできません。注意事項として、お客様は、認証情報をお客様のチーム以外の者と共有したり、第三者から認証情報を要求したりすることはできません。リクエストは審査され、組織に最初にアクセス権が与えられ、次に個人にアクセス権が与えられます。
このボックスにチェックを入れて送信をクリックすると、お客様は、App Store Connect APIを、お客様のチーム内での開発、テスト、および報告目的でのみ、ドキュメントに従って使用することに同意したことになります。
承認されると、APIキーが作れるようになるため、Generate API Keyをタップします。
API Keyの名前は適当な名前を入力し、AccessはAdmin
を選択し、Generateを押します。
(App Manager
等、Admin
以下な権限だと、後にCI上でアプリでのアーカイブをエクスポートする段階でエラーが出ます)
これで、App Store Connect API Keyが作成されました。
APIキーが作成できたら、あとは画面上に表示されているIssuer ID(認証トークンを作成した発行者を識別するID)と、KEY IDをコピーしてメモしておきます。
そして、Download API Keyをタップして、APIキーをダウンロードします。(APIキーは一回のみダウンロードできること、ダウンロードする準備ができていない場合はキャンセルをクリックして後日ダウンロードすること、キーのバックアップは必ず安全な場所に保管する旨が書かれているので、OKならダウンロードを押します)
APIキーがダウンロードできたら、GitHub Actionsでのsecretsへ、「KEY ID」「Issuer ID」「APIキー」を登録していきます。
secretsへ登録するために、まずはAPIキーをBase64でエンコードします。
base64 /path/to/<APIキー>.p8 | pbcopy
次に、GitHub Actionsのsecretsへ登録していきます。
- APP_STORE_CONNECT_API_KEY_BASE64 -> Base64でエンコードしたAPIキーを登録
- APP_STORE_CONNECT_API_KEY_ID → KEY IDを登録
- APP_STORE_CONNECT_API_KEY_ISSUER_ID → Issuer IDを登録
これで、App Store Connect API Keyの設定は完了です。
4. Fastfileの作成
次に、fastlaneを使ってアプリをFirebase App Distributionへアップロードするための、Fastfile
を作成します。
Fastfile
は、ios/fastlane
下へ作成し、次の内容を記載しておきます。
default_platform(:ios)
platform :ios do
desc "Distribute app to Firebase App Distribution"
lane :distribute_app_with_firebase do |options|
app_file_path = options[:app_file_path]
firebase_app_id = options[:firebase_app_id]
firebase_cli_token = options[:firebase_cli_token]
firebase_testers = options[:firebase_testers]
firebase_app_distribution(
app: firebase_app_id,
testers: firebase_testers,
firebase_cli_token: firebase_cli_token,
ipa_path: app_file_path
)
end
end
5. Gemfileの作成
最後に、CI上でfastlane、CocoaPodsを使うためのGemfile
を作成します。
Gemfile
は、プロジェクトのルートディレクトリに作成し、次の内容を記載しておきます。
source "https://rubygems.org"
gem "cocoapods"
gem "fastlane"
gem "fastlane-plugin-firebase_app_distribution"
以上で、準備は完了です。
GitHub Actionsの設定
Cloud-managed certificatesを使うための準備が整ったので、あとはGitHub Actionsでの設定を行っていきます。
まず、GitHub Actionsでのワークフローを書いていきます。
なお、ここではCloud-managed certificatesを使ってアプリへ署名し、Firebase App Distributionでの配布を行うための、最低限の設定を書いていきます。
まず、ワークフローの全体像は次となります。
name: Beta-distribute Dev app
on:
workflow_dispatch:
env:
export_options_plist: 'ios/export_options/ExportOptions.plist'
fastlane_directory: 'ios/fastlane'
firebase_app_id_ios: '<FirebaseでのアプリID>'
firebase_tester_groups: 'qa-team'
ipa_file: '../../build/ios/ipa/<アプリ名>.ipa'
jobs:
beta-distribute-ios-app:
name: Beta-distribute iOS app
runs-on: macos-latest
timeout-minutes: 30
steps:
- name: Checks-out the repository
uses: actions/checkout@v3
- name: Install Gems
run: >
bundle install
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.10.1'
- name: Get Flutter dependencies
run: >
flutter pub get
- name: Decode App Store Connect API Key
run: |
mkdir ./private_keys
echo "${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}" | base64 --decode > ./private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8
- name: Build IPA
run: |
flutter build ios --release --no-codesign
xcodebuild -workspace ios/Runner.xcworkspace -scheme Runner -sdk iphoneos -configuration Release archive -archivePath build/ios/Runner.xcarchive CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO
xcodebuild -exportArchive -archivePath build/ios/Runner.xcarchive -exportOptionsPlist ${{ env.export_options_plist }} -exportPath build/ios/ipa -allowProvisioningUpdates -authenticationKeyIssuerID ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} -authenticationKeyID ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} -authenticationKeyPath `pwd`/private_keys/AuthKey_${{ secrets.secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8
- name: Distribute app with Firebase App Distribution
run: >
bundle exec fastlane distribute_app_with_firebase
app_file_path:${{ env.ipa_file }}
firebase_app_id:${{ env.firebase_app_id_ios }}
firebase_cli_token:${{ secrets.FIREBASE_CLI_TOKEN }}
firebase_testers:${{ env.firebase_tester_groups }}
working-directory: ${{ env.fastlane_directory }}
- name: Remove App Store Connect API Key
if: ${{ always() }}
run: >
rm ./private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8
上記ワークフローにおける、Cloud-managed certificatesに関する部分に関して解説をしていきます。
まず、「Decode App Store Connect API Key」ステップでは、App Store Connect API Keyをデコードして、AuthKey_${{ secrets.secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8
という名前で保存しています。
次に、「Build IPA」ステップでは、iOSアプリをビルドし、IPAファイルをエクスポートしています。
flutter build ios --release --no-codesign
では、--no-codesign
を指定し、flutter build ios
でのビルド時に署名を行わないようにしています。
次でのxcodebuild
では、CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO
を指定し、xcodebuild
でのビルド(アーカイブ)時に署名を行わないようにしています。
最後のxcodebuild
では、アーカイブよりIPAファイルをエクスポートしています。
このタイミングで、authenticationKeyIssuerID
、authenticationKeyID
、authenticationKeyPath
を指定し、App Store Connect API Keyを使って署名を行っています。
ちなみに、authenticationKeyPath
は絶対パスで指定しないと次のエラーが出るため、絶対パスで指定してします。
xcodebuild: error: The -authenticationKeyPath flag must be an absolute path to an existing file.
Error: Process completed with exit code 64.
上記ワークフローを実行することで、Cloud-managed certificatesを使ってiOSアプリへ署名し、Firebase App Distributionでの配布を行うことができます。
参考文献
Discussion