Closed13

Flutter × GithubActions × DeployGateでアプリを配布する(iOS)

ohtsukiohtsuki

メリット

  • 配布用証明書をホストマシンに持たなくて良い
  • プロビジョニングプロファイルをホストマシンに持たなくて良い(import / download)
  • 証明書とプロビジョニングプロファイルの手動更新がなくなる

この記事も大変参考になりました。
PJの規模によってもベストプラクティスは異なることは念頭に入れておく。
https://qiita.com/ykyouhei/items/af728232ce1950b2c2de

ohtsukiohtsuki

事前準備

  • Apple Developer のユーザとアクセス > キー を開く
  • 権限はAdmin以上じゃないと見れないページなので、権限がない場合は偉い人に依頼しましょう
  • ➕ボタンからkeyを作り(Adminで作成)、以下を控えておく。p8は1回しかDLできないのでバックアップとりましょう
    • ✅ キーID
    • ✅ Issuer ID
    • ✅ AuthKey(p8)
ohtsukiohtsuki

事前準備(Xcode)

  • Automatically manage signingにチェック
  • ExportOptions.plistを用意する
    • 初回はXcodeからアーカイブしてじゃないと作れなかった古い記憶があるんだけど、今は初回コマンドでも自分の環境では生成できた。
fvm flutter build ipa --release --export-method=ad-hoc

生成したExportOptions.plistをお好みの場所にお好みの名前で設置。今回はこれで↓
ios/ExportOptions/ExportOptionsAdHock.plist



こちらの記事より
https://zenn.dev/welchi/articles/flutter-ios-ci-cd-xcode-cloud#2.-exportoptions.plistの作成

ohtsukiohtsuki

事前準備(GithubActions)

  • App Store Connect Issuer ID => secretに登録
  • App Store Connect API Key ID => secretに登録
  • AuthKey(p8)はgit secretで暗号化してプッシュ、GithubActionsワークフロー内で複合化して使用します。
    • 今参加してるPJはこの方法でenvなどのシークレットファイルを管理してるのでそのようにしてます。(この辺の説明は割愛)
    • 頻繁に変わるものでもないので、base64してID同様secretに登録にしても良いと思います。

以下で登録

  • ✅ APP_STORE_CONNECT_ISSUER_ID
  • ✅ APP_STORE_CONNECT_API_KEY_ID
  • ✅ p8を./ios/.private_keys/{AuthKeyファイル名}.p8でgit secret プッシュ
ohtsukiohtsuki

スクラップ内に貼った参考記事を見ながら、GithubActionsのワークフローを書いていく
毎朝6時にDeploy Gateへ開発版アプリをデプロイするワークフローです。

name: 'iOS deploy to DeployGate'
on:
  schedule:
    # JST AM6:00
    - cron: '0 21 * * *'

permissions:
  contents: read

defaults:
  run:
    working-directory: pj-dir-name

env:
  APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
  APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
  DEPLOYGATE_API_KEY: ${{ secrets.DEPLOYGATE_API_KEY }}
  DEPLOYGATE_USER: ${{ secrets.DEPLOYGATE_USER }}
  IOS_DISTRIBUTION_HASH: ${{ secrets.IOS_DISTRIBUTION_HASH }}

jobs:
  ios:
    runs-on: macos-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
        with:
          sparse-checkout: |
            .github
            .gitsecret
            pj-dir-name

      # git secretで暗号化してるファイルを複合化(カスタムアクションの中身は割愛)
      - uses: ./.github/actions/git-secret-reveal
        with:
          gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}

      # Flutterのインストールとpub getなどしてるカスタムアクション(割愛)
      - name: Set Up Flutter
        uses: ./.github/actions/flutter-setup
        with:
          working-directory: pj-dir-name

      # Flutter iOSを署名なしでビルド
      - name: Run flutter build iOS
        id: build
        run: flutter build ios --release --no-codesign --dart-define-from-file=dart_define/dev.json

      # Xcodeで署名なしでArchive
      - name: Archive by xcodebuild
        run: |
          xcodebuild archive \
            CODE_SIGNING_ALLOWED=NO \
            -workspace ./ios/Runner.xcworkspace \
            -scheme Runner \
            -configuration Release \
            -archivePath ./build/ios/Runner.xcarchive

      # XcodeでCloud Signで署名してExport
      - name: Export by xcodebuild
        run: |
          xcodebuild -exportArchive \
            -archivePath ./build/ios/Runner.xcarchive \
            -exportPath ./build/ios/ipa \
            -exportOptionsPlist ./ios/ExportOptions/ExportOptionsAdHock.plist \
            -allowProvisioningUpdates \
            -authenticationKeyID $APP_STORE_CONNECT_API_KEY_ID \
            -authenticationKeyIssuerID $APP_STORE_CONNECT_ISSUER_ID \
            -authenticationKeyPath $(pwd)/ios/.private_keys/AuthKey_$APP_STORE_CONNECT_API_KEY_ID.p8

      # ipaファイルのパスをenvにセット
      - name: Detect path for ipa file
        run: |
          echo "IPA_PATH=$(find build/ios/ipa -type f -name '*.ipa')" >> $GITHUB_ENV

      - name: Set Git Variables
        run: |
          echo "GIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
          echo "GIT_BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV

      # いってらっしゃいDeploy Gate
      - name: Upload to DeployGate
        run: |
          curl \
            -H "Authorization: token $DEPLOYGATE_API_KEY" \
            -F "file=@$IPA_PATH" \
            -F "message=git:$GIT_HASH" \
            -F "distribution_name=$GIT_BRANCH" \
            -F "release_note=new ios build" \
            -F "distribution_key=$IOS_DISTRIBUTION_HASH" \
            "https://deploygate.com/api/users/$DEPLOYGATE_USER/apps"
