Xcode Cloud: la opción integrada y cuándo compensa frente a montarlo tú
Xcode Cloud no es un servidor de integración continua más: es una integración vertical que elimina de golpe los tres problemas más caros de un pipeline de iOS, la firma, el acceso a App Store Connect y el mantenimiento de máquinas, a cambio de un modelo de configuración cerrado y sin lenguaje de flujo. Esta lección desmonta sus flujos, sus puntos de extensión reales y el cálculo honesto de cuándo esa integración vale su precio.
Todo el debate sobre Xcode Cloud se plantea mal cuando se compara como si fuera otro proveedor de agentes. No lo es. Lo que vende no es cómputo sino la desaparición de tres problemas que en cualquier otro montaje consumen semanas al año: no hay certificados que gestionar porque la firma la hace la propia infraestructura de Apple con tu cuenta, no hay credenciales de App Store Connect que rotar porque la autenticación es la relación que ya tienes con Apple, y no hay máquinas que actualizar porque las versiones de herramientas son un menú desplegable. A cambio renuncias a un lenguaje de flujo, a ejecutar cualquier cosa fuera del ecosistema y a mover tu pipeline a otro sitio sin reescribirlo. La decisión no es técnica en el sentido habitual: es una decisión sobre dónde quieres que viva la complejidad.
- Entender el modelo de ejecución de Xcode Cloud: flujos, condiciones de inicio, acciones y post-acciones.
- Usar los tres puntos de extensión reales, que son guiones de shell en momentos fijos del ciclo.
- Calcular el coste total honesto, incluyendo el tiempo de ingeniería que se ahorra y no solo los minutos facturados.
- Decidir con criterio entre la opción integrada, un montaje propio y el híbrido que usa la mayoría de equipos maduros.
El modelo: flujos con condiciones, acciones y post-acciones
Un flujo de Xcode Cloud tiene cuatro piezas y ninguna más. Un entorno, donde se fija la versión de Xcode y la de macOS y donde viven las variables y los secretos. Una condición de inicio, que puede ser un cambio en una rama, una propuesta de cambio, una etiqueta o una programación horaria. Una lista de acciones, que son las verbos disponibles y forman un conjunto cerrado: analizar, compilar, probar y archivar. Y unas post-acciones, que solo se ejecutan si la acción correspondiente tuvo éxito y sirven para distribuir el resultado o notificarlo.
Esa lista cerrada es la característica que más desconcierta a quien viene de un fichero de flujo con pasos arbitrarios, y conviene entenderla bien porque es deliberada. No existe la noción de paso genérico. Si quieres subir cobertura a un servicio externo, generar un informe, tocar un sistema de seguimiento de incidencias o instalar una herramienta, todo eso ocurre dentro de los guiones de extensión, no como acciones de primer nivel.
flowchart TB s[Condicion de inicio] --> cl[Clonar el repositorio] cl --> po[ci post clone sh] po --> res[Resolver dependencias de Swift Package Manager] res --> pre[ci pre xcodebuild sh] pre --> ac[Accion: analizar compilar probar o archivar] ac --> post[ci post xcodebuild sh] post --> ok[Post acciones si todo fue bien] ok --> tf[Distribuir en TestFlight] ok --> nt[Notificar al equipo] ok --> ar[Guardar artefactos]
Los tres guiones del diagrama son el punto de extensión completo. Viven en un directorio llamado ci_scripts en la raíz del repositorio, tienen que ser ejecutables y se invocan siempre en el mismo momento del ciclo. El primero corre después de clonar y antes de que se resuelvan las dependencias, que es el único hueco donde puedes instalar herramientas o generar ficheros que el proyecto necesite para existir. El segundo corre justo antes de invocar el compilador. El tercero corre después, con el resultado ya disponible.
#!/bin/sh
# ci_scripts/ci_post_clone.sh
set -e
# Herramientas que el proyecto necesita y que la imagen no trae.
brew install --quiet xcbeautify swiftlint
# Generar el fichero de configuracion que no vive en el repositorio.
cat > "$CI_PRIMARY_REPOSITORY_PATH/MiApp/Secretos.swift" <<EOF
enum Secretos {
static let apiBase = "$API_BASE"
static let clave = "$API_KEY"
}
EOF
echo "Preparado para el flujo $CI_WORKFLOW en la rama $CI_BRANCH"
Las variables que empiezan por CI_ las inyecta la propia plataforma y son la forma de que un guion sepa dónde está y por qué se ejecuta. Las más útiles en la práctica son CI_XCODEBUILD_ACTION, que dice si el flujo actual está probando o archivando y permite que un mismo guion se comporte distinto en cada caso; CI_BUILD_NUMBER, que es un contador monótono gestionado por Apple y resuelve gratis un problema que en un montaje propio cuesta trabajo; y CI_PULL_REQUEST_NUMBER, que solo existe cuando el flujo viene de una propuesta de cambio.
En cualquier montaje propio hay que inventar una fuente monótona para el número de compilación, y todas las opciones tienen pegas. Xcode Cloud entrega CI_BUILD_NUMBER ya incrementado y garantizado único por producto. Escribirlo en el Info.plist desde ci_pre_xcodebuild.sh con agvtool o PlistBuddy cierra el asunto en tres líneas y sin coordinación entre máquinas.
Lo que de verdad se está comprando
El argumento comercial habla de máquinas rápidas y eso es lo menos interesante. Lo que se compra son tres eliminaciones.
La firma desaparece
No hay certificados, ni perfiles, ni un llavero temporal que crear y destruir. Apple firma con tu cuenta desde dentro. Esto elimina la categoría de incidencias más frecuente y más frustrante de cualquier pipeline de iOS.
Las credenciales desaparecen
No hay claves de App Store Connect que generar, guardar como secreto ni rotar cuando alguien deja el equipo. La subida a TestFlight es una casilla en una post-acción.
El mantenimiento desaparece
La versión de Xcode es un desplegable, incluidas las betas el mismo día del anuncio. Nadie tiene que mantener imágenes ni averiguar por qué el agente tiene una versión distinta de las herramientas de línea de comandos.
Los simuladores vienen dados
Probar en varias combinaciones de dispositivo y sistema es marcar casillas en el flujo, sin instalar tiempos de ejecución ni pelear con descargas de varios gigabytes en cada agente.
Y hay que ser igual de preciso con lo que se pierde. No hay lenguaje de flujo: no puedes expresar dependencias entre trabajos, matrices arbitrarias, condiciones complejas ni reutilización entre proyectos más allá de copiar la configuración. Todo lo que no sea analizar, compilar, probar o archivar tiene que caber dentro de tres guiones de shell, lo que empuja a escribir guiones cada vez más grandes que acaban siendo un lenguaje de flujo casero y peor. El diagnóstico de fallos es más pobre que el de una máquina propia porque no hay acceso interactivo al agente. Y no existe nada parecido a un ejecutor local: reproducir un fallo que solo ocurre en la nube significa iterar mediante envíos.
El coste conviene calcularlo entero. Los minutos facturados de una plataforma gestionada son más caros por minuto que un agente equivalente, pero el sumando que decide la comparación casi nunca es ese: es el tiempo de ingeniería. Un equipo que dedique media jornada al mes a que la firma vuelva a funcionar, a actualizar imágenes y a perseguir credenciales caducadas está pagando, en salarios, bastante más de lo que cuesta cualquier plan razonable de minutos. La comparación honesta pone en el mismo lado de la balanza los minutos y las horas de persona, y en el otro lado la flexibilidad que se sacrifica.
El criterio de decisión y el híbrido que gana
Un equipo pequeño de una sola aplicación, sin backend propio ni herramientas exóticas, casi siempre debería usar la opción integrada: el ahorro es inmediato y el techo de flexibilidad queda muy lejos. Un equipo con un monorepo que mezcla iOS, web y servicios, con generación de código y despliegues coordinados, necesita un lenguaje de flujo y montarlo aparte; la opción integrada quedaría como una isla que no sabe hablar con el resto. Y un equipo mediano de producto suele acabar en el híbrido: la validación de cada cambio en la plataforma más barata y flexible, y la entrega firmada en la integrada.
Ese híbrido merece explicarse porque es el resultado al que converge la mayoría de equipos maduros y rara vez se plantea de entrada. La observación que lo sostiene es que las dos mitades del pipeline tienen perfiles de coste opuestos. La mitad de validación se ejecuta decenas de veces al día, no necesita firmar nada y se beneficia enormemente de poder expresar matrices, particiones y cachés; ahí un montaje propio con xcodebuild gana claramente. La mitad de entrega se ejecuta unas pocas veces por semana, es donde vive todo el dolor de la firma y las credenciales, y no necesita ninguna flexibilidad; ahí la opción integrada gana sin discusión. Separarlas por esa línea aprovecha lo mejor de cada una y deja la superficie de acoplamiento en un solo punto.
#!/bin/sh
# ci_scripts/ci_pre_xcodebuild.sh
set -e
cd "$CI_PRIMARY_REPOSITORY_PATH"
# Solo tocar la version cuando el flujo va a archivar de verdad.
if [ "$CI_XCODEBUILD_ACTION" = "archive" ]; then
agvtool new-version -all "$CI_BUILD_NUMBER"
echo "Version de compilacion fijada a $CI_BUILD_NUMBER"
fi
# En propuestas de cambio no hace falta trabajo extra de release.
if [ -n "$CI_PULL_REQUEST_NUMBER" ]; then
echo "Flujo de propuesta $CI_PULL_REQUEST_NUMBER: se omite la preparacion de entrega"
fi
Los detalles que solo aparecen después de un mes
Hay un conjunto de asperezas que ninguna documentación destaca y que aparecen todas hacia la cuarta semana de uso. Merecen enumerarse porque cambian el cálculo lo suficiente como para importar.
La primera es el clonado superficial. La plataforma clona por omisión con poca profundidad de historial, lo que rompe cualquier guion que necesite la etiqueta anterior para generar notas de versión o calcular un rango de commits. La solución es pedir el historial completo en el propio guion posterior al clonado, y conviene descubrirlo antes de que el primer intento de entrega produzca unas notas vacías.
La segunda es la granularidad de los secretos. Las variables de entorno se definen por entorno de flujo, no por repositorio ni por organización, de modo que un producto con varios flujos acaba con el mismo secreto duplicado en varios sitios y sin ninguna forma de rotarlo de una vez. Reducir el número de flujos y agrupar por entorno compartido es lo que mantiene ese problema pequeño.
La tercera es la caché entre ejecuciones. Las dependencias de Swift Package Manager se resuelven de forma gestionada y suelen ser rápidas, pero cualquier herramienta que se instale desde el guion posterior al clonado se instala entera en cada ejecución, y una instalación por red de dos minutos multiplicada por cien ejecuciones al mes es una cantidad de tiempo que nadie contabilizó. Cuando eso ocurre, la respuesta correcta es preferir binarios ya versionados en el repositorio o dependencias declaradas como complementos del propio proyecto.
#!/bin/sh
# ci_scripts/ci_post_xcodebuild.sh
set -e
# Recuperar el historial completo para poder generar notas de version.
git -C "$CI_PRIMARY_REPOSITORY_PATH" fetch --unshallow --tags || true
if [ "$CI_XCODEBUILD_ACTION" = "archive" ] && [ -n "$CI_APP_STORE_SIGNED_APP_PATH" ]; then
ANTERIOR=$(git -C "$CI_PRIMARY_REPOSITORY_PATH" describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -n "$ANTERIOR" ]; then
git -C "$CI_PRIMARY_REPOSITORY_PATH" log --no-merges \
--pretty=format:'- %s' "$ANTERIOR"..HEAD > "$CI_DERIVED_DATA_PATH/notas.txt"
fi
fi
La cuarta y última es el diagnóstico. Cuando algo falla solo en la nube, no hay acceso interactivo al agente y la única herramienta disponible es imprimir información desde los guiones. Eso empuja a escribir guiones defensivos, que registren la versión de las herramientas, el listado de dispositivos disponibles y el valor de las variables relevantes antes de hacer nada. Es trabajo que en una máquina propia no haría falta, y conviene contarlo en el lado del coste.
Lo que hace singular a Xcode Cloud no es su tecnología sino su posición: es el único servicio de integración continua cuyo proveedor controla también el compilador, el formato del artefacto, la autoridad de certificación que lo firma y la tienda que lo recibe. Esa coincidencia es lo que le permite hacer desaparecer la firma y las credenciales, porque no está resolviendo un problema de criptografía distribuida sino evitando que ese problema llegue a existir. Y ahí está el criterio que ordena la decisión entera, porque toda integración vertical ofrece el mismo trato: reduce la complejidad accidental a cambio de reducir tu grado de libertad. La pregunta útil no es cuál de los dos sistemas es mejor, que es una pregunta sin respuesta, sino cuánta de esa complejidad accidental está soportando hoy tu equipo y cuánta libertad va a necesitar de verdad en los próximos dos años. La respuesta cambia con el tamaño y con la forma del repositorio, y por eso el híbrido aparece tan a menudo: reconoce que las dos mitades del pipeline tienen necesidades opuestas y se niega a pagar el mismo precio por las dos. Lo que nunca funciona es la postura ideológica en cualquiera de las dos direcciones, porque quien elige la opción integrada por comodidad acaba escribiendo un lenguaje de flujo dentro de tres guiones de shell, y quien la rechaza por principio acaba manteniendo una infraestructura de firma que no aporta ninguna ventaja competitiva a su producto.
- Crea un flujo que se dispare en cada propuesta de cambio y ejecute solo la acción de probar. Mide su tiempo total, de cola a resultado, durante una semana.
- Añade un
ci_post_clone.shque instale una herramienta y genere un fichero de configuración a partir de una variable de entorno secreta. - Crea un segundo flujo, disparado por etiqueta, que archive y distribuya a un grupo interno de TestFlight, usando
CI_BUILD_NUMBERcomo número de compilación. - Anota durante un mes cuántas incidencias de firma o de credenciales tuvisteis en ese flujo. Compara con el mismo dato de vuestro montaje anterior.
- Con esos dos números, minutos facturados y horas de persona, decide de forma explícita entre integrado, propio o híbrido, y escribe la decisión con su motivo.