[Xcode/Swift] GithubActionsを使ってFirebase App Distributionにアプリ配信をする

↓ Slackにも通知を送る ↓

下準備

アップロードに必要な前提環境、項目は以下の通り

(今回はPodでライブラリを管理している前提です、SPM (Swift Package Manager)の考慮はありません)

Apple Developerアカウント & Fastlane

今回はFastlane Matchを使って証明書管理を行うため、Apple Develoerに登録する必要があります。

証明書管理、作成の方法は↓の記事を参考に進めてください、今回はadhocを使用します。

Fastlane関連のファイルも↓の記事の通りに進めてセットアップをしてください。

[Xcode/Swift] Fastlane Matchで証明書管理リポジトリを作る

Firebase

当たり前な気がしますが、Firebaseと繋ぐのでFirebaseのセットアップをして、プロジェクトにGoogleService-Info.plistを入れておいてください。

Base64でエンコーディングするもの

今回Githubリポジトリに環境変数として色々なものを入れます、その中でエンコーディングして保存しないといけないものがあるので先に準備をしておきましょう。

GoogleService-Info.plist

対象のディレクトリを開いて、以下を実行。

cat GoogleService-Info.plist | base64 | pbcopy

base64でエンコーディング、とても長い文字列の出力になるのでpbcopyでその場でコピーしておく。

コピーした値は一旦メモとかに残しておきましょう。

Certificates.p12

fastlane matchで自動生成した証明書をKeychainで探す、末尾にApple DeveloperのTeamIDがついていると思われるのでそれを辿れば正しいものが見つかります。

対象の証明書を選択して、Export (証明書名)を選択 → Desktopとかに一旦保存する。

こちらも対象のディレクトリを開いて、以下を実行。

cat Certificates.p12 | base64 | pbcopy

証明書名は特に指定しない限りデフォルトではCertificates.p12で保存されるはず(多分)。

こちらも一旦どこかにメモしておきましょう。

これで下準備は(ある程度)完了です。

自動実行環境を構築

workflows

対象のXcodeプロジェクトのルートディレクトリ直下に以下のファイルを作成

.github/workflows/distribution.yml

今回はdevelopブランチにPushされたときにトリガーされるようにします、この辺りのトリガータイミングは色々設定できるので調べてみてください (説明放棄)

name: Distribute IPA to Firebase App Distribution

on:
  push:
    branches:
      - develop  # developブランチへのpush時にジョブをトリガー

jobs:
  build:
    runs-on: macos-latest

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2  # リポジトリのソースコードをチェックアウト

    - name: Set up Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: '3.2'  # FastlaneとCocoaPodsに必要なRuby 3.2を使用

    - name: Cache CocoaPods
      uses: actions/cache@v2
      with:
        path: Pods  # インストールされたCocoaPods依存関係をキャッシュ
        key: ${{ runner.os }}-pods-${{ hashFiles('Podfile.lock') }}  # Podfile.lockに基づいたキャッシュキー
        restore-keys: |
          ${{ runner.os }}-pods-  # メインキーが見つからない場合のフォールバックキー

    - name: Install CocoaPods
      run: |
        gem install bundler
        bundle install
        pod install

    - name: Decrypt GoogleService-Info.plist
      run: |
        mkdir -p <#GoogleService-Info.plistを格納するパス#>
        echo "$GOOGLE_SERVICE_INFO_PLIST_BASE64" | base64 --decode > <#GoogleService-Info.plistへのパス#>
      env:
        GOOGLE_SERVICE_INFO_PLIST_BASE64: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST_BASE64 }}  # FirebaseのGoogleService-Info.plistをSecretsから復号

    - name: Decrypt and Import .p12 Certificate
      run: |
        echo "$P12_BASE64" | base64 --decode > certificate.p12
        security create-keychain -p "" build.keychain
        security list-keychains -s build.keychain
        security unlock-keychain -p "" build.keychain
        security import ./certificate.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
        security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" build.keychain
      env:
        P12_BASE64: ${{ secrets.P12_BASE64 }}  # Base64でエンコードされた証明書
        P12_PASSWORD: ${{ secrets.MATCH_PASSWORD }}  # Secretsからの証明書パスワード

    - name: Set up Fastlane
      run: bundle exec fastlane match adhoc --readonly  # Fastlane MatchでAdHocプロファイルを取得 (読み取り専用モード)
      env:
        MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}  # SecretsからのMatchパスワード

    - name: Enable Parallel Code Signing
      run: |
        defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks 4  # 並列コード署名を有効化してビルド速度を向上

    - name: Build and distribute to Firebase App Distribution
      run: bundle exec fastlane distribute  # Fastlaneでアプリをビルドし、Firebase App Distributionにアップロード
      env:
        FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }}
        FIREBASE_APP_DISTRIBUTION_API_TOKEN: ${{ secrets.FIREBASE_APP_DISTRIBUTION_API_TOKEN }}  # Firebase App Distribution APIトークン
        MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}  # SecretsからのMatchパスワード
        FASTLANE_USER: ${{ secrets.FASTLANE_USER }}  # Fastlaneユーザー (Apple IDなど)
        FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}  # Fastlaneパスワード

  notify:
    runs-on: ubuntu-latest  # Slack通知はUbuntuランナーを使用
    needs: build  # buildジョブの成功/失敗に依存
    if: always()
    steps:
    - name: Slack Notification on Success
      if: ${{ needs.build.result == 'success' }}
      uses: rtCamp/action-slack-notify@v2
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}  # Secretsに保存されたWebhook URL
        SLACK_TITLE: "Deploy / Success"
        SLACK_COLOR: good
        SLACK_MESSAGE: "ビルドが正常にFirebase App Distributionに配布されました :rocket:"

    - name: Slack Notification on Failure
      if: ${{ needs.build.result == 'failure' }}
      uses: rtCamp/action-slack-notify@v2
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}  # Secretsに保存されたWebhook URL
        SLACK_TITLE: "Deploy / Failure"
        SLACK_COLOR: danger
        SLACK_MESSAGE: "ビルドのFirebase App Distributionへの配布に失敗しました :sob:"

