「Azure PipelinesのYAMLでiOSアプリのCI/CD環境を構築する」は3部構成です。
記事を順番に読み進めると、Azure PipelinesでiOSアプリのCI/CD環境が構築できるようになります。
- 第一部:CI環境の構築 ←イマココ
- 第二部:App Center配布パイプラインの構築
- 第三部:TestFlight配布パイプラインの構築
はじめに
Azure Pipelinesを使い、iOSアプリのビルドと単体テストを行うCIを構築します。
本記事で説明しないこと
- Azure Pipelinesの概要や基本的な操作方法
私が以前書いた記事が参考になると思います
Makefileの作成
まず、ビルドや単体テストなどの実行コマンドをまとめたMakefileを作成します。
スクリプトを直接CIの定義ファイルに記述してもいいのですが、以下の理由から避けています。
- ローカルでもかんたんに各コマンドを実行したい
- 他のCIサービスに移行しやすくしたい
Makefileは長いので、折りたたみます。
Makefile
PRODUCT_NAME
は自分のプロジェクトの製品名に置き換えてください。
PRODUCT_NAME := Foo
SCHEME_NAME := ${PRODUCT_NAME}
UI_TESTS_TARGET_NAME := ${PRODUCT_NAME}UITests
TEST_SDK := iphonesimulator
TEST_CONFIGURATION := Debug
TEST_PLATFORM := iOS Simulator
TEST_DEVICE ?= iPhone 12 Pro Max
TEST_OS ?= 14.4
TEST_DESTINATION := 'platform=${TEST_PLATFORM},name=${TEST_DEVICE},OS=${TEST_OS}'
COVERAGE_OUTPUT ?= html_report
XCODEBUILD_BUILD_LOG_NAME := xcodebuild_build.log
XCODEBUILD_TEST_LOG_NAME := xcodebuild_test.log
.PHONY: install-bundler
install-bundler: # Install Bundler dependencies
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
.PHONY: generate-licenses
generate-licenses: # Generate licenses with LicensePlist
mint run LicensePlist license-plist --output-path ${PRODUCT_NAME}/Settings.bundle
.PHONY: generate-xcodeproj
generate-xcodeproj: # Generate project with XcodeGen
mint run xcodegen xcodegen generate
.PHONY: build-debug
build-debug: # Xcode build for debug
set -o pipefail && \
xcodebuild \
-sdk ${TEST_SDK} \
-configuration ${TEST_CONFIGURATION} \
-project ${PROJECT_NAME} \
-scheme ${SCHEME_NAME} \
-destination ${TEST_DESTINATION} \
-clonedSourcePackagesDirPath './SourcePackages' \
build \
| tee ./${XCODEBUILD_BUILD_LOG_NAME} \
| bundle exec xcpretty --color
.PHONY: test
test: # Xcode test # TEST_DEVICE=[device] TEST_OS=[OS]
set -o pipefail && \
xcodebuild \
-sdk ${TEST_SDK} \
-configuration ${TEST_CONFIGURATION} \
-project ${PROJECT_NAME} \
-scheme ${SCHEME_NAME} \
-destination ${TEST_DESTINATION} \
-skip-testing:${UI_TESTS_TARGET_NAME} \
-clonedSourcePackagesDirPath './SourcePackages' \
clean test \
| tee ./${XCODEBUILD_TEST_LOG_NAME} \
| bundle exec xcpretty --color --report html
.PHONY: get-coverage-html
get-coverage-html: # Get code coverage for HTML # COVERAGE_OUTPUT=[output directory]
bundle exec slather coverage --html --output-directory ${COVERAGE_OUTPUT}
.PHONY: get-coverage-cobertura
get-coverage-cobertura: # Get code coverage for Cobertura
bundle exec slather
.PHONY: show-devices
show-devices: # Show devices
xcrun xctrace list devices
今回のメインはAzure Pipelinesなので、Makefileの内容は解説しません。
私が以前書いたGitHub Actionsの記事が参考になると思うので、よかったらご参照ください。
今回のMakefileは、本記事で使うコマンドのみを抜粋および編集して紹介しています。
私が普段使っているMakefileの全容はこちらにあるので、よかったら参考にしてください。
設定ファイルの構成
今回はYAMLファイルを使ってCI環境を構築します。
Classic Editorを使ってGUIで構築する場合、私が以前書いた記事が参考になると思います。
最初に全体の構成を説明します。
name: $(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
trigger:
…
schedules:
…
variables:
DEVELOPER_DIR: '/Applications/Xcode_12.4.app'
MINT_PATH: 'mint/lib'
MINT_LINK_PATH: 'mint/bin'
jobs:
- job: build
pool:
vmImage: 'macos-latest'
steps:
…
名前 | 説明 |
---|---|
name | パイプライン実行の番号 |
trigger | パイプライン実行のトリガー |
schedules | パイプライン定期実行のスケジュール |
variables | 全ジョブで使う環境変数 詳細は こちら を参照 |
jobs | ワークフローのジョブ |
jobs > pool > vmImage | 対象ジョブを実行するマシン iOSアプリはmacOSでしかビルドできないため macos-latest を指定している |
jobs > variables | 対象ジョブで使う環境変数 今回は定義していない |
jobs > steps | パイプラインのステップ |
各項目の紹介
各項目を上から順に紹介します。
name
パイプラインの実行ごとに振られる番号です。
パイプライン自体の名前ではない のでご注意ください。
省略するとデフォルト値の $(Date:yyyyMMdd).$(Rev:r)
が適用されます。
例: #20211006.1
トリガーとなったブランチを含めるとわかりやすいので、私は $(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
を指定しています。
例: #develop_20211006.1
他にもいろいろカスタマイズできます。
詳細は公式ドキュメントをご参照ください。
trigger
パイプラインを実行するトリガーを設定します。
私は以下のブランチのプッシュをトリガーにしています。
main
develop
feature/*
ブランチや release/*
ブランチは、ブランチポリシーを設定してPR時に自動でCIを回すようにしているので、こちらでは指定していません。
trigger:
batch: true
branches:
include:
- main
- develop
paths:
exclude:
- Docs
- README.md
- LICENSE
- Rambafile
README.md
や Rambafile
など、iOSアプリに直接関係ないファイルは、プッシュしてもCIが動かないようにしています。
schedules
パイプラインを定期実行するスケジュールを設定します。
私は毎日0時に以下のブランチをCIするようにしています。
develop
main
schedules:
- cron: "0 15 * * *"
displayName: Daily midnight build
branches:
include:
- main
- develop
always: true
cron式で指定する時間はUTCであり、日本時間にするには9時間足す必要があります。
15:00 UTC
= 00:00 JST
alwaysで false
を指定すると、前回の実行から変更があったブランチのみを対象にします。
ライブラリのバージョンが変わらなくてもビルドできなくなるなど、自分のソース以外の変更によってCIが通らなくなることがあるため、 true
にするのがオススメです。
variables
「設定ファイルの構成」で説明した通りなので省略します。
ステップ間で環境変数を引き継がないので、以下のように環境変数をエクスポートするステップを作っても意味がありません。
steps:
- script: |
export DEVELOPER_DIR=/Applications/Xcode_12.4.app/Contents/Developer
export MINT_PATH=mint/lib
export MINT_LINK_PATH=mint/bin
displayName: Export environment variables
steps
ステップは各ジョブの steps
に記述します。
jobs:
- job: test
pool:
…
variables:
…
steps:
# ここにステップを記述する
…
注意
- GitHub Actionsと異なり、特別なことをしない限りはチェックアウトのステップは不要
jobs
今回はジョブを3つ用意しており、順番に紹介します。
build
ビルドを実行するジョブです。
GitHub ActionsでiOSアプリのCIを構築する方法 とほぼ同様なので、全体図のみ紹介します。
セットアップは test
ジョブでも使うため、テンプレートにしています。
parameters:
- name: environment
type: string
- name: destination
type: string
steps:
# 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-${{ parameters.environment }}-${{ parameters.destination }}
displayName: Generate Xcode project for ${{ parameters.environment }} and ${{ parameters.destination }}
# セットアップ
- template: templates/setup-template.yml
parameters:
environment: 'develop'
destination: 'appcenter'
# ビルド
- script: make build-debug
displayName: Xcode build for debug
# ログをアーティファクトのステージングへコピー
- task: CopyFiles@2
inputs:
contents: '**/xcodebuild_build.log'
targetFolder: '$(Build.ArtifactStagingDirectory)'
condition: failed()
# ログのアップロード
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'drop'
publishLocation: 'Container'
condition: failed()
test
単体テストを実行するジョブです。
build
ジョブと同様、詳細は省略します。
- job: test
pool:
vmImage: 'macos-latest'
steps:
# セットアップ
- template: templates/setup-template.yml
parameters:
environment: 'develop'
destination: 'appcenter'
# 単体テストの実行
- script: make test
displayName: Xcode test
# ログをアーティファクトのステージングへコピー
- task: CopyFiles@2
inputs:
contents: '**/xcodebuild_test.log'
targetFolder: '$(Build.ArtifactStagingDirectory)'
condition: failed()
# ログのアップロード
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'drop'
publishLocation: 'Container'
condition: failed()
# コードカバレッジをHTML形式で取得
- script: make get-coverage-html
displayName: Get code coverage for HTML
# テスト結果とコードカバレッジをアーティファクトのステージングへコピー
- task: CopyFiles@2
inputs:
Contents: |
**/build/reports/**/*
**/html_report/**/*
TargetFolder: '$(Build.ArtifactStagingDirectory)'
# アーティファクトへアップロード
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
# コードカバレッジをCobertura形式で取得
- script: make get-coverage-cobertura
displayName: Get code coverage for Cobertura
# コードカバレッジのアップロード
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: 'cobertura'
summaryFileLocation: '$(System.DefaultWorkingDirectory)/xml_report/cobertura.xml'
failIfCoverageEmpty: true
info
CIに関係ない情報を出力するジョブです。
build
や test
ジョブと同様、詳細は省略します。
他のワークフローでも使うため、テンプレートにしています。
steps:
# Xcodeのバージョン出力
- script: xcodebuild -version
displayName: Show Xcode version
# Makeのバージョン出力
- script: make --version
displayName: Show Make version
# Rubyのバージョン出力
- script: ruby --version
displayName: Show Ruby version
# Bundlerのバージョン出力
- script: bundle version
displayName: Show Bundler version
# Xcodeの一覧出力
- script: ls /Applications | grep 'Xcode'
displayName: Show Xcode list
# 端末の一覧出力
- script: make show-devices
displayName: Show devices
- job: info
pool:
vmImage: 'macos-latest'
steps:
# 情報の出力
- template: templates/info-template.yml
設定ファイルの全体図
最後に設定ファイルの全体図を載せます。
name: $(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
trigger:
batch: true
branches:
include:
- main
- develop
paths:
exclude:
- Docs
- README.md
- LICENSE
- Rambafile
schedules:
- cron: "0 15 * * *"
displayName: Daily midnight build
branches:
include:
- main
- develop
always: true
variables:
DEVELOPER_DIR: '/Applications/Xcode_12.4.app'
MINT_PATH: 'mint/lib'
MINT_LINK_PATH: 'mint/bin'
jobs:
- job: build
pool:
vmImage: 'macos-latest'
steps:
# セットアップ
- template: templates/setup-template.yml
parameters:
environment: 'develop'
destination: 'appcenter'
# ビルド
- script: make build-debug
displayName: Xcode build for debug
# ログをアーティファクトのステージングへコピー
- task: CopyFiles@2
inputs:
contents: '**/xcodebuild_build.log'
targetFolder: '$(Build.ArtifactStagingDirectory)'
condition: failed()
# ログのアップロード
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'drop'
publishLocation: 'Container'
condition: failed()
- job: test
pool:
vmImage: 'macos-latest'
steps:
# セットアップ
- template: templates/setup-template.yml
parameters:
environment: 'develop'
destination: 'appcenter'
# 単体テストの実行
- script: make test
displayName: Xcode test
# ログをアーティファクトのステージングへコピー
- task: CopyFiles@2
inputs:
contents: '**/xcodebuild_test.log'
targetFolder: '$(Build.ArtifactStagingDirectory)'
condition: failed()
# ログのアップロード
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'drop'
publishLocation: 'Container'
condition: failed()
# コードカバレッジをHTML形式で取得
- script: make get-coverage-html
displayName: Get code coverage for HTML
# テスト結果とコードカバレッジをアーティファクトのステージングへコピー
- task: CopyFiles@2
inputs:
Contents: |
**/build/reports/**/*
**/html_report/**/*
TargetFolder: '$(Build.ArtifactStagingDirectory)'
# アーティファクトへアップロード
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
# コードカバレッジをCobertura形式で取得
- script: make get-coverage-cobertura
displayName: Get code coverage for Cobertura
# コードカバレッジのアップロード
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: 'cobertura'
summaryFileLocation: '$(System.DefaultWorkingDirectory)/xml_report/cobertura.xml'
failIfCoverageEmpty: true
- job: info
pool:
vmImage: 'macos-latest'
steps:
# 情報の出力
- template: templates/info-template.yml
シンプルなYAMLファイルなので、慣れれば読みやすいと思います。
おまけ: PR時にCIを回す
PR時に自動でCIを回し、成功しないとマージできないように設定できます。
詳細は以下の記事をご参照ください。
https://qiita.com/uhooi/items/826e624f36e9a230ec3c
おわりに
Azure PipelinesのYAMLで基本的なiOSアプリのCIを回すことができました!
キャッシュが取れていなかったり、静的解析していなかったりと改善点もありますが、参考になれば幸いです。