概要
fastlaneを用いてTestFlight配信の自動化を目指します。
以下はその手順の美貌になります。
環境
- Xcode: ver16
- CI: GitHub Actions
- Tool: fastlane
- パッケージ管理: SPM
前提
- Xcodeがインストールされていること
- TestFlight配信したいアプリがGitHubリポジトリにPushされていること
- fastlaneがインストールされていること
- Apple Developer Programに登録していること
ざっくり手順
- Distribution用のProvisioning Profileの用意
- App Store Connect APIキーの作成
- fastlaneの初期化
- Fastfileの実装
- GitHub Actionsのworkflow実装
- 証明書用のGitHubのPrivateリポジトリを作成
- fastlaneのmatchで証明書をリポジトリにPush
- GitHubにシークレット変数を定義
- GitHub Actionsのworkflowを実行!
1. Provisioning Profileの用意
下記記事を参考にAppStore用の証明書を作成し、ローカルでipaファイルを作成できるようにします。
2. App Store Connect APIキーの作成
下記ドキュメントの手順よりAPIキーを作成します。
下記画面でAPIキーファイル(.p8ファイル)のダウンロードと赤枠のIssuerID、キーIDを控えておきます。
※APIキーファイルは一度ダウンロードすると再度ダウンロードできなくなるので注意
3. fastlaneの初期化
以下記事のfastlaneセットアップの手順を参考に初期化を行います。
fastlaneディレクトリがプロジェクトディレクトリ直下に生成されていればOKです。
4. Fastfileの実装
fastlane/Fastfileを以下のように実装します。
default_platform(:ios)
before_all do |lane, options|
if lane == :beta && ENV["ENVIRONMENT"] == "CI"
setup_ci(provider: ENV["SETUP_CI_PROVIDER"])
end
end
platform :ios do
desc "Upload App for TestFlight"
lane :beta do
scan # TestFlight前のビルド & テスト確認
fetch_appstore_profiles # provisioning profileの取得
increment_build_number # ビルドバージョンの自動化
gym # ipaファイル作成
upload_beta # TestFlightの配信要求
commit_version_bump
push_to_git_remote
end
desc "Upload ipa for TestFlight"
lane :upload_beta do |options|
asc_api_key
upload_to_testflight(
ipa: "./.build/TimeWatcher.ipa",
demo_account_required: false,
skip_waiting_for_build_processing: "true",
beta_app_review_info: {
contact_email: "taichis844@gmail.com",
contact_first_name: "Taichi",
contact_last_name: "Satou",
contact_phone: "080-9754-8211",
notes: "TimeWatcherの内部テスト"
}
)
end
desc "fetch appstore profiles and cert"
lane :fetch_appstore_profiles do
api_key = asc_api_key
match(
api_key: api_key,
type: "appstore",
readonly: ENV["MATCH_FETCH_READ_ONLY_MODE"]
)
end
desc "Select Build Number"
private_lane :select_build_number do |options|
increment_build_number(
xcodeproj: 'TimeWatcherPrj/TimeWatcher.xcodeproj',
build_number: latest_testflight_build_number + 0.1
)
end
def asc_api_key
app_store_connect_api_key(
key_id: ENV['ASC_KEY_ID'],
issuer_id: ENV['ASC_ISSUER_ID'],
key_content: ENV['ASC_KEY_CONTENT'],
is_key_content_base64: true
)
end
実装の説明
lane開始前の処理(before_all)
before_all do |lane, options|
if lane == :beta && ENV["ENVIRONMENT"] == "CI"
setup_ci(provider: ENV["SETUP_CI_PROVIDER"])
end
end
上記実装は、Fastfileで定義したlane実行時に動作する箇所です。
実装内容としては、"beta"が実行されたかつ環境変数"ENVIRONMENT"が"CI"のときに
setup_ci
を実行するという内容になります。
setup_ci
は後続で説明する証明書取得をCIで行ったときにキーチェーンとして証明書を保存するための設定を行なってくれる。
CI環境で取得したProvisioning Profileを使用できるようにするためのおまじないです。
ただローカルでこれを行うと、ローカルのキーチェーンに影響が出てよくないことが起きるため、
CI環境でしか行わないようにします。
TestFlight申請のlane
desc "Upload App for TestFlight"
lane :beta do
scan # TestFlight前のビルド & テスト確認
fetch_appstore_profiles # provisioning profileの取得
select_build_number # ビルドバージョンの自動化
gym # ipaファイル作成
upload_beta # TestFlightの配信要求
end
ビルド & テスト(scan)
まずscan
でビルド & テストを行うことでTestFlight配信しても良いコードかどうかを確認しています。
scan
は本来引数設定で必要な情報を指定する必要があるのですが、上記例のように引数なしで実行する方法もあります。
それはFastfileと同階層にScanfileを作成してそこでデフォルト値を設定する方法です。
今回の例では以下のように設定しています。
workspace("<ワークススペース名>")
scheme("<スキーム名>")
clean(true) # ビルド前にclean buildする設定
configuration("Debug") # ビルド対象の設定
output_directory("./.build") # 生成するレポートの配置ディレクトリ
destination("platform=iOS Simulator,name=iPhone 15") # ビルド & テストするシミュレーターの指定
cloned_source_packages_path("SourcePackages") # SPMの成果物のディレクトリ
xcargs("-skipPackagePluginValidation") # SPMでPluginを設定している場合にCI環境でビルドを通すために必要
scan
の挙動の詳細については以下を参照してみてください。
Provisioning Profile取得(fetch_appstore_profiles)
続いて、fetch_appstore_profiles
ではFastfileの以下を呼び出しています。
def asc_api_key
app_store_connect_api_key(
key_id: ENV['ASC_KEY_ID'],
issuer_id: ENV['ASC_ISSUER_ID'],
key_content: ENV['ASC_KEY_CONTENT'],
is_key_content_base64: true
)
end
desc "fetch appstore profiles and cert"
lane :fetch_appstore_profiles do
api_key = asc_api_key
match(
api_key: api_key,
type: "appstore",
readonly: ENV["MATCH_FETCH_READ_ONLY_MODE"]
)
end
ここでは後続で説明するApp Store用の証明書を格納しているGitHubリポジトリから証明書を取得し、キーチェーンに設定してipaファイルをビルドできるようにします。
App Store Connect APIを使用するため、API_KEYをapp_store_connect_api_key
で生成して設定しています。
key_id
、issuer_id
、key_content
は環境変数で設定していますが、内容については後ほど設定手順については後ほど説明します。
readonly
の設定も環境変数で設定していますが、CIで利用する分にはビルド時に読み出すだけで、証明書を生成する必要はないのでtrueで良いと思います。
上記コードでipaファイルを作成するための証明書の設定は完了しました。
ビルド番号更新(select_build_number)
続いて、自前のlaneであるselect_build_number
でTestFlightのビルド番号を更新します。
desc "Select Build Number"
private_lane :select_build_number do |options|
increment_build_number(
xcodeproj: 'TimeWatcherPrj/TimeWatcher.xcodeproj',
build_number: latest_testflight_build_number + 0.1
)
end
今回の例では、TestFlihgtにUploadしている最新のビルド番号から+0.1した番号をビルド番号として設定するようにしています。
基本increment_build_number
一行でビルド番号を+1更新してくれるのですが、
プロジェクトの構成によってはそうでない場合もあるので補足します。
・プロジェクトルートディレクトリにxcodeprojファイルが存在しない場合
increment_build_number
はデフォルトではプロジェクトルートディレクトリのxcodeprojファイルを参照して、ビルド番号を更新します。
もしデフォルトのディレクトリにxcodeprojがない場合は、上記例のようにxcodeproj
項目にxcodeprojディレクトリパスの設定が必要です。
※参考
・xcodeプロジェクトビルド設定のVersioning Systemを自動更新用の設定に更新していない場合
下記記事を参考に、Versioning Systemを"Apple Generic"に設定する必要があります。
少し脱線しますが、ビルド番号とアプリバージョンの違いが筆者はよくわからなかったので、少し調べてみました。
違いとしては以下の通りです。
番号の種類 | 意味合い |
---|---|
ビルド番号 | アプリバージョン内で一意の番号 |
アプリバージョン | App Storeで公開されるアプリのバージョン |
上記概念については、以下記事が参考になりました。
ipaファイル作成(gym)
続いて、gym
でTestFlightへ配信するためのipaファイルを生成します。
gym
もscan
と同じく本来引数が必要ですが、Gymfile
を用いて引数設定を省略できます。
今回の例では以下Gymfileを作成しています。
workspace("<ワークススペース名>")
scheme("<スキーム名>")
clean(true)
configuration("Release")
output_directory("./.build") # store the ipa in this folder
output_name("TimeWatcher") # the name of the ipa file
include_bitcode(true)
export_method("app-store")
xcargs("-skipPackagePluginValidation")
export_options({
method: "app-store",
provisioningProfiles: {
"<メインターゲットのバンドルID>" => "<Provisioning Profile名>",
"<Widgetなどがある場合はそのバンドルIDも設定する>" => "<Provisioning Profile名>" # option
}
})
gym
の詳細は以下を参照ください。
TestFlight配信(upload_beta)
最後に、下記自前のlaneを呼び出してTestFlight配信を行います。
desc "Upload ipa for TestFlight"
lane :upload_beta do |options|
asc_api_key # App Store Connect API Keyの設定
upload_to_testflight(
ipa: "./.build/TimeWatcher.ipa", # gymで作成したipaのパス指定
skip_waiting_for_build_processing: "true", # TestFlight Upload後のビルドは待たないようにする
)
end
skip_waiting_for_build_processing
を有効にしている理由として、
これを設定しないとApp Store Connect側でUploadしたipaファイルをビルドするまで待ち続けてしまい、CIの起動時間を増やしてしまうためです。
起動時間の長さで課金されてしまう系統なCIだと悲しいことになるため、ここではskip_waiting_for_build_processing
を有効にしておきます。
5. GitHub Actionsのworkflow実装
name: CD_For_TestFlight # workflowの名前
on: # workflowのトリガー定義
push:
branches:
- "release/*" # releaseディレクトリのブランチにpushした際に起動
workflow_dispatch: # 手動で起動
env: # 環境変数の定義
DEVELOPER_DIR: /Applications/Xcode_16.app
jobs:
build:
runs-on: macos-latest # workflow実行時の環境指定(macosの最新版)
steps:
# チェックアウト(リポジトリからソースコードを取得)
- name: Check Out repository
uses: actions/checkout@v3
# Xcodeの一覧出力
- name: Show Xcode list
run: ls /Applications | grep 'Xcode'
# Xcodeのバージョン指定
- name: Select Xcode version
run: sudo xcode-select -s $DEVELOPER_DIR
# Xcodeのバージョン出力
- name: Show Xcode version
run: xcodebuild -version
# Rudy製ライブラリのキャッシュ
- name: Cache Gems
uses: actions/cache@v2
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-
# Rudy製ライブラリのインストール
- name: Install Bundled Gems
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
# SPMのライブラリのキャッシュ
- name: Cache Swift Packages
uses: actions/cache@v2
with:
path: SourcePackages
key: ${{ runner.os }}-spm-${{ hashFiles('*.xcodeproj/project.xcworkspace/ xcshareddata/swiftpm/Package.resolved') }}
restore-keys: ${{ runner.os }}-spm-
# TestFlight用にデプロイ
- name: deploy
run: set -o pipefail &&
bundle exec fastlane beta
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}
MATCH_KEYCHAIN_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GITHUB_TOKEN }}
ENVIRONMENT: "CI"
MATCH_FETCH_READ_ONLY_MODE: true
SETUP_CI_PROVIDER: "travis"
workflowで行うことは上記実装例のコメントで記載しているので、細かい説明は省略します。
上記ymlファイルはリポジトリのルートディレクトリから見て以下ディレクトリに配置することで、GitHubは自動でactionを認識してくれます。
./github/workflows/
6. 証明書用のGitHubのPrivateリポジトリを作成
ipaファイルをCI環境で作成するために、Provisioning Profileや証明書の情報をCI環境でも必要になります。
そこで、今回の例ではfastlaneのmatchというツールを使用しています。
ざっくりの仕組みとして、PrivateリポジトリにProvisioning Profileと証明書の情報を格納し、
CIはmatchを通じてPrivagteリポジトリにアクセスしてProvisioning Profileと証明書の情報を取得するというものです。
ここの手順では上記説明で登場した証明書の情報などを格納するPrivateリポジトリを作成します。
作成手順は以下をご参考にしてください。(からのリポジトリを作成するだけなので、知っている方は特に参考にする必要はありません。)
7. fastlaneのmatchで証明書をリポジトリにPush
fastlaneのmatchを初期化します
手順としては、下記記事の設定ステップまでを実施します。
設定ステップ
まずは共通アカウントとレポジトリを作りますmatchをチームで使うにあたって共通アカウントを作成
証明書管理用のプライベートGitHubレポジトリを作成
作成したら早速matchを使っていきます
matchの初期化が終えたら、作成されたMachfileに少し手を入れます。
git_url("<githubリポジトリのURL>")
storage_mode("git")
type("development")
git_branch("main") # 追加行
git_branch("main")
では証明書などをPushブランチを指定します。
matchがデフォルトでPushするブランチがmaster
ですが、Githubのメインブランチのデフォルトがmain
のため、その差分を追加した設定で埋めています。
続いてFastfileに以下を追記します。
このlaneはmatchでProvisioning Profileと関連する証明書を作成して、
手順6で作成したGitHubリポジトリにPushします。
desc "create appstore cert and profiles"
lane :match_force_appstore do
api_key = asc_api_key
match(
api_key: api_key,
type: "appstore",
force: true
)
end
上記追記後、プロジェクトのルートディレクトリで以下コマンドを実行します。
fastlane match_force_appstore
上記実行時にpasswordの設定を行いますが、設定したpasswordは忘れないようにしておきましょう。
6の手順で作成したリポジトリに、以下がPushされていればOKです。
- cert/distribution
- profiles/appstore
8. GitHubにシークレット変数を定義
プロジェクトのGitHubリポジトリのSetting > Secrets and variables > Actions
で
Repository secrets
に下記を登録します。
変数名 | 内容 | 備考 |
---|---|---|
ASC_ISSUER_ID | App Store Connect APIのissuerId | App Store Connectで確認 |
ASC_KEY_CONTENT | 使用するApp Store Connect APIの.p8ファイルの内容をBase64エンコードした文字列 | |
ASC_KEY_ID | 使用するApp Store Connect APIのキーID | App Store Connectで確認 |
FASTLANE_USER | AppleID | |
MATCH_GITHUB_TOKEN |
echo -n "[GitHubのユーザー名]:[手順6で作成したリポジトリのPAT]" | base64 で出力した内容 |
PATの作成方法 |
MATCH_KEYCHAIN_PASSWORD | 手順7で設定したmatchのパスワード | |
MATCH_PASSWORD | 手順7で設定したmatchのパスワード |
9. GitHub Actionsのworkflowを実行!
あとは作成したfastlaneのファイルやGitHub ActionsのworkflowファイルをGitHubリポジトリのmainブランチにPushして、GitHub Actionsを実行するだけです。
以下コマンドでmainブランチにPushしましょう。
git push orign main
Push後にリポジトリのActionsタブのページを行くと、下記画面の赤枠の部分あたりに手順5で作成したymlファイルのnameで指定した名前が表示されているかと思います。
実行したworkflowの名前をクリックすると以下のような画面が表示されるので、下記赤枠のボタンを押下するとworkflowが実行されます。
上手くいけばこれでTestFlightが自動で配信されるはずです。
以上、TestFlihgt配信自動化の備忘録でした。