Fastlane関連ファイル

default_platform(:ios)

# iOSプラットフォームの設定
platform :ios do
  # 'Upload to Firebase'という処理の説明
  desc "Upload to Firebase"

  # distributeという名前のlane(処理のまとまり)を定義
  lane :distribute do

    # 'match'を使って証明書やプロビジョニングプロファイルを取得する
    # type: "adhoc" はAdHoc配布用の証明書・プロファイルを使う設定
    # readonly: true は読み取り専用で既存の証明書・プロファイルを使う設定
    match(
      type: "adhoc", 
      readonly: true
    )

    # ビルド番号を自動的に1つ増やす
    increment_build_number

    # Info.plistからバージョン番号を取得
    # CFBundleShortVersionStringキーの値(バージョン番号)を取得
    version_number = get_info_plist_value(path: "<#Info.plistのパス#>", key: "CFBundleShortVersionString")
    
    # 現在のビルド番号を取得
    build_number = get_build_number

    # gymを使ってアプリをビルド
    # schemeはXcodeプロジェクト内のスキームを指定
    # verbose: true は詳細なログを表示する設定
    gym(
      scheme: "<#スキーム名#>",
      verbose: true
    )

    # Firebase App Distributionにビルドをアップロード
    # appはFirebaseアプリIDを指定(環境変数FIREBASE_APP_IDから取得)
    # release_notesでリリースノートを指定
    # firebase_cli_tokenはFirebase CLI用のトークン(環境変数から取得)
    firebase_app_distribution(
      app: ENV["FIREBASE_APP_ID"],  # 環境変数FIREBASE_APP_IDからアプリIDを取得
      release_notes: "Beta Release: #{version_number} (Build: #{build_number})",  # リリースノートにバージョンとビルド番号を含める
      firebase_cli_token: ENV["FIREBASE_APP_DISTRIBUTION_API_TOKEN"]  # 環境変数からFirebase CLIのAPIトークンを取得
    )
  end
end
# Matchfile

# 使用するストレージモードの設定
storage_mode("git")

# 証明書のタイプを指定
type("adhoc") # 証明書のタイプ: appstore, adhoc, enterprise, development から選択

# 証明書管理用のGitリポジトリURLを設定
git_url("<#URL#>") 

# 使用するブランチを指定
git_branch("master") 

# Apple Developer アカウントに対応するチームID (Developerコンソールで確認可能)
team_id("<#TEAMID#>") 

# このMatch設定に対するアプリのバンドルIDを指定
app_identifier(["<#アプリのBundle Identifier#>"]) 

# Gitリポジトリにコミットする際のユーザー情報
git_full_name("<#Githubユーザ名#>")
git_user_email("<#メールアドレス#>")

# 新しいデバイス用に証明書を強制的に生成するオプション(必要に応じて)
force_for_new_devices(true)

環境変数設定

少し多いですが一個ずつ入力漏れがないように埋めていきましょう。

(Slack関連はSlackAPIも絡んでくるのでめんどくさい or そもそもSlack使ってないなら環境変数の設定は不要 & ↑で設定したSlack関連スクリプトは削除で大丈夫です。)

  • FASTLANE_PASSWORD
    • Apple Developerにログインする際のPWと思いきやApp Specific Passwordなるものらしい、詳細は↓の黄色セクションを確認してください
  • FASTLANE_USER
    • Apple Developerにログインする際のID
  • FIREBASE_APP_DISTRIBUTION_API_TOKEN
    • FirebaseをCLIで操作するために発行するTOKEN
  • FIREBASE_APP_ID
    • 名前の通り、Firebaseの設定項目で確認できます。
  • GOOGLE_SERVICE_INFO_PLIST_BASE64
    • 序盤でエンコードした値をここに保存
  • KEYCHAIN_PASSWORD
    • 多分Macにログインする時のPW
  • MATCH_PASSWORD
    • Matchセットアップしたときに設定したPW
  • P12_BASE64
    • 序盤でエンコードした証明書の値
  • SLACK_WEBHOOK_URL
    • Slack APIコンソールから確認可能、めんどうなら省略

こんな感じになればOK

FASTLANE_PASSWORDについて

通常にログインするPWだと2FA (2要素認証)関係でActions内から処理が進まない可能性があるそう、なのでAppleコンソールにログインしてApp Specific Passwordなるものを生成するのが良いらしいです。

Fastlaneという名前で作成、PWは自動で生成されるのでそれをメモして環境変数に入れましょう。

(Firebase TOKENの生成方法は以下の記事を参考に)

[Xcode/Swift] FastlaneでXcodeからFirebase App Distributionアプリを配信する

実行する

ymlで設定したように、developブランチに新規PushをするとActionsの処理が開始されます。

問題なく完了すれば青色、何かエラーがあって落ちると赤になります。

今回はPushでのトリガーですが、Pull Requestでdistribute betaみたいなコメントがされたとき、マージされたときのように色々な条件で実行できるので自分のプロジェクトに合わせて上手に活用してください。

まとめ

自動化は便利、ただ環境構築がめんどくさい。(周りに聞ける環境がなければ尚更)