Bitriseを利用してミニマルにiOSのCI / CD環境を構築する

この記事では、Bitriseを利用してiOSのCI/CDを構築します。
CI/CD環境の構築は日頃アプリを作るエンジニアにとっては慣れない作業であり、
利用するCIサービスによってやり方は異なるので、他のサービスを構築した知見が活きにくいものです。
また、チームの中でもCertificatesやProvisioning Profileといった、XcodeのCode Signingは経験がないとDeveloperであってもセットアップに苦慮するという状況にあります。

この記事で案内する方法は、BitriseのStepとApp Store Connect APIを利用します。
これによりCode Signingの管理を簡単にし、配布もTest Flightにすることで、できるだけミニマルにCI/CDを構築することができます。

**2022/01/26 更新**
`ios-auto-provision-appstoreconnect` step がdeprecatedとなり、各ステップに `automatic_code_signing: api-key` を指定するようになりました。

## 基本的なセットアップ

Bitriseの管理画面にある `Add New App`の手順に従い進めます。
最初のビルドが終わった時点で次の手順に進みます。

特筆することは特にありません。

## 目指すもの
次の２つができれば、ひとまずやりたいことは満たせるでしょう。

- Unit Testを実行する
- Test FlightでStaging / Production環境のアプリを配布する

## Unit Testの実行に必要なのはShared Scheme

Xcode上でSchemeを選択し、 `Manage Schemes...` 選択します。
CIで利用したいSchemeにSharedのチェックを入れます。
gitに含めてPushすることで、CI上でcloneしたときに利用できるようになります。

Schemeを作った場合は、Testが追加されていることを確認します。 
![Screen Shot 2021-08-25 at 16.46.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14789/e12af93d-6a6d-43db-f997-0d3431799577.png)


## ローカル環境に fastlane は必要かを考える
fastlaneはiOSアプリのビルドや配布を自動化するツールです。

https://fastlane.tools

iOSアプリのビルドや配信の自動化には `xcodebuild` や `xcrun` といった、Xcode同梱のコマンドラインツールを利用することができます。
fastlaneには、Certificate や Provisioning Profile を管理する仕組みも備わっています。
従来は fastlane なしでの開発は考えられないほど恩恵を大きく受けていました。

しかしながら、CI 環境で Ruby のセットアップが必要であったり、fastlane match の導入によりかえって証明書や Provisioning Profile の管理が煩雑になってしまっているのも事実です。

CocoaPodsを利用している場合はRubyのセットアップは求められるので問題ないですが、Swift Package Managerを利用している場合、fastlaneのためにRubyのセットアップをするのは少々ためらわれます。

Bitriseでは、`xcodebuild` や `xcrun` を利用したStepが作られてる他、内部ではfastlaneに依存しているものもあり、ローカル環境で fastlane を導入しなくとも fastlane の恩恵を受けることができます。 

## Bitrise に預けるのは Certificate と API Key

他のCI環境に比べて、Bitriseの開発体験がよいところがこのCertificateをアップロードするGUIが備わっていることです。
他のCIでは `fastlane match` を利用してCertificateを管理すると記載があります。

例: Circle CI https://circleci.com/docs/2.0/ios-codesigning/
> Note: CircleCI only officially supports Fastlane Match for code signing. Other methods may be used, but are not guaranteed to work and are unsupported.

厳密に管理がなされれば良いのは、Distribution Certificateのみで、App StoreやTest Flightで利用するapp-storeのProvisioning Profileは、なければ都度生成して問題ありません。

