Scripts personalizados en Xcode Cloud: automatización fiable más allá de compilar y probar
Arturo Rivas Arias
Xcode Cloud permite implementar Integración Continua sin mantener servidores: conecta el repositorio, ejecuta pruebas, archiva la aplicación y distribuye una compilación. Sin embargo, un flujo de entrega contínua real suele necesitar tareas que no forman parte de un simple xcodebuild: instalar una herramienta, validar archivos generados, preparar notas de una versión o avisar a un servicio externo.
Los scripts personalizados cubren ese espacio. No sustituyen las acciones del flujo de trabajo, sino que añaden lógica en tres momentos concretos de la ejecución. Entender esos límites resulta esencial: el mismo script que ahorra trabajo también puede filtrar un secret (o clave), ralentizar cada compilación o publicar dos veces una versión si no se diseña como una pieza de infraestructura reproducible.
Tres puntos de extensión con responsabilidades distintas
Xcode Cloud busca una carpeta llamada ci_scripts junto al proyecto o workspace. Dentro de ella reconoce tres nombres reservados:
ci_post_clone.sh, después de clonar el repositorio.ci_pre_xcodebuild.sh, inmediatamente antes de ejecutarxcodebuildpara una acción.ci_post_xcodebuild.sh, después de que terminexcodebuild, incluso cuando este falla.
La ubicación de una tarea expresa su dependencia. ci_post_clone.sh es apropiado para preparar el entorno o instalar una herramienta que necesitarán acciones posteriores. ci_pre_xcodebuild.sh sirve para generar una configuración o comprobar una precondición del proyecto. ci_post_xcodebuild.sh puede inspeccionar el resultado, publicar metadatos o enviar información sobre el fallo.
⚠️ El último punto tiene una consecuencia fácil de pasar por alto: «después de xcodebuild» no significa «después de una compilación correcta». Si una publicación solo debe producirse tras un archivo válido, el script tiene que comprobar explícitamente el contexto y la existencia de sus entradas.
Cada archivo debe estar incluido en el repositorio, tener permiso de ejecución y comenzar con un shebang que indica el intérprete. Apple indica que, si falta el shebang o el archivo no es ejecutable, Xcode Cloud lo ejecuta mediante zsh nombre-del-script; depender de ese comportamiento implícito hace el resultado menos potable y más difícil de reproducir localmente.
#!/bin/sh
set -eu
echo "Preparando recursos para la compilación"
Elegir #!/bin/sh obliga a utilizar sintaxis POSIX. Si el script necesita arrays, [[ ... ]] u otras extensiones, debe declarar #!/bin/zsh y asumir esa dependencia de forma deliberada. Mezclar un intérprete POSIX con sintaxis de Zsh o Bash es una causa habitual de errores que solo aparecen en CI.
El entorno es la entrada del script
Una máquina de Xcode Cloud es efímera. El script no debería deducir su contexto a partir de carpetas personales, herramientas instaladas manualmente o estado conservado por una ejecución anterior. Sus entradas reales son el repositorio, los archivos producidos por la acción y las variables de entorno.
Apple documenta variables como CI_BUILD_NUMBER, CI_COMMIT, CI_BRANCH, CI_XCODEBUILD_ACTION, CI_ARCHIVE_PATH y CI_PRIMARY_REPOSITORY_PATH. No todas están disponibles en todas las fases: algunas solo existen después de la acción correspondiente de xcodebuild. Por eso conviene validar cada valor antes de utilizarlo, en vez de permitir que una cadena vacía termine formando una ruta o una petición HTTP válida pero equivocada.
#!/bin/zsh
set -euo pipefail
require_variable() {
local variable_name="$1"
if [[ -z "${(P)variable_name:-}" ]]; then
print -u2 "error: Falta la variable $variable_name"
exit 1
fi
}
require_variable CI_BUILD_NUMBER
require_variable CI_COMMIT
require_variable CI_ARCHIVE_PATH
Este ejemplo usa indirección propia de Zsh para leer una variable cuyo nombre se recibe como argumento. Declarar el intérprete lo convierte en una decisión visible. set -euo pipefail detiene el script ante un comando fallido, una variable no definida o un fallo oculto dentro de un pipeline. No elimina la necesidad de tratar errores esperados, pero evita continuar con un estado corrupto.
Separar condiciones, datos y efectos externos
Un script de CI/CD se entiende mejor como tres capas. Primero decide si debe actuar; después obtiene y valida los datos; al final realiza el efecto irreversible. Este orden reduce el riesgo de publicar desde una compilación de una pull request, una acción de análisis o una rama de desarrollo.
Supongamos que una aplicación de inventario debe crear una publicación en GitHub solo al archivar main. Las salvaguardas pueden terminar correctamente con código cero cuando el contexto no corresponde:
if [[ "${CI_XCODEBUILD_ACTION:-}" != "archive" ]]; then
echo "No es una acción de archivo; se omite la publicación"
exit 0
fi
if [[ "${CI_BRANCH:-}" != "main" ]]; then
echo "La rama no es main; se omite la publicación"
exit 0
fi
if [[ -n "${CI_PULL_REQUEST_NUMBER:-}" ]]; then
echo "La compilación pertenece a una pull request"
exit 0
fi
Aquí exit 0 significa que no había trabajo que realizar, mientras que exit 1 debe reservarse para una precondición incumplida dentro de un contexto que sí exigía publicar. La diferencia importa porque Xcode Cloud interpreta una salida distinta de cero como un fallo del script y, por tanto, de la compilación.
Después de las salvaguardas se puede leer la versión del archivo generado. Consultar el Info.plist del .xcarchive evita confundir un valor fuente con el que realmente se empaquetó:
archive_info="$CI_ARCHIVE_PATH/Info.plist"
if [[ ! -f "$archive_info" ]]; then
print -u2 "error: No existe $archive_info"
exit 1
fi
marketing_version=$(/usr/libexec/PlistBuddy \
-c "Print :ApplicationProperties:CFBundleShortVersionString" \
"$archive_info")
release_tag="v${marketing_version}-${CI_BUILD_NUMBER}"
El número de compilación diferencia archivos de una misma versión comercial. El formato concreto del tag es una política del equipo; lo importante es que sea determinista y pueda reconstruirse con las mismas entradas.
Idempotencia: una repetición no debe duplicar una entrega
Los sistemas de CI reintentan trabajos automáticamente y además las personas vuelven a ejecutar compilaciones. Una automatización fiable asume que el mismo evento puede procesarse más de una vez. Antes de crear una publicación, debe comprobar si ya existe el recurso identificado por release_tag y salir sin cambios si lo encuentra.
La API REST de GitHub permite crear una release y generar automáticamente sus notas. El token de acceso necesita permiso de escritura sobre el contenido del repositorio. Conviene usar un token de alcance mínimo, comprobar el código HTTP y nunca considerar que curl ha tenido éxito solo porque consiguió conectarse.
response_file=$(mktemp)
trap 'rm -f "$response_file"' EXIT
payload=$(printf \
'{"tag_name":"%s","target_commitish":"%s","generate_release_notes":true}' \
"$release_tag" \
"$CI_COMMIT")
status=$(curl --silent --show-error \
--output "$response_file" \
--write-out "%{http_code}" \
--request POST \
--header "Accept: application/vnd.github+json" \
--header "Authorization: Bearer $GITHUB_RELEASE_TOKEN" \
--header "X-GitHub-Api-Version: 2026-03-10" \
--data "$payload" \
"https://api.github.com/repos/$GITHUB_REPOSITORY/releases")
if (( status < 200 || status >= 300 )); then
print -u2 "error: GitHub respondió con HTTP $status"
cat "$response_file" >&2
exit 1
fi
En una implementación completa, una consulta previa por tag proporciona la salida idempotente. También hay que contemplar carreras: dos ejecuciones podrían comprobar simultáneamente que el recurso no existe. En ese caso, una respuesta de conflicto o validación que confirme que el tag ya fue creado puede tratarse como éxito; otros errores deben conservar su condición de fallo.
Construir JSON con printf solo es razonable cuando los valores están controlados, como un número, un hash y un identificador de versión. Para textos arbitrarios, saltos de línea o contenido procedente de Git, debe usarse una herramienta que escape JSON correctamente. Concatenar notas de versión directamente en una cadena abre la puerta a cargas inválidas.
Secretos que no acaban en los registros
Un token no debe guardarse en el repositorio ni escribirse en el archivo del proyecto. Xcode Cloud permite definir variables personalizadas en el apartado Environment del flujo y marcarlas como secretas. También admite variables compartidas entre varios flujos. Al activar la opción de secreto, su valor aparece ofuscado (o redacted) en los registros.
🔐 La ofuscación es una protección, no una licencia para imprimir credenciales. set -x, volcar todo el entorno o incluir el token dentro de una URL puede exponer transformaciones del secreto o enviarlo a un destino no previsto. El script debe limitar los registros al nombre de la operación, los identificadores no sensibles y el código de respuesta.
También es preferible utilizar credenciales diferentes por servicio y finalidad. Un token destinado a crear publicaciones no necesita permisos de administrador sobre el repositorio. La rotación se simplifica cuando la credencial está configurada fuera del código y el script solo conoce el nombre de la variable.
Los archivos temporales no son un canal persistente
Apple advierte de una limitación específica de los scripts personalizados de Xcode Cloud: los archivos que crea uno de estos scripts no están disponibles para otros scripts personalizados, y el servicio los elimina. Por tanto, no deben utilizarse como un mecanismo general para pasar estado entre ci_post_clone.sh, ci_pre_xcodebuild.sh y ci_post_xcodebuild.sh ni como forma de producir artefactos descargables.
Si una tarea necesita compartir información, resulta más robusto derivarla de entradas estables o integrarla en el proceso de compilación que produce el artefacto. Si necesita publicar un fichero fuera de Xcode Cloud, debe subirlo explícitamente al servicio correspondiente durante la fase en la que está disponible.
Los scripts auxiliares sí permiten dividir lógica compleja: Xcode Cloud reconoce un único ci_scripts y solo ejecuta automáticamente los tres nombres reservados, pero estos pueden invocar otros archivos del directorio. Así se mantiene acotado cada punto de entrada y se pueden probar funciones comunes de forma local.
Recomendaciones para una automatización mantenible
✅ Un buen script de CI es pequeño, determinista, en definitiva aburrido. Declara su intérprete, comprueba datos al inicio para fallar cuanto antes y diferencia claramente «no corresponde ejecutar» de «la ejecución ha fallado». Sus efectos externos son idempotentes y ocurren solo después de completar todas las comprobaciones.
También conviene registrar decisiones sin ruido: rama, acción, versión y motivo de una omisión suelen bastar. La salida de error debe explicar qué precondición falta sin revelar datos sensibles. Antes de activar el flujo definitivo, puede probarse la lógica pura localmente proporcionando variables simuladas, y duplicar temporalmente el flujo de Xcode Cloud con condiciones de inicio restringidas.
Los scripts personalizados convierten Xcode Cloud en algo más que una secuencia cerrada de compilación y pruebas. Su valor no procede de acumular comandos, sino de colocar cada automatización en el momento adecuado y diseñarla con las mismas propiedades que se esperan del resto del sistema: entradas explícitas, errores observables, privilegios mínimos y resultados seguros ante repeticiones.