La primera pregunta en cualquier demo de Xcode Cloud en una empresa de stack mixto es siempre la misma: «Nuestro backend está en Kotlin, tenemos equipo de Android y todo corre en GitHub Actions. ¿Eso también se muda allí?» Respuesta corta: no. La respuesta larga es este artículo: Xcode Cloud no reemplaza el pipeline compartido, pero encaja bastante bien como ejecutor de un solo rol — si conoces los tres puentes: App Store Connect API de entrada, webhooks de salida y artefactos bajo demanda.
Lo básico del servicio — workflows, precios, pros y contras — está en el primer artículo de la serie. Aquí, solo integración.
Fronteras: qué compila y qué no#
El entorno de Xcode Cloud es macOS sobre Apple silicon. Sin Linux, sin Docker, sin runners propios. Una app de Android no se puede compilar ahí — no porque esté prohibido, sino porque el entorno para hacerlo no existe: ni Android SDK, ni contenedores donde meterlo. El backend, menos todavía.
El reparto para una empresa típica queda así: backend y Android se quedan donde viven — GitHub Actions, GitLab CI, Jenkins. Xcode Cloud toma la parte iOS/macOS: build, tests, firma, TestFlight. La tarea se reduce a que los dos sistemas se vean entre sí.
Entrada: lanzar una build desde fuera#
La App Store Connect API puede manejar Xcode Cloud por completo: leer productos y workflows, revisar resultados y — lo clave — lanzar builds: POST /v1/ciBuildRuns.
Escenario real: el backend acaba de desplegar en staging una nueva versión del contrato de API, y quieres que la app de iOS corra sus tests de integración contra ese staging de inmediato. El script es Python normal; las únicas dependencias son pyjwt y 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"])El token es un JWT ES256 con vida útil de hasta 20 minutos; la clave se emite una sola vez en App Store Connect, y el rol Developer basta para lanzar builds. Lo más fácil para obtener WORKFLOW_ID es un GET único a mano, y luego dejarlo fijo en los secretos.
En GitHub Actions esto se convierte en un job barato de Linux al final del deploy de staging:
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 }}Ojo con esto: Xcode Cloud compila lo que está en el repositorio git, no lo que tú le mandes: la build parte del HEAD de la rama configurada en el workflow. Lo que no esté pusheado no se compila. Para Jenkins existe el plugin oficial xcode-cloud-for-pipeline, que funciona con el mismo esquema.
Salida: webhooks de vuelta al pipeline#
La dirección inversa la cubren los webhooks: en la configuración del workflow se indica una URL, y Xcode Cloud envía un POST al crearse y al terminar cada build run — con la descripción JSON de la build, su estado y enlaces. La integración con Slack viene de fábrica, así que si solo necesitas notificaciones en un canal, puedes ahorrarte el servidor de webhooks por completo.
Si el pipeline compartido de verdad tiene que esperar el resultado de la build de iOS (por ejemplo, para armar un tren de release con backend, Android e iOS en una misma versión), hay dos opciones. Primera: recibir el webhook en tu propio endpoint y continuar el pipeline desde ahí. La documentación de Apple no describe firma de la petición, así que conviene tener el endpoint en una ruta secreta y verificar el id recibido con un GET de vuelta a la API — sin confiar en el cuerpo del webhook. Segunda: la tonta y confiable — tras el POST /v1/ciBuildRuns, consultar GET /v1/ciBuildRuns/{id} una vez por minuto hasta que reporte completed. Frente a la duración de la build en sí, el polling no arruina nada.
Artefactos y estados#
Dentro de un repositorio de GitHub, Xcode Cloud publica los estados en el pull request por su cuenta, y puedes hacerlos obligatorios para el merge — esa parte funciona sin una línea de código.
Los artefactos salen por la misma API: un build run tiene build actions, y estas tienen artifacts con URLs de descarga. Ahí están tu .ipa, .xcresult y los logs. Escenario nocturno típico: workflow programado → webhook de finalización → tu job recoge el .ipa y el .xcresult y los guarda en el archivo corporativo donde QA y seguridad puedan verlos.
Monorepos y contratos compartidos#
Las condiciones de inicio del workflow saben filtrar por archivos y carpetas: si la app de iOS vive en ios/ del monorepo, la build se dispara solo con cambios en esa carpeta — los commits del equipo de Android no quemarán tus horas.
El código compartido con el backend — esquemas protobuf, contratos OpenAPI — se genera en ci_scripts/ci_post_clone.sh: instalas el generador vía Homebrew, lo ejecutas, y la build sigue con tipos actualizados. Las variables de entorno pueden compartirse entre varios workflows, para que la URL de staging no se propague por copy-paste.
Lo que sigue torcido#
Tres cosas que la integración no cura. Los tests de integración solo llegan a un staging externo — no puedes levantar un docker-compose con mocks junto a la build. La configuración del workflow vive en App Store Connect, no en git: exportarla por API se puede, pero la «infraestructura como código» aquí es tu script, no una función de la plataforma. Y el calendario de builds queda repartido entre dos sistemas, así que «qué se compila y cuándo» ya no se responde mirando un solo repositorio — hazle una página en la wiki, en serio.
Checklist de decisión#
El híbrido «CI compartido + Xcode Cloud» tiene sentido si se cumplen la mayoría de estos puntos:
- El producto es multiplataforma, pero lo que de verdad sufre el equipo de iOS es la firma y los agentes Mac.
- El pipeline compartido ya vive en GitHub Actions / GitLab / Jenkins y nadie te dejará tirarlo.
- A los tests de integración les basta un entorno de staging por red.
- Guardar los secretos de la build de iOS en App Store Connect es aceptable.
- El volumen de builds cabe en 25–250 horas al mes.
Si cuadra — empieza en pequeño: un workflow para los checks de PR de la carpeta iOS, estados en GitHub, notificaciones en Slack. Los puentes por API y webhooks constrúyelos cuando el tren de release necesite de verdad esperar la build de iOS — no porque el diagrama de arquitectura se vea incompleto sin ellos.



