TravisCIとfastlaneでiOSアプリのCI【完成版】

前回の記事からはや3ヶ月・・・
[TravisCIとfastlaneでiOSアプリのCI【途中】](https://qiita.com/Yaruki00/items/1aa79c276095362ace7c)
その間何をやっていたかというと、、、特に何もしていませんでした:smirk_cat:
つい最近またCI熱が高まり、ついに使えるところまで持っていけたので、まとめて記事にします:star2:
前回の記事と方針が変わっているところもありますが、それはご愛嬌:yum:

# この記事で実現できること、前提条件
ちょっと量が多いですが、大事なことなので書いておきます。
特にビルドコンフィギュレーションとプロビジョニングファイルについては把握しておかないと、CI上でアーカイブする時にCode Signing Errorで苦しむことになります(僕はなりました)。

## AppleアカウントとかバンドルIDとかチームID
**一つしか使いません。**開発環境と本番環境で変えるとか、Developer CenterとiTunesConnectで違うアカウント使うとか、そういったことは対象外になります。

## 環境
**開発環境と本番環境の２つがあります。**アプリ側はビルドコンフィギュレーション(厳密にはフラグ)でつなぎ先を変えます。

```swift:こんなコードが含まれています
static var domain: String {
    #if DEBUG
        return "https://開発ドメイン/"
    #else
        return "https://本番ドメイン/"
    #endif
}
```

## ビルドコンフィギュレーション
**Xcodeで使用するビルドコンフィギュレーションは以下の4つとします。**

|名前|フラグ|環境|プロビジョニングファイル種別|
|:--:|:--:|:--:|:--:|:--:|
|Debug|DEBUG|開発|Development|
|AdHoc_dev|DEBUG|開発|AdHoc|
|AdHoc_dis|なし|本番|AdHoc|
|Release|なし|本番|AppStore|

## Gitブランチ
**CIで使用するブランチは以下の3つとします。**それ以外のブランチではCIが走らないようにします。

|ブランチ名|ビルドコンフィギュレーション|環境|プロビジョニングファイル種別|デプロイ先|
|:--:|:--:|:--:|:--:|:--:|
|deploy_debug|AdHoc_dev|開発|AdHoc|DeployGate|
|deploy_release|AdHoc_dis|本番|AdHoc|DeployGate|
|release|Release|本番|AppStore|iTunesConnect|

## 開発者証明書(certificate)
**手動で作ってリポジトリに含めます。**CI上ではリポジトリに含まれる証明書を使います。fastlaneの`match`や`cert`は使いません。

## プロビジョニングファイル
**fastlaneの`sigh`を使ってダウンロード/インストールします。Xcode上ではmanualで指定します。**Xcodeの`Automatically manage signing`はチェックを外します。
TargetのGeneralタブは以下のようになります。
![Signing.png](https://qiita-image-store.s3.amazonaws.com/0/84917/852dc7f3-4215-2c8f-8a4e-b51665b76409.png)

## fastlaneの実行
**fastlaneは直で実行します。**bundleは使いません。

## メタデータ/スクリーンショットの管理
**管理はしません。**`deliver`でiTunesConnectにアップロードするのはビルドだけです。

## デプロイツールと通知ツール
**DeployGateとslackを使います。**その他のツールについては扱いません。

# Travis

## リポジトリと連携
自身のアカウントページへ行き、下の方にスクロールすると、自分がアクセスできるリポジトリ一覧が表示されます。CIに載せたいリポジトリのスイッチをONにします。そして設定画面へ。
![Travis_repository.png](https://qiita-image-store.s3.amazonaws.com/0/84917/57239532-0266-a697-9caf-449a2d9401f8.png)

## 設定
環境変数に`FASTLANE_PASSWORD`を追加します。値は使用するAppleアカウントのパスワードです。
後はお好みで。
![Travis_setting.png](https://qiita-image-store.s3.amazonaws.com/0/84917/0f922521-4530-b55a-b4ab-93ea75b4578f.png)

## プロジェクトルートに`.travis.yml`を追加
エディタなどでファイルを生成し、プロジェクトルートに配置します。中身は以下の通り。

```yaml:.travis.yml
language: objective-c
osx_image: xcode9.1

notifications:
  slack: slackで発行したトークン

cache: cocoapods

branches:
  only:
    - deploy_debug
    - deploy_release
    - release

script:
  - if [ $TRAVIS_BRANCH == deploy_debug  ] && [ $TRAVIS_PULL_REQUEST == false ]; then fastlane deploy_debug; else true; fi
  - if [ $TRAVIS_BRANCH == deploy_release  ] && [ $TRAVIS_PULL_REQUEST == false ]; then fastlane deploy_release; else true; fi
  - if [ $TRAVIS_BRANCH == release  ] && [ $TRAVIS_PULL_REQUEST == false ]; then fastlane release; else true; fi
```

## 言語とXcodeのバージョン指定
```yaml:.travis.ymlから抜粋
language: objective-c
osx_image: xcode9.1
```
言語はSwiftであっても`objective-c`と指定します。
xcodeのバージョンは使いたいものを指定してください。
参考: [Building an Objective-C or Swift Project](https://docs.travis-ci.com/user/languages/objective-c/)

## 通知設定
```yaml:.travis.ymlから抜粋
notifications:
  slack: slackで発行したトークン
```
結果をslackで通知したいので、トークンを設定します。
参考: [Configuring Build Notifications](https://docs.travis-ci.com/user/notifications/)

## キャッシュ
```yaml:.travis.ymlから抜粋
cache: cocoapods
```
TravisはルートディレクトリにPodfileがある場合、自動でpod installを実行してくれます。
参考: [Building an Objective-C or Swift Project(Dependency Management)](Building an Objective-C or Swift Projecthttps://docs.travis-ci.com/user/languages/objective-c/#Dependency-Management)
ですが、毎回`pod install`されると時間かかるのでキャッシュさせます。
参考: [Caching Dependencies and Directories](https://docs.travis-ci.com/user/caching/#CocoaPods)

## ブランチを制限
```yaml:.travis.ymlから抜粋
branches:
  only:
    - deploy_debug
    - deploy_release
    - release
```
今回CIを走らせたいブランチは3つだけなので、それらを指定します。
参考: [Customizing the Build(Building Specific Branches)](https://docs.travis-ci.com/user/customizing-the-build#Building-Specific-Branches)

## fastlaneを実行
```yaml:.travis.ymlから抜粋
script:
  - if [ $TRAVIS_BRANCH == deploy_debug  ] && [ $TRAVIS_PULL_REQUEST == false ]; then fastlane deploy_debug; else true; fi
  - if [ $TRAVIS_BRANCH == deploy_release  ] && [ $TRAVIS_PULL_REQUEST == false ]; then fastlane deploy_release; else true; fi
  - if [ $TRAVIS_BRANCH == release  ] && [ $TRAVIS_PULL_REQUEST == false ]; then fastlane release; else true; fi
```
ブランチに応じてfastlaneの`lane`(後述)を実行します。
`$TRAVIS_BRANCH`と`$TRAVIS_PULL_REQUEST`はTravisの環境変数で、それぞれブランチ名とPRか否かを取得できます。
参考: [Environment Variables](https://docs.travis-ci.com/user/environment-variables)

# fastlane

## `init`を実行
プロジェクトルートで`fastlane init`を実行します。
使うアカウントやプロジェクトによって若干の違いはありますが、ガイドに従ってアカウントやプロジェクトファイルorワークスペース、スキームの設定をします。`deliver`でメタデータやスクリーンショットを管理するための処理も入ってしまいますが、今回は使いません。

```:実行例(途中省略、一部改変)
$ fastlane init
[13:56:15]: Get started using a Gemfile for fastlane https://docs.fastlane.tools/getting-started/ios/setup/#use-a-gemfile
[13:56:18]: Detected iOS/Mac project in current directory...
[13:56:18]: This setup will help you get up and running in no time.
[13:56:18]: fastlane will check what tools you're already using and set up
[13:56:18]: the tool automatically for you. Have fun! 
[13:56:18]: $ xcodebuild -list -workspace ./app.xcworkspace
...
Select Scheme:
1. hoge
2. fuga
3. Alamofire
5. Pods-hoge
?  1 // スキームが複数ある場合は選ぶ
[13:56:21]: $ xcodebuild -showBuildSettings -workspace ./app.xcworkspace -scheme hoge
...
[13:56:23]: Your Apple ID (e.g. fastlane@krausefx.com): some@account.com // IDを入力
[13:56:29]: Verifying that app is available on the Apple Developer Portal and iTunes Connect...
[13:56:29]: Starting login with user 'アカウントID'

+----------------+--------------------------------------+
|                    Detected Values                    |
+----------------+--------------------------------------+
| Apple ID       | アカウントID                           |
| App Name       | アプリ名                              |
| App Identifier | バンドルID                            |
| Workspace      | パス                                  |
+----------------+--------------------------------------+

[13:56:40]: Note: If the values above are incorrect, it is possible the wrong scheme was selected
[13:56:40]: Please confirm the above values (y/n)
y // 問題無ければy
[13:56:49]: Created new file './fastlane/Appfile'. Edit it to manage your preferred app metadata information.
[13:56:49]: Loading up 'deliver', this might take a few seconds
[13:56:49]: Login to iTunes Connect (アカウントID)
[13:56:52]: Login successful

+--------------------------------------+------------------------+
|                    deliver 2.66.0 Summary                     |
+--------------------------------------+------------------------+
| run_precheck_before_submit           | false                  |
| screenshots_path                     | ./fastlane/screenshots |
| metadata_path                        | ./fastlane/metadata    |
| username                             | アカウントID             |
| app_identifier                       | バンドルID              |
| edit_live                            | false                  |
| platform                             | ios                    |
| skip_binary_upload                   | false                  |
| skip_screenshots                     | false                  |
| skip_metadata                        | false                  |
| skip_app_version_update              | false                  |
| force                                | false                  |
| submit_for_review                    | false                  |
| automatic_release                    | false                  |
| dev_portal_team_id                   | チームID                |
| overwrite_screenshots                | false                  |
| precheck_default_rule_level          | warn                   |
| ignore_language_directory_validatio  | false                  |
| n                                    |                        |
| precheck_include_in_app_purchases    | true                   |
+--------------------------------------+------------------------+

...
[13:59:26]: Successfully downloaded all existing screenshots
[13:59:26]: Successfully created new Deliverfile at path './fastlane/Deliverfile'
[13:59:26]: 'snapshot' not enabled.
[13:59:26]: 'cocoapods' enabled.
[13:59:26]: 'carthage' not enabled.
[13:59:26]: Created new file './fastlane/Fastfile'. Edit it to manage your own deployment lanes.
[13:59:26]: fastlane will collect the number of errors for each action to detect integration issues
[13:59:26]: No sensitive/private information will be uploaded
[13:59:26]: Learn more at https://github.com/fastlane/fastlane#metrics
[13:59:26]: Successfully finished setting up fastlane
```

## Appfile
`fastlane init`で自動生成されます。アカウント情報を正しく入力できていれば変更する必要はありません。もし間違っている場合は手動で正しいIDを入力してください。

## Fastfile
`fastlane init`で自動生成されます。fastlaneが実行する処理をlaneという単位で記述します。
中身は以下の通り。

```ruby:fastfile
fastlane_version "2.61.0"

default_platform :ios

platform :ios do

    BUNDLE_ID  = "バンドルID"
    SCHEME     = "スキーム"
    PLIST_PATH = "info.plistのパス"
    VERSION    = get_info_plist_value(path: PLIST_PATH, key: "CFBundleShortVersionString")
    BUILD      = get_info_plist_value(path: PLIST_PATH, key: "CFBundleVersion")

    DEPLOYGATE_API_TOKEN = "APIトークン"
    DEPLOYGATE_USER      = "ユーザ/グループ名"

    before_all do
        ENV["SLACK_URL"] = "Incoming WebHooksのURL"
    end

    desc "archive release and upload to iTunesConnect"
    lane :release do
        import_cert
        sigh
        gym(
            scheme: SCHEME,
        )
        deliver(
            skip_screenshots: true,
            skip_metadata: true,
        )
        release_tag
    end

    desc "archive release and deploy by DeployGate"
    lane :deploy_release do
        import_cert
        sigh(
            adhoc: true,
        )
        gym(
            scheme: SCHEME,
            configuration: "AdHoc_dis",
        )
        message = "本番環境 v#{VERSION}(#{BUILD})"
        deploygate(
            api_token: DEPLOYGATE_API_TOKEN,
            user: DEPLOYGATE_USER,
            message: message,
        )
        slack(
            message: message
        )
    end

    desc "archive debug and deploy by DeployGate"
    lane :deploy_debug do
        import_cert
        sigh(
            adhoc: true,
        )
        gym(
            scheme: SCHEME,
            configuration: "AdHoc_dev",
        )
        message = "開発環境 v#{VERSION}(#{BUILD})"
        deploygate(
            api_token: DEPLOYGATE_API_TOKEN,
            user: DEPLOYGATE_USER,
            message: message,
        )
        slack(
            message: message
        )
    end

    desc "add release tag"
    private_lane :release_tag do |tag|
        add_git_tag(
            tag: "v#{VERSION}",
            force: true,
        )
        push_git_tags(
            force: true
        )
    end

    desc "import cert"
    private_lane :import_cert do
        next unless Helper.is_ci?
        KEYCHAIN_NAME     = "キーチェーン名.keychain"
        KEYCHAIN_PASSWORD = "パスワード"
        create_keychain(
            name: KEYCHAIN_NAME,
            password: KEYCHAIN_PASSWORD,
            default_keychain: true,
            unlock: true,
            timeout: 3600,
        )
        import_certificate(
            certificate_path: "./certs/distribution.p12",
            keychain_name: KEYCHAIN_NAME,
            keychain_password: KEYCHAIN_PASSWORD,
        )
    end

    error do |lane, exception|
        next unless Helper.is_ci?
        slack(
            message: exception.message,
            success: false
        )
    end
end
```

### おやくそく
```ruby:fastfileから抜粋
fastlane_version "2.61.0"

default_platform :ios

platform :ios do
    ....
end
```
バージョン、プラットフォームの指定を書きます。

### 使いまわす変数の宣言
```ruby:fastfileから抜粋
BUNDLE_ID  = "バンドルID"
SCHEME     = "スキーム"
PLIST_PATH = "info.plistのパス"
VERSION    = get_info_plist_value(path: PLIST_PATH, key: "CFBundleShortVersionString")
BUILD      = get_info_plist_value(path: PLIST_PATH, key: "CFBundleVersion")

DEPLOYGATE_API_TOKEN = "APIトークン"
DEPLOYGATE_USER      = "ユーザ/グループ名"
```

複数のlaneで使う変数は予め定義しておくと便利です。
`get_info_plist_value`は名前の通り`info.plist`の値を取得できる関数です。書いていて思いましたがバンドルIDもこれで取得できますね。。。

参考:
[get_info_plist_value](https://docs.fastlane.tools/actions/get_info_plist_value/)

### 全てのlaneの前に実行される処理
```ruby:fastfileから抜粋
before_all do
    ENV["SLACK_URL"] = "Incoming WebHooksのURL"
end
```
fastlaneの結果もslackに流したいので、URLをセットしておきます。

### エラー時の処理
```ruby:fastfileから抜粋
error do |lane, exception|
    next unless Helper.is_ci?
    slack(
        message: exception.message,
        success: false
    )
end
```
エラーが起きたときはその内容をslackに流します。
ただし、ローカルでfastlaneを実行する時はすぐにログを見れるので、CIでの実行時のみslackに通知します。
`Helper.is_ci?`はCIでの実行か否かを返すアクションです。

参考:
[slack](https://docs.fastlane.tools/actions/slack/)
[is_ci](https://docs.fastlane.tools/actions/is_ci/)

### キーチェンの生成と開発者証明書の取り込み
```ruby:fastfileから抜粋
desc "import cert"
private_lane :import_cert do
    next unless Helper.is_ci?
    KEYCHAIN_NAME     = "キーチェーン名.keychain"
    KEYCHAIN_PASSWORD = "パスワード"
    create_keychain(
        name: KEYCHAIN_NAME,
        password: KEYCHAIN_PASSWORD,
        default_keychain: true,
        unlock: true,
        timeout: 3600,
    )
    import_certificate(
        certificate_path: "./certs/distribution.p12",
        keychain_name: KEYCHAIN_NAME,
        keychain_password: KEYCHAIN_PASSWORD,
    )
end
```
CI環境は開発者証明書が使える状態になっていないので、キーチェーンの生成とそのキーチェーンへ開発者証明書を取り込んでやります。
`private_lane`は外部からの呼び出し不可なlaneの宣言です。このlaneは他のlaneから呼び出して使うのでprivateにしています。
`create_keychain`はキーチェーンを生成するアクションです。オプションで`unlock: true`を渡してやらないと、CIで実行した時にキーチェーンへのアクセス許可を求めるポップアップが出てしまい、タイムアウトしてしまいます。
`import_certificate`はキーチェーンに開発者証明書を取り込むアクションです。上の例は証明書にパスワードを設定していないのですが、もしパスワードを設定している場合は`certificate_password `というオプションでパスワードを渡してやる必要があります。
ローカルでは既にキーチェーンに開発者証明書が取り込まれている前提なので、この処理は実行しません。

参考:
[create_keychain](https://docs.fastlane.tools/actions/create_keychain/)
[import_certificate](https://docs.fastlane.tools/actions/import_certificate/)

### 開発環境ビルドの配布
```ruby:fastfileから抜粋
desc "archive debug and deploy by DeployGate"
lane :deploy_debug do
    import_cert
    sigh(
        adhoc: true,
    )
    gym(
        scheme: SCHEME,
        configuration: "AdHoc_dev",
    )
    message = "開発環境 v#{VERSION}(#{BUILD})"
    deploygate(
        api_token: DEPLOYGATE_API_TOKEN,
        user: DEPLOYGATE_USER,
        message: message,
    )
    slack(
        message: message
    )
end
```
以下の流れになります。
1. 開発者証明書の取り込み(import_cert)
2. プロビジョニングファイルのダウンロード/インストール(sigh)
3. ipaファイル生成(gym)
4. DeployGateで配信(deploygate)
5. slackに通知(slack)
`import_cert`は上で定義したprivate_laneです。キーチェーンの生成と証明書の取り込みを行います。
`sigh`はプロビジョニングファイルのダウンロード/インストールを行うアクションです。`fastlane init`で入力したアカウント/バンドルIDに紐づくプロビジョニングファイルを、無ければ作成し、あればそれを取得します。デフォルトで`AppStore`のプロビジョニングファイルを取得しようとするので、`adhoc: true`をつけて、AdHocのものを取得させます。
`gym`はipaを生成するアクションです。`fastlane init`時に設定したプロジェクトorワークスペースとスキームでアーカイブします。そのはずなんですが、何故かスキームをうまく見つけてくれなかったので、`scheme`オプションで指定しています。CIがスキームを見つけられるように、スキームはsharedにしておく必要があります。デフォルトのビルドコンフィギュレーションは`Release`です。今回は`AdHoc_dev`でアーカイブさせたいので、`configuration`オプションで指定します。
`deploygate`はDeployGateにデプロイするアクションです。`api_token`, `user`, `message`をオプションで指定します。
いずれかのアクションでエラーが発生すると、そこでlaneは中断され、`error`が呼び出されます。

参考:
[sigh](https://docs.fastlane.tools/actions/sigh/)
[gym](https://docs.fastlane.tools/actions/gym/)
[deploygate](https://docs.fastlane.tools/actions/deploygate/)

### 本番環境ビルドの配布
```ruby:fastfileから抜粋
desc "archive release and deploy by DeployGate"
lane :deploy_release do
    import_cert
    sigh(
        adhoc: true,
    )
    gym(
        scheme: SCHEME,
        configuration: "AdHoc_dis",
    )
    message = "本番環境 v#{VERSION}(#{BUILD})"
    deploygate(
        api_token: DEPLOYGATE_API_TOKEN,
        user: DEPLOYGATE_USER,
        message: message,
    )
    slack(
        message: message
    )
end
```
ほとんど開発環境ビルドの配布と同じです。違うのはアーカイブのときのコンフィギュレーションが`AdHoc_dis`であることと、メッセージのみです。

### リリースビルドのアップロード
```ruby:fastfileから抜粋
desc "archive release and upload to iTunesConnect"
lane :release do
    import_cert
    sigh
    gym(
        scheme: SCHEME,
    )
    deliver(
        skip_screenshots: true,
        skip_metadata: true,
    )
    release_tag
end

desc "add release tag"
private_lane :release_tag do |tag|
    add_git_tag(
        tag: "v#{VERSION}",
        force: true,
    )
    push_git_tags(
        force: true
    )
end
```
アーカイブまでは上の２つとほとんど同じです。今回は`AppStore`のプロビジョニングファイルが必要なので、`sigh`の`adhoc`オプションはつけません。
`deliver`はiTunesConnectにアップロードするためのアクションです。スクリーンショットやメタデータもアップロードできるのですが、今回はビルドのアップロードのみ使用したいので、その他の機能はオプションで無効化します。
`release_tag`はタグ付けして`push`するlaneです。githubに`push`するので、権限がある鍵をTravisに登録しておく必要があります。

参考:
[deliver](https://docs.fastlane.tools/actions/deliver/)
[add_git_tag](https://docs.fastlane.tools/actions/add_git_tag/)
[push_git_tags](https://docs.fastlane.tools/actions/push_git_tags/)

# おわりに
今まで手動でアーカイブしていたものが、特定のブランチにプッシュしたらそのうちDeployGateで配信されたり、iTunesConnectにアップロードされるというのは、非常に感動します:sob:
ただ、割とビルドに時間がかかるみたいなので、もっとビルド時間を短縮できたらなと思います:wrench:
証明書とプロビジョニングファイルは`match`を使うともう少しラクになりそうなので、そちらも挑戦してみたいです:muscle: