iOS project を CircleCI 2.0 に移行してみた

## はじめに

トレタでは、2年ほど前から CI 環境として CircleCI を利用していますが、この度めでたく CircleCI 2.0 が iOS プロジェクトのビルドにも対応したということで、試しに移行してみました。

CircleCI 1.0 と 2.0 の違いをまとめつつ、移行時に調べたことやはまったことをまとめます。

## ジョブを定義できるように
CircleCI 1.0 時代のビルドには、3つのジョブ `dependencies` `test` `deployment` があらかじめ用意されており、各ジョブに手順を定義することでビルドの設定を行うというものでした。

```1.0
machine:
  xcode:
    version: 8.3.3
dependencies:
  override:
    - (commands)
test:
  override:
    - (commands)
deployment:
  development:
    commands:
      - (commands)
```

CircleCI 2.0 では、あらかじめ用意されたジョブを利用するのではなく、ジョブ自体を自由に定義できるようになりました。また、ジョブ毎に環境を変更することもできます。

設定ファイルは `circle.yml` ではなく `.circleci/config.yml` に記述します。

```2.0
version: 2
jobs:
  job1:
    macos:
      xcode: 8.3.3
    steps:
      - (commands)
  job2:
    macos:
      xcode: 8.3.3
    steps:
      - (commands)
  job3:
    macos:
      xcode: 8.3.3
    steps:
      - (commands)
  job4:
    macos:
      xcode: 8.3.3
    steps:
      - (commands)
  job5:
    macos:
      xcode: 8.3.3
    steps:
      - (commands)
```

## ジョブを並列実行できるように
CircleCI 1.0 では、3つのジョブ `dependencies` `test` `deployment` が直列に実行されますが、2.0 では自分で定義したジョブを並列で実行することもできるようになりました。

「ジョブCを実行するためにはジョブAとジョブBが完了している必要がある」などジョブ同士の依存関係を定義することで、ジョブが並列に実行されるようになります。

トレタでは

* キャッシュが存在しない場合は RubyGems, CocoaPods をインストールしてキャッシュを作成（`setup`）
* ↑が終わったら、3つのテストを並列実行
    * `lint` SwiftLintによる構文チェック
    * `scan` ユニットテスト
    * `snapshot` UI テスト
* ↑が終わったら、2つのアプリのビルドを並列実行
    * `deploy_staging` : ステージング環境向けのアプリ作成
    * `deploy_prodtest` : 本番環境向けのアプリ作成

という風に移行してみました（サンプルで試して見たときのものなのでビルド時間は気にしないでください）。

<img width="727" alt="31801826-9f032da2-b585-11e7-8cd0-045203f6f1a2.png" src="https://qiita-image-store.s3.amazonaws.com/0/20102/2b79abf3-f259-ed7c-6b3f-490b86bfdb63.png">

CircleCI ではこのビルドのパイプラインを workflow と呼んでいます。設定ファイルでは `workflows` の下に登場人物となるジョブと、彼らの依存関係を puppet のようなフォーマットで定義していきます。

```2.0
version: 2
jobs:
  setup:
    ...
  lint:
    ...
  scan:
    ...
  snapshot:
    ...
  deploy_staging:
    ...
  deploy_prodtest:
    ...
workflows:
  version: 2
  build-and-deploy:
    jobs:
      - setup
      - lint:
          requires:
            - setup
      - scan:
          requires:
            - setup
      - snapshot:
          requires:
            - setup
      - deploy_staging:
          requires:
            - lint
            - scan
            - snapshot
      - deploy_prodtest:
          requires:
            - lint
            - scan
            - snapshot
```

アプリのテストもビルドもそれなりに時間がかかるので、CircleCI 2.0 に移行して並列実行するだけでも時間をかなり節約できるはずです。

## キャッシュを自分でコントロールできるように
1.0 ではサービス側でよしなにキャッシュコントロールしてくれていましたが、2.0 ではユーザー自身で管理する必要があります。

キャッシュの保存 `save_cache` は、cache key に任意の文字列を指定して行います。キャッシュを復元 `restore_cache` する時は、指定した cache key で呼び出せばOKです。

キャッシュはプロジェクト単位で利用可能なため、各 lock ファイルの checksum や、ブランチごとにキャッシュを作っておくなどしておくと効率がよさそうです。

先ほど紹介した `setup` ジョブでは、下記のような設定で RubyGems と CocoaPods のキャッシュを必要に応じて作成するようにしています。