ohtsukiohtsuki

署名方法が沢山あって旧来版の署名と途中混在してしまって紆余曲折したけど、ようやっと腹落ちした。
署名まわりは苦手意識持ってる(私もその1人)人沢山いるイメージなので、簡潔にできる方法に出会えて感謝!!

ohtsukiohtsuki

後日談

DeployGate配信と同時に、Apple Storeへのデプロイワークフロー作成時に詰まったところメモ

何が起きた

  • PUSH通知の設定をXcodeやプロビジョニングプロファイルで正しく設定しているにも関わらず、ないとAppleさんに怒られる
    • ITMS-90078: Missing Push Notification Entitlement
  • deep linkの設定などもしれっと抜けてしまう

何が原因だった?

対応

アーカイブ時のCODE_SIGNING_ALLOWED=NOをやめて、exportと同じように署名してあげる
これでaps-environmentを含んだ状態でIPAを生成できた。

        xcodebuild archive \
          -workspace ./ios/Runner.xcworkspace \
          -scheme Runner \
          -configuration Release \
          -archivePath ./build/ios/Runner.xcarchive \
          -allowProvisioningUpdates \
          -authenticationKeyID ${{ inputs.app-store-connect-api-key-id }} \
          -authenticationKeyIssuerID ${{ inputs.app-store-connect-issuer-id }} \
          -authenticationKeyPath ${{ inputs.authentication-key-path }}

IPAにenvironmentが入っているか確認方法

  • IPAをunzipして解凍する
  • codesign -d --entitlements - Payload/Runner.appを実行
❯ codesign -d --entitlements - Payload/Runner.app                                            

[Dict]
        [Key] application-identifier
        [Value]
                [String] XXXXX
        [Key] aps-environment // こいつがIPAに含まれてないとXcodeでPUSH通知の機能ONにしててもないと言われる(罠)
        [Value]
                [String] production
        [Key] beta-reports-active
        [Value]
                [Bool] true
        [Key] com.apple.developer.associated-domains
        [Value]
                [Array]
                        [String] applinks:XXXXX.onelink.me
        [Key] com.apple.developer.team-identifier
        [Value]
                [String] XXXXX
        [Key] get-task-allow
        [Value]
                [Bool] false
ohtsukiohtsuki

後日談アーカイブ時に署名しないと、必要なentitlementsがIPAに含まれないよってことでこうしてたわけですが、GHAでワークフローが成功したり、しなかったり、しなかったりする事象に陥る。

        xcodebuild archive \
          -workspace ./ios/Runner.xcworkspace \
          -scheme Runner \
          -configuration Release \
          -archivePath ./build/ios/Runner.xcarchive \
          -allowProvisioningUpdates \
          -authenticationKeyID ${{ inputs.app-store-connect-api-key-id }} \
          -authenticationKeyIssuerID ${{ inputs.app-store-connect-issuer-id }} \
          -authenticationKeyPath ${{ inputs.authentication-key-path }}

てっきりアーカイブの時も勝手に配布ようと判断して署名してくれんでしょって、思ってたらとんだ勘違い

今回の肝はここ、archiveする段階で何もせずにxcodebuildコマンドに allowProvisioningUpdates を渡すと開発証明書を要求する。Cloud-managed certificatesでは配布証明書の自動管理にフォーカスしているためそのままarchiveすることは証明書がないことから署名できないため失敗してしまう。開発証明書を自前で管理してもよいが、ここで署名のタイミングについて考える。署名はipaを作成する段階で配布証明書によりされる、そのため開発証明書で署名することはこの時点で必要性がない。そこでオプションとして CODE_SIGNING_REQUIRED, CODE_SIGNING_ALLOWED を用いて開発証明書による署名ステップを飛ばすことにする。そうすると署名スキップされるため開発証明書は管理しなくて良い。

allowProvisioningUpdates を渡すと開発証明書を要求する

allowProvisioningUpdates を渡すと開発証明書を要求する

allowProvisioningUpdates を渡すと開発証明書を要求する

https://qiita.com/arasan01/items/7521255be581ac451c4f#4-署名なしでアーカイブxcarchiveを生成する

