はじめに
GitHub Actionsを使い、iOSアプリのビルドと単体テストを行うCIを構築します。
「GitHub Actions」とは?
GitHubが提供しているクラウドのCI/CDサービスです。
2019/11/14に一般公開されました。
https://twitter.com/GitHubJapan/status/1194674026607562753?s=20
環境
- Xcode:11.6 (11E708)
- Swift:5.2.4
注意
今回紹介するCIはもっとブラッシュアップできると思います。
あくまでGitHub ActionsでiOSアプリをCIする参考としてください。
この記事も随時更新していく予定です。
設定ファイルの構成
ワークフローはYAMLファイルに記述します。
最初に全体の構成を説明します。
name: CI
on:
…
env:
DEVELOPER_DIR: /Applications/Xcode_11.6.app
jobs:
test:
runs-on: macOS-latest
env:
MINT_PATH: mint/lib
MINT_LINK_PATH: mint/bin
steps:
…
名前 | 説明 |
---|---|
name | ワークフローの名前 |
on | ワークフロー実行のトリガーpush や pull_request などが指定できる |
env | 全ジョブで使う環境変数DEVELOPER_DIR でXcodeのパスを指定している。本記事ではジョブが1つしかないが、Xcodeのバージョンは全ジョブで統一したいため、ジョブごとに指定していない |
jobs | ワークフローのジョブ |
jobs > runs-on | 対象ジョブを実行するマシン iOSアプリはmacOSでしかビルドできないため macOS-latest を指定している |
jobs > env | 対象ジョブで使う環境変数MINT_PATH でMintのビルドキャッシュのパスを指定しているMINT_LINK_PATH でMintのグローバルインストールのシンボリックリンクのパスを指定している。グローバルインストールしないので指定は不要かもしれない |
jobs > steps | パイプラインのステップ |
詳細は以下をご参照ください。
https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
トリガー
トリガーはAndroidのCIとほぼ同様にしているので、私が書いた以下の記事をご参照ください。
https://qiita.com/uhooi/items/70ffe67ba65d33189db2#on
ステップの紹介
ワークフロー内のステップを1つずつ紹介します。
ステップは steps
に記述します。
name: CI
on:
…
env:
…
jobs:
test:
runs-on: macOS-latest
steps:
# ここにステップを記述する
…
チェックアウト
まずはチェックアウトです。
GitHubリポジトリからソースを取得します。
定義されているアクションは uses
で使えます。
- uses: actions/checkout@v2
Xcodeの選択(不要)
どのXcodeを使うか選択します。
コメント を頂いて初めて知ったのですが、 DEVELOPER_DIR
という環境変数でXcodeのパスを指定できます。
sudo
を使わずに指定できるため、環境変数を使った方がよさそうです。
- name: Set Xcode Path
run: sudo xcode-select --switch /Applications/Xcode_11.6.app/Contents/Developer
Xcodeの一覧出力(任意)
CIマシンにインストールされているXcodeの一覧を出力します。
- name: Show Xcode list
run: ls /Applications | grep 'Xcode'
必須ではありませんが、毎回実行するとどのタイミングでXcodeのバージョンが増減したかわかるので便利です。
Xcodeのバージョン出力(任意)
現在使われているXcodeのバージョンを出力します。
- name: Show Xcode version
run: xcodebuild -version
こちらも必須ではありませんが、 DEVELOPER_DIR
が問題なく指定されているかの確認や、CI失敗時のXcodeのバージョン確認に使えるので便利です。
Ruby製ライブラリのキャッシュ
Bundlerで管理しているライブラリのキャッシュを復元します。
公式が用意している actions/cache@v2
アクションを使います。
公式の例からコピペするだけでキャッシュできます。
https://github.com/actions/cache/blob/master/examples.md#ruby---gem
name
に任意のステップ名を付けられます。
他のキャッシュと区別するため、私は何のキャッシュを復元するか記述しています。
- name: Cache Gems
uses: actions/cache@v2
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-
ちなみにキャッシュの取得はワークフローの最後に自動で行ってくれます。
非常に便利です。
Ruby製ライブラリのインストール
Bundlerで管理しているライブラリをインストールします。
run
にシェルスクリプトを指定して実行します。
- name: Install Bundled Gems
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
キャッシュを取得するため、ライブラリのインストールパスを明示的に指定しています。
https://github.com/actions/cache/blob/master/examples.md#ruby---gem
Gemfileは以下の通りです。
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "xcpretty"
gem "cocoapods"
gem "slather"
各ライブラリの用途は使うときに説明します。
Mintのインストール
MintはSwift製コマンドラインツールの管理ツールです。
デフォルトではインストールされていないので、Homebrewからインストールします。
- name: Install Mint
run: brew install mint
私が個人で開発しているアプリのMintfileは以下の通りです。
realm/SwiftLint@0.38.0
uber/mockolo@1.1.1
fromkk/SpellChecker@0.0.3
Carthage/Carthage@0.34.0
yonaskolb/xcodegen@2.11.0
mono0926/LicensePlist@2.12.0
Mintで管理しているライブラリのキャッシュ
Mintで管理しているライブラリのキャッシュを復元します。
公式に例がないので自分で考えました。
せっかく考えたので、公式にドキュメント更新の PR を送ったのですが、2020/08/26現在はマージされていません。
- name: Cache Mint packages
uses: actions/cache@v2
with:
path: mint
key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }}
restore-keys: |
${{ runner.os }}-mint-
Mintは mint run
時にキャッシュの有無を判断してインストールするので、 mint bootstrap
は実行しないことにします。
Carthageで管理しているライブラリのキャッシュ
Carthageで管理しているライブラリのキャッシュを復元します。
CocoaPodsと同様、公式の例からコピペしてステップ名を付けます。
https://github.com/actions/cache/blob/master/examples.md#swift-objective-c---carthage
- name: Cache Carthage packages
uses: actions/cache@v2
with:
path: Carthage
key: ${{ runner.os }}-carthage-${{ hashFiles('**/Cartfile.resolved') }}
restore-keys: |
${{ runner.os }}-carthage-
Carthageで管理しているライブラリのインストール
Carthageで管理しているライブラリをインストールします。
私はMintでCarthageを管理しているため、先頭に mint run carthage
を付けています。
- name: Install Carthage frameworks
run: |
mint run carthage carthage bootstrap --platform iOS --cache-builds
echo '*** Resolved dependencies:'
cat 'Cartfile.resolved'
env:
GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--cache-builds
でキャッシュしているライブラリのビルドをスキップします。
キャッシュヒットを見てステップごとスキップするより安全です。
CarthageはGitHubへのアクセス数が多いため、GITHUB_ACCESS_TOKEN
を指定することで、呼び出し制限に引っかからないようにしています。
ライセンス表示の生成
LicensePlistでOSSのライセンス表示を生成します。
- name: Generate licenses with LicensePlist
run: mint run LicensePlist license-plist --output-path {製品ターゲット名}/Settings.bundle --github-token ${{ secrets.GITHUB_TOKEN }}
プロジェクトファイルの生成
XcodeGenでプロジェクトファイルを生成します。
- name: Generate Xcode project with XcodeGen
run: mint run xcodegen xcodegen generate
CocoaPodsで管理しているライブラリのキャッシュ
CocoaPodsで管理しているライブラリのキャッシュを復元します。
Bundlerと同様、公式の例からコピペしてステップ名を付けます。
https://github.com/actions/cache/blob/master/examples.md#swift-objective-c---cocoapods
- name: Cache Pods
uses: actions/cache@v2
with:
path: Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
CocoaPodsで管理しているライブラリのインストール
CocoaPodsで管理しているライブラリをインストールします。
私はBundlerでCocoaPodsを管理しているため、先頭に bundle exec
を付けています。
Bundlerについては以下の記事をご参照ください。
https://qiita.com/uhooi/items/4abf8c282ae23a259e4f
- name: Install Pods
run: bundle exec pod install
ビルド
ビルドを実行し、ビルドエラーにならないか確認します。
run
内では改行がスペースに変換されるため、可読性のために改行しています。
- name: Xcode build
run: set -o pipefail &&
xcodebuild
-sdk iphonesimulator
-configuration Debug
-workspace {ワークスペース名}.xcworkspace
-scheme {スキーム名}
build
| bundle exec xcpretty
CocoaPodsなどでワークスペースを使っている場合、ワークスペースとスキームの指定が必要です。
xcodebuild
コマンドを使ってビルドしていますが、デフォルトだとログの内容が煩雑で読みづらいです。
そのため、ログ整形ツールの xcpretty
を噛ませて読みやすくしています。
GitHub ActionsのmacOSではデフォルトで xcpretty
がインストールされているのですが、できる限りバージョンをロックしたいため、Bundlerでインストールして bundle exec xcpretty
で実行しています。
コマンドをパイプで繋げると、最後のコマンドのエラー判定が使われます。
xcpretty
は xcodebuild
の失敗にかかわらず成功するため、ビルドエラーでもワークフローが成功扱いになってしまいます。
そのため、 set -o pipefail &&
を先頭に付けています。
単体テストの実行
単体テストを実行します。
- name: Xcode test
run: set -o pipefail &&
xcodebuild
-sdk iphonesimulator
-configuration Debug
-workspace {ワークスペース名}.xcworkspace
-scheme {スキーム名}
-destination 'platform=iOS Simulator,name=iPhone 11 Pro Max'
-skip-testing:{UIテスト}
clean test
| bundle exec xcpretty --report html
単体テストの実行も xcodebuild
コマンドを使います。
今回はiPhone 11 Pro Maxのシミュレータで実行しています。
-skip-testing
オプションでUIテストの実行をスキップしています。
私の場合、UIテストは単体テストほど頻繁には実施しません。
xcpretty --report html
で単体テストの結果をHTMLで出力します。
コードカバレッジの出力
Slatherを使ってコードカバレッジをHTMLに変換します。
- name: Convert Code coverage to HTML
run: bundle exec slather coverage --html --output-directory html_report
--html
オプションを付けることで、コードカバレッジをHTMLで出力します。
Slatherの設定ファイルは以下の通りです。
coverage_service: cobertura_xml
xcodeproj: {プロジェクト名}.xcodeproj
workspace: {ワークスペース名}.xcworkspace
scheme: {スキーム名}
Slatherの詳細は以下の記事をご参照ください。
https://qiita.com/uhooi/items/e1e464777d2163286c59
テスト結果のアップロード
テスト結果をアーティファクト化してアップロードします。
- uses: actions/upload-artifact@v2
with:
name: test-results
path: build/reports
name
に任意のアーティファクト名、 path
にアップロードしたいファイルやフォルダのパスを指定します。
xcprettyで出力したテスト結果は build/reports
フォルダに配置されるので、そこを指定すればOKです。
コードカバレッジのアップロード
テスト結果と同様、コードカバレッジをアーティファクト化してアップロードします。
- uses: actions/upload-artifact@v2
with:
name: test-coverage
path: html_report
フォルダは先ほど --output-directory
オプションで指定した html_report
を指定します。
全体図
最後に設定ファイルの全体図を載せます。
name: CI
on:
push:
branches:
- master
- develop
paths-ignore:
- Docs/**
- README.md
- LICENSE
- Rambafile
pull_request:
branches:
- develop
paths-ignore:
- Docs/**
- README.md
- LICENSE
- Rambafile
env:
DEVELOPER_DIR: /Applications/Xcode_11.6.app
jobs:
test:
runs-on: macOS-latest
env:
MINT_PATH: mint/lib
MINT_LINK_PATH: mint/bin
steps:
# チェックアウト
- uses: actions/checkout@v2
# Xcodeの一覧出力
- name: Show Xcode list
run: ls /Applications | grep 'Xcode'
# Xcodeのバージョン出力
- name: Show Xcode version
run: xcodebuild -version
# Gemsのキャッシュ
- name: Cache Gems
uses: actions/cache@v2
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-
# Gemsのインストール
- name: Install Bundled Gems
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
# Mintのインストール
- name: Install Mint
run: brew install mint
# Mintで管理しているライブラリのキャッシュ
- name: Cache Mint packages
uses: actions/cache@v2
with:
path: mint
key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }}
restore-keys: |
${{ runner.os }}-mint-
# Carthageで管理しているライブラリのキャッシュ
- name: Cache Carthage packages
uses: actions/cache@v2
with:
path: Carthage
key: ${{ runner.os }}-carthage-${{ hashFiles('**/Cartfile.resolved') }}
restore-keys: |
${{ runner.os }}-carthage-
# Carthageで管理しているライブラリのインストール
- name: Install Carthage frameworks
run: |
mint run carthage carthage bootstrap --platform iOS --cache-builds
echo '*** Resolved dependencies:'
cat 'Cartfile.resolved'
env:
GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ライセンス表示の生成
- name: Generate licenses with LicensePlist
run: mint run LicensePlist license-plist --output-path {製品ターゲット名}/Settings.bundle --github-token ${{ secrets.GITHUB_TOKEN }}
# プロジェクトファイルの生成
- name: Generate Xcode project with XcodeGen
run: mint run xcodegen xcodegen generate
# CocoaPodsで管理しているライブラリのキャッシュ
- name: Cache Pods
uses: actions/cache@v2
with:
path: Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
# CocoaPodsで管理しているライブラリのインストール
- name: Install Pods
run: bundle exec pod install
# ビルド
- name: Xcode build
run: set -o pipefail &&
xcodebuild
-sdk iphonesimulator
-configuration Debug
-workspace {ワークスペース名}.xcworkspace
-scheme {スキーム名}
build
| bundle exec xcpretty
# 単体テストの実行
- name: Xcode test
run: set -o pipefail &&
xcodebuild
-sdk iphonesimulator
-configuration Debug
-workspace {ワークスペース名}.xcworkspace
-scheme {スキーム名}
-destination 'platform=iOS Simulator,name=iPhone 11 Pro Max'
-skip-testing:{UIテスト}
clean test
| bundle exec xcpretty --report html
# コードカバレッジの出力
- name: Convert Code coverage to HTML
run: bundle exec slather coverage --html --output-directory html_report
# テスト結果のアップロード
- uses: actions/upload-artifact@v2
with:
name: test-results
path: build/reports
# コードカバレッジのアップロード
- uses: actions/upload-artifact@v2
with:
name: test-coverage
path: html_report
シンプルなYAMLファイルなので、慣れれば読みやすいと思います。
アーティファクトの確認
アップロードしたアーティファクトはGitHub Actions上からダウンロードできます。
テスト結果とコードカバレッジをHTMLでいい感じに出力できました🎉
S3などにアップロードし、直接HTMLを確認できるようにするとさらによさそうです。
他にやりたいこと
時間の都合上で実現できていないことを、備忘録として残します。
静的解析
Lintなどで静的解析します。
- SwiftLint
- SpellChecker
Slackに通知
通知は多過ぎると読まれない ため、アクションを起こす必要がある場合のみSlackに通知します。
具体的には以下の通りです。
- 静的解析で警告やエラーが発生する
- ファイル名
- クラス名
- メソッド名
- 行数
- メッセージ
- ビルドに失敗する
- エラーメッセージ
- ビルドで警告が発生する
- 警告メッセージ
- 失敗するテストがある
- ファイル名
- クラス名
- メソッド名
- 行数
- 失敗メッセージ
- コードカバレッジが80%以下のファイルがある
- ファイル名
- コードカバレッジ
おまけ:公式のアクション
公式が用意している actions/*
アクションは、OSSとしてGitHubに公開されています。
本記事で使っている公式のアクションは以下の3つです。
- actions/checkout
https://github.com/actions/checkout - actions/cache
https://github.com/actions/cache - actions/upload-artifact
https://github.com/actions/upload-artifact
他によく使われる公式のアクションは、 actions/setup-java
(AndroidアプリのCIに必要)や actions/github-script
(Issueの作成などをトリガーにスクリプトを実行する)などがあります。
- actions/setup-java
https://github.com/actions/setup-java - actions/github-script
https://github.com/actions/github-script
メジャーバージョンの更新が行われることもあるので、リリースをウォッチしておくと、バージョンアップに気づいてすぐに対応できます。
使い方が変わることもありますが、基本的にはバージョンを上げるだけで最新バージョンを使えます。
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v2
おわりに
GitHub Actionsで基本的なiOSアプリのCIを回すことができました!
ほぼ全てシェルのコマンドをラップせずに使っているため、GitHub Actions以外のサービスを使っていても役立つ内容となっています😊