```
version: 2
jobs:
  setup:
    steps:
      - checkout
      - restore_cache:
          keys:
            - gems-{{ checksum "Gemfile.lock" }}
            - gems-
      - run:
          name: Running bundle install
          command: bundle check || bundle install
          environment:
            BUNDLE_JOBS: 4
            BUNDLE_RETRY: 3
      - save_cache:
          key: gems-{{ checksum "Gemfile.lock" }}
          paths:
            - vendor/bundle
      - restore_cache:
          keys:
            - pods-{{ checksum "Podfile.lock" }}
            - pods-
      - run:
          name: Running pod install
          command: |
            curl https://cocoapods-specs.circleci.com/fetch-cocoapods-repo-from-s3.sh | bash -s cf
            bundle exec pod install
      - save_cache:
          key: pods-{{ checksum "Podfile.lock" }}
          paths:
            - Pods
```

リストア時の secondary cache key として `gems-` や `pods-` を設定しておくと、 cache key にマッチするキャッシュが存在しない場合に最後保存されたキャッシュをリストアしてくれるため、 `bundle install` `pod install` の時短が期待できます。

保存しておいたキャッシュを別のジョブで利用する場合も、同様に `restore_cache` で復元できます。

```
  scan:
    macos:
      xcode: 8.3.3
    steps:
      - checkout
      - restore_cache:
          keys:
            - gems-{{ checksum "Gemfile.lock" }}
            - gems-
      - restore_cache:
          keys:
            - pods-{{ checksum "Podfile.lock" }}
            - pods-
      - run:
          name: Running fastlane scan
          command: bundle exec fastlane scan
```

## Artifacts 保存方法の変更

CircleCI 1.0 では、テストのログやUIテストのスクリーンショットなどのファイルを後から参照するためには、`$CIRCLE_ARTIFACTS` ディレクトリに格納しておく必要がありました。

CircleCI 2.0 では、ジョブのステップ定義の中に `store_artifacts` を記述し、保存しておきたいファイル/ディレクトリの名前と、保存先のパスを指定します。

```
  snapshot:
    steps:
      - checkout
      - restore_cache:
          ...
      - run:
          name: Running fastlane snapshot
          command: bundle exec fastlane snapshot
      - store_artifacts:
          path: fastlane/screenshots
          destination: screenshots
```

## ruby バージョンを変える

デフォルトでは ruby 2.0.0 が利用されるため、もっと新しいバージョンの ruby を利用したい場面があると思います。 CircleCI 2.0 では `ruby-install` と `chruby` がインストールされているようで、それらを利用すれば別のバージョンの ruby が使えそうです。

**(1) ジョブの定義に下記のように shell の設定を追加**

```circleci/config.yml
setup: 
  macos:
    xcode: 8.3.3
  shell: /bin/bash --login -eo pipefail
```

**(2) .ruby-version をファイルをプロジェクトルートに追加**

```
ruby-2.4
```

- See: [Custom Ruby Versions](https://circleci.com/docs/2.0/testing-ios/#custom-ruby-versions)

## circle.keychain がなくなった
CircleCI 1.0 では `circle.keychain` という名前のキーチェーンがありましたが、 CircleCI 2.0 のドキュメントからは消えているので、どうやらなくなっているぽいです。

Fastlane の [`create_keychain`](https://docs.fastlane.tools/actions/create_keychain/) で keychain を作成してから、証明書のインポートを行う必要があります。

```Fastfile
lane :certificates do
  create_keychain(
    name: ENV["KEYCHAIN_NAME"],
    password: ENV["KEYCHAIN_PASSWORD"],
    default_keychain: true,
    unlock: true,
    timeout: 3600,
    lock_when_sleeps: true
  )

  import_certificate(
    certificate_path: "certificates/development.p12",
    keychain_password: ENV["KEYCHAIN_PASSWORD"]
  )
  ...
end
```

thanks to @horimislime

## さいごに

まだまだ機能を使いきれておらず、最適化されていない感じが満載ですが、なんとなく移行してジョブを並列実行するだけでも、十分なリターンが得られるので試してみる価値は十分にありました。もっとよい方法や tips などあれば教えてもらえると嬉しいです。

そんな CircleCI 2.0 の気になるところとしては、なぜかビルドが失敗することと、[Workflow API がまだないこと](https://discuss.circleci.com/t/trigger-workflow-through-rest-api/13931/34)（Hubot から API 叩きたいので欲しいところ）。

以上、現場よりお伝えしました。Happy CI-ing!

* [iOS Project Tutorial - CircleCI](https://circleci.com/docs/2.0/ios-tutorial/)
* [Testing iOS Applications on macOS - CircleCI](https://circleci.com/docs/2.0/testing-ios/)
* [Migrating Your iOS Project From 1.0 to 2.0 - CircleCI](https://circleci.com/docs/2.0/ios-migrating-from-1-2/)