14
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Azure PipelinesのYAMLでiOSアプリのCI環境を構築する方法

Last updated at Posted at 2020-03-18

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

はじめに

Azure Pipelinesを使い、iOSアプリのビルドと単体テストを行うCIを構築します。

本記事で説明しないこと

Makefileの作成

まず、ビルドや単体テストなどの実行コマンドをまとめたMakefileを作成します。
スクリプトを直接CIの定義ファイルに記述してもいいのですが、以下の理由から避けています。

  • ローカルでもかんたんに各コマンドを実行したい
  • 他のCIサービスに移行しやすくしたい

Makefileは長いので、折りたたみます。

Makefile

PRODUCT_NAME は自分のプロジェクトの製品名に置き換えてください。

Makefile
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.mdRambafile など、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 ジョブでも使うため、テンプレートにしています。

templates/setup-template.yml
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 }}
ci.yml
  # セットアップ
  - 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 ジョブと同様、詳細は省略します。

ci.yml
- 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に関係ない情報を出力するジョブです。

buildtest ジョブと同様、詳細は省略します。

他のワークフローでも使うため、テンプレートにしています。

templates/info-template.yml
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
ci.yml
- 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を回すことができました!

キャッシュが取れていなかったり、静的解析していなかったりと改善点もありますが、参考になれば幸いです。

参考リンク

14
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?