🤖

Flutterアプリ(iOS)へ、Cloud-managed certificatesで署名して、GitHub Actions経由で配信する

2023/05/27に公開

はじめに

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以降で利用可能となった、クラウド上で管理される証明書のことです。
https://developer.apple.com/help/account/create-certificates/cloud-managed-certificates
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)へ署名する事前準備として、次を行います。

  1. Xcodeで、Cloud-managed certificatesを使うための設定
  2. ExportOptions.plistの作成
  3. App Store Connect API Keyの作成
  4. Fastfileの作成 (アプリをFirebase App Distributionで配信するため)
  5. 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>&lt;none&gt;</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ファイルをエクスポートしています。
このタイミングで、authenticationKeyIssuerIDauthenticationKeyIDauthenticationKeyPathを指定し、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での配布を行うことができます。

参考文献

https://developer.apple.com/help/account/create-certificates/cloud-managed-certificates/
https://docs.github.com/ja/actions/deployment/deploying-xcode-applications/installing-an-apple-certificate-on-macos-runners-for-xcode-development
https://qiita.com/ykyouhei/items/af728232ce1950b2c2de
https://developer.apple.com/forums/thread/690676
https://zenn.dev/yorifuji/articles/build-automatically-manage-singin-on-ci
https://stackoverflow.com/questions/11034133/building-ios-applications-using-xcodebuild-without-codesign

Discussion