FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

Xcode Cloudを使ってみた

FLINTERSでGANMA!のiOSアプリの開発をしている宗像です。この記事はFLINTERS Advent Calendar 2022の10日目の記事です! 数ヶ月前にXcode CloudをGANMA!で使ってみたのでやったことや使った感想をまとめてみました。

背景

これまでのGANMA!のCI/CD事情をざっくり書くと

  • 社内のiMac上で、fastlane経由でテストやビルドを実行
  • Bitriseを導入し、実行は変わらずfastlaneを使いテストやビルドをするようにした

というような流れでした。しかしBitriseの料金プラン改定で今の使い方だとコストが増えることがわかり、別の手段が取れないか考え始めました。 そのあたりでXcode CloudがGeneral Availabilityになったので使ってみようということになりました。

Xcode Cloud とは

Xcode Cloud はAppleが提供しているCI/CDサービスです。WWDC21でベータ版が発表され、その後WWDC22でGeneral Availabilityになることが発表されました。2202年の夏から有料プランがスタートしています。iOSのアプリケーションのCI/CDが簡単に行えて、ワークフローの編集や実行がXcodeから行えます。

使い始める前に

Xcode Cloud の詳しい使い方については、WWDC21の動画 Meet Xcode Cloud から見始めるのが良いと思います。実際に画面を見せながら初歩的な部分から解説してくれています。

WWDC21では他にもワークフローのカスタマイズに関する動画もあります。https://developer.apple.com/videos/play/wwdc2021/10268

https://developer.apple.com/videos/play/wwdc2021/10269/

WWDC22でもXcode Cloudについて取り上げた動画がいくつかあります。こちらは導入よりは運用にフォーカスした内容が多めになっています。

公式ドキュメントの方にはより詳細な情報が書かれています。利用条件やトラブルシューティングのページもあるのでみておくと良いでしょう。

ワークフロー

今回は以下の4つのワークフローを作成することを目標にしました。

  1. 週に一度、複数種類の端末、複数バージョンのOSでテストを実行する
  2. ブランチに push したときにテスト実行
  3. 本番用アプリの TestFlight 配信
  4. Staging 環境アプリの TestFlight 配信

依存関係の解決

Xcode CloudはSwift Package Manager経由の依存であれば、Package .resolvedを見て依存を解決してくれます。しかしそれ以外のCocoaPodsや、Carthageを使っている場合はそれらのインストールから始めないといけません。GANMA!ではSwiftPMをメインで使いながらも、一部CocoaPods経由でインストールしているライブラリがあるため、そこを解決する必要がありました。

プロジェクトのルートディレクトリの下にci_scriptsという名前でディレクトリを作成し、そこにカスタムスクリプトを作成することでワークフロー実行中にスクリプトを実行することができます。

Xcode Cloudではカスタムスクリプトの名前は実行タイミングによって決められています。

  • ci_post_clone.sh
  • ci_pre_xcodebuild.sh
    • Xcode Cloud が xcodebuild コマンドを実行する前に実行される
  • ci_post_xcodebuild.sh
    • Xcode Cloud が xcodebuild コマンドを実行した後に実行される

今回は依存関係の解決と、TestFlight実行時に必要な作業のために以下のようなスクリプトci_post_cloneを作成しました。

#!/bin/sh

cd ..

echo ">>> SETUP ENVIRONMENT"
echo 'export GEM_HOME=$HOME/gems' >>~/.bash_profile
echo 'export PATH=$HOME/gems/bin:$PATH' >>~/.bash_profile
export GEM_HOME=$HOME/gems
export PATH="$GEM_HOME/bin:$PATH"

echo ">>> INSTALL BUNDLER"
gem install bundler --install-dir $GEM_HOME

echo ">>> INSTALL DEPENDENCIES"
bundle install

echo ">>> INSTALL PODS"
bundle exec pod install

echo ">>> EXECUTE CODE GENERATE"
bundle exec fastlane code_generate

echo ">>> execute TestFlight lane"

./ci_scripts/TestFlightScript.swift

Xcode Cloudの環境ではRubyが使えるのでci_scriptsディレクトリにGemfileも用意して、bundler経由でfastlaneとCocoaPodsをインストールしています。fastlaneを入れることができたので、既存のFastfileに定義されていたコード生成のコマンドなどを利用することができ、導入は比較的簡単にできました。

Swiftでヘルパースクリプトを書いた

カスタムスクリプトの名前は決められているため、どのワークフローであっても上記のスクリプトが実行されます。ワークフローによって実行する処理を分けたいというような場合は、ci_post_clone 内で分岐させる必要があります。GANMA!では本番用アプリの TestFlight配信とStaging環境アプリのTestFlight配信で、アプリ名やbundle idなどを書き換えたいので以下のようなヘルパースクリプトをci_scripts以下に用意しました。

#!/usr/bin/env swift

import Foundation

func makeProcess(arguments: [String]) -> Process {
    let p = Process()
    p.executableURL = URL(fileURLWithPath: "/usr/bin/env")
    p.arguments = arguments
    return p
}

func fastlane(lane: String, options: [String] = []) -> [String] {
    var base = ["bundle", "exec", "fastlane"]
    base.append(lane)
    return base + options
}

guard let workflowName = ProcessInfo.processInfo.environment["CI_WORKFLOW"] else {
    print("Error: Can not get workflowname")
    exit(1)
}

// GANMA! への TestFlight 配信準備
if workflowName == "TestFlight" {
    let testFlightProdction = makeProcess(arguments: fastlane(lane: "xcode_cloud_testflight", options: ["--env", "pruduction"]))

    do {
        try testFlightProdction.run()
        testFlightProdction.waitUntilExit()
    } catch {
        print("Error: \(error)")
        exit(1)
    }
}

