【iOS】fastlane (or xcodebuild) で `TestFlight Internal Only` を使う

## はじめに

Xcode Cloud の登場など、 1st party の CD 環境も提供されている今、 fastlane への依存は少しずつ減っているかもしれません。  
とはいえ、開発現場ではまだまだ活用されているツールかと思うので、今回は `TestFlight Internal Only` のビルドを fastlane を利用して作成する方法をご紹介します。

## そもそも `TestFlight Internal Only` とは？

WWDC 2023 にて発表された TestFlight の新しい機能です。[^1] 
従来、TestFlight で配布するためのビルドは、内部テスターだけではなく、外部テスターへの配布や App Store への配信が可能なものを利用するしかありませんでした。  
Xcode 15 以降、 `TestFlight Internal Only` の設定を行なってビルドを作成することによって、そのビルドは「内部テスター」への配布に限定できるようになります。  
「社内関係者のみに配布したいケース」などにおいて、意図せず App Store への配信を行なってしまうリスクを軽減することができます。  
前述の通りですが、 `TestFlight Internal Only` の設定でビルドができるのは Xcode 15 以降になります。

## 結論

[fastlane の GitHub Discussion のコメント](https://github.com/fastlane/fastlane/discussions/21525#discussioncomment-7329340)の通り、 `fastlane gym` のオプションに設定を追加して TestFlight に上げることで内部用の TestFlight ビルドになります。

```ruby
lane :internal_only_beta do
  gym(
    # ...
    export_options: {
      # ⭐️ これを追加
      testFlightInternalTestingOnly: true
    }
  )
  pilot(
    # ...
  )
end
```

これで配布された TestFlight のビルドは「内部」という表示がされ、内部テスターのみの配信に限定されます。

![ScreenShot_2023-12-03_14_51_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11914/5b266854-f947-042f-8563-8e02fe97be41.png)


fastlane に詳しい方や単純に利用できれば良い方は、ここまで見ていただければ大丈夫かと思います :ok_hand: 
もう少し詳細を知りたい方はもう少々お付き合いください。

## fastlane gym の `export_options` は何者？

fastlane gym は（強引な言い方をすると） xcodebuild のラッパー[^2] のようなものです。
gym の `export_options` オプションに渡す Hash の key/value は、 xcodebuild コマンドの `-exportOptionsPlist` オプションで指定する plist ファイルに設定可能な key/value を設定することができます。
gym では、 `export_options` で設定した key/value から 一時的な plist ファイルを生成し、その plist ファイルのパスを xcodebuild コマンドの `-exportOptionsPlist` に渡すようになっています。

## xcodebuild コマンドを確認してみる

ということで、 xcodebuild コマンドのヘルプを見て `-exportOptionsPlist` オプションについて調べてみます。 (必要部分のみ抜粋)

```sh
xcodebuild -help
```

```sh
Usage: xcodebuild [-project <projectname>] [[-target <targetname>]...|-alltargets] [-configuration <configurationname>] [-arch <architecture>]... [-sdk [<sdkname>|<sdkpath>]] [-showBuildSettings [-json]] [<buildsetting>=<value>]... [<buildaction>]...
       xcodebuild [-project <projectname>] -scheme <schemeName> [-destination <destinationspecifier>]... [-configuration <configurationname>] [-arch <architecture>]... [-sdk [<sdkname>|<sdkpath>]] [-showBuildSettings [-json]] [-showdestinations] [<buildsetting>=<value>]... [<buildaction>]...
       xcodebuild -workspace <workspacename> -scheme <schemeName> [-destination <destinationspecifier>]... [-configuration <configurationname>] [-arch <architecture>]... [-sdk [<sdkname>|<sdkpath>]] [-showBuildSettings] [-showdestinations] [<buildsetting>=<value>]... [<buildaction>]...
       xcodebuild -version [-sdk [<sdkfullpath>|<sdkname>] [-json] [<infoitem>] ]
       xcodebuild -list [[-project <projectname>]|[-workspace <workspacename>]] [-json]
       xcodebuild -showsdks [-json]
       xcodebuild -exportArchive -archivePath <xcarchivepath> [-exportPath <destinationpath>] -exportOptionsPlist <plistpath>
       xcodebuild -exportNotarizedApp -archivePath <xcarchivepath> -exportPath <destinationpath>
       xcodebuild -exportLocalizations -localizationPath <path> -project <projectname> [-defaultLanguage <language>] [-exportLanguage <targetlanguage>... [-includeScreenshots]]
       xcodebuild -importLocalizations -localizationPath <path> -project <projectname> [-mergeImport]
       xcodebuild -resolvePackageDependencies [-project <projectname>|-workspace <workspacename>] -clonedSourcePackagesDirPath <path>
       xcodebuild -create-xcframework [-help] [-framework <path>] [-library <path> [-headers <path>]] -output <path>

Options:
    # ~ 略 ~
    -exportOptionsPlist PATH                                 specifies a path to a plist file that configures archive exporting
    # ~ 略 ~

Available keys for -exportOptionsPlist:

    # ~ 略 ~
	testFlightInternalTestingOnly : Bool

		When enabled, this build cannot be distributed via external TestFlight or the App Store. This is recommended for pull requests, development branches, and other builds that are not suitable for external distribution.

	# ~ 略 ~
```

`-exportOptionsPlist` オプションに利用可能なキー一覧も出力されますが、ここに `testFlightInternalTestingOnly` というものがあることが分かります。

説明にも、このフラグを立てることで外部テスターや App Store への配布ができないビルドになることが書かれています。

> When enabled, this build cannot be distributed via external TestFlight or the App Store.

尚、筆者環境では Xcode 15.0.1 の xcodebuild を利用していますが、 Xcode 15 以降であれば利用可能なはずです。

```sh
xcodebuild -version                                                                                                                     [14:41:17]

Xcode 15.0.1
Build version 15A507
```

## xcodebuild コマンドから `TestFlight Internal Only` を使う

ここまで分かれば、 fastlane を使用しない場合でも、 xcodebuild コマンドでのアーカイブでも `TestFlight Internal Only` の設定ができることが分かってきます。

要するに、 `-exportOptionsPlist` に渡す plist ファイルに `testFlightInternalTestingOnly` キーを Boolean で YES に設定して xcodebuild コマンドから Archive を行えば良いのです。

![ScreenShot 2023-12-03 14.47.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11914/80686428-22b5-5fad-5094-96ff3a629ee6.png)

Archive できれば、 altool などで生成したビルドをアップロードすれば内部テスター限定のビルドとして TestFlight で利用できそうですね :ok_hand: （そこまでは試していないですが）

## おわりに

ということで、 fastlane や xcodebuild から `TestFlight Internal Only` を使う方法の紹介でした。
私自身、「fastlane が `TestFlight Internal Only` に対応しないかな〜」と思っていたところ、冒頭にも記載の [fastlane の GitHub Discussion の回答](https://github.com/fastlane/fastlane/discussions/21525#discussioncomment-7329340) を見つけ、内部的にどのように作用しているのかを調べてみたというのがこの記事の経緯になります。
ニッチな内容なだけに、あまり知らない方もいるかもなと思い記事にしてみました。
誰かのお役に立てれば幸いです。

## 参照

- https://docs.fastlane.tools/actions/gym/
- https://developer.apple.com/jp/help/app-store-connect/test-a-beta-version/add-internal-testers/

[^1]: https://developer.apple.com/videos/play/wwdc2023/10117/?time=275
[^2]: xcodebuild の機能の中でも ipa や app ファイルの生成部分にフォーカスしたもので、 xcodebuild コマンドが持つさまざまな機能(例えば xcframework の生成など)を網羅しているものではないので、ラッパーというのは少々強引かもしれないですが、簡易的に説明するためラッパーと表現しました