99
41

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.

iOSAdvent Calendar 2019

Day 22

GitHub ActionsでiOSアプリのCI環境を構築する方法

Last updated at Posted at 2019-12-23

はじめに

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ファイルに記述します。
最初に全体の構成を説明します。

ci.yml
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 ワークフロー実行のトリガー
pushpull_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 に記述します。

ci.yml
name: CI

on:
  

env:
  

jobs:
  test:
    runs-on: macOS-latest
    steps:
      # ここにステップを記述する
      

チェックアウト

まずはチェックアウトです。
GitHubリポジトリからソースを取得します。

定義されているアクションは uses で使えます。

ci.yml
- uses: actions/checkout@v2

Xcodeの選択(不要)

どのXcodeを使うか選択します。

コメント を頂いて初めて知ったのですが、 DEVELOPER_DIR という環境変数でXcodeのパスを指定できます。
sudo を使わずに指定できるため、環境変数を使った方がよさそうです。

ci.yml(このスクリプトの実行は不要ですが、一応残しておきます)
- name: Set Xcode Path
  run: sudo xcode-select --switch /Applications/Xcode_11.6.app/Contents/Developer

Xcodeの一覧出力(任意)

CIマシンにインストールされているXcodeの一覧を出力します。

ci.yml
- name: Show Xcode list
  run: ls /Applications | grep 'Xcode'

必須ではありませんが、毎回実行するとどのタイミングでXcodeのバージョンが増減したかわかるので便利です。

Xcodeのバージョン出力(任意)

現在使われているXcodeのバージョンを出力します。

ci.yml
- 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 に任意のステップ名を付けられます。
他のキャッシュと区別するため、私は何のキャッシュを復元するか記述しています。

ci.yml
- 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 にシェルスクリプトを指定して実行します。

ci.yml
- 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は以下の通りです。

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からインストールします。

ci.yml
- name: Install Mint
  run: brew install mint

私が個人で開発しているアプリのMintfileは以下の通りです。

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現在はマージされていません。

ci.yml
- 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

ci.yml
- 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 を付けています。

ci.yml
- 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のライセンス表示を生成します。

ci.yml
 - name: Generate licenses with LicensePlist
  run: mint run LicensePlist license-plist --output-path {製品ターゲット名}/Settings.bundle --github-token ${{ secrets.GITHUB_TOKEN }}

プロジェクトファイルの生成

XcodeGenでプロジェクトファイルを生成します。

ci.yml
- 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

ci.yml
- 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

ci.yml
- name: Install Pods
  run: bundle exec pod install

ビルド

ビルドを実行し、ビルドエラーにならないか確認します。

run 内では改行がスペースに変換されるため、可読性のために改行しています。

ci.yml
- 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 で実行しています。

コマンドをパイプで繋げると、最後のコマンドのエラー判定が使われます。
xcprettyxcodebuild の失敗にかかわらず成功するため、ビルドエラーでもワークフローが成功扱いになってしまいます。
そのため、 set -o pipefail && を先頭に付けています。

単体テストの実行

単体テストを実行します。

ci.yml
- 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に変換します。

ci.yml
- name: Convert Code coverage to HTML
  run: bundle exec slather coverage --html --output-directory html_report

--html オプションを付けることで、コードカバレッジをHTMLで出力します。

Slatherの設定ファイルは以下の通りです。

.slather.yml
coverage_service: cobertura_xml
xcodeproj: {プロジェクト名}.xcodeproj
workspace: {ワークスペース名}.xcworkspace
scheme: {スキーム名}

Slatherの詳細は以下の記事をご参照ください。
https://qiita.com/uhooi/items/e1e464777d2163286c59

テスト結果のアップロード

テスト結果をアーティファクト化してアップロードします。

ci.yml
- uses: actions/upload-artifact@v2
  with:
    name: test-results
    path: build/reports

name に任意のアーティファクト名、 path にアップロードしたいファイルやフォルダのパスを指定します。
xcprettyで出力したテスト結果は build/reports フォルダに配置されるので、そこを指定すればOKです。

コードカバレッジのアップロード

テスト結果と同様、コードカバレッジをアーティファクト化してアップロードします。

ci.yml
- uses: actions/upload-artifact@v2
  with:
    name: test-coverage
    path: html_report

フォルダは先ほど --output-directory オプションで指定した html_report を指定します。

全体図

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

ci.yml
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上からダウンロードできます。
スクリーンショット_2019-12-24_0_17_03.jpg

テスト結果
スクリーンショット_2019-12-24_0_35_56.jpg

コードカバレッジ
スクリーンショット_2019-12-24_0_36_13.jpg

テスト結果とコードカバレッジをHTMLでいい感じに出力できました🎉

S3などにアップロードし、直接HTMLを確認できるようにするとさらによさそうです。

他にやりたいこと

時間の都合上で実現できていないことを、備忘録として残します。

静的解析

Lintなどで静的解析します。

  • SwiftLint
  • SpellChecker

Slackに通知

通知は多過ぎると読まれない ため、アクションを起こす必要がある場合のみSlackに通知します。
具体的には以下の通りです。

  • 静的解析で警告やエラーが発生する
    • ファイル名
    • クラス名
    • メソッド名
    • 行数
    • メッセージ
  • ビルドに失敗する
    • エラーメッセージ
  • ビルドで警告が発生する
    • 警告メッセージ
  • 失敗するテストがある
    • ファイル名
    • クラス名
    • メソッド名
    • 行数
    • 失敗メッセージ
  • コードカバレッジが80%以下のファイルがある
    • ファイル名
    • コードカバレッジ

おまけ:公式のアクション

公式が用意している actions/* アクションは、OSSとしてGitHubに公開されています。
本記事で使っている公式のアクションは以下の3つです。

他によく使われる公式のアクションは、 actions/setup-java (AndroidアプリのCIに必要)や actions/github-script (Issueの作成などをトリガーにスクリプトを実行する)などがあります。

メジャーバージョンの更新が行われることもあるので、リリースをウォッチしておくと、バージョンアップに気づいてすぐに対応できます。
使い方が変わることもありますが、基本的にはバージョンを上げるだけで最新バージョンを使えます。

例:actions/checkoutのバージョンを1から2に更新する
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v2

おわりに

GitHub Actionsで基本的なiOSアプリのCIを回すことができました!

ほぼ全てシェルのコマンドをラップせずに使っているため、GitHub Actions以外のサービスを使っていても役立つ内容となっています😊

参考リンク

99
41
7

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
99
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?