受託開発での iOS アプリプロジェクト新規作成プラクティス（下編：Bitrise 編）

# 前書き

1. 本記事は**受託開発**前提で書いております。そのため、受託開発における特殊な要件がいくつかあります。もちろん通常の開発にも通用する部分は多いですが、どこまで流用するかは読者の皆さん自身にご判断をゆだねます。
2. 本記事のサンプルとして使われたプロジェクトのバイナリデプロイ[^プロジェクトデプロイ]は[こちらの GitHub リリースページ](https://github.com/el-hoshino/EnterpriseProjectSample/releases) から確認できます。
3. 本記事は下編：Bitrise 編で、CI 運用について説明します。Xcode プロジェクトの作成については[上編](https://qiita.com/lovee/items/38cdfd5d2f5827f27427)をご覧ください。

[^プロジェクトデプロイ]: 本来は Bitrise Public App として公開する予定でしたが、その場合はユーザが設定した Bitrise Secret を Pull Request に公開できず、Danger を利用する CI がコケることが発覚したため、Bitrise Private App として作る必要がありました。その代わりに書き出したバイナリのインストールページだけ公開できました。

# 要件

1. 基本開発環境は [Xcode](https://developer.apple.com/xcode/) を利用します。
2. 基本 CI/CD 環境は [Bitrise](https://www.bitrise.io) を利用します。なお本記事も Bitrise アカウントを所有している前提で書かれており、登録についての説明は割愛します。
3. 動作環境は社内開発環境（Development）、納品先検証環境（Staging）と本番環境（Production）の三つがあると想定します。
4. 受託開発のため、社内開発環境と納品／本番環境はそれぞれ違う Bundle ID や開発者証明書を利用する[^証明書]と想定します。
5. PR のチェック等はビルド時間短縮のためなるべくライブラリーのキャッシュを使いたいが、デプロイはなるべくキャッシュを一切使わずにクリーンなビルドを行います；また bootstrap.sh スクリプトの確認も同時に行いたいです。
5. 開発メンバーの入れ替えがそれなりに発生すると想定されるため、コードレビューは機械的にレビューできる事はなるべく機械に任せ、品質担保の属人化をある程度避けたいです。
6. PR が発生するたびに、機械によるコードレビュー込みの CI テストが動きます；ただし通常は CI 独自のスクリプトでキャッシュをめいいっぱい利用して大丈夫ですが、master ブランチへの PR のみ新規メンバーのための bootstrap スクリプトの有効性の確認のため、該当スクリプトを利用します；また、必要に応じて、開発メンバーによる動作確認用のバイナリも配布します。
7. PR がマージされるたびに、QA チームや納品先への動作確認用のビルドを配布します。

[^証明書]: Apple の開発者登録は機種ごとに年間最大 100 台までしか登録できず、そのため自社にクライアント様のデバイスを登録するのも、クライアント様に自社のデバイスを登録するのも何かとデバイスの登録上限が気になる事が多いです。（もちろん技術上 Enterprise プログラムで In-House 配布すれば開発端末登録の問題が無くなりますが、ライセンス上発注会社の Enterprise 証明書で In-House 配布して受託開発会社にインストールさせるのは OK ですが、逆に受託開発会社の Enterprise 証明書で In-House 配布して発注会社にインストールさせるのは NG です；そして発注会社がわざわざ App Store 配布不可の Enterprise プログラムに登録するのは非常にレアなケースと考えられます。）

# 手順

## 1. Dangerfile を追加

[Danger](https://github.com/danger/danger) は CI/CD 環境で利用する機械的なコードレビューツールです。そのツールとしては Dangerfile の利用が必要になります。

そもそもまだ CI も設定していないのにいきなりチェックの話になるのは話の順番が前後してしまう形になりますが、ただ[要件](#要件)でも説明した通り、大まかなチェックルートは決まってありますので、一先ず Dangerfile の内容を簡単に説明します。

```ruby:Dangerfile
# PR チェック結果を定義
class CheckResult

    attr_accessor :warnings, :errors, :title, :message

    def initialize(title)
        @warnings = 0
        @errors = 0
        @title = "## " + title
        @message = markdown_message_template
    end

    def markdown_message_template
        template = "確認項目 | 結果\n"
        template << "|--- | --- |\n"
        return template
    end

end

# Xcode_summary を導入して確認
def common_xcode_summary_check
    
    xcode_summary.ignored_files = 'Pods/**'
    xcode_summary.inline_mode = true
    xcode_summary.report 'xcodebuild.json'
    
end

# SwiftLint を導入して確認
def common_swiftlint_check
    
    swiftlint.config_file = '.swiftlint.yml'
    swiftlint.lint_files inline_mode: true
    
end

def is_develop_pr

    ## とりあえず develop 向けの PR は develop PR とみなす
    is_to_develop = github.branch_for_base == "develop"
    if is_to_develop
        return true
    else
        return false
    end

end

def is_release_pr

    ## とりあえず master 向けの PR は release PR とみなす
    is_to_master = github.branch_for_base == "master"
    if is_to_master
        return true
    else
        return false
    end

end

# develop PR レビュールーチン
def develop_pr_check

    result = CheckResult.new("develop PR チェック")

    ## PR は `feature/`、`refactor/` 、`fix/` もしくは `issue/` で始まるブランチから出す
    result.message << "PR From ブランチ確認 |"
    is_from_feature = github.branch_for_head.start_with?("feature/")
    is_from_refactor = github.branch_for_head.start_with?("refactor/")
    is_from_fix = github.branch_for_head.start_with?("fix/")
    is_from_issue = github.branch_for_head.start_with?("issue/")
    if is_from_feature || is_from_refactor || is_from_fix || is_from_issue
        result.message << ":o:\n"
    else
        fail "デベロップ PR は Feature、Refactor、Fix もしくは Issue ブランチから出してください。"
        result.message << ":x:\n"
        result.errors += 1
    end

    ## PR は `develop` ブランチへ出す
    result.message << "PR To ブランチ確認 |"
    is_to_develop = github.branch_for_base == "develop"
    if is_to_develop
        result.message << ":o:\n"
    else
        fail "デベロップ PR は develop ブランチへマージしてください。"
        result.message << ":x:\n"
        result.errors += 1
    end

    ## コミットにマージコミットを含めてはいけない
    result.message << "マージコミット無し確認 |"
    contains_merge_commits = git.commits.any? { |c| c.parents.length > 1 }
    unless contains_merge_commits
        result.message << ":o:\n"
    else
        fail "デベロップ PR は他のブランチをマージしないでください；必要に応じてリベースしてください。"
        result.message << ":x:\n"
        result.errors += 1
    end

    ## PR の修正 1,000 行超えてはいけない
    result.message << "修正量確認 |"
    is_fix_too_big = git.lines_of_code > 1_000
    unless is_fix_too_big
        result.message << ":o:\n"
    else
        warn "修正が多すぎます。PR を小さく分割してください。"
        result.message << ":heavy_exclamation_mark:\n"
        result.warnings += 1
    end
    
    ## Brewfile もしくは Mintfile に修正を加えたら、Bitrise のキャッシュ設定の更新ワーニングを出す
    result.message << "Brewfile 修正確認 |"
    contains_brewfile_modification = git.modified_files.include? "Brewfile"
    unless contains_brewfile_modification
        result.message << ":o:\n"
    else
        message "Brewfile に修正が入っています。Bitrise の Cache 設定の更新を忘れずに更新しましょう。"
        result.message << ":heavy_exclamation_mark:\n"
    end
    
    return result
    
end

# リリース時 master ブランチがマージされる時の PR レビュールーチン
def release_pr_check

    result = CheckResult.new("リリース PR チェック")

    ## PR は `develop` ブランチから出す
    result.message << "PR From ブランチ確認 |"
    is_from_develop = github.branch_for_head == "develop"
    if is_from_develop
        result.message += ":o:\n"
    else
        fail "リリース PR は develop ブランチから出してください。"
        result.message += ":x:\n"
        result.errors += 1
    end

    ## PR は `master` ブランチへ出す
    result.message << "PR To ブランチ確認 |"
    is_to_master = github.branch_for_base == "master"
    if is_to_master
        result.message += ":o:\n"
    else
        fail "リリース PR は master ブランチへ出してください。"
        result.message += ":x:\n"
        result.errors += 1
    end

    return result

end

# Main routine

## SwiftLint のワーニング等確認
common_swiftlint_check

## Xcode Summary のワーニング等確認
common_xcode_summary_check

## チェックルーチンの設定
if is_develop_pr
    check_result = develop_pr_check
elsif is_release_pr
    check_result = release_pr_check
end

if check_result
    markdown(check_result.title)
    markdown(check_result.message)

    if check_result.errors == 0 && check_result.warnings == 0
        message "よくできました:white_flower:"
    end

else
    fail "チェックルーチンが設定されていない PR です。PR を確認してください。"
end
```

まず最初に出てくる `CheckResult` ですが、これは Danger で GitHub にチェックの結果をコメントで上げるためのテキストフォーマットを管理するクラスです。チェック項目がパスされているかどうかをまとめて書き出します。書き出しの結果は行うチェックによりますが大体こんな形になります：

![スクリーンショット 2019-01-30 15.59.34.png](https://qiita-image-store.s3.amazonaws.com/0/63998/fab0a498-4fad-a52a-42ef-fd67ec3822f8.png)

次に `common_xcode_summary_check` と `common_swiftlint_check` 関数は、Xcode-Summary と Swiftlint の Danger Plugin を使って、それぞれ Xcode によるチェック結果と Swiftlint によるチェック結果を、PR にコメントとして書き出すものです。結果にもよりますが大体こんな感じにコメントしてくれます：

![スクリーンショット 2019-01-29 16.57.29.png](https://qiita-image-store.s3.amazonaws.com/0/63998/b494eb7f-98aa-2554-08fe-8ffe1cb8238e.png)

次に `is_xxx_pr` や `xxx_pr_check` が PR の種類の確認と、それぞれに応じた確認項目の設定です。今 2 種類の確認があります：master ブランチへのマージと、それ以外のマージです。前者は develop からのマージのみを許容する、と言う確認を行うのでやることはあまりないです。後者に関してはさらに Rebase[^rebase] しているかどうか（正確には Merge を使っていないかどうか）と、修正量の確認も一緒に見ています。また、これは後述する [Bitrise のキャッシュの仕様](#4-_push-cache)とも関係ありますが、Brewfile の修正があるかどうかも確認し、もしあったら Bitrise のキャッシュにも修正入れるよう催すメッセージも出力します。

[^rebase]: 基本的に Rebase は元のコミットを壊すので、一人開発するときにメリットはほとんどなく、またネットで検索するとよく「[Rebase するな](https://frasco.io/why-you-should-stop-using-git-rebase-535fa30d7e25)」的な記事が目にしますが、ところがチームで開発するときに、Rebase をしないと差分の変更が非常にわかりにくいので、レビュアーへの負担が高いので、Rebase を推奨しています。

## 2. Bitrise で新規 App を追加

Bitrise の Dashboard に行けば「Add New App」のボタンがありますので、それで新しい App を追加します。

<img width="493" alt="スクリーンショット 2019-01-28 17.37.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/63998/3b6a49ae-361a-a10e-a326-d6763187f704.png">

その後は基本 Bitrise の手順通りに進めていけば問題ないはずです。Bitrise は GitHub や GitLab 等の主流なホストサービスを対応しており、アカウントの連携が設定されていれば Webhook の設定など含めてスムーズに App を設定できます；また対応していないサービスでも直接リポジトリーの URL から設定できるので、手動で Webhook  設定すれば OK です。

また、もし[上編通りにプロジェクトを設定](https://qiita.com/lovee/items/38cdfd5d2f5827f27427#9-scheme-の設定)していたら、App の設定途中で Scheme を選択することが出てくるかと思いますが、どれを選んでも大丈夫です（後で YAML ファイルを完全に上書きするので）。

## 3. バイナリ書き出しに必要な証明書をアップロード

Bitrise は手軽に署名関係のものをやってくれるスクリプトを提供してくれていますので、Workflow タグの Code Signing に行ってスクリプトを実行すれば OK です。

![スクリーンショット 2019-01-29 14.58.32.png](https://qiita-image-store.s3.amazonaws.com/0/63998/2c6067fb-ad3c-d6a1-4c14-c3c8bb387ed6.png)

ただこのスクリプトを実行する前に、まずは Xcode の Archive 命令で、CI/CD での配布を想定しているバイナリを一回書き出しておいたほうがいいでしょう。例えば Development 環境の Ad Hoc 配布とか、Staging 環境の Enterprise 配布とかです。一回書き出しておかないと、スクリプトだけで必要な Provisioning Profile が検出できないことがあります。ここでお勧めするのは**開発メンバー用の Development 配布**[^dev-dev-dis]と、**QA チームもしくは発注元の動作確認用の配布**[^ad-hoc-dis]です。これらは大体 Bitrise からの配布にしたいと考えています。あとは余力があれば Production 環境の iOS App Store 配布もやっておくのもいいかもしれません（現状社内プロジェクトではまだ Bitrise を使った App Store 配布の例がないので、詳しい説明は割愛します）

[^dev-dev-dis]: 基本的には Development 環境の Development 配布になります。Development 配布のメリットは LLDB が使えるので、開発メンバーとしての動作確認がしやすく、PR のための動作確認に便利です。

[^ad-hoc-dis]: 基本的には Development 環境、Staging 環境及び Production 環境の Ad Hoc 配布もしくは Enterprise 配布になると思います。Ad Hoc 及び Enterprise 配布は LLDB によるデバッグができないので、基本的には開発メンバー以外への配布に使います。Ad Hoc を利用するのか、Enterprise を利用するのかはプロジェクトによって決めれば大丈夫かと思います。

## 4. Bitrise を設定

細かく説明するのは非常に大変なので、現在の設定 YAML ファイルを上げながら、大事なところだけピックアップして細かく解説していきます：

```yml:bitrise.yml
---
format_version: '6'
default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
project_type: ios
trigger_map:
- push_branch: develop
  workflow: dev-ad_hoc-deploy
- pull_request_source_branch: "*"
  pull_request_target_branch: master
  workflow: clean-test
- pull_request_source_branch: "*--a"
  workflow: fast-test-with-deploy
- pull_request_source_branch: "*"
  workflow: fast-test
- tag: de*.*.*
  workflow: dev-enterprise-deploy
- tag: sh*.*.*
  workflow: stg-ad_hoc-deploy
- tag: ph*.*.*
  workflow: pro-ad_hoc-deploy
workflows:
  fast-test:
    envs:
    - opts:
        is_expand: false
      BITRISE_SCHEME: EnterpriseProjectSample-Development
    - opts:
        is_expand: false
      ADDITIONAL_XCODE_BUILD_OPTIONS: EXTRA_SWIFT_ACTIVE_COMPILATION_CONDITIONS=TEST_ON_CI
    before_run:
    - _preparation
    - _pull-cache
    - _install-dependencies
    - _pr-prebuild
    - _push-cache
    after_run:
    - _xcode-test-terminal
    - _send-slack-message
  fast-test-with-deploy:
    envs:
    - opts:
        is_expand: false
      BITRISE_SCHEME: EnterpriseProjectSample-Development
    - opts:
        is_expand: false
      ADDITIONAL_XCODE_BUILD_OPTIONS: EXTRA_SWIFT_ACTIVE_COMPILATION_CONDITIONS=TEST_ON_CI
    - opts:
        is_expand: false
      XCODE_EXPORT_METHOD: development
    before_run:
    - _preparation
    - _pull-cache
    - _install-dependencies
    - _pr-prebuild
    - _push-cache
    - _deploy-modification
    after_run:
    - _xcode-test-terminal
    - _archive-export
    - _deploy-to-bitrise
    - _comment-on-github
    - _send-slack-message
  clean-test:
    envs:
    - opts:
        is_expand: false
      BITRISE_SCHEME: EnterpriseProjectSample-Development
    - opts:
        is_expand: false
      ADDITIONAL_XCODE_BUILD_OPTIONS: EXTRA_SWIFT_ACTIVE_COMPILATION_CONDITIONS=TEST_ON_CI
    before_run:
    - _preparation
    - _bootstrap
    - _pr-prebuild
    after_run:
    - _xcode-test-terminal
    - _send-slack-message
  dev-ad_hoc-deploy:
    envs:
    - opts:
        is_expand: false
      BITRISE_SCHEME: EnterpriseProjectSample-Development
    - opts:
        is_expand: false
      XCODE_INFO_PLIST_PATH: EnterpriseProjectSample/Info.plist
    - opts:
        is_expand: false
      XCODE_EXPORT_METHOD: ad-hoc
    before_run:
    - _preparation
    - _pull-cache
    - _install-dependencies
    - _push-cache
    - _deploy-modification
    after_run:
    - _archive-export
    - _deploy-to-bitrise
    - _send-slack-message
  dev-enterprise-deploy:
    envs:
    - opts:
        is_expand: false
      BITRISE_SCHEME: EnterpriseProjectSample-Development
    - opts:
        is_expand: false
      XCODE_INFO_PLIST_PATH: EnterpriseProjectSample/Info.plist
    - opts:
        is_expand: false
      XCODE_EXPORT_METHOD: enterprise
    before_run:
    - _preparation
    - _pull-cache
    - _install-dependencies
    - _push-cache
    - _deploy-modification
    after_run:
    - _archive-export
    - _deploy-to-bitrise
    - _send-slack-message
  stg-ad_hoc-deploy:
    envs:
    - opts:
        is_expand: false
      BITRISE_SCHEME: EnterpriseProjectSample-Staging
    - opts:
        is_expand: false
      XCODE_INFO_PLIST_PATH: EnterpriseProjectSample/Info.plist
    - opts:
        is_expand: false
      XCODE_EXPORT_METHOD: ad-hoc
    before_run:
    - _preparation
    - _bootstrap
    - _deploy-modification
    after_run:
    - _archive-export
    - _deploy-to-bitrise
    - _send-slack-message
  pro-ad_hoc-deploy:
    envs:
    - opts:
        is_expand: false
      BITRISE_SCHEME: EnterpriseProjectSample-Production
    - opts:
        is_expand: false
      XCODE_INFO_PLIST_PATH: EnterpriseProjectSample/Info.plist
    - opts:
        is_expand: false
      XCODE_EXPORT_METHOD: ad-hoc
    before_run:
    - _preparation
    - _bootstrap
    - _deploy-modification
    after_run:
    - _archive-export
    - _deploy-to-bitrise
    - _send-slack-message
  _preparation:
    steps:
    - activate-ssh-key:
        run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
    - git-clone: {}
  _bootstrap:
    steps:
    - script:
        title: Set Git Credential
        inputs:
        - content: |
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            # write your script here
            echo "Set Git Credential"

            git credential-osxkeychain store
            host=github.com
            protocol=https
            password="${CARTHAGE_GITHUB_API_TOKEN}"
    - script:
        title: Bootstrap
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            # write your script here
            echo "Bootstrap"
            sh bootstrap.sh
    before_run: []
  _pull-cache:
    steps:
    - cache-pull: {}
    before_run: []
  _push-cache:
    steps:
    - script:
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            # write your script here
            set -ev

            envman add --key GEM_HOME --value "$(gem environment gemdir)"
        title: Gem Cache Settings
    - script:
        title: Brew Cache Settings
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            # write your script here
            set -ev

            #echo "$(brew bundle list)"

            #bundle_paths=""
            #install_paths=""
            cellar_path=$(brew --cellar)
            install_path="/usr/local/opt"
            #bundles=$(brew bundle list)

            #for bundle in ${bundles[@]}; do
            #  bundle_paths+=$cellar_path/$bundle$'\n'
            #  install_paths+=$install_path/$bundle$'\n'
            #done

            #envman add --key BREW_BUNDLE_PATHS --value $bundle_paths
            #envman add --key BREW_INSTALL_PATHS --value $install_paths

            envman add --key BREW_SWIFTLINT_BUNDLE --value $cellar_path/swiftlint
            envman add --key BREW_SWIFTLINT_INSTALL --value $install_path/swiftlint

            envman add --key BREW_CARTHAGE_BUNDLE --value $cellar_path/carthage
            envman add --key BREW_CARTHAGE_INSTALL --value $install_path/carthage

            envman add --key BREW_MINT_BUNDLE --value $cellar_path/mint
            envman add --key BREW_MINT_INSTALL --value $install_path/mint
    - script:
        title: Mint Cache Settings
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            # write your script here
            set -ev

            envman add --key MINT_HOME_PATH --value "/usr/local/lib/mint"
    - cache-push:
        run_if: ".IsCI"
        inputs:
        - compress_archive: 'true'
        - cache_paths: |-
            $BITRISE_CACHE_DIR
            $GEM_HOME
            $BREW_SWIFTLINT_BUNDLE
            $BREW_SWIFTLINT_INSTALL
            $BREW_CARTHAGE_BUNDLE
            $BREW_CARTHAGE_INSTALL
            $BREW_MINT_BUNDLE
            $BREW_MINT_INSTALL
            $MINT_HOME_PATH
    before_run: []
  _install-dependencies:
    steps:
    - script:
        inputs:
        - content: |-
            #!/usr/bin/env bash

            # fail if any commands fails
            set -e
            # debug log
            set -x

            # write your script here
            echo "Install Dependencies via Gem"
            bundle install
        title: Gem Dependencies
    - script:
        title: Brew Dependencies
        inputs:
        - content: |-
            #!/usr/bin/env bash

            # fail if any commands fails
            set -e
            # debug log
            set -x

            # write your script here
            echo "Install Dependencies via Brew"
            brew bundle

            bundles=$(brew bundle list)

            for bundle in ${bundles[@]}; do
              if [ "$(which $bundle)" = "" ]; then
                brew link $bundle
              fi
            done
    - script:
        title: Mint Dependencies
        inputs:
        - content: |-
            #!/usr/bin/env bash

            # fail if any commands fails
            set -e
            # debug log
            set -x

            # write your script here
            echo "Install Dependencies via Mint"
            mint bootstrap
    - cocoapods-install:
        inputs:
        - verbose: 'false'
    - carthage:
        inputs:
        - carthage_options: "--no-use-binaries --cache-builds --platform ios"
        - github_access_token: "$CARTHAGE_GITHUB_API_TOKEN"
        - carthage_command: bootstrap
    before_run: []
  _pr-prebuild:
    steps:
    - script:
        title: Install XCPretty JSON Formatter
        inputs:
        - content: |
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            # write your script here
            gem install xcpretty-json-formatter
    - script:
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            # write your script here

            # Install Danger
            echo "Install Danger"
            gem install danger

            # Install Danger-XcodeSummary
            gem install danger-xcode_summary

            # Install Danger-SwiftLint
            gem install danger-swiftlint
        title: Install Danger with Plugins
    before_run: []
  _deploy-modification:
    before_run: []
    after_run: []
    steps:
    - script:
        title: Retrieve Version String from Git Tag
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            # write your script here
            TAG_VERSION=${BITRISE_GIT_TAG:2}
            if [ -n "$TAG_VERSION" ]; then
              VERSION_STRING=$TAG_VERSION
            else
              VERSION_STRING="0.0.0"
            fi
            envman add --key VERSION_STRING --value $VERSION_STRING
    - set-ios-version:
        inputs:
        - bundle_version: "$BITRISE_BUILD_NUMBER"
        - bundle_version_short: "$VERSION_STRING"
        - info_plist_file: "$XCODE_INFO_PLIST_PATH"
        is_always_run: true
  _xcode-test-terminal:
    steps:
    - script:
        title: Xcode Test
        inputs:
        - content: |
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            # write your script here

            # Xcode Test
            set -o pipefail && env "NSUnbufferedIO=YES" xcodebuild -workspace $XCODE_WORKSPACE_PATH -scheme $BITRISE_SCHEME clean build test -destination 'platform=iOS Simulator,name=iPhone X' GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES $ADDITIONAL_XCODE_BUILD_OPTIONS | XCPRETTY_JSON_FILE_OUTPUT=xcodebuild.json xcpretty -f `xcpretty-json-formatter`
    - script:
        title: Run Danger
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            # write your script here

            # Run danger
            danger
        is_always_run: true
    before_run: []
    description: 通常の Xcode Test Integration 使うと、XCPretty の出力が必ず HTML になってしまい、「-f `xcpretty-json-formatter`」
      を書いても勝手に「"-f" "`xcpretty-json-formatter`"」になるせいで正しくパースできないので、こちらを利用することにしました
  _archive-export:
    steps:
    - certificate-and-profile-installer:
        is_always_run: true
    - xcode-archive:
        inputs:
        - project_path: "$XCODE_WORKSPACE_PATH"
        - scheme: "$BITRISE_SCHEME"
        - compile_bitcode: 'no'
        - team_id: ''
        - export_method: "$XCODE_EXPORT_METHOD"
    before_run: []
  _deploy-to-bitrise:
    steps:
    - deploy-to-bitrise-io:
        inputs:
        - notify_user_groups: none
        is_always_run: false
    - create-install-page-qr-code: {}
    before_run: []
  _send-slack-message:
    steps:
    - slack:
        inputs:
        - image_url: "$BITRISE_PUBLIC_INSTALL_PAGE_QR_CODE_IMAGE_URL"
        - is_debug_mode: 'yes'
        - webhook_url: "$SLACK_INCOMMING_WEBHOOK"
    before_run: []
  _comment-on-github:
    steps:
    - comment-on-github-pull-request:
        inputs:
        - body: |-
            Install this build from here:
            ![Install URL QRCode]($BITRISE_PUBLIC_INSTALL_PAGE_QR_CODE_IMAGE_URL)
        - personal_access_token: "$BITRISE_GITHUB_API_TOKEN"
    before_run: []
app:
  envs:
  - opts:
      is_expand: false
    XCODE_WORKSPACE_PATH: EnterpriseProjectSample.xcworkspace
  - opts:
      is_expand: false
    XCODE_PROJECT_PATH: EnterpriseProjectSample.xcodeproj
```

Bitrise は様々なイベントをトリガーに、指定の Workflow を動かせます。Workflow に基本構成単位は Step で、各 Step は例えば「Git Clone Repository」とかなどの具体的なタスクになります。

ところが Workflow を構築していくと気づいてくると思いますが、似たような Workflow が発生したり、いくつかの Steps が一つのまとまりになったりします。それを全て Workflow ごとに定義していくと、例えば途中でどこかで何か微調整を入れたら、それを全ての Workflow で修正入れないといけないですので非常に非効率的ですし間違いやすいです。それを解決するのは、別の Workflow に組み込まれること前提で作られる Workflow のこと：Utility Workflow です。

Utility Workflow の定義自体は別に難しいことは何もなく、普通の Workflow と同じように定義していって、あとは別の Workflow からそれを追加すれば OK です。また、個人的にはそれを通常の Workflow と区別しやすいように、名前はいつも最初に `_` を入れています。

よって上記の YAML ファイルからみてわかる通り、通常のワークフローは：

- fast-test
- fast-test-with-deploy
- clean-test
- dev-ad_hoc-deploy
- dev-enterprise-deploy
- stg-ad_hoc-deploy
- pro-ad_hoc-deploy

の 7 つで、Utility Workflow は

- _preparation
- _bootstrap
- _pull-cache
- _push-cache
- _install-dependencies
- _pr-prebuild
- _deploy-modification
- _xcode-test-terminal
- _archive-export
- _deploy-to-bitrise
- _send-slack-message
- _comment-on-github

の 12 個です。そしてトリガーの設定としては、

- develop ブランチが更新されたら dev-ad_hoc-deploy
- master への PR があった場合には clean-test
- 上記以外の PR で、元ブランチの名前が `--a` で終わる場合は fast-test-with-deploy
- 上記以外の全ての PR で fast-test
- de\*.\*.\* のタグがつけたれたときは dev-enterprise-deploy
- sh\*.\*.\* のタグがつけられたときは stg-ad_hoc-deploy
- ph\*.\*.\* のタグがつけられたときは pro-ad_hoc-deploy

の 7 つが設定されています。ひとまず Utility Workflow の方を見ていきます。

### 1. _preparation

こちらは特に大したやることがありません、SSH キーを有効化してリポジトリーを落とすだけです。基本的には全ての Workflow が必要とします。

### 2. _bootstrap

こちらは bootstrap.sh ファイルで環境構築を行うための Workflow です。またその際に Carthage のアップデートに GitHub API Token が必要なケースがありますので、それの設定も一緒に行います。

### 3. _pull-cache

こちらは bootstrap.sh ファイルを**使わずに**環境構築を行うために、既存のキャッシュを落としてくるための Workflow です。特に尖ったことはやってないです。

### 4. _push-cache

こちらは bootstrap.sh ファイルを**使わずに**環境構築を行うために、既存のキャッシュをアップロードするための Workflow です。キャッシュをアップロードする前には、Bitrise が自動で設定できない Bundler、Homebrew と Mint のキャッシュパスも設定しています。

ちなみに Bundler と Mint のキャッシュパスは楽にできますが、Homebrew の場合はそのまま Cellar パスをキャッシュすると、Bitrise 環境にすでにインストールされている関係ないものまで一緒にキャッシュされてしまいます；それで本来ならば Brewfile を参照して必要なもののパスだけ抽出することも可能ですが、Bitrise.io Cache:Push の Step の設定としてはパスを改行でしか設定できず、一つの変数に複数のパスをどうしても設定できないです[^キャッシュパス]。なので仕方なくここは手動で Brewfile を参照しながら、自分で一つ一つのパッケージのパスを書いてます；また Brewfile に変更があったときこちらの修正も忘れないように、Dangerfile にも[対応](#1-dangerfile-を追加)入れています。

[^キャッシュパス]: 筆者としては、配列変数を渡す方法／改行文字が入る文字列変数を渡す方法／`\n`文字が入る文字列変数を渡す方法全部試してみましたが、どれもダメでした。

### 5. _install-dependencies

こちらは bootstrap.sh ファイルを**使わずに**環境構築を行うために、必要な依存をインストールするための Workflow です。Bundler、Homebrew と Mint を順番にインストールした後、CocoaPods と Carthage のインストールを行います。

### 6. _pr-prebuild

こちらは PR の機械的レビューのために必要なものをインストールするための Workflow です。ここでは二つのことを行います：XCPretty JSON Formatter のインストールと、Danger 及び必要な Plugin のインストールです。Danger の紹介は最初の Dangerfile のセクションで済んだのでここでは細かい説明を割愛します。ここではさらに二つのプラグイン：Danger XcodeSummary と Danger Swiftlint をインストールします。これらのプラグインによって、Xcode のビルドワーニングやエラー、そして Swiftlint によるワーニングやエラーも Danger にまとめられ、PR ページにコメントしてくれます。ただしDanger XcodeSummary の利用には XCPretty JSON Formatter の利用が必要なので、これもここで一緒にインストールしておきます。

ちなみに Danger による GitHub の PR ページへのコメントには GitHub トークンが必要ですが、もし可能なら既存メンバーの誰かの GitHub トークンではなく、Danger もしくは Bitrise 専用の GitHub アカウントを作って、そのアカウントのトークンを渡した方が、運用面では回しやすいです。既存メンバーのトークンを使うと、コメント主もそのメンバーのアカウントになってしまいますので、コメントもらった！と思ったところで、それは本人によるコメントなのか、Danger によるコメントなのかがコメント内容読まないと判断しにくいですので。

### 7. _deploy-modification

こちらはバイナリをデプロイする前に、必要な修正を行うための Workflow です。とはいえ、やってることはそんなに大層なものではなく、バージョンとビルド番号の更新だけです。

まずバージョン番号に関しては、トリガーの説明で話した通り、基本タグでデプロイを発火させているので、タグからバージョン番号を取得するようにしています。その取得は `${BITRISE_GIT_TAG:2}`（git tag から先頭 2 文字取り除いた文字列）で取れますので、スクリプトでこれを取得できたらそのまま `$VERSION_STRING` にセットし、取得できなかった場合（マージしたことによって発火したデプロイ等）は `0.0.0` を `$VERSION_STRING` にセットします。

次にビルド番号は基本 Bitrise のビルド番号がそのまま使えます。

バージョン番号とビルド番号が取れたら、`Set iOS Info.plist` でそれぞれ `Bundle Short Version String` と `Bundle Version` にセットすれば OK です。

### 8. _xcode-test-terminal

さていよいよ実際のテストに来ました。まず Bitrise は Xcode Test の Step を提供していますが、それをそのまま使うと、Xcode のビルド結果は JSON ではなく XML で出力してしまい、Danger XcodeSummary が使えないので、仕方なく直接 Terminal からコマンド叩くことにしました。またビルドする際に、追加で `$ADDITIONAL_XCODE_BUILD_OPTIONS` の変数も渡しています。これは CI 環境で行いたくないテストをスキップさせるためのフラグなどを追加するためのもので、詳しくは[こちらの記事](https://qiita.com/lovee/items/e8b0686db1c590eb7fa2)からご確認できます。

そして Test が終わったら、その結果を Danger で回して、PR のページに書き込みます。

### 9. _archive-export

こちらはプロジェクトをアーカイブするための Workflow です。これも特に難しいことは何もなく、先に Bitrise にあげた証明書関係を落としてきたら、次に指定の export method でアーカイブするだけです。ここで export method の指定は `$XCODE_EXPORT_METHOD` 変数で指定しています。これの何がいいかというと、Workflow の Env Vars で、各 Workflow ごとに欲しい export method を定義できることです。

### 10. _deploy-to-bitrise

こちらは出来上がったアプリを Bitrise にアップロードするための Workflow です。これも特に特殊なことはやっていませんが、アップロードが終わった後にダウンロードアドレスを QR コードに書き出す Step を追加しています。これはテスト端末は必ずしも GitHub や Slack に素早く接続できるとは限らないので、QR コード読み取ればすぐにインストールページを開けたら便利、というニーズで入れています。

### 11. _send-slack-message

こちらはテスト結果やダウンロードアドレス等を Slack にメッセージを流すための Workflow です。QR コードの画像も一緒に流せるので、Slack に流れてきた QR コードを読み取ればすぐインストールできる環境があるととても便利です。

### 12. _comment-on-github

こちらはダウンロードアドレスを GitHub の PR ページにコメントするための Workflow です。これも QR コードの画像を流せるので、PR ページを検証端末で読み取ればすぐ動作確認したいビルドが落とせて便利です。

以上が各 Utility Workflow です。これらをさらに組み合わせてできたのが通常の Workflow です。特に特筆すべきものはないので、細かい説明は割愛します。

ちなみに、Bitrise は Env Vars で各 Step で使える環境変数が設定できるのは便利ですが、場合によっては例えばユーザトークンなどの機密情報を扱うこともあります。それらは共有されては困りますので、Bitrise では Secrets の方でそれらの情報を設定でき、YAML ファイルには公開されません。上記のスクリプトにもこれらの Secrets を追加で設定する必要があります：

- `$BITRISE_GITHUB_API_TOKEN`：これは Bitrise が GitHub の PR ページにコメントを残すための GitHub トークンです。
- `$DANGER_GITHUB_API_TOKEN`：これは Danger が GitHub の PR ページにコメントを残すための GitHub トークンです。
- `$CARTHAGE_GITHUB_API_TOKEN`：これは Carthage が CI 上で動作するために必要になるかもしれない git credentials で使う GitHub トークンです。
- `$SLACK_INCOMMING_WEBHOOK`：これは Bitrise が Slack にメッセージを流すための Slack Incoming Webhook です。

上記の変数を設定し終わったら、Bitrise が正常に動くはずです。