GitHub Actions で Automatically manage signing を使って Flutter の ipa ビルドする

2022/12/30に公開約13,800字

GitHub Actions で Flutter アプリの TestFlight 用バイナリ(ipa)をビルドする方法を調べると「Automatically manage signing」 を無効にしてビルドする記事が出てきます。

オプションを有効にしたままビルドする方法が見つからなかったので調べました。検証は Flutter で行いましたが、Flutter を使わない iOS ネイティブのプロジェクトでも同様の手順でビルドできると思います。

Automatically manage signing と Cloud signing

Automatically manage signing について改めて調べると、Xcode13 以降では Automatically manage signing を有効にすると Cloud signing という機能が使われるようです。

Cloud signing については WWDC21 のビデオと Xcode13 のリリースノートに記述があります。

https://developer.apple.com/videos/play/wwdc2021/10204/

Xcode 13 Release Notes | Apple Developer Documentation

抜粋

xcodebuild now supports the use of App Store Connect API keys for authentication with the Apple Developer website. This enables the use of automatic signing via xcodebuild in headless environments, such as build machines and continuous integration setups. To use API keys with xcodebuild, create an API key on App Store Connect and pass the key along with its identifier and your team’s issuer identifier to xcodebuild using the new parameters authenticationKeyPath, authenticationKeyID, and authenticationKeyIssuerID, respectively. When creating a key, you can assign it a role to control its permissions for performing automatic signing tasks. To learn more about creating and managing keys, see Creating API Keys for App Store Connect API. (51444716)


xcodebuild は、Apple Developer ウェブサイトでの認証に App Store Connect API キーの使用をサポートするようになりました。これにより、ビルドマシンや継続的インテグレーション設定などのヘッドレス環境において、xcodebuild を介した自動署名の利用が可能になります。xcodebuild で API キーを使用するには、App Store Connect で API キーを作成し、新しいパラメータ authenticationKeyPath, authenticationKeyID, および authenticationKeyIssuerID をそれぞれ使用して、その識別子とあなたのチームの発行者識別子と一緒にキーを xcodebuild に渡します。鍵を作成するとき、自動署名タスクを実行するためのその権限を制御するために、その役割を割り当てることができます。キーの作成と管理について詳しくは、「App Store Connect API の API キーを作成する」を参照してください。(51444716)

Automatic signing in the Xcode distribution assistant now supports cloud signing. With cloud signing, Xcode distribution signs your app using signing certificates created and managed on Apple servers, requiring no setup on your local Mac other than signing in to Xcode with your Apple ID. Cloud signing is available when signing for App Store Connect, Ad Hoc, Enterprise, or Developer ID distribution. Cloud signing certificates are stored securely on Apple servers; you can’t transmit or store the private key on your Mac. Similar to standard distribution signing certificates, cloud signing certificates are accessible only to members of your development team with the Admin role (or Account Holder for Developer ID). Use App Store Connect (Users and Access) to set permissions for users with other roles. You don’t need to save or share cloud signing certificates with other developers on your team, as any team member with the necessary permissions can sign them for distribution with cloud signing. If you already have a valid distribution signing certificate and matching provisioning profiles installed on your Mac, Xcode uses those and signs locally rather than using cloud signing. Additionally, cloud signing isn’t available when using manual distribution signing. (70706409)


Xcode 配布アシスタントでの自動署名は、クラウド署名をサポートするようになりました。クラウド署名では、Xcode 配布は、Apple サーバ上で作成および管理された署名証明書を使用してアプリケーションに署名し、Apple ID で Xcode にサインインする以外に、ローカル Mac でセットアップが必要ではありません。クラウド署名は、App Store Connect、アドホック、エンタープライズ、または開発者 ID の配布のために署名するときに利用可能です。クラウド署名証明書は、Apple のサーバーに安全に保存されます。あなたの Mac 上で秘密鍵を送信または保存することはできません。標準の配布用署名証明書と同様に、クラウド署名証明書は、管理者ロールを持つ開発チームのメンバー(または Developer ID の場合はアカウント所有者)だけがアクセスできます。他のロールを持つユーザーのアクセス許可を設定するには、App Store Connect(ユーザーとアクセス)を使用します。必要な権限を持つチームメンバーであれば、クラウド署名で配布用の署名ができるため、チームの他の開発者とクラウド署名証明書を保存または共有する必要はありません。あなたが既に有効な配布署名証明書と一致するプロビジョニングプロファイルがあなたの Mac にインストールされている場合、Xcode はそれらを使用し、クラウドサインを使用するのではなく、ローカルに署名します。さらに、手動配布署名を使用する場合、クラウド署名は利用できません。(70706409)

簡単に説明すると、配布用バイナリを作成するために従来は配布証明書(Apple Distribution)とプロビジョニングプロファイルを用意する必要がありましたが、Cloud signing を利用すると Apple のサーバー上で署名が行われるのでこれらの作成や管理が不要になります、という内容です。

Xcode をローカルで利用する際に GUI から AppleID でログインすると思いますが、ローカルではこの認証情報を利用してサーバー上で署名が行われるようです。そのため、普段の開発では Cloud signing の存在を意識することがないかもしれません。

