2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

fastlaneでのTestFlight自動配信備忘

Last updated at Posted at 2024-09-25

概要

fastlaneを用いてTestFlight配信の自動化を目指します。
以下はその手順の美貌になります。

環境

  • Xcode: ver16
  • CI: GitHub Actions
  • Tool: fastlane
  • パッケージ管理: SPM

前提

  • Xcodeがインストールされていること
  • TestFlight配信したいアプリがGitHubリポジトリにPushされていること
  • fastlaneがインストールされていること
  • Apple Developer Programに登録していること

ざっくり手順

  1. Distribution用のProvisioning Profileの用意
  2. App Store Connect APIキーの作成
  3. fastlaneの初期化
  4. Fastfileの実装
  5. GitHub Actionsのworkflow実装
  6. 証明書用のGitHubのPrivateリポジトリを作成
  7. fastlaneのmatchで証明書をリポジトリにPush
  8. GitHubにシークレット変数を定義
  9. GitHub Actionsのworkflowを実行!

1. Provisioning Profileの用意

下記記事を参考にAppStore用の証明書を作成し、ローカルでipaファイルを作成できるようにします。

2. App Store Connect APIキーの作成

下記ドキュメントの手順よりAPIキーを作成します。

下記画面でAPIキーファイル(.p8ファイル)のダウンロードと赤枠のIssuerID、キーIDを控えておきます。
スクリーンショット 2024-09-24 12.56.08.png

※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_idissuer_idkey_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ファイルを生成します。
gymscanと同じく本来引数が必要ですが、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で指定した名前が表示されているかと思います。

image.png

実行したworkflowの名前をクリックすると以下のような画面が表示されるので、下記赤枠のボタンを押下するとworkflowが実行されます。
上手くいけばこれでTestFlightが自動で配信されるはずです。
image.png

以上、TestFlihgt配信自動化の備忘録でした。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?