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

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

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

## はじめに

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

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

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

## App Centerのコネクションを追加

Azure DevOpsのプロジェクトに、App Centerを接続します。

__Azure DevOps > Project Settings > Pipelines - Service connections > 「New service connection」をクリック__
<img width="1676" alt="スクリーンショット_2020-03-18_17_43_19.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/fcfc6330-6d6b-ced3-d9dd-b07904d91f4c.jpeg">

以下のURLからでもアクセスできる
https://dev.azure.com/{Organization}/{Project}/_settings/adminservices

__「Visual Studio App Center」を選択し、「Next」をクリック__
<img width="500" alt="スクリーンショット 2020-03-18 17.53.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/0bb1cffe-dc69-f96d-5f5a-642787ab21e6.png">

__API TokenにApp CenterのAPIトークン、Service connection nameに「App Center」など適当な名前を入力し、「save」をクリック__
<img width="500" alt="スクリーンショット 2020-03-18 17.55.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/61e0264f-fede-062e-91aa-bbb3b4d64c5e.png">

スクリーンショットを撮るのが手間だったので、API Tokenの発行方法とサービスの追加方法は記載できていません。
以下の記事が参考になるので、こちらに沿って行ってください。
https://qiita.com/gremito/items/08d44b5e18afe05910a5

これでApp Centerの接続は完了です。

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

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

__Azure DevOps > Pipelines > Library > [Secure files]タブ > 「+ Secure file」をクリック__
<img width="1672" alt="スクリーンショット_2020-03-18_17_59_51.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/e714acdd-de53-ac52-27cc-a5ab9868641b.jpeg">

__ファイルをドラッグ&ドロップし、「OK」をクリック__
<img width="522" alt="スクリーンショット 2020-03-18 18.03.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/673a18ea-cae1-ec6d-603f-2431b01f2f5f.png">

これをファイル数分繰り返せば、証明書とProvisioning Profileのアップロードは完了です。

## P12ファイルのパスワードの追加

P12ファイルのパスワードをAzure Pipelinesの安全な場所で保持します。

まず変数のグループを作成し、その中に変数を追加します。

__Azure DevOps > Pipelines > Library > [Variable group]タブ > 「+ Variable group」をクリック__
<img width="1675" alt="スクリーンショット_2020-03-18_18_15_32.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/5ecadd0b-c79a-9d83-24de-e3c295da5c5f.jpeg">

__Variable group nameに適当な名前を入力し、VariablesにP12ファイルのパスワードを追加し、「Save」をクリック__
<img width="1249" alt="スクリーンショット_2020-03-18_18_39_13.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/43dbdd3a-882e-9048-83d5-8a8a67abd0f2.jpeg">

パスワードの変数名は適当な名前でOKです。
Standardのみ契約している場合、単純に `P12Password` でいいと思います。

これでP12ファイルのパスワードの追加は完了です。

## Makefileの作成

