Watchアプリを含むiOSアプリをApp Store ConnectへデプロイするGithub Actions

# 概要
業務内でWatchアプリ込みのiOSアプリについてGithubActionsを使用し、
App Store ConnectにアップロードするようなActionを作成したので備忘録として

※今回はxcode build + Xcode自動署名方式を使用しています

## 構成
アプリ：iOS + WatchOS + Widget
xcodeproj形式のプロジェクトファイル
Swift-6
XCode-16

## 完成したコード

```yml
name: Deploy To App Store Connect

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: macos-15
    steps:
      - name: Checkout source code
        uses: actions/checkout@v4

      - name: Set up Xcode
        uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: "16.1.0"

      - name: Archive Scheme
        run: |
          xcodebuild -project ProjectName.xcodeproj \
                     -scheme SchemeName \
                     -configuration Release \
                     archive -archivePath SchemeName.xcarchive \
                     CODE_SIGNING_ALLOWED=NO

      - name: Create ExportOptions.plist
        run: |
          echo '${{ secrets.EXPORT_OPTIONS }}' > ExportOptions.plist

      - name: Save App Store Connect Private Key to File
        run: |
          mkdir private_keys
          echo "${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}" > ./private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8

      - name: Export .ipa
        run: |
          xcodebuild -exportArchive \
                     -archivePath SchemeName.xcarchive \
                     -exportOptionsPlist ExportOptions.plist \
                     -exportPath export \
                     -allowProvisioningUpdates \
                     -authenticationKeyPath $PWD/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8 \
                     -authenticationKeyID ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} \
                     -authenticationKeyIssuerID ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}

      - name: Upload ipa to TestFlight
        run: |
          xcrun altool --upload-app -f export/SchemeName.ipa \
                       --type ios \
                       --apiKey ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} \
                       --apiIssuer ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} \
                       --apiPrivateKey $pwd/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8

      - name: Cleanup
        run: rm ExportOptions.plist `pwd`/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8

```

### アプリ固有の環境値

下記については各々のProjectに合わせて変更してください

ProjectName.xcodeproj：プロジェクト名
SchemeName：スキーム名

## 各種設定

### Github
Secrets And Variablesに必要な値を取得します

#### App Store Connect API Keyの作成

API Keyを発行してください

※App Store Connect -> ユーザーとアクセス -> 統合 -> チームキーの作成

発行したら、下記三点をコピーしておきましょう

・Issue ID
・KeyID
・API Key ID

#### Export Options

Xcodeからipaファイルをexportするとexport先のフォルダにExportOptions.plistが作成されるのでそちらを使用します

Xcode -> Archive -> Distribute App -> Custom -> Export 

![スクリーンショット 2024-12-17 15.10.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3961356/368ef086-8531-7c31-7756-37d98c19b798.png)

![スクリーンショット 2024-12-17 15.11.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3961356/0dc75bac-94c6-10e8-a6eb-404f4b9c5b4a.png)


※ TestFlightのみアップするように設定もできる

stgブランチ -> Test Flight
mainブランチ -> AppStoreConnect 

のようにブランチごとに分けると良いかも

![スクリーンショット 2024-12-17 15.15.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3961356/d7b35706-e7f5-cc52-5106-84f7b602e009.png)

#### Github Secrets And Variables設定

上記で取得した値を
Github Secrets And Variables -> Repository secretsに設定

![スクリーンショット 2024-12-18 12.53.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3961356/0a21f5f9-8525-124e-10c0-63952a39d1db.png)

```
APP_STORE_CONNECT_PRIVATE_KEY: App Store Connectにて取得したPrivate Key 
↑ VSCodeなどで証明書を開いてコピペ
APP_STORE_CONNECT_API_KEY_ID: App Store Connectにて取得したAPI Key
APP_STORE_CONNECT_ISSUER_ID:  App Store Connectにて取得したIssuer ID
EXPORT_OPTIONS: XcodeでExportしたExportOption.plistの値
↑ VSCodeなどでplistを開いてコピペ
```

ここまで設定すれば動くようになります
あとは指定したブランチへマージし、Giahub Actionが動くことを確認してください

