[Xcode/Swift] GithubActionsを使ってAppStoreConnectへのアプリデプロイ作業を自動化する

今回はvがついたタグがPushされた時にトリガーされるように実装します。

下準備

証明書管理

デプロイに際して必要な証明書、Provisioning Profile関連の管理はFastlane Matchを使って1個のリポジトリで管理します、以下の記事を読んでセットアップを完了させてください。

今回はAppStoreタイプを使用します

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

環境変数系

今回使用するのはこちら

  • FASTLANE_USER
    • Apple Developerにログインする時のID
  • FASTLANE_PASSWORD
    • Apple Developerにログインする時のPW (App Specific Passwordなるものが必要、詳細は以下の関連記事に)
  • GOOGLE_SERVICE_INFO_PLIST_BASE64
    • GoogleService-Info.plistをエンコーディングしたもの、詳細は以下の関連記事に
  • P12_BASE64
    • 証明書をエンコーディングしたもの、詳細は以下の関連記事に
  • KEYCHAIN_PASSWORD
    • Macにログインする時のPW、いらないかも? (この辺よくわかってない)
  • APP_STORE_CONNECT_API_KEY
    • Apple Developerに外部から接続するためのKEY、JSON形式で保存します
  • MATCH_PASSWORD
    • Matchのセットアップ時に設定したPW
  • SLACK_WEBHOOK_URL
    • Slackに通知を送るためのURL、めんどくさい or Slack使ってない等であれば無視でOK

GOOGLE_SERVICE_INFO_PLIST_BASE64P12_BASE64FASTLANE_PASSWORDのセットアップ詳細はこちらの記事を参考に進めてください。

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

APP_STORE_CONNECT_API_KEYのセットアップ

Apple DeveloperからAPI Keyを発行する必要があります。

User and Access → Integration → Team Keysで新規にAPIKeyを発行、一旦どこかにメモしておく。

以下の形式で環境変数に保存する。

{
    "key_id": "<#KeyID#>",
    "issuer_id": "<#Issuer ID#>",
    "key": "-----BEGIN PRIVATE KEY-----\n<#APIKey#>\n-----END PRIVATE KEY-----"
}

keyは1行で扱うため、\nで改行を入れる必要があります。

下準備はこれで完了です。

自動デプロイフロー実装

以下のディレクトリにupload_appstore.ymlを作成

.github/workflows/upload_appstore.yml

upload_appstore.yml

name: Upload to App Store  # App Storeにアップロード

on:
  push:
    tags:
      - 'v*' # vのついたタグがPushされた時にトリガー

jobs:
  upload-to-appstore:
    runs-on: macos-latest  # 最新のmacOSで実行

    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  # CocoaPodsの依存関係をインストール

    - 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 appstore --readonly  # Fastlane Matchを使用してAppStoreプロファイルを取得
      env:
        MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}  # SecretsからのMatchパスワード

    - name: Build and upload to App Store
      run: bundle exec fastlane upload_appstore  # Fastlaneでビルドし、App Storeにアップロード
      env:
        FASTLANE_USER: ${{ secrets.FASTLANE_USER }}  # Fastlaneユーザー(Apple IDなど)
        FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}  # Fastlaneパスワード
        APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}  # SecretsからのApp Store Connect APIキー
        MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}  # SecretsからのMatchパスワード

  notify:
    runs-on: ubuntu-latest
    needs: upload-to-appstore
    if: always()
    steps:
    - name: Slack Notification on Success
      if: ${{ needs.upload-to-appstore.result == 'success' }}
      uses: rtCamp/action-slack-notify@v2
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
        SLACK_TITLE: "Deploy / Success"
        SLACK_COLOR: good
        SLACK_MESSAGE: "Successfully uploaded build to AppStoreConnect :rocket:"

    - name: Slack Notification on Failure
      if: ${{ needs.upload-to-appstore.result == 'failure' }}
      uses: rtCamp/action-slack-notify@v2
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
        SLACK_TITLE: "Deploy / Failure"
        SLACK_COLOR: danger
        SLACK_MESSAGE: "Failed to upload build to AppStoreConnect :sob:"

Fastfile

default_platform(:ios)

platform :ios do
  desc "Build and upload to the App Store"
  lane :upload_appstore do
    match(
      type: "appstore", 
      readonly: true
    )

    increment_build_number(
      build_number: latest_testflight_build_number(app_identifier: "<#App Identifier#>") + 1
    )

    gym(
      scheme: "<#Scheme名#>",
      export_method: "app-store",
      configuration: "Release"
    )
    
    upload_to_app_store(
      skip_metadata: true,
      skip_screenshots: true,
      automatic_release: false,
      precheck_include_in_app_purchases: false
    )
  end
end

upload_to_app_storeのパラメータ一覧は以下を参考に、必要に応じて自分好みに設定してください。

upload_to_app_storeの主要なパラメータとその説明

  1. skip_metadata
    • 説明: アプリのメタデータ(アプリ名、説明、カテゴリなど)をアップロードせずにスキップするかを指定します。
    • デフォルト: false(メタデータをアップロード)
    • 使用例: skip_metadata: true(メタデータのアップロードをスキップ)
  2. skip_screenshots
    • 説明: アプリのスクリーンショットをアップロードせずにスキップするかを指定します。
    • デフォルト: false(スクリーンショットをアップロード)
    • 使用例: skip_screenshots: true(スクリーンショットのアップロードをスキップ)
  3. automatic_release
    • 説明: アップロード後、App Storeで自動的にリリースするかどうかを指定します。
    • デフォルト: false(自動リリースしない)
    • 使用例: automatic_release: false(手動でリリースを管理)
  4. precheck_include_in_app_purchases
    • 説明: アプリ内課金の確認を含むかどうかを指定します。precheckは、一般的なApp Store提出の問題を事前にチェックする機能です。
    • デフォルト: true(アプリ内課金の確認を含む)
    • 使用例: precheck_include_in_app_purchases: false(アプリ内課金の確認をスキップ)
  5. force
    • 説明: アップロード時にすでに同じバージョンが存在しても強制的に上書きするかどうかを指定します。
    • デフォルト: false(強制しない)
    • 使用例: force: true(強制的に上書き)
  6. submit_for_review
    • 説明: ビルドをApp Store審査に自動的に提出するかどうかを指定します。
    • デフォルト: false(自動提出しない)
    • 使用例: submit_for_review: true(審査に提出)
  7. release_on_approval
    • 説明: 審査が承認された後、App Storeで自動的にリリースするかどうかを指定します。
    • デフォルト: false(自動リリースしない)
    • 使用例: release_on_approval: true(承認後に自動リリース)
  8. build_number
    • 説明: 明示的にアップロードするビルド番号を指定します。
    • デフォルト: 指定なし(最新のビルドを使用)
    • 使用例: build_number: "42"(ビルド番号42をアップロード)
  9. overwrite_screenshots
    • 説明: 既存のスクリーンショットを上書きするかどうかを指定します。
    • デフォルト: false(上書きしない)
    • 使用例: overwrite_screenshots: true(既存のスクリーンショットを上書き)
  10. app_identifier
    • 説明: アプリのバンドルIDを明示的に指定します。これにより、正しいアプリケーションがターゲットになります。
    • デフォルト: 指定なし(Fastfileで定義されたものを使用)
    • 使用例: app_identifier: "com.example.app"

これでvを含むタグをPushした時にActionsが走ってアップロードされます。

まとめ

自動化できるものは自動化するのが吉、ヒューマンエラーを防ぐために。