アーカイブやIPAファイルの作成などの実行コマンドをまとめたMakefileを作成します。
[CI環境の構築](https://qiita.com/uhooi/items/3e1bd3e64f339f6c0d0f)で紹介したコマンドは省略します。

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

例はAd Hocなので、In-Houseの場合は変数名などを適当に修正してください。

`DEVELOPMENT_TEAM` 、 `PRODUCT_BUNDLE_IDENTIFIER` 、 `ADHOC_PROVISIONING_PROFILE_SPECIFIER` は自分のプロジェクトの値に置き換えてください。

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

```make:Makefile
SDK := iphoneos
CONFIGURATION := Release
ARCHIVE_PATH := ./build/${PRODUCT_NAME}.xcarchive
EXPORT_PATH := ./output/${SDK}/${CONFIGURATION}

XCODEBUILD_ARCHIVE_LOG_NAME := xcodebuild_archive.log
XCODEBUILD_EXPORT_ARCHIVE_LOG_NAME := xcodebuild_exportArchive.log

DEVELOPMENT_TEAM := XXXXXXXXXX
PRODUCT_BUNDLE_IDENTIFIER := com.example.Foo
ADHOC_PROVISIONING_PROFILE_SPECIFIER := Foo_AdHoc
ADHOC_EXPORT_OPTIONS_PATH := ./ExportOptions/ExportOptionsAdHoc.plist

.PHONY: generate-ipa-adhoc
generate-ipa-adhoc: # Generate IPA file for Ad Hoc
	$(MAKE) archive-adhoc
	$(MAKE) export-archive-adhoc

.PHONY: archive-adhoc
archive-adhoc:
	$(MAKE) archive PROVISIONING_PROFILE_SPECIFIER=${ADHOC_PROVISIONING_PROFILE_SPECIFIER}

.PHONY: export-archive-adhoc
export-archive-adhoc:
	$(MAKE) export-archive EXPORT_OPTIONS_PATH=${ADHOC_EXPORT_OPTIONS_PATH}

.PHONY: archive
archive:
	set -o pipefail && \
xcodebuild \
-sdk ${SDK} \
-configuration ${CONFIGURATION} \
-project ${PROJECT_NAME} \
-scheme ${SCHEME_NAME} \
-archivePath ${ARCHIVE_PATH} \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="iPhone Distribution" \
PROVISIONING_PROFILE_SPECIFIER=${PROVISIONING_PROFILE_SPECIFIER} \
DEVELOPMENT_TEAM=${DEVELOPMENT_TEAM} \
PRODUCT_BUNDLE_IDENTIFIER=${PRODUCT_BUNDLE_IDENTIFIER} \
clean archive \
| tee ./${XCODEBUILD_ARCHIVE_LOG_NAME} \
| bundle exec xcpretty --color

.PHONY: export-archive
export-archive:
	set -o pipefail && \
xcodebuild \
-exportArchive \
-archivePath ${ARCHIVE_PATH} \
-exportPath ${EXPORT_PATH} \
-exportOptionsPlist ${EXPORT_OPTIONS_PATH} \
| tee ./${XCODEBUILD_EXPORT_ARCHIVE_LOG_NAME} \
| bundle exec xcpretty --color
```

アーカイブ時に `CODE_SIGN_STYLE=Manual` を指定し、手動で署名しているのがポイントです。
これにより、開発時に自動署名していても確実に署名できます。

今回の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

私は手動でApp Centerへ配布したいため、トリガーをOFFにしています。

```yaml
trigger: none
```

### pool

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

### variables

先ほど追加した変数を使うには、変数グループを明記する必要があります。
変数グループと変数を同時に指定する場合、変数は `name` ・ `value` で定義しなければいけません。

```yaml
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'
```

```yaml:×変数グループと同時に変数を指定する場合、かんたんな書き方はできない
variables:
  DEVELOPER_DIR: '/Applications/Xcode_12.4.app/Contents/Developer'
  MINT_PATH: 'mint/lib'
  MINT_LINK_PATH: 'mint/bin'
- group: Foo-iOS
```

### steps

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

#### 証明書のインストール

証明書をインストールします。

標準で用意されているタスクを使います。
`certSecureFile` にP12ファイルの名前、 `certPwd` に先ほど追加したP12ファイルのパスワードの変数を指定します。

```yaml
- task: InstallAppleCertificate@2
  inputs:
    certSecureFile: 'Certificate.p12'
    certPwd: $(P12Password)
    keychain: 'temp'
```

#### Provisioning Profileのインストール

Provisioning Profileをインストールします。

証明書のインストールと同様、標準で用意されているタスクを使います。
`provProfileSecureFile` にProvisioning Profileの名前を指定します。

```yaml
- task: InstallAppleProvisioningProfile@1
  inputs:
    provisioningProfileLocation: 'secureFiles'
    provProfileSecureFile: 'Foo_AdHoc.mobileprovision'
```

#### IPAファイルの作成

IPAファイルを作成します。

```yaml
- script: make generate-ipa-adhoc
  displayName: Generate IPA file for Ad Hoc
```

#### アーティファクトのステージングへコピー

配布に問題があった場合に調査するため、生ログとアーカイブ、IPAファイルをダウンロードできるようにします。

そのためには「ステージングへコピー→アップロード」と2つのタスクを実行します。

```yaml
- task: CopyFiles@2
  inputs:
    Contents: |
      **/xcodebuild_*.log
      **/*.xcarchive/**/*
      **/output/iphoneos/Release/*
    TargetFolder: '$(Build.ArtifactStagingDirectory)'
```

`Contents` では、Makefileで定義した生ログと `ARCHIVE_PATH` 、 `EXPORT_PATH` をワイルドカードで指定しています。
.xcarchiveファイルはフォルダ扱いのため、末尾を `**/*` としています。

#### アーティファクトへアップロード

直前のタスクでステージングへコピーしたので、本タスクでアップロードします。

```yaml
- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'drop'
    publishLocation: 'Container'
```

これにより、パイプラインの実行結果からアーカイブとIPAファイルがダウンロードできるようになります。

#### App Centerへ配布

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

`serverEndpoint` は先ほど追加したコネクションの名前を指定します。
`appSlug` は自分のApp Centerの値に置き換えてください。
`releaseNotesInput` はテストアプリの配布なのでいろいろな情報を表示しています。必要に応じて変更してください。
最後の `$(ReleaseNote)` はパイプラインの手動実行時に値を入れることで、リリースノートを書けるようにしています。

```yaml
- task: AppCenterDistribute@3
  inputs:
    serverEndpoint: 'App Center'
    appSlug: '{Organization}/{App}'
    appFile: '**/*.ipa'
    releaseNotesOption: 'input'
    releaseNotesInput: |
      - Environment: 'Ad Hoc'
      - Configuration: Release
      - Certificate: $(APPLE_CERTIFICATE_SIGNING_IDENTITY)
      - Provisioning Profile: $(APPLE_PROV_PROFILE_UUID)
      - Build ID: $(Build.BuildId)
      - Build Number: $(Build.BuildNumber)
      - Reason: $(Build.Reason)
      - Requested For: $(Build.RequestedFor)
      - Source Branch: $(Build.SourceBranch)
      - Last Commit ID: $(Build.SourceVersion)
      - Last Commit Comment: $(Build.SourceVersionMessage)
      - Release Note: $(ReleaseNote)
    destinationType: 'groups'
```

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

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

```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_AdHoc.mobileprovision'

# IPAファイルの生成
- script: make generate-ipa-adhoc
  displayName: Generate IPA file for Ad Hoc

# アーティファクトのステージングへコピー
- 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'

# App Centerへ配布
- task: AppCenterDistribute@3
  inputs:
    serverEndpoint: 'App Center'
    appSlug: '{Organization}/{App}'
    appFile: '**/*.ipa'
    releaseNotesOption: 'input'
    releaseNotesInput: |
      - Environment: 'Ad Hoc'
      - Configuration: Release
      - Certificate: $(APPLE_CERTIFICATE_SIGNING_IDENTITY)
      - Provisioning Profile: $(APPLE_PROV_PROFILE_UUID)
      - Build ID: $(Build.BuildId)
      - Build Number: $(Build.BuildNumber)
      - Reason: $(Build.Reason)
      - Requested For: $(Build.RequestedFor)
      - Source Branch: $(Build.SourceBranch)
      - Last Commit ID: $(Build.SourceVersion)
      - Last Commit Comment: $(Build.SourceVersionMessage)
      - Release Note: $(ReleaseNote)
    destinationType: 'groups'
```

## おわりに

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

fastlaneを使えばもう少しかんたんに実現できそうですが、知識不足なのでできませんでした…。

## 参考リンク

- [Variable groups for Azure Pipelines and TFS - Azure Pipelines | Microsoft Docs](https://docs.microsoft.com/en-us/azure/devops/pipelines/library/variable-groups?view=azure-devops&tabs=yaml)
- [File matching patterns reference - Azure Pipelines | Microsoft Docs](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/file-matching-patterns?view=azure-devops)
- [Predefined variables - Azure Pipelines | Microsoft Docs](https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml)