Bitbucket Pipelines no puede compilar una app de iOS en sus runners alojados, porque cada step se ejecuta en un contenedor Linux y Xcode solo se ejecuta en macOS. Tienes dos opciones viables: registrar tu propio Mac como runner self-hosted de Bitbucket, o mantener el step en Linux y delegar la compilación de iOS, la firma y la subida a TestFlight a un servicio de build en la nube. Esta guía muestra ambas, con archivos bitbucket-pipelines.yml completos para una app Capacitor.
Si tu equipo ya vive en Bitbucket para revisar code, enlaces de Jira y despliegues, no necesitas una segunda herramienta de CI solo para iOS. El pipeline se queda en Bitbucket. Solo se mueve la parte que necesita un Mac.
Por qué fallan los builds de iOS en Bitbucket Pipelines
Bitbucket Pipelines ejecuta cada paso dentro de una imagen Docker que tú eliges (node:22, cimg/android, etc.) en la infraestructura Linux de Atlassian. Eso está bien para bun install, pruebas unitarias, linting e incluso builds completos de Android. Se detiene en iOS:
xcodebuild,codesign, y los SDK de iOS solo existen en macOS.- La licencia de Apple solo permite ejecutar macOS en hardware de Apple, por lo que no hay ninguna imagen Linux que pueda generar un
.ipa. - Desde el 28 de abril de 2026, App Store Connect rechaza las subidas que no se hayan compilado con Xcode 26 y el SDK de iOS 26, así que el Mac que uses debe estar actualizado. Ver Requisito de Xcode 26 de Apple para apps de Capacitor.
Bitbucket admite runners autoalojados en Linux, Windows y macOS. Atlassian no cobra minutos de build para los pasos que se ejecutan en tus propios runners, pero pagas en hardware y mantenimiento.
Tus tres opciones
| Opción | Dónde se ejecuta el build de iOS | Lo que mantienes | Ideal para ti |
|---|---|---|---|
| Runner macOS autoalojado | Tu Mac mini o un Mac alquilado | macOS, Xcode, keychain, certificados, espacio en disco, uptime | Equipos que ya tienen Macs y alguien para gestionarlos |
| Segunda CI solo para móvil | Macs de otro proveedor | Dos pipelines, dos conjuntos de secretos y triggers | Equipos que quieren una CI móvil dedicada de todos modos |
| Build en la nube llamado desde un paso Linux | Flota de Macs de Capgo Build | Una llamada a CLI y unas pocas variables seguras | Equipos de Capacitor que quieren que Bitbucket siga siendo la única CI |
El resto de esta guía cubre en detalle la primera y la tercera opción. Para una visión más amplia de las plataformas, lee Comparativa de plataformas CI/CD para apps de iOS.
Opción 1: hacer build de iOS desde un paso de Linux con Capgo Build
Build de Capgo compila y firma las apps de Capacitor en Macs gestionados por Capgo. Tu paso de Bitbucket hace lo que ya hace bien en Linux: instalar dependencias, compilar la capa web y ejecutar cap sync ios. Luego, el Capgo CLI sube el proyecto nativo preparado (no tu código fuente web code ni node_modules), ejecuta la compilación de Xcode, lo firma con tu certificado y puede subir el resultado a TestFlight. Los registros se transmiten de vuelta al paso de Bitbucket, y el comando sale con un código no cero si la compilación falla, por lo que una compilación de iOS fallida hace fallar la pipeline.
Como tu runner sube el proyecto a Capgo, Capgo nunca necesita acceso a tu repositorio de Bitbucket. No hay conexión de repositorio que autorizar ni app de OAuth que aprobar.
Requisitos
- Una app de Capacitor que se compila localmente (Capacitor 8 requiere Node.js 22 o superior)
- Una membresía en el Apple Developer Program con permisos de administrador en el equipo
- Una cuenta de Capgo y una clave de API con permiso de subida
- La app registrada en Capgo:
bunx @capgo/cli@latest app add
Configuración única en tu máquina
Ejecuta el onboarding una vez. En iOS, crea o reutiliza tu certificado de distribución y perfil de aprovisionamiento, y solicita una clave de App Store Connect API:
bunx @capgo/cli@latest login
bunx @capgo/cli@latest build init --platform ios
Si no tienes un Mac a mano, aún puedes crear la solicitud de firma del certificado en el navegador con el generador de certificados iOS, o sigue Compila una app iOS desde Windows con Capgo Build.
Ejecuta un build desde tu portátil antes de tocar la CI. La CI es un mal lugar para depurar un primer build:
bun run build
bunx cap sync ios
bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release
Exporta las credenciales para Bitbucket
El CLI guarda las credenciales en tu máquina. Para moverlas a Bitbucket, expórtalas a un archivo .env:
bunx @capgo/cli@latest build credentials manage --appId com.example.app --platform ios
# choose "Export to .env" in the menu
Esto escribe .env.capgo.com.example.app.ios con el modo 0600. Contiene estas claves:
| Variable | Contenido |
|---|---|
BUILD_CERTIFICATE_BASE64 |
Base64 de tu .p12 certificado de distribución |
P12_PASSWORD |
Contraseña del certificado (vacía si no hay) |
CAPGO_IOS_PROVISIONING_MAP_BASE64 |
Mapeo del provisioning profile generado por el CLI |
APPLE_KEY_ID |
ID de la clave API de App Store Connect |
APPLE_ISSUER_ID |
Issuer ID de App Store Connect |
APPLE_KEY_CONTENT |
Base64 de la .p8 clave de API |
APP_STORE_CONNECT_TEAM_ID |
Tu ID de equipo de Apple |
Añade cada línea como una seguro variable en Bitbucket bajo Configuración del repositorio > Pipelines > Variables del repositorio, además CAPGO_TOKEN con tu clave de Capgo API. Las variables protegidas se ocultan en los logs y no se pueden leer desde la interfaz.
Si prefieres el API, este bucle los crea de una vez. Utiliza un token de API de Bitbucket con el alcance de escritura de variables de pipeline:
WORKSPACE=my-team
REPO=my-app
ENV_FILE=.env.capgo.com.example.app.ios
grep -v '^#' "$ENV_FILE" | grep '=' | while IFS= read -r line; do
key="${line%%=*}"
value="${line#*=}"
value="${value%\"}"; value="${value#\"}"
jq -n --arg k "$key" --arg v "$value" '{key:$k, value:$v, secured:true}' |
curl -s -u "$BB_EMAIL:$BB_API_TOKEN" \
-H 'Content-Type: application/json' \
-X POST --data @- \
"https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO/pipelines_config/variables" > /dev/null
echo "created $key"
done
rm "$ENV_FILE"
Elimina el archivo después y añade .env.capgo.* a .gitignore. Contiene material de firma en texto plano.
Para credenciales de producción, considera variables de despliegue en lugar de variables de repositorio. Están limitadas a un entorno de despliegue (por ejemplo, Production), y puedes restringir ese entorno a administradores o ramas específicas en los planes Premium.
El archivo de pipeline
Bitbucket inyecta las variables del repositorio y de despliegue como variables de entorno, por lo que Capgo CLI las recoge sin necesidad de mapeo. Este archivo ejecuta pruebas en cada push, compila un build ad hoc de iOS bajo demanda y lo envía a TestFlight en tags de versión:
# bitbucket-pipelines.yml
image: node:22
definitions:
caches:
bun: ~/.bun/install/cache
pipelines:
default:
- step:
name: Lint and test
caches:
- bun
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile
- bun run lint
- bun run test
tags:
'v*':
- step:
name: Build iOS and upload to TestFlight
deployment: Production
caches:
- bun
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile
- bun run build
- bunx cap sync ios
- >
bunx @capgo/cli@latest build request com.example.app
--platform ios
--build-mode release
--ios-distribution app_store
custom:
ios-adhoc:
- step:
name: Ad hoc iOS build for testers
caches:
- bun
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile
- bun run build
- bunx cap sync ios
- echo "$ADHOC_PROFILE_BASE64" | base64 --decode > adhoc.mobileprovision
- >
bunx @capgo/cli@latest build request com.example.app
--platform ios
--build-mode release
--ios-distribution ad_hoc
--ios-provisioning-profile ./adhoc.mobileprovision
--output-upload
--output-retention 2d
--output-record /tmp/build.json
- bunx @capgo/cli@latest build last-output --path /tmp/build.json --field outputUrl
Algunos detalles importantes:
- La imagen es un Linux plano
node:22. Nada en este archivo necesita macOS. - En Linux,
cap sync ioscopia los assets web y actualiza la configuración nativa, pero no puede ejecutar CocoaPods. No hay problema. El Mac que ejecuta el build instala los pods o resuelve los paquetes Swift por sí mismo. - Capgo lee el número de build más reciente de App Store Connect y lo incrementa, para que nunca te encuentres con el error “bundle version must be higher than the previously uploaded version”. Pasa
--skip-build-number-bumpsi lo gestionas tú mismo. - El build ad hoc necesita un perfil de aprovisionamiento Ad Hoc, que solo se instala en los dispositivos que lista. Guárdalo como una variable
ADHOC_PROFILE_BASE64segura; la--ios-provisioning-profileflag sobrescribe el perfil de App Store del entorno solo para este build. Recopila los UDIDs de los testers con el Buscador de UDID de iOS. - Fija el CLI a una versión exacta (por ejemplo
@capgo/cli@7.104.0) una vez que tu pipeline sea estable, para que un release de CLI no cambie el comportamiento a mitad de sprint.
Ejecuta el pipeline personalizado desde Pipelines > Run pipeline > ios-adhoc. El último comando muestra un enlace de descarga que caduca tras el periodo de retención.
Aprobación manual antes de TestFlight
Bitbucket soporta trigger: manual en un paso. Divide el release en un paso de build y un paso de upload con aprobación si quieres que una persona apruebe cada upload a TestFlight:
tags:
'v*':
- step:
name: Test
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile
- bun run test
- step:
name: Ship iOS to TestFlight
trigger: manual
deployment: Production
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile
- bun run build
- bunx cap sync ios
- bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release
Para ir directamente a App Review después del upload a TestFlight, añade --submit-to-store-review con --store-release-name "$BITBUCKET_TAG". Tu ficha de la App Store, las capturas de pantalla y las respuestas de cumplimiento deben estar completas primero en App Store Connect.
Añade Android en paralelo
Los builds de Android pueden ejecutarse directamente en los runners Linux de Bitbucket, pero también puedes enviarlos a Capgo Build para que ambas plataformas usen el mismo flujo de firma y subida. Usa un parallel bloque:
tags:
'v*':
- parallel:
- step:
name: iOS release
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile && bun run build
- bunx cap sync ios
- bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release
- step:
name: Android release
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile && bun run build
- bunx cap sync android
- bunx @capgo/cli@latest build request com.example.app --platform android --build-mode release --android-track internal
El paso de Android necesita ANDROID_KEYSTORE_FILE, KEYSTORE_KEY_ALIAS, KEYSTORE_KEY_PASSWORD, KEYSTORE_STORE_PASSWORD, y PLAY_CONFIG_JSON como variables seguras. Expórtalas de la misma manera con --platform android.
Publica cambios solo de web sin un build nativo
La mayoría de los commits en una app de Capacitor solo tocan HTML, CSS y JavaScript. No necesitan un nuevo binario. Con actualizaciones Live Update de Capgo, un paso de Bitbucket puede decidir entre un build para la tienda y un bundle over-the-air:
branches:
main:
- step:
name: Release (live update or native)
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile
- bun run build
- |
if bunx @capgo/cli@latest build needed com.example.app --channel production; then
echo "No native change, shipping a live update"
bunx @capgo/cli@latest bundle upload com.example.app --channel production
else
echo "Native change detected, building iOS"
bunx cap sync ios
bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release
fi
build needed termina con code 0 cuando no se necesita un build nativo y con 1 cuando sí. Compara las versiones de los plugins nativos con lo que está live en el channel. No detecta ediciones manuales en ios/, así que fuerza también la ruta nativa cuando esos archivos cambian. El patrón completo está en Elige automáticamente live update o Native Build.
Opción 2: Un runner de macOS self-hosted
Si ya tienes Macs, puedes registrar uno como runner de Bitbucket y ejecutar Xcode tú mismo.
Registrar el runner
- En Bitbucket, abre Configuración del repositorio > Runners (o Configuración del workspace > Workspace runners para compartirlo) y haz clic en Añadir runner.
- Elige MacOS como sistema. Bitbucket muestra un comando de inicio con un UUID del runner y credenciales OAuth.
- En el Mac, instala un JDK compatible (el runner es un programa Java), Xcode 26 y fastlane. Luego pega el comando de inicio.
- Mantén el runner ejecutándose como launch agent para que sobreviva a los reinicios.
Un step apunta al runner mediante etiquetas:
pipelines:
tags:
'v*':
- step:
name: iOS build on our Mac
runs-on:
- self.hosted
- macos
script:
- bun install --frozen-lockfile
- bun run build
- bunx cap sync ios
- echo "$BUILD_CERTIFICATE_BASE64" | base64 --decode > dist.p12
- echo "$PROFILE_BASE64" | base64 --decode > profile.mobileprovision
- bundle exec fastlane ios beta
Los runners de macOS se ejecutan directamente en el host con el entorno de tu usuario, no en un contenedor. Instala Bun y Ruby en el propio Mac.
Un Fastfile mínimo
default_platform(:ios)
platform :ios do
lane :beta do
setup_ci(force: true)
api_key = app_store_connect_api_key(
key_id: ENV["APPLE_KEY_ID"],
issuer_id: ENV["APPLE_ISSUER_ID"],
key_content: ENV["APPLE_KEY_CONTENT"],
is_key_content_base64: true
)
import_certificate(
certificate_path: "dist.p12",
certificate_password: ENV["P12_PASSWORD"],
keychain_name: "fastlane_tmp_keychain",
keychain_password: ""
)
install_provisioning_profile(path: "profile.mobileprovision")
update_code_signing_settings(
path: "ios/App/App.xcodeproj",
use_automatic_signing: false,
team_id: ENV["APP_STORE_CONNECT_TEAM_ID"],
code_sign_identity: "Apple Distribution",
profile_name: ENV["PROFILE_NAME"]
)
increment_build_number(
xcodeproj: "ios/App/App.xcodeproj",
build_number: latest_testflight_build_number(api_key: api_key) + 1
)
build_app(
project: "ios/App/App.xcodeproj", # use workspace: "ios/App/App.xcworkspace" for CocoaPods projects
scheme: "App",
export_method: "app-store"
)
upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: true)
end
end
Los nuevos 8 proyectos de Capacitor usan Swift Package Manager, así que construyes App.xcodeproj. Proyectos antiguos que aún usan la compilación con CocoaPods App.xcworkspace y necesitan pod install en el runner. Nuestro guía de GitHub Actions para iOS explica cada acción de fastlane con más detalle; el lane es el mismo en Bitbucket.
Lo que contratas
El archive es la parte fácil. Un Mac autoalojado también implica:
- Actualizaciones de Xcode cuando Apple sube la versión mínima de SDK, probado antes del plazo.
- Estado del Keychain. Un job que falla puede dejar un keychain temporal, y el siguiente job usa la identidad equivocada.
- Sin aislamiento. Los jobs se ejecutan en el mismo disco. DerivedData, archives antiguos y simuladores lo llenan.
- Un build a la vez por runner, a menos que compres más Macs.
- Certificados y perfiles que expiran cada año y hay que renovarlos en la máquina.
Es un intercambio razonable si tu equipo ya gestiona Macs. Si nadie quiere encargarse de esa máquina, la Opción 1 la elimina.
Solución de problemas
| Síntoma | Causa probable y solución |
|---|---|
CAPGO_TOKEN is not set |
Falta la variable, o es una variable de deployment y el step no tiene deployment: clave. |
El step funciona en main pero no en tags |
Las variables de despliegue solo se aplican a los pasos que declaran ese entorno de despliegue. |
Provisioning profile doesn't match bundle ID |
El perfil se creó para otro ID de app. Vuelve a ejecutar build init y vuelve a exportar. |
| Error de autenticación en App Store Connect | ID de emisor o ID de clave incorrecto, o clave revocada. Revisa la clave en App Store Connect > Usuarios y acceso > Integraciones. |
| Upload rechazado para la versión SDK | El build usó una versión de Xcode anterior a la 26. Actualiza el Mac autoalojado. |
| Paso autoalojado atascado en “Pendiente” | Runner sin conexión o las etiquetas no coinciden self.hosted y macos. |
errSecInternalComponent durante codesign |
El keychain está bloqueado en una sesión no interactiva. Usa setup_ci e importa el certificado en el keychain temporal. |
| El build pasa pero no aparece nada en TestFlight | El procesamiento tarda varios minutos y el cumplimiento de exportación puede estar pendiente. Añade ITSAppUsesNonExemptEncryption a Info.plist si tu app usa solo cifrado estándar. |
Hay más correcciones para pipelines que fallan en Cómo solucionar fallos de build en pipelines de CI/CD de Capacitor y la guía de resolución de problemas de Capgo Build.
¿Qué opción deberías elegir?
Elige un Mac autoalojado si ya tienes el hardware, necesitas control total del entorno de Xcode o compilas objetivos nativos en Swift que no son apps de Capacitor. Elige una compilación en la nube desde un paso de Linux si tu app es una app de Capacitor y quieres que Bitbucket sea el único lugar donde tu equipo configura la CI. En cualquier caso, mantén la lógica de compilación en bitbucket-pipelines.yml junto a tu code, coloca los materiales de firma en variables seguras y ejecuta solo builds nativos cuando el code nativo haya cambiado realmente.