ニコニコ動画 iOS アプリでの Xcode Cloud の活用
はじめに
こんにちは。ニコニコ開発部で ニコニコ動画 iOS アプリ を開発している兎澤です。
ニコニコ動画 iOS アプリでは2023年末より Jenkins から Xcode Cloud への移行を開始し、現在は完了しています。今回は、この移行をどのように進めていったのか?移行した結果どうだったか?について紹介します。
iOSDC 2024 のチャレンジトークンが記事内に 1つ 含まれています。ぜひ探してみてください! (※ 記事の見出しの横についている「#」はチャレンジトークンではありません。チャレンジトークンは文中に配置されています。)
課題
ニコニコ動画 iOS アプリの従来の CI/CD サービスは以下のような状況でした。
- Jenkins を利用
- 社内クラウド上のマシンを Controller とする
- 社内ネットワークに接続された複数台の Mac mini を Agent とする
- 社内ネットワークからのみアクセス可能
- ボカコレ iOS アプリチーム と共有
しかし、この環境には以下のような課題がありました。
- Mac mini の運用保守にコストがかかる
- macOS/Jenkins/Xcode の定期的な更新が必要
- キャッシュ、Keychain 周りでの不定期な問題の発生
- 作業の属人化
- 並列数に限度がある
- マシンあたり単一ジョブのみを動作させていたため、管理している Mac mini の台数 (3台) が並列数の限界だった
- ボカコレ iOS アプリチームと共有していたので、一方のチームで占有してしまうと他方のチームに影響があった
さらに、組織の方針により社内クラウドから別環境へ移行する必要が出てきました。そこで、上記のような課題もあったため、これを機に別の CI/CD サービスへの全面的な移行を検討することになりました。
CI/CD サービスの選定
CI/CD サービスを選定するために、簡単な試算を行いました。Jenkins API で1日あたりの Jenkins ジョブの実行時間を取得する cron を設定し1〜2月程収集したところ、以下のような結果となりました。
- 中央値は232時間/月
- ワーストケースで440時間/月
- 最も実行時間が長かった日をベースに計算
Mac mini が M1 であるのに対し、Xcode Cloud は 2024/07 現在 Intel 版が使用されているようです。当時手元にあった Intel 版 Mac Pro でビルドしたところ、M1 Mac mini に対しておおよそ 70% の性能だったので、70% 程度の性能ベースで計算すると、中央値は332時間/月、ワーストケースで 629時間/月となります。予測が正しかった場合、Xcode Cloud のプランであれば 1000時間/月のプランが妥当そうです。
CI/CD サービスの候補としては以下を考えました。
- Codemagic
- Xcode Cloud
- GitHub Actions
- Bitrise
各社で公開されているプランをベースに、性能をなるだけ揃えて単位時間あたりの金額を計算すると、Codemagic と Xcode Cloud が同程度の安さでした。GitHub Actions および Bitrise は性能が良いのもありますが、例えば1000時間あたりの値段で比較すると桁が1つ違いそうです。
最終的には、以下のような理由から Xcode Cloud の1000時間/月プランを第一候補として考えることになりました。
- 他と比べてコストが安い
- Xcode と統合されており、扱いやすそうだった
- App Store Connect との統合により、審査提出がしやすくなりそうだった
- Apple 公式が出しているものなので、チーム内にトライしたいというモチベーションがあった
Xcode Cloud について
概要
Xcode Cloud は、Apple プラットフォームのアプリ開発者のための CI/CD サービスです。Apple が既に提供している Xcode、TestFlight、App Store Connect 等のツール、 サービスと統合されているのが特徴で、SaaS でありながら各開発者ローカルの Xcode 上で閲覧/操作が行えます。一方で App Store Connect 上でも Xcode 上と同等の操作が行えるようになっています。
ワークフローとその閲覧方法
Xcode Cloud は他の CI/CD サービスと同様に、何らかの契機 (開始条件) によりソースコードのビルド&デプロイフローを走らせます。このフローはワークフロー[1][2]と呼ばれます。
Xcode プロジェクト/ワークスペースを開いた後に Report ナビゲーターの Cloud タブに移行すると、Xcode はプロジェクト/ワークスペースを解析し、解決できた Product の一覧を表示してくれます[3]。
この時解決される Product は、そのプロジェクト/ワークスペース内に存在し、かつ Xcode 上でログイン中の Apple ID で閲覧可能なものになるようです。例えば、ニコニコ動画 iOS アプリは複数の Swift Package に依存していますが、プロジェクトを開くとニコニコ動画アプリの Product だけでなく、依存する Swift Package に対応する Framework 群のワークフローも閲覧できるようになりました。
![Xcode上のプロダクト一覧](https://dwango.github.io/images/2024-07_nicoiphone_xcode_cloud/xcode_products.png)
また、複数の Apple ID で Xcode にログインしていた場合でも、各 Apple ID に紐づく Product がプロジェクト内に存在すれば、それらも全て閲覧できるようでした。
プランの選択
2024/07 現在、Xcode Cloud には以下のプランがあります。
- 25コンピューティング時間/月: 無料
- 100コンピューティング時間/月: 49.99USD/月
- 250コンピューティング時間/月: 99.99USD/月
- 1000コンピューティング時間/月: 399.99USD/月
無料プラン[4]があるので、まずは無料で試してみるのが良さそうです。ニコニコ動画 iOS アプリチームでも、まずは無料プランで動作の検証を行いました。
プランの選択は Apple Developer アプリから行えます。公式ドキュメントにある通り、まずは適当な Xcode Cloud ワークフローを作成しておく必要があります。
サブスクリプションの管理 ワークフローを設定すると、Apple Developer ProgramメンバーシップのAccount Holderは、iPhoneおよびiPadのApple Developerアプリの「アカウント」タブでサブスクリプションに登録し、追加のコンピューティング時間を利用できるようになります。サブスクリプションプランの価格は米ドル、または現地通貨(利用可能な場合)で設定されています。
![Developer アプリでの Xcode Cloud プランの選択](https://dwango.github.io/images/2024-07_nicoiphone_xcode_cloud/xcode_cloud_plans.png)
移行の準備
Xcode Cloud ワークフローの作成前に行ったことについて紹介します。
Swift Package に移行する
Xcode Cloud における問題の一つはキャッシュです。
ニコニコ動画 iOS アプリは数多くのライブラリに依存しており、CI 実行の度にこれらをインストールすると時間がかかってしまいます。CI 実行時間削減のためにもキャッシュ機能が必要です。
デフォルトでは DerivedData がキャッシュされ[5]、その中に存在する Swift Package も同様にキャッシュされますが、CocoaPods/Carthage 等でインストールされたライブラリは基本的にキャッシュされません。
ニコニコ動画 iOS アプリでは、将来的な Xcode Cloud 移行も見据えて、既にほぼ Swift Package 移行を完了した後でした。しかし一部の広告SDKが Swift Package をサポートしていなかったため、CocoaPods と Swift Package を併用する形となっています。
それでもほとんどのライブラリは Swift Package に移行済みであったため、Xcode Cloud のキャッシュの恩恵を受けることができそうでした。
必要な Xcode Cloud ワークフローについて整理する
Xcode Cloud 移行の前に、従来の Jenkins ジョブを Xcode Cloud ワークフローで置き換えられるか検討します。従来の Jenkins ジョブには以下のような種類がありました。
- ユニットテストの実行
- DeployGate への InHouse/AdHoc/Development ビルドのアップロード
- fastlane deliver によるストアページの作成と TestFlight アップロード
- fastlane pilot による TestFlight アップロード
社内ライブラリの多くはユニットテストの実行のみで、ニコニコ動画 iOS アプリのリポジトリでは DeployGate や TestFlight でのアップロードも行っていました。
ユニットテストの実行については、適切なスキームと事前準備のための Custom Build Script (後述) を書けば問題なく行えそうです。TestFlight アップロードも Xcode Cloud だとスムーズに行えます。一方で、DeployGateについてはどうすべきかの検討が必要でした。
DeployGate は開発者以外の企画/デザイナ/品証チームの方々とのアプリの共有のために使っています。そのまま利用しても良いですが、Xcode Cloud では TestFlight へのアップロードが少ない手間で行えるので、これを機に DeployGate の利用を TestFlightの内部テストへ置き換えられないか調査しました。 双方の違いをまとめると、以下になります。
TestFlight(内部テスト) | DeployGate | |
---|---|---|
制限 |
|
|
ビルドの識別 |
|
|
メンバー数 |
|
|
過去バージョンの保存 |
|
|
チーム内で相談したところ、メンバー毎の Apple ID の発行やメンバー数、メンバー毎の台数や過去バージョンの保存期間等の制約は、大きな問題にならないことがわかりました。
運用上問題になりそうだったのはビルドの識別です。Jenkins の場合はビルドパラメータとしてリリースノートを指定して、配布ページ上でどの案件向けのアプリが配布されているか?を利用者にわかるようにしていました。Xcode Cloud の場合はワークフロー実行時にビルドパラメータのような任意の引数は取れません。しかし、テスター向けの情報をテストノートに記載することができます[6]。事前に指定のファイルをリポジトリ内に配置しておく必要があるのですが、Custom Build Script で動的に作成することもできそうです。
あるいは、ビルドグループも活用できます。Xcode Cloud ワークフローとその実行時の Start Condition によって、TestFlight 上でビルドグループという単位でビルドがグルーピングされます。「特定の案件用のビルドは特定のブランチからビルドする」などの運用ルールにしておけば、ブランチ名を識別子として利用者にビルドを識別してもらうことができそうです。
以下は、TestFlight アプリのビルドグループ一覧です。タグとワークフロー名で分類されていることがわかります。
![TestFlightアプリで見るビルドグループの一覧](https://dwango.github.io/images/2024-07_nicoiphone_xcode_cloud/build_groups.png)
チーム内で相談した結果、DeployGate に関しては以下のような結論になりました。
- DeployGate も Xcode Cloud 経由でアップロードできるようにする
- 関係者が多く、一気に運用を変更するのが難しそうであったため
- TestFlight 内部テスターの運用は並行して試していく
- 少ない利用者から始めて、徐々に適用範囲を広げていく
- 問題がありそうなら DeployGate の運用に戻す
- 問題なさそうなら TestFlight 内部テスターに移行していく
ビルドの識別については、TestFlight 内部テスト用のブランチを案件毎に各自で切って、そこにマージ後にアップロードを行うルールとしました。
以上を踏まえると、最終的に必要な Xcode Cloud ワークフローの一覧は下記となります。
ワークフロー名 | 開始条件 | 概要 |
---|---|---|
Unit Test | 任意のPRの更新 | ユニットテストを実行する |
DeployGate Development-XXX upload | ブランチ指定 (手動) | DeployGate に Development ビルドをアップロードする XXX は配布ページの識別子であり、配布ページ毎にワークフローを用意する |
DeployGate AdHoc-XXX upload | ブランチ指定 (手動) | DeployGate に AdHoc ビルドをアップロードする XXX は配布ページの識別子であり、配布ページ毎にワークフローを用意する |
TestFlight upload for Internal Testing | ブランチ指定 (手動) | 内部テスト向けのアーカイブと内部テスト用の TestFlight アップロードを行う |
TestFlight upload for Release | タグ指定 (手動) | ストア向けのアーカイブと内部テスト用の TestFlight アップロードを行う ストアページ作成後、ビルドのみアップロードしたい場合に利用する |
Release | タグ指定 (手動) | ストア向けのアーカイブと内部テスト用の TestFlight アップロード、ストアページの作成を行う |
基本的な社内ライブラリでは Unit Test のみ必要で、ニコニコ動画 iOS アプリの場合は全てのワークフローが必要です。
DeployGate には複数の配布ページが存在し、Jenkins ジョブではジョブのビルドパラメータから対象ページを切り替えていました。しかし、Xcode Cloud ワークフローの実行時にはそのような任意のパラメータを指定することはできないので、仕方なく配布ページ毎にワークフローを作成します。また、Apple Developer Enterprise Program には Xcode Cloud が存在しないので、InHouse ビルドはそのままだと行えません。無理やり実行することもできますが、今回は割愛します。
一部ワークフローについては、手動ではなくブランチやタグの push を開始条件にしても良さそうですが、一旦手動で運用して様子を見ています。
Swift Package に対して Xcode Cloud を利用できるか調査する
Xcode Cloud ワークフローの実行対象となる Product には、App と Framework の2種類があります。以下は、複数のローカルの Swift Package を含んだニコニコ動画 iOS アプリにおいて、 Xcode Cluod ワークフロー作成を開始した時の画面です。
![Xcode Cloud ワークフローのプロダクト選択画面](https://dwango.github.io/images/2024-07_nicoiphone_xcode_cloud/xcode_cloud_workflow_products.png)
ここで App を選択すると、作成した Xcode Cloud ワークフローは App Store Connect 上に存在するアプリ定義と紐付きます。Framework を選択すると、まだ存在しなかった場合、App Store Connect 上に Framework が作成されます。これは現状 Xcode Cloud 関連操作のためだけの項目であるようです。
![App Store Connect 上の Framework 一覧表示](https://dwango.github.io/images/2024-07_nicoiphone_xcode_cloud/app_store_connect_frameworks.png)
iOS アプリの場合は App、アプリケーションではない社内ライブラリであれば Framework を選択すると良さそうです。ニコニコ動画 iOS アプリは複数の社内ライブラリに依存しており、それら社内ライブラリも Xcode Cloud に移行する必要があったため、それらは Framework として追加しました。
ただし、Xcode Cloud ワークフローの追加のためには Xcode プロジェクトもしくはワークスペースが必要です。例えば、Package.swift のみが配置されている Swift Package リポジトリの場合、そのままだと Xcode Cloud は利用できません。Apple のドキュメントでも以下のように言及されています。
You use a consistent Xcode project or workspace. https://developer.apple.com/documentation/xcode/requirements-for-using-xcode-cloud
これを解決するために、Swift Package を配布しているリポジトリでは、以下のように workspace を追加する必要があります。
- {任意の名前}.xcworkspace を追加する
- {任意の名前}.xcworkspace にローカルの Swift Package を追加する
- ワークスペースを開いた後に、Swift Package 配置先のディレクトリをドラッグ&ドロップする
- ローカルの Swift Package のテスト実行用スキームを追加する
手順 2 では、{任意の名前}.xcworkspace/contents.xcworkspacedata が以下のようになっていれば良いです。
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:{ローカルのSwiftPackageへのパス}">
</FileRef>
</Workspace>
手順 3 のテスト実行用スキームの追加をしないと、Xcode が Swift Package を Framework として認識しません。ここで作成したスキームは、Xcode Cloud ワークフローでのテスト実行にも利用します。
Custom Build Script の作成
ニコニコ動画 iOS アプリでは複数のワークフローを実行します。ワークフローの作成手順自体は特筆すべきものはないので割愛して、今回はワークフロー毎に必要な Custom Build Script [7] 設定を紹介します。
Custom Build Script は Xcode Cloud ワークフローの特定タイミングで実行することができるスクリプトです。標準出力/標準エラー出力は Build Report のログに出力され、exit code も反映されます。実行タイミングには以下の種類があります。
ファイル名 | タイミング | 利用例 |
---|---|---|
ci_post_clone.sh | Git リポジトリを clone した後 | 必要に応じて Info.plist を更新する ビルドに必要なツールをインストールする |
ci_pre_xcodebuild.sh | xcodebuild コマンドを実行する前 | 追加の依存がある場合にそのビルドを行う |
ci_post_xcodebuild.sh | xcodebuild コマンドを実行した後 | 外部サービスのストレージに成果物をアップロードする |
CocoaPods による依存のインストール
CocoaPods による依存のダウンロードは全てのワークフローで必要です。これを Xcode Cloud 上で行う場合、いくつかの選択肢があります[8][9]。
- Homebrew を利用する
- Bundler を利用する
- RubyGems を利用する
元々開発チームでは Bundler を利用して fastlane や CocoaPods をインストールしていたため、Bundler を利用しようと考えていました。しかし、Xcode Cloud 上の Ruby のバージョンが古いために一部ツールのインストールがうまくいかず、最終的には Homebrew を利用することになりました。rbenv 等で Ruby の環境を整えることから始めると時間がかかってしまうためです。
実行タイミングは ci_post_clone.sh であり、以下のように記述します。ニコニコ動画 iOS アプリではプロジェクトルートに Podfile が存在するので、まずプロジェクトルートに移動しています。
# ci_post_clone
cd ${CI_PRIMARY_REPOSITORY_PATH}
brew install cocoapods
pod install
ニコニコ動画 iOS アプリでは、クラッシュ情報の収集に Firebase Crashlytics を利用しています。よってアーカイブを実施するワークフローでは dSYM アップロードが必要です。従来は fastlane の upload_symbols_to_crashlytics アクション を利用していましたが、Xcode Cloud 移行によって fastlane の責務も減ったので、なるべく依存を少なくしていきたいと考えました。
実行タイミングはアーカイブ後なので、ci_post_xcode_build.sh に以下のように記述します。
# ci_post_xcodebuild
if [[ -d "${CI_ARCHIVE_PATH}/dSYMs" ]]; then
echo "🔄 FirebaseにdSYMをアップロードします"
${CI_DERIVED_DATA_PATH}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/upload-symbols \
-gsp ${CI_PRIMARY_REPOSITORY_PATH}/NicoNico/Resources/GoogleService-Info.plist \
-p ios ${CI_ARCHIVE_PATH}/dSYMs
echo "✅ FirebaseにdSYMをアップロードしました"
fi
GoogleService-Info.plist は、Buid Configuration やバンドル ID に合わせた適切なものを Custom Build Phase でコピーしています。これは Xcode Cloud 移行前からプロジェクトにあった設定です。
ストアページの作成
Release ワークフローの実行時には、fastlane によるストアページの作成を行いたいです。TestFlight アップロードは Xcode Cloud 側で行えますが、fastlane 実行については Custom Build Script の記述が必要です。
まず、Fastfile 側のレーン定義です。こちらは app_store_connect_api_key と deliver のみのシンプルな定義になります。
# Fastfile
platform :ios do
desc "次バージョンのストアページを作成する"
lane :store_page do |options|
app_store_connect_api_key
deliver
end
end
次に、app_store_connect_api_key の実行のために Xcode Cloud ワークフローに環境変数を設定します。この時のポイントは、APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64
を true に設定することです[10]。APP_STORE_CONNECT_API_KEY_KEY
に設定する p8 ファイルの内容には改行が含まれますが、これを環境変数に設定すると改行が失われてしまいます。また、app_store_connect_api_key はデフォルトだと改行を含む p8 ファイルの内容を期待しているので、APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64
を true に設定した上で、base64
コマンドによってエンコードした文字列を APP_STORE_CONNECT_API_KEY_KEY
に設定する必要があります。エンコードのためにはターミナル上で cat key.p8 | base64
を走らせます。
![ストアページ作成用のワークフローのEnvironments管理画面](https://dwango.github.io/images/2024-07_nicoiphone_xcode_cloud/store_page_workflow_environments.png)
以上で fastlane 実行のための準備が整ったので、以下のように ci_post_xcodebuild.sh を編集します。CocoaPods と同様の理由から、fastlane も Homebrew 経由でインストールすることにしています。
# ci_post_xcodebuild
if [[ $CI_WORKFLOW = "Release" ]]; then
echo "🔄 fastlaneをインストールします"
brew install fastlane
echo "✅ fastlaneをインストールしました"
echo "🔄 審査出しページを作成します"
fastlane ios store_page
echo "✅ 審査出しページを作成しました"
fi
DeployGate による配信
DeployGate による配信のために、以下を実施します。
タイミング | 作業 |
---|---|
ci_post_clone.sh | (必要であれば) アーカイブ時の Build Configuration を切り替える |
ci_post_clone.sh | アプリ表示名を変更する |
ci_post_xcodebuild.sh | DeployGate へアップロードする |
まず、ci_post_clone.sh の設定は以下のようになります。
# ci_post_clone
if [[ $CI_WORKFLOW == "DeployGate "* ]]; then
WORDS=($CI_WORKFLOW)
TARGET=${WORDS[1]}
#
# Build Configuration の書き換え
#
SCHEME_PATH="NicoMediaClient.xcodeproj/xcshareddata/xcschemes/NicoMediaClient.xcscheme"
if [[ $TARGET == "Development"* ]]; then
/usr/bin/xmllint --shell "${SCHEME_PATH}" <<EOF
cd //ArchiveAction/@buildConfiguration
set DebugArchive
save
EOF
fi
#
# アプリ表示名の書き換え
#
INFO_PLIST="${CI_PRIMARY_REPOSITORY_PATH}/NicoNico/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName ${TARGET} ニコニコ動画" "${INFO_PLIST}"
fi
ワークフロー名が「DeployGate 」から開始する場合に必要な作業を実施します。ニコニコ動画 iOS アプリのデフォルトのスキームだと、アーカイブ時の Build Configuration は Release になっています。しかし、開発中の動作確認では DEBUG フラグをオンにした状態でアーカイブしたいです。とはいえ、このためだけに専用のスキームを用意するのも大げさなので、動的に Build Configuration を DebugArchive という専用の Build Configuration に切り替えています。アプリ表示名の切り替えは、Info.plist を PlistBuddy で書き換えることで実現します。
ci_post_xcodebuild.sh の設定は以下のようになります。
# ci_post_xcodebuild
cd ${CI_PRIMARY_REPOSITORY_PATH}
if [[ $CI_WORKFLOW == "DeployGate "* ]]; then
WORDS=($CI_WORKFLOW)
TARGET=${WORDS[1]}
if [[ $TARGET == "AdHoc"* ]]; then
FILE_PATH="${CI_AD_HOC_SIGNED_APP_PATH}/${CI_PRODUCT}.ipa"
elif [[ $TARGET == "Development"* ]]; then
FILE_PATH="${CI_DEVELOPMENT_SIGNED_APP_PATH}/${CI_PRODUCT}.ipa"
else
echo "🛑 予期しないターゲット(${TARGET})が指定されました"
exit 1
fi
if [[ ! -f $FILE_PATH ]]; then
echo "🛑 ipaファイル ${FILE_PATH} が存在しませんでした"
exit 1
fi
echo "🔄 DeployGate ${TARGET} にアップロードします"
# https://docs.deploygate.com/docs/api/application/upload/
curl \
--url "https://deploygate.com/api/users/${DEPLOYGATE_USER}/apps" \
-H "Authorization: Bearer ${DEPLOYGATE_API_TOKEN}" \
-X POST \
-F "file=@${FILE_PATH}" \
--form-string "message=${CI_BRANCH}" \
--form-string "distribution_key=${DEPLOYGATE_DISTRIBUTION_KEY}" \
--form-string "release_note=${CI_BRANCH}"
echo "✅ DeployGate にアップロードしました"
fi
Xcode Cloud でアーカイブを実行すると、各配布設定でコード署名されたアーカイブが専用のパスに出力されます[11]。それを振り分けた上で、DeployGate の API を叩けば良さそうです。Jenkins ではリリースノートはビルドパラメータとして受け取った任意の文字列だったのですが、Xcode Cloud にはビルドパラメータのような仕組みはないので、ブランチ名を代わりに設定します。
Xcode Cloud ワークフローの環境変数には、以下をそれぞれ設定します。
環境変数名 | 値 |
---|---|
DEPLOYGATE_USER | DeployGate ユーザー名 |
DEPLOYGATE_API_TOKEN | DeployGate の API トークン |
DEPLOYGATE_DISTRIBUTION_KEY | 配布ページの Distribution Key |
移行後の問題とその対応策
移行時や移行後にハマった問題と、その解決策を紹介します。
一部 Pod のインストールに失敗する問題
CocoaPods 経由でとあるライブラリをダウンロードしてくる際に、ほぼ確実に以下のような curl のエラーでダウンロードに失敗してしまうようになりました。
curl: (35) Recv failure: Connection reset by peer
色々と調査してみたものの原因を割り出すことができず、結局対象の Pod を Git 管理の対象に追加することで解決しました。 ちなみにこのエラーは、以下のような状況でも稀に発生することが確認されています。
- 一部環境のために Java が必要なので、Xcode Cloud 上で Java をインストールする
- Xcode Cloud 上で DeployGate へバイナリをアップロードする
幸い頻度はそこまで多くはないですが、不定期に CI が失敗するような状況は避けたいです。こちらで報告されているのと似たような状況かとは思われますが、有効な解決策はなさそうでした。
この問題については、現在Apple に問い合わせ中です。
テスト実行時間が増加してしまう問題
ニコニコ動画 iOS アプリには 15,000 件超のテストがあります。このように大量のテストがあるリポジトリでテストアクションを走らせると、謎の処理時間 (スタックする時間) が発生してしまうことがわかりました。
試しにとあるビルド結果を覗いてみます。 Xcode Cloud のログで Run xcodebuild test-without-building 時の時間を確認すると、23分ほどかかっています。ユニットテストの実行に 23 分というのはかなりかかっているように思えます。
![ニコニコ動画iOSアプリのテスト実行時間](https://dwango.github.io/images/2024-07_nicoiphone_xcode_cloud/nicoiphone_test_time.png)
しかし、成果物から test-without-building 時のログを取得し、xcodebuild-test-without-building.log の内容を確認すると、開始から終了までの間は 4 分ほどしかありません。
2024-07-04T05:10:40.068483841Z Command line invocation:
...
2024-07-04T05:14:30.824263538Z Test session results, code coverage, and logs:
2024-07-04T05:14:30.824309186Z /Volumes/workspace/resultbundle.xcresult
2024-07-04T05:14:30.824359593Z
2024-07-04T05:14:30.824427909Z ** TEST EXECUTE SUCCEEDED **
2024-07-04T05:14:30.824470677Z
2024-07-04T05:14:35.869509216Z Testing started
別途 XCResult を取得し確認しても、同様に 4 分ほどしかかかっていませんでした。
![ニコニコ動画iOSアプリのテストのXCResultファイル](https://dwango.github.io/images/2024-07_nicoiphone_xcode_cloud/nicoiphone_xcresult.png)
一方で、Logs for Update Result Bundle with Metadata Command-stdout.log を確認すると以下のようになっており、この標準出力までの間に約20分が経過していることがわかります。
2024-07-04T05:34:09.853383922Z Added external location with identifier "Xcode Cloud" to /Volumes/workspace/resultbundle.xcresult/Info.plist.
ログに現れていないこの時間を改善すべく、以下のような対応を試しましたが、いずれも効果はありませんでした。
- Parallel Testing を有効にする
- Collect Test Diagnostics on Failure をオフにする
- OS_ACTIVITY_MODE = disable にする
- fatalError の throw のテストをしている箇所の無効化
このスタックは Xcode Cloud に移行した各リポジトリで大なり小なり発生しているようでした。その後、各リポジトリにおいて、テストケース数とスタックしている時間の相関を調べたところ、おおよそ 0.06-0.09秒/テストケース分のオーバーヘッドが発生していると考えると辻褄が合いそうなことがわかりました。この予測が仮に正しかった場合、1ケースあたり0.06秒だとしても、15000件あれば 900秒=15分 となり、日々の CI に関わる時間と考えると決して少なくありません。
多くのリポジトリではそこまで問題にならないのですが、ニコニコ動画 iOS アプリではテストケース数が今も増え続けているのもあり、開発生産性に悪影響を及ぼします。実は、移行前にこの問題があることはわかっており、ユニットテストの実行については代替案として GitHub Actions を用意しつつ、開発に支障があればユニットテストの実行のみそちらに切り替えることを考えていました。
結果として、Xcode Cloud はそれなりの数並列実行することが可能なので、平時の開発業務ではそこまで問題になりませんでした。しかし急ぎのリリース作業時にはどうしてもボトルネックになってしまいます。
よって、GitHub Actions への切り替えも視野に入れつつ、何か改善する術がないか Apple に問い合わせており、現在はその返答を待っている状態です。
まとめ
良かったところ/悪かったところ
ニコニコ動画 iOS アプリ開発チームでは、既に #XcodeCloudへ移行 してから数ヶ月が経過しています。現時点での良かったところと悪かったところをまとめます。
良かったところ
- fastlane match による証明書の管理が減った
- fastlane の責務が減った
- 最新の Xcode がすぐ使える
- 最近で言うと、Xcode 16 beta が公開されてすぐに使えるようになっていました
- 自動でビルドワーニングを Check notice として GitHub 上で報告してくれる
- 手元の Xcode で CI 結果をすぐに確認できるのは便利
悪かったところ
- 一部プロジェクトでテスト実行が非常に遅くなった
- Apple に問い合わせ中
- 通信が安定しないことがある
- Apple に問い合わせ中
- App Store Connect がたまに不安定
- ページの表示が重かったり
- Framework が閲覧できなくなったり
- 一部ページが閲覧できなくなったり
- Xcode Cloud の bug fix については、リリースノート を確認すると良い
今後の展望
まだまだ不安定な部分もありますが、一部テスト実行が遅くなってしまった点以外については概ね満足しています。チーム管理の Mac mini の保守も必要なくなり、運用が楽になりました。
移行自体は完了したので、今後は DeployGate から TestFlight 内部テストへの運用の切り替えを試していきます。また、ワークフローの開始条件が手動になっている箇所は、様子を見てルールを定め、できるだけ作業を自動化できるように改善していきます。
ドワンゴでは、ニコニコ動画を一緒に作ってくれる仲間を募集しています!iOSやAndroidのネイティブアプリ開発が好きな方、ドワンゴに興味がある、または応募しようか迷っている方がいれば、気軽に応募してみてください。
[1]: Configuring your first Xcode Cloud workflow - Apple Developer ↩
[2]: Xcode Cloud Workflow Reference - Apple Developer ↩
[3]: Configuring your first Xcode Cloud workflow - Apple Developer ↩
[4]: Xcode Cloudの利用開始 ↩
[5]: Xcode Cloud workflow reference - Apple Developer ↩
[6]: Including notes for testers with a beta release of your app - Apple Developer ↩
[7]: Writing Custom Build Scripts - Apple Developer ↩
[8]: Xcode Cloud – overview & setup - Wojciech Kulik ↩
[9]: Making dependencies available to Xcode Cloud - Apple Developer ↩
[10]: iOS: using App Store Connect API with fastlane ↩
[11]: Environment variable reference - Apple Developer ↩