Первый вопрос на любом демо Xcode Cloud в компании со смешанным стеком звучит одинаково: «У нас бек на Kotlin, Android-команда и всё это в GitHub Actions. Оно тоже туда переедет?» Короткий ответ: нет. Длинный ответ — эта статья: Xcode Cloud не заменяет общий пайплайн, но неплохо встраивается в него как исполнитель одной роли, если знать три моста — App Store Connect API на вход, вебхуки на выход и артефакты по запросу.
Базовый разбор сервиса — что такое workflows, цены, плюсы и минусы — в первой статье серии. Здесь только интеграция.
Границы: что он собирает, а что нет#
Среда Xcode Cloud — macOS на Apple silicon. Без Linux, без Docker, без своих раннеров. Android-приложение там собрать нельзя — не потому что «не положено», а потому что среды для этого нет: ни Android SDK, ни контейнеров, в которых его можно было бы притащить. Бекенд — тем более.
Значит, расклад для типичной компании такой: бек и Android остаются где жили — GitHub Actions, GitLab CI, Jenkins. Xcode Cloud забирает iOS/macOS-часть: сборку, тесты, подпись, TestFlight. Задача сводится к тому, чтобы две системы видели друг друга.
Вход: запускаем сборку снаружи#
App Store Connect API умеет управлять Xcode Cloud целиком: читать продукты и workflow, смотреть результаты и — главное — запускать сборки: POST /v1/ciBuildRuns.
Живой сценарий: бекенд выкатил на staging новую версию API-контракта, и надо тут же прогнать iOS-приложение с интеграционными тестами против этого staging. Скрипт запуска — обычный Python, из зависимостей только pyjwt и requests:
"""trigger_xcode_cloud.py — запуск workflow через 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"] # содержимое AuthKey_XXXX.p8
WORKFLOW_ID = os.environ["XC_WORKFLOW_ID"] # GET /v1/ciProducts/{id}/workflows — один раз, руками
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"])Токен — ES256 JWT со сроком жизни до 20 минут; ключ выпускается в App Store Connect один раз, роль Developer для запуска сборок достаточна. WORKFLOW_ID проще всего подсмотреть однократным GET-запросом и захардкодить в секреты.
В GitHub Actions это превращается в дешёвый Linux-job в самом конце деплоя staging:
ios-contract-tests:
needs: deploy-staging
runs-on: ubuntu-latest # Linux за $0.008/мин — мы только дёргаем 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-репозитории, а не то, что вы ему прислали: сборка пойдёт с HEAD ветки, на которую настроен workflow. Незапушенное не соберётся. Для Jenkins существует официальный плагин xcode-cloud-for-pipeline, работающий по той же схеме.
Выход: вебхуки обратно в пайплайн#
Обратное направление закрывают вебхуки: в настройках workflow указывается URL, и Xcode Cloud шлёт POST при создании и завершении build run — с JSON-описанием сборки, её статусом и ссылками. Готовая интеграция со Slack есть из коробки, так что если нужны только уведомления в канал — вебхук-сервер можно не писать вообще.
Если же общий пайплайн должен дождаться результата iOS-сборки (например, чтобы собрать релизный поезд из бека, Android и iOS одной версии), варианта два. Первый — принять вебхук своим эндпоинтом и продолжить пайплайн по нему. Документация Apple не описывает подпись запроса, поэтому эндпоинт стоит держать на секретном пути, а пришедший id сборки перепроверять обратным GET-запросом к API — не доверяя телу вебхука. Второй вариант — тупой и надёжный: после POST /v1/ciBuildRuns опрашивать GET /v1/ciBuildRuns/{id} раз в минуту до статуса completed. На фоне длительности самой сборки поллинг ничего не портит.
Артефакты и статусы#
Внутри GitHub-репозитория Xcode Cloud сам ставит статусы в pull request, и их можно сделать обязательными для merge — это работает без всякого кода.
Артефакты добываются через тот же API: у build run есть build actions, у них — artifacts со ссылками на скачивание. Это .ipa, .xcresult и логи. Типичный ночной сценарий: workflow по расписанию → вебхук о завершении → ваш job забирает .ipa и .xcresult и складывает в корпоративный архив, где их увидят QA и безопасность.
Монорепо и общие контракты#
Условия запуска workflow умеют фильтровать по файлам и папкам: если iOS-приложение живёт в ios/ монорепы, сборка триггерится только на изменения этой папки — коммиты Android-команды часы жечь не будут.
Общий код с бекендом — protobuf-схемы, OpenAPI-контракты — генерируется в ci_scripts/ci_post_clone.sh: ставите генератор через Homebrew, запускаете, и сборка идёт уже с актуальными типами. Переменные окружения при этом можно расшарить между несколькими workflow, чтобы адрес staging не расползался копипастой.
Что остаётся кривым#
Три вещи, которые интеграция не лечит. Интеграционные тесты ходят только во внешний staging — поднять docker-compose с моками рядом со сборкой нельзя. Конфигурация workflow живёт в App Store Connect, а не в git: экспортировать её через API можно, но «инфраструктура как код» здесь — ваш скрипт, а не фича платформы. И расписание сборок оказывается размазано по двум системам, так что понять, «что и когда собирается», по одному репозиторию больше не получится — заведите об этом страницу в вики, серьёзно.
Чек-лист решения#
Гибрид «общий CI + Xcode Cloud» имеет смысл, если сходится большинство пунктов:
- Продукт мультиплатформенный, но iOS-команда страдает именно от подписи и Mac-агентов.
- Общий пайплайн уже живёт в GitHub Actions / GitLab / Jenkins, и выкидывать его никто не даст.
- Интеграционным тестам достаточно staging-окружения по сети.
- Секреты iOS-сборки допустимо хранить в App Store Connect.
- Объём сборок вписывается в 25–250 часов в месяц.
Если сошлось — начните с малого: один workflow на PR-проверки iOS-папки, статусы в GitHub, Slack-уведомления. Мосты через API и вебхуки достраивайте тогда, когда релизному поезду реально понадобится ждать iOS-сборку, — а не потому что архитектурная схема без них выглядит неполной.