大事な事なのでいっぱい書いておく。
ここでハマった。

ohtsukiohtsuki

AppleDeveloperを見たら、APIが開発証明書を作ってるのを目撃。
Created By APIみたいなやつが開発証明書を作ってる。

ローカルのMacだったら、毎回同じMacでビルドするからキーチェーンに開発証明書の鍵を持ってるけど、CIで使用するMacは毎回違うマシンなのでAppleDeveloperに開発証明書はあるけど、マシンのキーチェーンに鍵がねえです、ってエラーになる。(そりゃそうだ)

仕方ないので、開発証明書だけリポジトリにgit secretでコミットして、ワークフロー内でインポートするstepを追加することにする。(モヤモヤするが仕方ない)

ohtsukiohtsuki

最終的なyaml
(カスタムアクションに切り出してます)

name: 'Build and Export IPA'
description: 'Archive and export an iOS app using xcodebuild'

inputs:
  app-store-connect-api-key-id:
    description: 'app store connect api key id'
    required: true
  app-store-connect-issuer-id:
    description: 'app store connect issuer id'
    required: true
  authentication-key-path:
    description: 'Path to the authentication key'
    required: true
  developer-cer-p12:
    description: 'Path to the developer cer .p12'
    required: true
  developer-cer-p12-pass:
    description: 'Path to the developer cer .p12 password'
    required: true
  export-options-plist:
    description: 'Path to the ExportOptions.plist'
    required: true
  working-directory:
    description: |
      The working directory to run commands in.
    required: true

runs:
  using: 'composite'
  steps:
    - run: |
        P12_PASSWORD=$(cat ${{ inputs.developer-cer-p12-pass }})
        echo "::add-mask::$P12_PASSWORD"
        echo "P12_PASSWORD=$P12_PASSWORD" >> $GITHUB_ENV
      shell: bash
      working-directory: ${{ inputs.working-directory }}

    # これで開発証明書をキーチェーンにインポートするよ
    - name: Import Code Signing Certificates
      uses: apple-actions/import-codesign-certs@v2
      with:
        p12-filepath: ${{ inputs.developer-cer-p12 }}
        p12-password: ${{ env.P12_PASSWORD }}

    - run: |
        xcodebuild archive \
          -workspace ./ios/Runner.xcworkspace \
          -scheme Runner \
          -configuration Release \
          -archivePath ./build/ios/Runner.xcarchive \
          -allowProvisioningUpdates \
          -authenticationKeyID ${{ inputs.app-store-connect-api-key-id }} \
          -authenticationKeyIssuerID ${{ inputs.app-store-connect-issuer-id }} \
          -authenticationKeyPath ${{ inputs.authentication-key-path }}
      shell: bash
      working-directory: ${{ inputs.working-directory }}

    - run: |
        xcodebuild -exportArchive \
          -archivePath ./build/ios/Runner.xcarchive \
          -exportPath ./build/ios/ipa \
          -exportOptionsPlist ${{ inputs.export-options-plist }} \
          -allowProvisioningUpdates \
          -authenticationKeyID ${{ inputs.app-store-connect-api-key-id }} \
          -authenticationKeyIssuerID ${{ inputs.app-store-connect-issuer-id }} \
          -authenticationKeyPath ${{ inputs.authentication-key-path }}
      shell: bash
      working-directory: ${{ inputs.working-directory }}

    - run: |
        echo "IPA_PATH=$(find build/ios/ipa -type f -name '*.ipa')" >> $GITHUB_ENV
      shell: bash
      working-directory: ${{ inputs.working-directory }}

ワークフローでの呼び出し


      # 前段でgit secretに設定してるp8 / p12ファイルなど復号化してるからそのままファイルパスを渡してるよ

      - name: Build and Export IPA
        uses: ./.github/actions/build-export-ipa
        with:
          app-store-connect-api-key-id: $APP_STORE_CONNECT_API_KEY_ID
          app-store-connect-issuer-id: $APP_STORE_CONNECT_ISSUER_ID
          authentication-key-path: '${{ env.WORKING_DIRECTORY }}/ios/.private_keys/XXXXX.p8'
          developer-cer-p12: '${{ env.WORKING_DIRECTORY }}/ios/.private_keys/XXXXX.p12'
          developer-cer-p12-pass: '${{ env.WORKING_DIRECTORY }}/ios/.private_keys/XXXXX.p12.pass'
          export-options-plist: './ios/ExportOptions/ExportOptionsAdHock.plist'
          working-directory: dir-name
ohtsukiohtsuki

この開発証明書を持たせるに少々モヤモヤしているが、とりあえずはこれで終わり(にしたい)
inputs多くなってしまった。
配布署名に必要なものや、開発用のプロビジョニングプロファイルはマシンに所持しなくて良いので、そこは良い!

FlutterのipaビルドのオプションでCloud-managed signができるようになったらいいなあ。

このスクラップは2ヶ月前にクローズされました