ベタなiOSアプリのCI/CDのワークフローを組む

こんにちは。モバイルアプリの開発を担当をしているzigeninです。

ANDPADのiOSアプリのCI/CDワークフローを紹介します。 ベタなツールにベタなワークフローですので、他のiOSアプリにも適用できると思います。 一例として参考になればと思います。

目次

事前情報

ビルドに使用しているツール

ツール 用途
Bitrise 特定のブランチにpushした時にワークフローを実行
Firebase App Distribution アプリの配布
fastlane matchでアプリの署名用のcertificates & provisioning profileを取得
pod 依存ライブラリを入手

iOSアプリのビルドScheme

Scheme 用途
andpad Release版アプリをビルドする。
andpadUITests Release版アプリで、安定したUIテストのみ実行する用。
andpad_develop Develop版アプリをビルドする。Unitテスト実行用でもある。
andpadUITestsDevelop Develop版アプリで、安定したUIテストのみ実行する。
andpadNightlyUITests 全UIテストを実行する。

ANDPADではビルドSchemeによって、普段の開発に使うDevelop版アプリと、Storeに公開する用のRelease版アプリを分けています。 Develop版アプリは開発用アプリで、developサーバと接続しています。 Release版アプリはStoreに公開するアプリと同一で、本番サーバに接続しています。

UIテストのSchemeを3つ分けていますが、状況によってUIテストの実行の仕方を変えるためです。 UIテストをCI/CDのワークフローに組み込むための肝です。 詳細は、後述のNightly UI Testのワークフローで説明します。

ブランチ戦略

大体、GitHub Flowです。