また、Xcode 13 以降では xcodebuild から Apple Developer ウェブサイトとの認証に App Store Connect API を使用することができるようになり、これによってヘッドレスな CI 環境でも Cloud signing の利用がサポートされました。

Automatically manage signing と Cloud signing を利用するモチベーション

Cloud signing を使うと

  • 開発用証明書(Apple Development)
  • App Store Connect API で払い出した API 情報(IssueID / API Key / .p8 ファイル)

だけ用意すれば TestFlight 向けの ipa に署名ができます。このうち App Store Connect API を利用するための API 情報は払い出したあと無期限で利用できるので、開発用証明書(Apple Development)のみを 1 年に 1 回更新することになります。

既に altool を使って CI 環境から ipa を TestFlight にアップロードしている環境では App Store Connect API を利用しているケースもあり、そのような環境では開発用証明書(Apple Development)を用意するだけで署名ができるというメリットがあります。

一方で、Automatically manage signing を無効にして、従来通りの方法で署名を行う場合は、

  • 配布用証明書(Apple Distribution)
  • 配布用プロビジョニングプロファイル

が必要になります、どちらも 1 年に一度更新が必要です(プロビジョニングプロファイルは配布用証明書に紐づいている)。

署名するためのワークフロー

実際に GitHub Actions で Cloud signing を使って署名をするワークフローです、ワークフローの全体は記事の末尾に掲載しています。

用意するもの

  • App Store Connect API で払い出した API 情報(IssueID / API Key / .p8)
  • 開発用証明書(Apple Development)

が必要なので以下のような感じで secrets に登録します、APPLE_API_で始まるものが App Store Connect API の API 情報です。ファイルは base64 に変換して文字列として登録します。

開発用証明書のインポート

開発用証明書をインポートしている step です

- name: Import Apple Development Certificate
    env:
        APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
        APPLE_DEVELOPMENT_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_DEVELOPMENT_CERTIFICATE_P12_BASE64 }}
        APPLE_DEVELOPMENT_CERTIFICATE_P12_PASSWORD: ${{ secrets.APPLE_DEVELOPMENT_CERTIFICATE_P12_PASSWORD }}
    run: |
        APPLE_DEVELOPMENT_CERTIFICATE=$RUNNER_TEMP/development_certificate.p12
        KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db

        # import certificate from secrets
        echo -n "$APPLE_DEVELOPMENT_CERTIFICATE_P12_BASE64" | base64 --decode --output $APPLE_DEVELOPMENT_CERTIFICATE

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

        # import certificate to keychain
        security import $APPLE_DEVELOPMENT_CERTIFICATE -P "$APPLE_DEVELOPMENT_CERTIFICATE_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
        security list-keychain -d user -s $KEYCHAIN_PATH

secrets に登録しておいた証明書(APPLE_DEVELOPMENT_CERTIFICATE_P12_BASE64、base64 の文字列)を取り出し、 base64 をデコードしてファイルとして key-chain に取り込んでいます。
なお APPLE_KEYCHAIN_PASSWORD は空でも登録が可能なので今回は secrets に登録していません。

App Store Connect API の private key の配置

App Store Connect API の private key (.p8)をローカルに配置します

- name: Extract App Store Connect API Private Key in ./private_keys
    env:
        APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
        APPLE_API_AUTHKEY_P8_BASE64: ${{ secrets.APPLE_API_AUTHKEY_P8_BASE64 }}
    run: |
        mkdir ./private_keys
        echo -n "$APPLE_API_AUTHKEY_P8_BASE64" | base64 --decode --output ./private_keys/AuthKey_$APPLE_API_KEY_ID.p8

xcrun altool -h の内容を読むと .p8 ファイルの配置場所はいくつかのパターンがあるようです、上記のスクリプトでは ./private_keys 配下に置きました。

--apiKey <api_key>        apiKey. Required for JWT authentication (in lieu of username/password).
                          This option will search the following directories in sequence for a private key file
                          with the name of 'AuthKey_<api_key>.p8': './private_keys', '~/private_keys', '~/.private_keys',
                          and '~/.appstoreconnect/private_keys'. Additionally, you can set environment variable $API_PRIVATE_KEYS_DIR
                          or a user default API_PRIVATE_KEYS_DIR to specify the directory where your AuthKey file is located.
--apiIssuer <issuer_id>   Issuer ID. Required if --apiKey is specified.

Archive and Export by xcodebuild

.xcarchive と .ipa の作成

- name: Run flutter build
    id: build
    run: flutter build ios --release --no-codesign

- name: Archive by xcodebuild
    env:
        APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
        APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
    run: xcodebuild archive -workspace ./ios/Runner.xcworkspace -scheme Runner -configuration Release -archivePath ./build/ios/Runner.xcarchive -allowProvisioningUpdates -authenticationKeyIssuerID $APPLE_API_ISSUER_ID -authenticationKeyID $APPLE_API_KEY_ID -authenticationKeyPath `pwd`/private_keys/AuthKey_$APPLE_API_KEY_ID.p8