~~Bitriseの `iOS Auto Provisioning` Stepではこの方式が取られており、とてもリーズナブルです。~~
Xcode archiveで `automatic_code_signing` を指定するように変更となりました。
(すでに存在している場合はそのProvisioning Profileを利用します）

Automatic Signingに対応しているので、Provisioning Profileを固定する必要はありません。

背景と設定手順この記事にまとめました[Ad hoc配信をやめてTestFlightで配信する](https://qiita.com/d_date/items/db540e9ed127b1b23e40)

## 実はこれで終わり

実はこれ以上書くことはないほどにBitriseのセットアップは簡単です。
次に記載するyamlの内容で、Pull Requestが作成されたらTestが動いたり、`main` ブランチにマージコミットされたらStaging用のTest Flightアプリが配布されるところまで実現できます。

Production用のアプリはBitriseのGUIから実行させたり、専用のトリガーを作って外部から起動させるという方法も取れます。


## 実際のbitrise.yml

関わっているプロジェクトで運用されているbitrise.ymlは次の通りです。

```yaml
format_version: "11"
default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
project_type: ios
workflows:
  _finish:
    steps:
      - slack:
          inputs:
            - api_token: "$SLACK_API_TOKEN"
            - webhook_url: "$SLACK_WEBHOOK_URL"
      - cache-push: {}
  _prepare:
    steps:
      - activate-ssh-key:
          run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
      - script:
          inputs:
            - content: |-
                #!/usr/bin/env bash
                set -ex
                for ip in $(dig @8.8.8.8 github.com +short); do ssh-keyscan github.com,$ip; ssh-keyscan $ip; done 2>/dev/null >> ~/.ssh/known_hosts
          title: Add SSH
      - git-clone: {}
      - cache-pull: {}
      - script:
          title: Prepare CLI
          run_if: ".IsCI"
          inputs:
            - is_debug: "yes"
            - content: |-
                #!/usr/bin/env bash
                set -eux

                make bootstrap
  deliver-staging:
    steps:
      - ios-auto-provision-appstoreconnect:
          inputs:
            - distribution_type: app-store
            - scheme: Staging
            - api_issuer: "$APP_STORE_CONNECT_API_KEY_ISSUER_ID"
            - api_key_path: "$BITRISEIO_APPSTORE_CONNECT_API_KEY_URL"
      - set-xcode-build-number:
          inputs:
            - plist_path: "$BITRISE_SOURCE_DIR/App/iOS/Info.plist"
      - xcode-archive:
          inputs:
            - export_method: app-store
            - scheme: Staging
            - team_id: "$BITRISE_APPSTORE_TEAM_ID"
      - deploy-to-itunesconnect-application-loader:
          inputs:
            - api_key_path: "$BITRISEIO_APPSTORE_CONNECT_API_KEY_URL"
            - api_issuer: "$APP_STORE_CONNECT_API_KEY_ISSUER_ID"
            - team_id: "$BITRISE_APPSTORE_TEAM_ID"
    before_run:
      - _prepare
    after_run:
      - _finish
  deliver-production:
    steps:
      - set-xcode-build-number:
          inputs:
            - plist_path: "$BITRISE_SOURCE_DIR/App/iOS/Info.plist"
      - xcode-archive:
          inputs:
            - export_method: app-store
            - scheme: App
            - distribution_method: app-store
            - automatic_code_signing: api-key
            - team_id: "$BITRISE_APPSTORE_TEAM_ID"
      - deploy-to-itunesconnect-application-loader:
          inputs:
            - api_key_path: "$BITRISEIO_APPSTORE_CONNECT_API_KEY_URL"
            - api_issuer: "$APP_STORE_CONNECT_API_KEY_ISSUER_ID"
            - team_id: "$BITRISE_APPSTORE_TEAM_ID"
    before_run:
      - _prepare
    after_run:
      - _finish
  test:
    steps:
      - xcode-test:
          inputs:
            - scheme: AppFeature
            - simulator_device: iPhone 12
            - simulator_os_version: "14.5"
            - project_path: "$BITRISE_PROJECT_PATH"
    before_run:
      - _prepare
    after_run:
      - _finish
app:
  envs:
    - opts:
        is_expand: false
      BITRISE_PROJECT_PATH: xxxxxxxx.xcworkspace
    - opts:
        is_expand: false
      BITRISE_APPSTORE_TEAM_ID: XXXXXXXXXX
trigger_map:
  - push_branch: main
    workflow: deliver-staging
  - pull_request_target_branch: "*"
    workflow: test
```

## FAQ
### Q. デバイスをDeveloper Portalに追加する必要ないのですか？
TestFlightで配布する場合は、Provisioning Profileにデバイスを登録する必要がありません。

### Q. fastlane deliverでメタデータのアップデートを行なってました
`deploy-to-itunesconnect-deliver` を利用すると、中では `fastlane deliver`を利用しているので、オプションにKey Valueを渡すことで同じように利用することができます。

筆者が試したときは、 `deploy-to-itunesconnect-deliver` がうまく動かず、 `deploy-to-itunesconnect-application-loader` でユースケースは満たせたので、こちらを利用しています。

## Reference
- [Managing iOS code signing files - automatic provisioning](https://devcenter.bitrise.io/code-signing/ios-code-signing/ios-auto-provisioning/)