Azure PipelinesのYAMLでiOSアプリのTestFlight配布パイプラインを構築する方法

「Azure PipelinesのYAMLでiOSアプリのCI/CD環境を構築する」は3部構成です。
記事を順番に読み進めると、Azure PipelinesでiOSアプリのCI/CD環境が構築できるようになります。

- 第一部：[CI環境の構築](https://qiita.com/uhooi/items/3e1bd3e64f339f6c0d0f)
- 第二部：[App Center配布パイプラインの構築](https://qiita.com/uhooi/items/1245344a6add3e86cdc8)
- 第三部：TestFlight配布パイプラインの構築　__←イマココ__

## はじめに

Azure Pipelinesを使い、iOSアプリをTestFlightへ配布するCDを構築します。

## 本記事で説明しないこと

- Azure Pipelinesの概要や基本的な操作方法  
[私が以前書いた記事](https://qiita.com/uhooi/items/5ed8db6aaec01425e453)が参考になると思います
- [CI環境の構築](https://qiita.com/uhooi/items/3e1bd3e64f339f6c0d0f)と[AppCenter配布パイプラインの構築](https://qiita.com/uhooi/items/1245344a6add3e86cdc8#variables)で説明した内容  
以前説明した内容を説明するのは冗長なのでしません

## 証明書とProvisioning Profileのアップロード

証明書とProvisioning ProfileをAzure Pipelines内の安全な場所へアップロードします。

[AppCenter配布パイプラインの構築](https://qiita.com/uhooi/items/1245344a6add3e86cdc8#variables)で紹介したので省略します。

## App Store Connectのユーザー名とパスワードの追加

App Store Connectのユーザー名とパスワードをAzure Pipelinesの安全な場所で保持します。

[AppCenter配布パイプラインの構築](https://qiita.com/uhooi/items/1245344a6add3e86cdc8#variables)を参考に、 `AppStoreConnectUserName` 、 `AppStoreConnectPassword` のようにわかりやすい名前で変数を追加してください。
パスワードにロックをかけるのを忘れないようにしましょう。

__Apple IDに2ファクタ認証を設定している場合、パスワードはApp用パスワードを使う必要があります__ 。
App用パスワードの設定方法は[こちらの記事](https://qiita.com/uhooi/items/ec9dcd480a04bce8a963)をご参照ください。

Apple IDのパスワードを使った場合、認証が通らず以下のエラーになります。

```shell-session
$ xcrun altool --upload-app -f ./Foo.ipa -t ios -u {username} -p {password}　
…
"Error Domain=ITunesSoftwareServiceErrorDomain Code=-22014 \"We are unable to create an authentication session.\" UserInfo={NSLocalizedDescription=We are unable to create an authentication session
```

## Makefileの作成

IPAファイルのバリデーションやTestFlight配布などの実行コマンドをまとめたMakefileを作成します。
[CI環境の構築](https://qiita.com/uhooi/items/3e1bd3e64f339f6c0d0f)と[AppCenter配布パイプラインの構築](https://qiita.com/uhooi/items/1245344a6add3e86cdc8#variables)で紹介したコマンドは省略します。

<details><summary>Makefile</summary><div>

`APPSTORE_EXPORT_OPTIONS_PATH := ./ExportOptions/ExportOptionsAppStore.plist` としているので、このパスに.plistファイルを配置してください。
Xcode上からアーカイブ→エクスポートすると.plistが生成されるので、それをそのまま使うのがオススメです。

```make:Makefile
IPA_PATH := ${EXPORT_PATH}/${PRODUCT_NAME}.ipa

APPSTORE_PROVISIONING_PROFILE_SPECIFIER := Foo_AppStore
APPSTORE_EXPORT_OPTIONS_PATH := ./ExportOptions/ExportOptionsAppStore.plist

.PHONY: generate-ipa-appstore
generate-ipa-appstore: # Generate IPA file for App Store
	$(MAKE) archive-appstore
	$(MAKE) export-archive-appstore

.PHONY: archive-appstore
archive-appstore:
	$(MAKE) archive PROVISIONING_PROFILE_SPECIFIER=${APPSTORE_PROVISIONING_PROFILE_SPECIFIER}

.PHONY: export-archive-appstore
export-archive-appstore:
	$(MAKE) export-archive EXPORT_OPTIONS_PATH=${APPSTORE_EXPORT_OPTIONS_PATH}

.PHONY: validate
validate: # Validate IPA file # ASC_USERNAME=[user name] ASC_PASSWORD=[password]
	xcrun altool --validate-app -f ${IPA_PATH} -t ios -u ${ASC_USERNAME} -p ${ASC_PASSWORD}

.PHONY: upload
upload: # Distribute IPA file to App Store Connect # ASC_USERNAME=[user name] ASC_PASSWORD=[password]
	xcrun altool --upload-app -f ${IPA_PATH} -t ios -u ${ASC_USERNAME} -p ${ASC_PASSWORD}
```

今回のMakefileは、本記事で使うコマンドのみを抜粋および編集して紹介しています。
私が普段使っているMakefileの全容は[こちら](https://github.com/uhooi/UhooiPicBook/blob/develop/Makefile)にあるので、よかったら参考にしてください。

</div></details>

## 設定ファイルの構成

[CI環境の構築](https://qiita.com/uhooi/items/3e1bd3e64f339f6c0d0f)で紹介したので省略します。

## 各項目の紹介

各項目を上から順に紹介します。

### name

[CI環境の構築](https://qiita.com/uhooi/items/3e1bd3e64f339f6c0d0f#name)と同様なので省略します。

### trigger

[AppCenter配布パイプラインの構築](https://qiita.com/uhooi/items/1245344a6add3e86cdc8#variables)と同様、私は手動で配布したいため、トリガーをOFFにしています。

```yaml
trigger: none
```

### pool

[CI環境の構築](https://qiita.com/uhooi/items/3e1bd3e64f339f6c0d0f)と同様なので省略します。

### variables

[AppCenter配布パイプラインの構築](https://qiita.com/uhooi/items/1245344a6add3e86cdc8#variables)と同様なので省略します。

### steps

パイプライン内のステップを1つずつ紹介します。
ライブラリや証明書のインストールなど、[CI環境の構築](https://qiita.com/uhooi/items/3e1bd3e64f339f6c0d0f)や[AppCenter配布パイプラインの構築](https://qiita.com/uhooi/items/1245344a6add3e86cdc8#variables)と同様のステップは省略します。

#### IPAファイルのバリデーション（任意）

IPAファイルをバリデーションします。
必須ではありませんが、配布前にアプリアイコンの画像が不足しているなどの問題に気づけます。

```yaml
- script: make validate ASC_USERNAME=$(AppStoreConnectUserName) ASC_PASSWORD=$(AppStoreConnectPassword)
  displayName: Validate IPA file
```

#### TestFlightへ配布

作成したIPAファイルをTestFlightへ配布します。

```yaml
- script: make upload ASC_USERNAME=$(AppStoreConnectUserName) ASC_PASSWORD=$(AppStoreConnectPassword)
  displayName: Distribute IPA file to TestFlight
```

## 設定ファイルの全体図

最後に設定ファイルの全体図を載せます。

```yaml
name: $(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)

trigger: none

pool:
  vmImage: 'macos-latest'

variables:
- group: Foo-iOS
- name: DEVELOPER_DIR
  value: '/Applications/Xcode_12.4.app/Contents/Developer'
- name: MINT_PATH
  value: 'mint/lib'
- name: MINT_LINK_PATH
  value: 'mint/bin'

steps:
# Xcodeのバージョン出力
- script: xcodebuild -version
  displayName: Show Xcode version

# Makeのバージョン出力
- script: make --version
  displayName: Show Make version

# Bundlerで管理しているライブラリのインストール
- script: make install-bundler
  displayName: Bundle install

# Mintのインストール
- script: brew install mint
  displayName: Install Mint

# ライセンス情報の生成
- script: make generate-licenses
  displayName: Generate licenses

# プロジェクトファイルの生成
- script: make generate-xcodeproj
  displayName: Generate Xcode project

# 証明書のインストール
- task: InstallAppleCertificate@2
  inputs:
    certSecureFile: 'Certificate.p12'
    certPwd: $(P12Password)
    keychain: 'temp'

# Provisioning Profileのインストール
- task: InstallAppleProvisioningProfile@1
  inputs:
    provisioningProfileLocation: 'secureFiles'
    provProfileSecureFile: 'Foo_AppStore.mobileprovision'

# IPAファイルの作成
- script: make generate-ipa-appstore
  displayName: Generate IPA file for App Store

# アーティファクトのステージングへコピー
- task: CopyFiles@2
  inputs:
    Contents: |
      **/xcodebuild_*.log
      **/*.xcarchive/**/*
      **/output/iphoneos/Release/*
    TargetFolder: '$(Build.ArtifactStagingDirectory)'

# アーティファクトへアップロード
- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'drop'
    publishLocation: 'Container'

# IPAファイルのバリデーション
- script: make validate ASC_USERNAME=$(AppStoreConnectUserName) ASC_PASSWORD=$(AppStoreConnectPassword)
  displayName: Validate IPA file

# TestFlightへ配布
- script: make upload ASC_USERNAME=$(AppStoreConnectUserName) ASC_PASSWORD=$(AppStoreConnectPassword)
  displayName: Distribute IPA file to TestFlight
```

## おまけ：App Store Connect API

App Store Connect APIを使うと、ユーザー名やパスワードを指定せずにTestFlightへの配布などを自動化できるようです。
https://developer.apple.com/app-store-connect/api/

私は業務で使っているアカウントに権限がなかったので使えませんでした。

## おわりに

Azure PipelinesのYAMLでiOSアプリをTestFlightへ配布することができました！

これでiOSアプリのCI/CD環境が整いました。
どんなアプリにも適用できるので、ぜひCI/CD環境構築の参考にしてください。

## 参考リンク

- [Monaca - monacaの「App Store Connectにアップロード」エラーについて｜teratail](https://teratail.com/questions/172225)