- name: Export by xcodebuild
    env:
        APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
        APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
    run: xcodebuild -exportArchive -archivePath ./build/ios/Runner.xcarchive -exportPath ./build/ios/ipa -exportOptionsPlist ./ios/ExportOptions.plist -allowProvisioningUpdates -authenticationKeyIssuerID $APPLE_API_ISSUER_ID -authenticationKeyID $APPLE_API_KEY_ID -authenticationKeyPath `pwd`/private_keys/AuthKey_$APPLE_API_KEY_ID.p8

最初に flutter build ios で Flutter をビルドしますがこの段階で署名をするとエラーになるので --no-codesign オプションをつけて無効にしています。

その次の xcodebuild archive ... でアーカイブを作成しますが、コマンドの後半の

-allowProvisioningUpdates -authenticationKeyIssuerID $APPLE_API_ISSUER_ID -authenticationKeyID $APPLE_API_KEY_ID -authenticationKeyPath `pwd`/private_keys/AuthKey_$APPLE_API_KEY_ID.p8

を指定することで Cloud signing による署名が行われます(この段階では配布用ではなく開発用署名が行われているようです)。

最後の xcodebuild -exportArchive で .ipa を作成するコマンドも同じパラメータを付与します、このタイミングで配布用の署名が行われるようです。

Upload

こちらは署名とは関係がありませんが altool と App Store Connect API を使って .ipa をアップロードする処理です。

- name: Upload to App Store Connect
    env:
        APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
        APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
    run: xcrun altool --upload-app --type ios -f $IPA_PATH --apiKey $APPLE_API_KEY_ID --apiIssuer $APPLE_API_ISSUER_ID

ワークフロー(全体)

ワークフロー全体はこちら

name: Automatically manage signing with Cloud signing

on:
  workflow_dispatch:

permissions:
  contents: read

jobs:
  ios:
    runs-on: macos-12
    timeout-minutes: 30
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Import Apple Development Certificate
        env:
          APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
          APPLE_DEVELOPMENT_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_DEVELOPMENT_CERTIFICATE_P12_BASE64 }}
          APPLE_DEVELOPMENT_CERTIFICATE_P12_PASSWORD: ${{ secrets.APPLE_DEVELOPMENT_CERTIFICATE_P12_PASSWORD }}
        run: |
          APPLE_DEVELOPMENT_CERTIFICATE=$RUNNER_TEMP/development_certificate.p12
          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db

          # import certificate from secrets
          echo -n "$APPLE_DEVELOPMENT_CERTIFICATE_P12_BASE64" | base64 --decode --output $APPLE_DEVELOPMENT_CERTIFICATE

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

          # import certificate to keychain
          security import $APPLE_DEVELOPMENT_CERTIFICATE -P "$APPLE_DEVELOPMENT_CERTIFICATE_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
          security list-keychain -d user -s $KEYCHAIN_PATH

      - name: Extract App Store Connect API Private Key in ./private_keys
        env:
          APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
          APPLE_API_AUTHKEY_P8_BASE64: ${{ secrets.APPLE_API_AUTHKEY_P8_BASE64 }}
        run: |
          mkdir ./private_keys
          echo -n "$APPLE_API_AUTHKEY_P8_BASE64" | base64 --decode --output ./private_keys/AuthKey_$APPLE_API_KEY_ID.p8

      - name: Install Flutter
        uses: subosito/flutter-action@v2
        with:
          channel: 'stable'
          cache: true

      - name: Run flutter pub get
        run: flutter pub get

      - name: Run flutter build
        id: build
        run: flutter build ios --release --no-codesign

      - name: Archive by xcodebuild
        env:
          APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
          APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
        run: xcodebuild archive -workspace ./ios/Runner.xcworkspace -scheme Runner -configuration Release -archivePath ./build/ios/Runner.xcarchive -allowProvisioningUpdates -authenticationKeyIssuerID $APPLE_API_ISSUER_ID -authenticationKeyID $APPLE_API_KEY_ID -authenticationKeyPath `pwd`/private_keys/AuthKey_$APPLE_API_KEY_ID.p8

      - name: Export by xcodebuild
        env:
          APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
          APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
        run: xcodebuild -exportArchive -archivePath ./build/ios/Runner.xcarchive -exportPath ./build/ios/ipa -exportOptionsPlist ./ios/ExportOptions.plist -allowProvisioningUpdates -authenticationKeyIssuerID $APPLE_API_ISSUER_ID -authenticationKeyID $APPLE_API_KEY_ID -authenticationKeyPath `pwd`/private_keys/AuthKey_$APPLE_API_KEY_ID.p8

      - name: Detect path for ipa file
        run: |
          echo "IPA_PATH=$(find build/ios/ipa -type f -name '*.ipa')" >> $GITHUB_ENV

      - name: Upload to App Store Connect
        env:
          APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
          APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
        run: xcrun altool --upload-app --type ios -f $IPA_PATH --apiKey $APPLE_API_KEY_ID --apiIssuer $APPLE_API_ISSUER_ID

記事が参考になったら Like や Twitter をフォローしてもらえると嬉しいです、Flutter アプリの CI のご相談があれば DM ください

Discussion

ログインするとコメントできます