混合スタックの会社でXcode Cloudのデモをやると、最初の質問はいつも同じだ。「うちのバックエンドはKotlinで、Androidチームがいて、全部GitHub Actionsで回ってる。それも全部あっちに引っ越すの?」短い答え:いいえ。長い答えがこの記事だ。Xcode Cloudは共通パイプラインを置き換えるものではないが、3本の橋 — 入口のApp Store Connect API、出口のwebhook、必要に応じたアーティファクト取得 — を知っていれば、単一の役割を担う実行者としてうまく収まる。
サービスの基礎 — workflow、料金、長所と短所 — はシリーズ第1回にある。ここでは統合だけを扱う。
境界線:何をビルドできて、何ができないか#
Xcode Cloudの環境はApple silicon上のmacOSだ。Linuxなし、Dockerなし、持ち込みランナーなし。Androidアプリはそこでビルドできない。禁止されているからではなく、そのための環境が存在しないからだ。Android SDKもなければ、それを持ち込むコンテナもない。バックエンドはなおさらだ。
つまり典型的な会社での配置はこうなる:バックエンドとAndroidは今の住処 — GitHub Actions、GitLab CI、Jenkins — に残る。Xcode CloudはiOS/macOS部分 — ビルド、テスト、署名、TestFlight — を引き受ける。課題は、2つのシステムがお互いを見えるようにすることに絞られる。
入口:外からビルドを起動する#
App Store Connect APIはXcode Cloudを端から端まで操作できる。プロダクトとworkflowの読み取り、結果の確認、そして肝心のビルド起動:POST /v1/ciBuildRuns。
現実的なシナリオ:バックエンドがstagingに新しいAPIコントラクトをデプロイした直後、iOSアプリの統合テストをそのstagingに対してすぐ回したい。起動スクリプトは普通のPythonで、依存はpyjwtとrequestsだけだ。
"""trigger_xcode_cloud.py — start a workflow via the App Store Connect API."""
import os
import time
import jwt
import requests
ISSUER_ID = os.environ["ASC_ISSUER_ID"] # App Store Connect → Users and Access → Integrations
KEY_ID = os.environ["ASC_KEY_ID"]
PRIVATE_KEY = os.environ["ASC_PRIVATE_KEY"] # contents of AuthKey_XXXX.p8
WORKFLOW_ID = os.environ["XC_WORKFLOW_ID"] # GET /v1/ciProducts/{id}/workflows — once, by hand
token = jwt.encode(
{"iss": ISSUER_ID, "aud": "appstoreconnect-v1", "exp": int(time.time()) + 600},
PRIVATE_KEY,
algorithm="ES256",
headers={"kid": KEY_ID},
)
resp = requests.post(
"https://api.appstoreconnect.apple.com/v1/ciBuildRuns",
json={"data": {
"type": "ciBuildRuns",
"relationships": {
"workflow": {"data": {"type": "ciWorkflows", "id": WORKFLOW_ID}},
},
}},
headers={"Authorization": f"Bearer {token}"},
timeout=30,
)
resp.raise_for_status()
print("Started build run:", resp.json()["data"]["id"])トークンは有効期限最大20分のES256 JWT。キーはApp Store Connectで一度だけ発行し、ビルド起動にはDeveloperロールで足りる。WORKFLOW_IDは一度だけ手動でGETして、シークレットに固定してしまうのが一番楽だ。
GitHub Actionsでは、stagingデプロイの最後に安価なLinuxジョブとして置く。
ios-contract-tests:
needs: deploy-staging
runs-on: ubuntu-latest # Linux at $0.008/min — we only poke an API
steps:
- uses: actions/checkout@v4
- run: pip install "pyjwt[crypto]" requests
- run: python ci/trigger_xcode_cloud.py
env:
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_PRIVATE_KEY: ${{ secrets.ASC_PRIVATE_KEY }}
XC_WORKFLOW_ID: ${{ secrets.XC_WORKFLOW_ID }}ひとつ注意:Xcode Cloudがビルドするのはgitリポジトリにあるもので、あなたが送りつけたものではない。ビルドはworkflowに設定されたブランチのHEADから走る。pushしていないものはビルドされない。Jenkinsには同じ仕組みで動く公式プラグインxcode-cloud-for-pipelineがある。
出口:webhookでパイプラインに返す#
逆方向はwebhookが受け持つ。workflow設定にURLを指定すると、build runの作成時と完了時にXcode CloudがPOSTを送ってくる。中身はビルドのJSON記述、ステータス、各種リンクだ。Slack連携は標準装備なので、チャンネル通知だけが目的ならwebhookサーバーを書く必要すらない。
共通パイプラインがiOSビルドの結果を本当に待つ必要がある場合(たとえばバックエンド・Android・iOSを同一バージョンで束ねるリリーストレインを組むとき)、選択肢は2つ。ひとつ目は、自前のエンドポイントでwebhookを受けてそこからパイプラインを続ける方法。Appleのドキュメントにはリクエスト署名の記述がないため、エンドポイントは秘密のパスに置き、届いたビルドのidはAPIへの逆GETで検証すべきだ。webhookのボディを信用してはいけない。ふたつ目は、愚直で確実な方法:POST /v1/ciBuildRunsのあと、GET /v1/ciBuildRuns/{id}をcompletedになるまで1分おきにポーリングする。ビルド自体の長さを考えれば、ポーリングで損なわれるものは何もない。
アーティファクトとステータス#
GitHubリポジトリの中では、Xcode Cloudが自力でpull requestにステータスを付け、mergeの必須条件にもできる。この部分はコードゼロで動く。
アーティファクトは同じAPIから取る。build runにはbuild actionsがあり、そこにダウンロードURL付きのartifactsがぶら下がる。.ipa、.xcresult、ログだ。典型的な夜間シナリオ:スケジュール実行のworkflow → 完了webhook → 自分のジョブが.ipaと.xcresultを回収し、QAとセキュリティが見られる社内アーカイブに格納する。
モノレポと共有コントラクト#
workflowの開始条件はファイル・フォルダ単位のフィルタに対応している。iOSアプリがモノレポのios/に住んでいるなら、そのフォルダの変更だけでビルドが走る。Androidチームのコミットであなたの時間が燃えることはない。
バックエンドと共有するコード — protobufスキーマ、OpenAPIコントラクト — はci_scripts/ci_post_clone.shで生成する。Homebrewでジェネレータを入れ、実行すれば、ビルドは最新の型で進む。環境変数は複数のworkflow間で共有できるので、stagingのURLがコピペで散らばることもない。
曲がったまま残るもの#
統合でも治らないものが3つある。統合テストが届くのは外部のstagingだけで、ビルドの隣にモック入りのdocker-composeを立てることはできない。workflow設定はgitではなくApp Store Connectに住んでいて、APIでエクスポートはできるが、ここでの「infrastructure as code」はプラットフォームの機能ではなくあなたのスクリプトだ。そしてビルドのスケジュールは2つのシステムにまたがってしまうので、「何がいつビルドされるのか」にひとつのリポジトリだけで答えることはもうできない。wikiにページを作っておくこと。本気で。
判断チェックリスト#
「共通CI + Xcode Cloud」のハイブリッドは、以下の大半が当てはまるなら意味がある。
- プロダクトはマルチプラットフォームだが、iOSチームが実際に苦しんでいるのは署名とMacエージェントだ。
- 共通パイプラインはすでにGitHub Actions / GitLab / Jenkinsにあり、誰も捨てさせてくれない。
- 統合テストはネットワーク越しのstaging環境で足りる。
- iOSビルドのシークレットをApp Store Connectに置くことが許容できる。
- ビルド量が月25〜250時間に収まる。
当てはまったら、小さく始めることだ。iOSフォルダのPRチェック用workflowをひとつ、GitHubにステータス、Slackに通知。APIとwebhookの橋は、リリーストレインが本当にiOSビルドを待つ必要が出てから架ければいい。アーキテクチャ図がそれなしでは不完全に見えるから、という理由で架けるものではない。