## 解説

### 仮想環境のセットアップ

仮想環境のセットアップしxcodeをインストール

```yml
jobs:
  deploy:
    runs-on: macos-15
    steps:
      - name: Checkout source code
        uses: actions/checkout@v4

      - name: Set up Xcode
        uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: "16.1.0"
```

### アーカイブの作成

*configuration*
Archive時のBuild Configration

*CODE_SIGNING_ALLOWED=NO*
アーカイブ時は署名が不要なのでoptionをNOで設定

```yml
      - name: Archive Scheme
        run: |
          xcodebuild -project ProjectName.xcodeproj \
                     -scheme SchemeName \
                     -configuration Release \
                     archive -archivePath SchemeName.xcarchive \
                     CODE_SIGNING_ALLOWED=NO
```

### 各種設定ファイルの作成

ExportOptions.plistとApp Storeアップロード用の証明書を作成します

※ 後述しますが、p8形式の秘密鍵を書き出す際に、ファイル名はApp Store Connectで取得した名称をそのまま使用してください。

```yml
      - name: Create ExportOptions.plist
        run: |
          echo '${{ secrets.EXPORT_OPTIONS }}' > ExportOptions.plist

      - name: Save App Store Connect Private Key to File
        run: |
          mkdir private_keys
          echo "${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}" > ./private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8

```

### ipaファイルの書き出し

App Store ConnectのAPIKey情報をもとに署名を行い、ipaファイルを作成します

```yml
      - name: Export .ipa
        run: |
          xcodebuild -exportArchive \
                     -archivePath SchemeName.xcarchive \
                     -exportOptionsPlist ExportOptions.plist \
                     -exportPath export \
                     -allowProvisioningUpdates \
                     -authenticationKeyPath $PWD/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8 \
                     -authenticationKeyID ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} \
                     -authenticationKeyIssuerID ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}

```

### ipaファイルのアップロード

*--type ios*
アップロードする際にはtypeをiOSを指定 

App Store API Keyを使用して、App Store Connectへアップロードします

```yml
 - name: Upload ipa to TestFlight
        run: |
          xcrun altool --upload-app -f export/SchemeName.ipa \
                       --type ios \
                       --apiKey ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} \
                       --apiIssuer ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} \
                       --apiPrivateKey $pwd/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8
```

### 事後処理

不要ファイルを削除します

```yml
      - name: Cleanup
        run: rm ExportOptions.plist `pwd`/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8
```

## 詰まったところ

### MacOS 仮想環境のバージョンについて

mac-osの仮想環境ではXCodeが１バージョンしか指定できなくなっていた。

