Flutter × GithubActions × DeployGateでアプリを配布する(iOS)
証明書とプロビジョニングプロファイルの管理をどうするか問題。
Automatically manage signingとCloud signingでクラウド上で署名してくれる、素晴らしい機能をこの記事で知る。ありがたい🙏
メリット
- 配布用証明書をホストマシンに持たなくて良い
- プロビジョニングプロファイルをホストマシンに持たなくて良い(import / download)
- 証明書とプロビジョニングプロファイルの手動更新がなくなる
この記事も大変参考になりました。
PJの規模によってもベストプラクティスは異なることは念頭に入れておく。
事前準備
- Apple Developer のユーザとアクセス > キー を開く
- 権限はAdmin以上じゃないと見れないページなので、権限がない場合は偉い人に依頼しましょう
- ➕ボタンからkeyを作り(Adminで作成)、以下を控えておく。p8は1回しかDLできないのでバックアップとりましょう
- ✅ キーID
- ✅ Issuer ID
- ✅ AuthKey(p8)
事前準備(Xcode)
- Automatically manage signingにチェック
- ExportOptions.plistを用意する
- 初回はXcodeからアーカイブしてじゃないと作れなかった古い記憶があるんだけど、今は初回コマンドでも自分の環境では生成できた。
fvm flutter build ipa --release --export-method=ad-hoc
生成したExportOptions.plistをお好みの場所にお好みの名前で設置。今回はこれで↓
ios/ExportOptions/ExportOptionsAdHock.plist
こちらの記事より
事前準備(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 プッシュ
事前準備(GithubActions)
- Deploy Gate APIキー => secretに登録
- Deploy Gateユーザ名 => secretに登録
- iOS配布ページURLに記載されているハッシュ値 => secretに登録
以下で登録
- ✅ DEPLOYGATE_API_KEY
- ✅ DEPLOYGATE_USER
- ✅ IOS_DISTRIBUTION_HASH
参考にさせてもらいました🙏
スクラップ内に貼った参考記事を見ながら、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"
署名方法が沢山あって旧来版の署名と途中混在してしまって紆余曲折したけど、ようやっと腹落ちした。
署名まわりは苦手意識持ってる(私もその1人)人沢山いるイメージなので、簡潔にできる方法に出会えて感謝!!
後日談
DeployGate配信と同時に、Apple Storeへのデプロイワークフロー作成時に詰まったところメモ
何が起きた
- PUSH通知の設定をXcodeやプロビジョニングプロファイルで正しく設定しているにも関わらず、ないとAppleさんに怒られる
- ITMS-90078: Missing Push Notification Entitlement
- deep linkの設定などもしれっと抜けてしまう
何が原因だった?
- https://github.com/flutter/flutter/issues/129075
- 署名なしでIPAを生成しようとするとうまくいかないことがあるらしい
対応
アーカイブ時の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
後日談アーカイブ時に署名しないと、必要な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 を渡すと開発証明書を要求する
大事な事なのでいっぱい書いておく。
ここでハマった。
AppleDeveloperを見たら、APIが開発証明書を作ってるのを目撃。
Created By APIみたいなやつが開発証明書を作ってる。
ローカルのMacだったら、毎回同じMacでビルドするからキーチェーンに開発証明書の鍵を持ってるけど、CIで使用するMacは毎回違うマシンなのでAppleDeveloperに開発証明書はあるけど、マシンのキーチェーンに鍵がねえです、ってエラーになる。(そりゃそうだ)
仕方ないので、開発証明書だけリポジトリにgit secretでコミットして、ワークフロー内でインポートするstepを追加することにする。(モヤモヤするが仕方ない)
最終的な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
この開発証明書を持たせるに少々モヤモヤしているが、とりあえずはこれで終わり(にしたい)
inputs多くなってしまった。
配布署名に必要なものや、開発用のプロビジョニングプロファイルはマシンに所持しなくて良いので、そこは良い!
FlutterのipaビルドのオプションでCloud-managed signができるようになったらいいなあ。