// GANMA!staging への TestFlight 配信準備
if workflowName == "TestFlight-stage"
    let xcodeCloudTestFlightStage = makeProcess(arguments: fastlane(lane: "xcode_cloud_testflight", options: ["--env", "stage"]))

    do {
        try xcodeCloudTestFlightStage.run()
        xcodeCloudTestFlightStage.waitUntilExit()
    } catch {
        print("Error: \(error)")
        exit(1)
    }
}

TestFlight配信時に実行したい作業は、既存のFastfileに書かれているのでそれをSwiftから実行しています。Xcode Cloudではデフォルトで用意されている環境変数があり、その中からワークフロー名を取得して実行するlaneを切り替えています。

Swiftでヘルパースクリプトを書いたのは、Xcode CloudはiOS専用のためiOSエンジニアが慣れているSwiftで書かれている方が良いかなと思ったからですが、やっていることに対して冗長になってしまっている感があるので普通にシェルを書いてもよかったかもしれません。

ワークフローを作成

依存関係の解決やカスタムスクリプトの準備ができたのでワークフローを作成します。開始条件となるStart Conditionと、ビルドやテスト、配布を行うActionsを設定します。それぞれのワークフローでの設定を簡単に書くと以下のようになります。

  1. 週に一度、複数種類の端末、複数バージョンのOSでテストを実行する
    1. Start ConditionでOn a schedule for a branchに設定する
    2. ActionsでTestを追加する。DestinationでRecommended iPhones, iPadsを選ぶと複数の端末でテストを実行してくれる。Recommended iPhonesではOSが固定されるので、他のOSで実行したい場合はDestinationを追加する。
  2. ブランチにpushしたときにテストを実行
    1. Start ConditionをBranch Changesに設定する。Source branchをmasterにしておき、masterブランチにpushされたときにワークフローが実行されるようにする
    2. ActionsでTestを追加する
  3. 本番用アプリのTestFlight配信
    1. Start ConditionをBranch Changesに設定し、Source branchをreleaseにする。
    2. ActionsでArchiveを追加する。Deployment preparationでTestFlight and App storeを選ぶ。
    3. Post ActionsでTestFlight External Testing(または Internal Testing)を追加する
  4. Staging環境アプリのTestFlight配信
    1. 3とほぼ同じ
    2. bundle id が異なることで一点後述する注意点がある。

同じプロジェクトのコードから別のbundle idのワークフローを設定する場合の注意点

前項のStaging環境アプリのTestFlight配信ですが、本番用のアプリとは別のbundle idにして配信をしようとしました。

Xcode CloudはXcodeとAppStoreConnectからワークフローの編集、実行ができますが一番最初のワークフローはXcodeから作成する必要があります。しかし、Xcode上のReport Navigatorでは本番用のアプリのワークフローしか確認ができず、staging用のワークフローをどうやって設定すれば良いのか最初分かりませんでした。

ここにstaging用のアプリを出したい

ドキュメントを細かく見て行くと、https://developer.apple.com/documentation/xcode/configuring-your-first-xcode-cloud-workflow#Review-Xcode-Cloud-workflowsのnoteに以下のような記述がありました。

If you use .xcconfig  files to set the bundle identifier or use them to automatically change the bundle identifier, you need to take extra steps to start using Xcode Cloud. First, set the bundle identifier for your app target in the Signing & Capabilities tab of your project or workspace. Then configure your first workflow as described below. Repeat this process for each bundle identifier. Note that you’ll need to use App Store Connect to view your workflows and builds because Xcode relies on the explicitly set bundle identifier to show them.

これを見て、targetのSigning & Capabilitiesのbundle idをstaging用のものに書き換えると、本番用アプリの方はXcodeから見えなくなり、staging環境のアプリのワークフローを設定できるようになりました。

おそらくXcodeはtargetのSigning & Capabilitiesのbundle idを見てXcode Cloudの設定を行なっているようです。毎回targetを書き換えるのは大変なので複数のbundle idを使い分ける場合は、一度ワークフローを作成したら以降はAppStoreConnectから操作するのが良さそうです。

ワークフローの結果の通知

通知は今のところ、Slackとメールに対応しています。

ワークフローのPost ActionのNotifyを追加して、特定のSlackのチャンネルやメールアドレスを指定することができます。GANMA!ではSlackへの通知を設定しました。

コスト

月25時間までは無料で、100時間でおよそ$50、250時間で$100、1000時間で$400という価格設定になっています。GANMA!でのBitriseの利用時間をみると、Bitriseよりコストはかなり低く使えそうでした。

まとめ

Xcode Cloudを試してみました。

TestFlight配信が簡単に行える、SwiftPMとの相性が良いといった部分は良い点だと感じました。Xcodeからワークフローを編集、実行ができるので使い始めるのも難しくなく、Testが失敗したときにコードを修正して再度実行、というのがすぐできたのは良い体験でした。

業務外で個人で開発したアプリにXcode Cloudを使ってみたのですが非常に簡単にリリースまで持って行くことができました。個人でiOSアプリを作るというケースでは現状でも良い選択肢だと思います。

一方でSwiftPMを使っていないようなアプリだと依存関係の解決が大変になるかもしれません。またワークフローを手軽に編集できる一方で編集履歴を管理したり、設定ファイルにしておくといったことはできない(編集の制限はできる)ので規模の大きいチームでの利用は運用方法を決めておくなどの工夫が必要かもしれません。

まだ新しいサービスなので今後のアップデートにも注目していきたいですね。

参考資料