ブランチ 用途
master Storeに公開したアプリのコードの置き場所
develop 開発本線。最新のコードの置き場所
feature/* 機能開発用ブランチ

運用図は、ワークフローの全体像と一緒に示します。

ワークフローの全体像

GitHubのブランチをベースに、ワークフローを示します。

ブランチ戦略とワークフローの関係性

主要なワークフローは3つです。

  • 内部配信のワークフロー
    • 用途
      • Develop版アプリの自動テストを実施 + 社内にアプリを配布する
    • トリガー
      • ブランチをdevelopにマージした時
    • やること
      • ビルドの準備をする
        • コードのチェックアウト
        • 証明書をfastlane matchで入手
        • 依存ライブラリをpodで入手
      • Unitテストを実行する
      • アプリをビルドする
      • ビルドしたアプリをFirebase App Distributionに配布する
      • 安定しているUIテストを実行する*1
      • ビルドの後始末をする
        • テスト結果を保存する
        • Slackにビルド結果を通知する
  • Store配信ワークフロー
    • 用途:Release版アプリの自動テストを実施 + Storeにアップロードする
    • トリガー:developブランチをmasterブランチにマージした時*2
    • やること(内部配信フローと大体同じ。違いは太字で強調)
      • ビルドの準備をする
      • Unitテストを実行する
      • アプリをビルドする
      • ビルドしたアプリをStoreに配布する
      • 安定しているUIテストを実行する
      • ビルドの後始末をする
  • Nightly UI Testワークフロー
    • 用途:追加したばかりのUIテストが安定していることを確認する
    • トリガー:毎日定刻に、developブランチに対して実行
    • やること(内部配信フローと大体同じ。違いは太字で強調)
      • ビルドの準備をする
      • Develop版アプリで 全てのUIテストを実行する
      • ビルドの後始末をする

以降、この3つのワークフローとそれらを構成するユーテリティワークフローを、Bitriseでどのように設定しているかを紹介します。

ユーティリティワークフロー

先程示した3つのワークフローには、共通部分が多いです。 共通部分は、4つのBitriseのユーテリティワークフローとしてまとめています*3

  1. ビルドの準備(_BeforeCommonWorkflow)
  2. Unitテスト実行&アプリのビルド(_BuildAndUnitTest)
  3. UIテスト実行(_UITest)
  4. ビルドの後始末(_AfterCommonWorkflow)

ワークフロー名の先頭に"_"を付けると、ユーティリティワークフローとなります。ユーティリティワークフローにすると、他のワークフローの部品として扱われ、独立して実行することができなくなります*4

4つのユーテリティワークフローの設定の詳細を紹介します。

ビルドの準備(_BeforeCommonWorkflow)

_BeforeCommonWorkflowを構成するStep

  • Activate SSH Key
  • Git Clone Repository
  • Bitrise.io Cache:Pull
  • fastlane match
    • Develop版アプリの署名用のcertificates & provisioning profileを取得するStep*5
    • "fastlane lane"に"match development"を設定
    • fastlaneに必要なFASTLANE_USER, MATCH_PASSWORD, FASTLANE_PASSWORDは、BitriseのSecretsに設定
  • fastlane match
    • リリース版アプリの署名用のcertificates & provisioning profileを取得するStep*6
    • "fastlane lane"に"match appstore"を設定
  • Run CocoaPods install

このワークフローは、ワークフローごとにパラメータや処理に違いはありません。

Unitテスト実行&アプリのビルド(_BuildAndUnitTest)

_BuildAndUnitTestのStep

  • Set Xcode Project Build Number
    • Bitriseのビルド番号をアプリのBundle Versionとして設定するStep
    • 配布したアプリと対応するBitriseのビルドが分かりやすくなり、問題が起きた時の追跡などの時に便利です
    • "Info.plist file path"に"$BITRISE_SOURCE_DIR/$INFO_PLIST_PATH"を設定
      • AndpadではDevelop版とRelease版でInfo.plistが違うので、フローごとに切り替える必要がある
  • Xcode Test for iOS
    • Unit Testを実行するStep
    • Schemeとして、"andpad_develop"を設定
    • Simulator Deviceには、"iPhone Xs Max"を設定
  • Xcode Archive & Export for iOS
    • アプリをビルドするStep
    • "Rebuild from bitcode"を"no"に設定
      • export methodが"development"の時にbitcodeを生成するかのオプション
      • 內部配信の場合、bitcodeをリビルドする意味があまりない*7 かつ ビルド時間短縮のために、Offにしている
    • "Select method for export"に"$BITRISE_EXPORT_METHOD"を設定
    • "Configuration name"に"$BUILD_CONFIG"を設定
    • "iCloud container environment"に"$ICLOUD_CONTAINER_ENVIRONMEN"を設定
  • IPA info
    • ビルドしたアプリの情報をBitriseの環境変数に設定してくれるStep
    • ここで設定される$IOS_IPA_PACKAGE_NAME(値はBundle Identifier)をビルド結果をSlackに通知する時に使う

UIテスト実行(_UITest)

_UITestを構成するStep

  • Xcode Build for testing for iOS
    • UIテスト用のアプリをビルドするStep
    • "Configuration name"に"$BUILD_CONFIG"を設定
    • "Scheme name"に"$BITRISE_UITEST_SCHEME"を設定
  • iOS Device Testing
    • UIテストをFirebase Test Labで実行するStep
    • "Test devices"に"iphonexs,12.3,ja,portrait"を設定(iPadや複数OSバージョンをTestするようにする予定です))

ビルドの後始末(_AfterCommonWorkflow)

_AfterCommonWorkflowを構成するStep

  • Bitrise.io Cache: Push
  • Deploy to Bitrise.io - Apps, Logs, Artifacts*8
    • Slackで結果を通知しているのでEmail通知は無効化
      • "Notify: User Roles"を"none"に設定
  • Send a Slack message
    • ワークフローごとに通知先のチャンネルを切り替えられるよう設定
      • "Slack API token"に$SLACK_API_TOKENを設定($SLACK_API_TOKENの値はBitriseのSecretsで設定)
      • "Target Slack channel"に$SLACK_TARGET_CHANNELを設定
        • Web Hook URLを使わないのは、Web Hook URLだとチャンネルの数だけSecrets変数が設定が必要なため
    • SlackのメッセージにBundle IDを含めるように設定
      • "A list of fields to be displayed in a table inside the attachment"に次の内容を記述。
      • App|${BITRISE_APP_TITLE}
        BundleID|${IOS_IPA_PACKAGE_NAME}
        Branch|${BITRISE_GIT_BRANCH}
        Workflow|${BITRISE_TRIGGERED_WORKFLOW_ID}

      • BundleIDを含めるのは、ビルドされたのがDevelop版なのかRelease版なのかを区別するため

内部配信ワークフロー

Steps

內部配信ワークフローを構成するStep

ワークフローの大まかな構成を示します。 - BeforeCommonWorkflow - BuildAndUnitTest - 內部配信ワークフロー本体のStep - UITest - AfterCommonWorkflow

長く見えますが、ほぼ前述のユーテリティワークフローで構成されています。 このワークフロー本体のStepは、Firebase App Distributionのみです。

  • Firebase App Distribution*9
    • ビルドしたアプリをFirebase App Distributionに配布するStep
    • "Firebase Token"には、Firebaseの認証トークンを設定
    • "Release Notes"には以下を設定
      • $GIT_CLONE_COMMIT_MESSAGE_SUBJECT

        $GIT_CLONE_COMMIT_MESSAGE_BODY

    • "Test Groups"には、Firebase App DistributionのTester Groupを設定
    • "Firebase App ID"には、Firebase上のアプリのIDを設定

環境変数の設定

本体のStepが少ない分、環境変数の設定が重要になるので、示しておきます。

環境変数 用途 使っているワークフロー
BITRISE_PROJECT_PATH xcworkspaceファイルの場所を指定 andpad.xcworkspace BuildAndUnitTest, UITest
BITRISE_SCHEME アプリのビルドSchemeを指定 andpad_develop _BuildAndUnitTest
BITRISE_EXPORT_METHOD アプリのビルド時のExport Methodを指定 development _BuildAndUnitTest
SLACK_TARGET_CHANNEL ビルド結果の通知先のSlackチャンネル名 #andpad-app-build _AfterCommonWorkflow
ICLOUD_CONTAINER_ENVIRONMENT アプリビルド時のiCloud Containerを指定 Development _BuildAndUnitTest
BITRISE_UITEST_SCHEME UIテストアプリのビルドSchemeを指定 andpadUITestsDevelop _UITest
BUILD_CONFIG アプリのビルドconfigを指定 Debug BuildAndUnitTest, UITest
INFO_PLIST_PATH Info.plistの場所を指定 andpad-develop-Info.plist _AfterCommonWorkflow

Store配信ワークフロー

Steps

Store配信ワークフローを構成するStep

ワークフローの全体構成を示します。 - BeforeCommonWorkflow - BuildAndUnitTest - Store配信ワークフロー本体のStep - UITest - AfterCommonWorkflow

このワークフロー本体のStepは、Deploy to iTunes Connectのみです。

  • Deploy to iTunes Connect
    • ビルドしたアプリをStoreに配布するStep
    • "Apple ID"に、Storeにアップロードする権限のあるユーザIDを設定
    • "Password"には、Storeにアップロードする権限のあるユーザのパスワードを設定
    • "Team ID"には、iTunes ConnectのアプリのTeam IDを設定
    • "App Bundle ID"には、アプリのBundle IDを設定。
      • _BuildAndUnitTestのStep "IPA Info"のおかげで$IOS_IPA_PACKAGE_NAMEが使えるので、それを設定

環境変数の設定

本体のStepが少ない分、環境変数の設定が重要になるので、示しておきます。

環境変数 用途 使っているワークフロー
BITRISE_PROJECT_PATH xcworkspaceファイルの場所を指定 andpad.xcworkspace _BuildAndUnitTest, _UITest
BITRISE_SCHEME アプリのビルドSchemeを指定 andpad _BuildAndUnitTest
BITRISE_EXPORT_METHOD アプリのビルド時のExport Methodを指定 app-store _BuildAndUnitTest
SLACK_TARGET_CHANNEL ビルド結果の通知先のSlackチャンネル名 #andpad-app-build _AfterCommonWorkflow
ICLOUD_CONTAINER_ENVIRONMENT アプリビルド時のiCloud Containerを指定 Production _BuildAndUnitTest
BITRISE_UITEST_SCHEME UIテストアプリのビルドSchemeを指定 andpadUITests _UITest
BUILD_CONFIG アプリのビルドconfigを指定 Release _BuildAndUnitTest, _UITest
INFO_PLIST_PATH Info.plistの場所を指定 andpad/Info.plist _AfterCommonWorkflow

Nightly UI Testワークフロー

Steps

Nightly UI Testワークフローを構成するStep

ワークフローの全体構成を示します。 - BeforeCommonWorkflow - UITest - _AfterCommonWorkflow

このワークフローは、本体のStepはなしです。

環境変数の設定

環境変数の設定は內部配信のワークフローとほぼ同じです。違いだけ示します。

環境変数 用途 使っているワークフロー
BITRISE_UITEST_SCHEME UIテストアプリのビルドSchemeを指定 andpadNightlyUITests _UITest
SLACK_TARGET_CHANNEL ビルド結果の通知先のSlackチャンネル名 #team-qa-mobile-cicd _AfterCommonWorkflow
IOS_IPA_PACKAGE_NAME Slackで通知のアプリのBundle ID jp.reformpad.ios.develop _AfterCommonWorkflow

Slackの通知先のチャンネルを、弊社のアプリのCI/CDを整備する活動のチャンネルにしています。 定期実行のテスト結果を、配信用のチャンネルに通知すると、配信の情報が埋もれるためです。

補足

このワークフローは、追加したばかりのUIテストの安定性を確認するために、毎日定時に実行しています。

UIテストはUnitテストに比べると不安定です*10。 そのため、最初から內部配信ワークフローやStore配信ワークフローに組み込んでしまうと、ビルドエラーの頻度が上がります。 そうなってしまうと、UIテストのせいで却って開発効率が落ちてしまいます。

そこで、毎日定時にUIテストを実行し、連続で7回成功したら、配信ワークフローに組み込む運用にしています。 それであれば、配信ワークフローで実行するUIテストを安定したものだけに限定できます*11。 全UIテストを実行するSchemeを"andpadNightlyUITests"と、配信ワークフロー用のUI TestのSchemeを"andpadUITests", "andpadUITestsDevelop"に分けることで、この運用を実現しています。

参考情報

参考書籍

本ワークフローを構築にするにあたって、以下の書籍の「9章 CI/CD」を参考にしています*12

iOSテスト全書

ここで紹介したワークフローは、書籍で紹介されているワークフローを大分デチューンしています。 もっとしっかりしたワークフローを組みたいという方は、この本がおすすめです。テストやCI/CDのそもそも論も学べます。

Bitriseの設定のソースコード

---
format_version: '8'
default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
project_type: ios
trigger_map:
- push_branch: develop
  workflow: InternalRelease
- push_branch: master
  workflow: Release
- push_branch: test/*
  workflow: RunAllUITests
workflows:
  InternalRelease:
    steps:
    - firebase-app-distribution@0.5.0:
        inputs:
        - firebase_token: "$FIREBASE_TOKEN"
        - release_notes: |-
            $GIT_CLONE_COMMIT_MESSAGE_SUBJECT

            $GIT_CLONE_COMMIT_MESSAGE_BODY
        - groups: Andpad
        - app: "$FIREBASE_APP_ID"
    before_run:
    - _BeforeCommonWorkflow
    - _BuildAndUnitTest
    after_run:
    - _UITest
    - _AfterCommonWorkflow
  Release:
    steps:
    - deploy-to-itunesconnect-deliver@2.17.0:
        inputs:
        - itunescon_user: "<アプリのアップロードする権限を持つユーザID>"
        - team_id: <アプリのTeamID>
        - bundle_id: "$IOS_IPA_PACKAGE_NAME"
        - password: "<アプリのアップロードする権限を持つユーザのパスワード>"
    envs:
    - opts:
        is_expand: false
      BITRISE_SCHEME: andpad
    - opts:
        is_expand: false
      BITRISE_EXPORT_METHOD: app-store
    - opts:
        is_expand: false
      APP_BUNDLE_ID: jp.reformpad.ios.production
    - opts:
        is_expand: false
      FASTLANE_MATCH_TYPE: appstore
    - opts:
        is_expand: false
      ICLOUD_CONTAINER_ENVIRONMENT: Production
    - opts:
        is_expand: false
      BITRISE_UITEST_SCHEME: andpadUITests
    - opts:
        is_expand: false
      BUILD_CONFIG: Release
    - opts:
        is_expand: false
      INFO_PLIST_PATH: andpad/Info.plist
    before_run:
    - _BeforeCommonWorkflow
    - _BuildAndUnitTest
    after_run:
    - _UITest
    - _AfterCommonWorkflow
  _BeforeCommonWorkflow:
    steps:
    - activate-ssh-key@4.0.5:
        run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
    - git-clone@4.0.17: {}
    - cache-pull@2.1.3: {}
    - fastlane@2.7.0:
        inputs:
        - lane: match development
        title: fastlane match
    - fastlane@2.7.0:
        inputs:
        - lane: match appstore
        title: fastlane match
    - cocoapods-install@1.10.0: {}
  _AfterCommonWorkflow:
    steps:
    - cache-push@2.2.3: {}
    - deploy-to-bitrise-io@1.9.5:
        inputs:
        - notify_user_groups: none
    - slack@3.1.3:
        inputs:
        - channel: "$SLACK_TARGET_CHANNEL"
        - fields: |
            App|${BITRISE_APP_TITLE}
            BundleID|${IOS_IPA_PACKAGE_NAME}
            Branch|${BITRISE_GIT_BRANCH}
            Workflow|${BITRISE_TRIGGERED_WORKFLOW_ID}
        - api_token: "$SLACK_API_TOKEN"
    before_run: []
  _BuildAndUnitTest:
    steps:
    - set-xcode-build-number@1.0.9:
        inputs:
        - plist_path: "$BITRISE_SOURCE_DIR/$INFO_PLIST_PATH"
    - xcode-test@2.4.3:
        inputs:
        - generate_code_coverage_files: 'yes'
        - scheme: "andpad_develop"
        - simulator_device: iPhone Xs Max
    - xcode-archive@2.7.1:
        inputs:
        - compile_bitcode: 'no'
        - export_method: "$BITRISE_EXPORT_METHOD"
        - configuration: "$BUILD_CONFIG"
        - icloud_container_environment: "$ICLOUD_CONTAINER_ENVIRONMENT"
    - ipa-info@1.1.0: {}
    before_run: []
    after_run: []
  _UITest:
    steps:
    - xcode-build-for-test@0.4.0:
        inputs:
        - configuration: "$BUILD_CONFIG"
        - scheme: "$BITRISE_UITEST_SCHEME"
    - virtual-device-testing-for-ios@0.9.10:
        inputs:
        - test_devices: iphonexs,12.3,ja,portrait
    before_run: []
    after_run: []
  NightlyUITests:
    envs:
    - opts:
        is_expand: false
      SLACK_TARGET_CHANNEL: "#team-qa-mobile-cicd"
    - opts:
        is_expand: false
      BITRISE_UITEST_SCHEME: andpadNightlyUITests
    - opts:
        is_expand: false
      IOS_IPA_PACKAGE_NAME: jp.reformpad.ios.develop
    before_run:
    - _BeforeCommonWorkflow
    - _UITest
    after_run:
    - _AfterCommonWorkflow
app:
  envs:
  - opts:
      is_expand: false
    BITRISE_PROJECT_PATH: andpad.xcworkspace
  - BITRISE_SCHEME: andpad_develop
    opts:
      is_expand: false
  - opts:
      is_expand: false
    BITRISE_EXPORT_METHOD: development
  - opts:
      is_expand: false
    SLACK_TARGET_CHANNEL: "#andpad-app-build"
  - opts:
      is_expand: false
    ICLOUD_CONTAINER_ENVIRONMENT: Development
  - opts:
      is_expand: false
    BITRISE_UITEST_SCHEME: andpadUITestsDevelop
  - opts:
      is_expand: false
    BUILD_CONFIG: Debug
  - opts:
      is_expand: false
    INFO_PLIST_PATH: andpad-develop-Info.plist
  - opts:
      is_expand: false
    FIREBASE_APP_ID: "<FirebaseのアプリのID>"
meta:
  bitrise.io:
    machine_type: elite

*1:アプリ配布後にUI Testを実行させているのは、アプリ外の要因でUIテストが失敗することがあるためです。テストが失敗した時、ログを見て外部要因であれば、アプリの再配信はしない運用にしています。

*2:masterにマージする前に、Develop版アプリを使って手動テストを実施しています。それで問題なければ、masterにマージしています。

*3:詳細は https://devcenter.bitrise.io/bitrise-cli/workflows/#utility-workflows を参照

*4:これがユーテリティワークフローの良い所です。手動でビルドを開始する時などにワークフローリストに表示されなくなるので見通しが良くなります。

*5:fastlane matchを使っているのは、Bitrise導入前からfastlane matchでcertificates & provisioning profileを管理していたのを踏襲したからです。

*6:Develop版とリリース版アプリの両方のcertificates & provisioning profileが必要になるケースがある

*7:bitcodeは、Storeにアップロードした時に、Apple側で最適化をするためのもの。

*8:iOSテスト全書の事後ワークフローに含まれていませんが、個人的には含めておいた方が良いと思います。Unitテストの結果がBitriseのTest Reportで見れる、ビルドログやビルドしたアプリがArtifactに保存される、BitriseのSlackやメールの通知からアプリをインストールできるなどの利点があります。

*9:設定の意味の詳細は、 https://firebase.google.com/docs/app-distribution/ios/distribute-cli?hl=ja を参照。

*10:通信が発生する、OSバージョン、Deviveの置かれた環境に依存するためです。弊社固有の事情ですが、最近まで弊社にモバイルアプリのUIテストのノウハウがなかったことも原因です

*11:なお、OSのバージョンアップ等で、配信ワークフローに組み込んだUIテストが不安定になってしまった場合は、配信ワークフローから外す運用です

*12:弊社のテスト面の技術顧問の平田さんが記述しています