(作成した当時はmac-osのlatestを選択するとsonoma(MacOS14 - XCode15)が指定され、Xcode16が見つからんぞ〜って怒られる。
※2024年11月中旬

https://github.com/actions/runner-images/issues/10703

#### ログ

```log
Switching Xcode to version '16.1.0'...
Available versions:
┌─────────┬──────────┬─────────────┬─────────────┬────────┬──────────────────────────────────┐
│ (index) │ version  │ buildNumber │ releaseType │ stable │ path                             │
├─────────┼──────────┼─────────────┼─────────────┼────────┼──────────────────────────────────┤
│ 0       │ '15.4.0' │ '15F31d'    │ 'GM'        │ true   │ '/Applications/Xcode_15.4.app'   │
│ 1       │ '15.3.0' │ '15E204a'   │ 'GM'        │ true   │ '/Applications/Xcode_15.3.app'   │
│ 2       │ '15.2.0' │ '15C500b'   │ 'GM'        │ true   │ '/Applications/Xcode_15.2.app'   │
│ 3       │ '15.1.0' │ '15C65'     │ 'GM'        │ true   │ '/Applications/Xcode_15.1.app'   │
│ 4       │ '15.0.1' │ '15A507'    │ 'GM'        │ true   │ '/Applications/Xcode_15.0.1.app' │
└─────────┴──────────┴─────────────┴─────────────┴────────┴──────────────────────────────────┘
Error: Could not find Xcode version that satisfied version spec: '16.1.0'
```

そのため、macos-15(sequoia)とXcodeのバージョンを直接指定

```yml
jobs:
  deploy:
    # MacOS 15(sequoia)を指定
    runs-on: macos-15
    steps:
      - name: Checkout source code
        uses: actions/checkout@v4

      - name: Set up Xcode
        uses: maxim-lobanov/setup-xcode@v1
        with:
            # Xcodeのバージョンを直接指定
            xcode-version: "16.1.0"
```

### Archive commandについて

WatchOSを含む場合は　-sdk optionを設定したらビルドでこける

(...Watch系のPackageを取得後にこけていた。いまいち理解できなくて時間がたくさん溶けました笑

```log
note: Disabling previews because SWIFT_VERSION is set and SWIFT_OPTIMIZATION_LEVEL=-Owholemodule, expected -Onone (in target 'SwiftUI Apple Watch Decimal Pad' from project 'SwiftUI Apple Watch Decimal Pad')
note: Disabling previews because SWIFT_VERSION is set and SWIFT_OPTIMIZATION_LEVEL=-Owholemodule, expected -Onone (in target 'Supabase' from project 'Supabase')
** ARCHIVE FAILED **
```

古い記事ですが、ここを参照

https://stackoverflow.com/questions/31478959/how-to-use-xcodebuild-in-xcode-7-with-a-watch-extension

コメントの直後の行を削除すると動くようになりました。

```zsh
      - name: Archive Scheme
        run: |
          xcodebuild -project ProjectName.xcodeproj \
                     -scheme SchemeName \
                     -configuration Release \
                     # ↓ sdkを指定するとbuildエラーになる
                     -sdk iphonesimulator
                     archive -archivePath SchemeName.xcarchive \
                     CODE_SIGNING_ALLOWED=NO
```

### 証明書の名前について

証明書の命名には規約があるようです。
（自分はapp_store_connect_key.p8のような命名にして8時間ぐらい時間を溶かしました...）


```log
"Error Domain=ITunesConnectionAuthenticationErrorDomain Code=-26000 \"Failed to generate JWT token: Error Domain=NSCocoaErrorDomain Code=-43 \"Failed to load AuthKey file.\" UserInfo={NSLocalizedDescription=Failed to load AuthKey file., NSLocalizedFailureReason=The file \U2018AuthKey_***.p8\U2019 could not be found in any of these locations: '~/path/to/root/private_keys', '~/private_keys', '~/.private_keys', '~/.appstoreconnect/private_keys'.}\" UserInfo={NSLocalizedRecoverySuggestion=Failed to generate JWT token: Error Domain=NSCocoaErrorDomain Code=-43 \"Failed to load AuthKey file.\" UserInfo={NSLocalizedDescription=Failed to load AuthKey file., NSLocalizedFailureReason=The file \U2018AuthKey_***.p8\U2019 could not be found in any of these locations: '~/path/to/root/private_keys', '~/private_keys', '~/.private_keys', '~/.appstoreconnect/private_keys'.}, NSLocalizedDescription=Failed to generate JWT token: Error Domain=NSCocoaErrorDomain Code=-43 \"Failed to load AuthKey file.\" UserInfo={NSLocalizedDescription=Failed to load AuthKey file., NSLocalizedFailureReason=The file \U2018AuthKey_***.p8\U2019 could not be found in any of these locations: '~/path/to/root/private_keys', '~/private_keys', '~/.private_keys', '~/.appstoreconnect/private_keys'.}, NSLocalizedFailureReason=App Store operation failed.}"
```

AuthKey_{API_KEY_ID}.p8という形式で指定しましょう

https://discuss.bitrise.io/t/deploy-to-itunesconnect-application-loader-step-fails-when-using-api-key

```yml
      - name: Save App Store Connect Private Key to File
        run: |
          mkdir private_keys
          echo "${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}" > ./private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}.p8
```

と、まぁ色々引っかかりましたがなんとかデプロイするGithub Actionが完成しました。

iOSのみのアプリを書き出す際のxcodebuildコマンドは割とネットに転がっていましたが、AppleWatchアプリを含む場合のxcodebuildコマンドはなかなか見つからなかったので苦労しました〜