Bitbucket Pipelines no puede hacer el build de una app iOS en sus runners, ya que cada step corre en un contenedor Linux y Xcode solo funciona en macOS. Tienes dos opciones: registrar tu propio Mac como un runner de Bitbucket self-hosted, o mantener el step en Linux y delegar la compilación, firma y el upload a TestFlight a un servicio de cloud build. Esta guía muestra ambas, con los bitbucket-pipelines.yml archivos completos para una app Capacitor.
Si tu equipo ya usa Bitbucket para 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 requiere un Mac.
Por qué los builds de iOS fallan en Bitbucket Pipelines
Bitbucket Pipelines ejecuta cada paso dentro de una imagen Docker que elijas (node:22, cimg/android, etc.) en la infraestructura Linux de Atlassian. Eso funciona para bun install, unit tests, linting e incluso builds completos de Android. Pero falla con iOS:
xcodebuild,codesign, y el SDK de iOS solo existe en macOS.- La licencia de Apple solo permite ejecutar macOS en hardware de Apple, por lo que no existe una imagen de Linux que pueda generar un signed
.ipa. - Desde el 28 de abril de 2026, App Store Connect rechaza uploads que no se hayan construido con Xcode 26 y el SDK de iOS, así que el Mac que uses debe estar actualizado. Mira El requisito de Xcode 26 de Apple para apps Capacitor.
Bitbucket permite usar self-hosted runners en Linux, Windows y macOS. Atlassian no cobra minutos de build por pasos que corren en tus propios runners, pero tú pagas el hardware y el mantenimiento.
Tus tres opciones
| Opción | Dónde se ejecuta el build de iOS | Lo que debes mantener | Ideal para |
|---|---|---|---|
| Runner de macOS self-hosted | Tu Mac mini o un Mac alquilado | macOS, Xcode, keychain, certificados, espacio en disco, uptime | Equipos que ya tienen Macs y personal para gestionarlos |
| Segundo CI solo para mobile | Los Macs de otro proveedor | Dos pipelines, dos sets de secrets y triggers | Equipos que buscan un CI mobile dedicado |
| Cloud build llamado desde un step de Linux | La flota de Macs de Capgo Build | Una llamada a CLI y unas pocas variables seguras | Equipos de Capacitor que prefieren Bitbucket como único CI |
El resto de esta guía detalla 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 iOS.
Opción 1: Build de iOS desde Linux con Capgo Build
Capgo Build compila y firma apps Capacitor en Macs gestionadas por Capgo. Tu step de Bitbucket hace lo que ya hace bien en Linux: instalar dependencias, build del web layer y ejecutar cap sync ios. Luego, Capgo CLI sube el proyecto nativo preparado (no tu código web code o node_modules), ejecuta el build de Xcode, lo firma con tu certificado y puede subir el resultado a TestFlight. Los logs se envían al step de Bitbucket, y el comando devuelve un error si el build falla, deteniendo el pipeline.
Como tu runner envía el proyecto a Capgo, Capgo nunca necesita acceso a tu repo de Bitbucket. No hay que autorizar conexiones al repo ni aprobar apps OAuth.
Prerrequisitos
- Una app Capacitor que compila localmente (Capacitor 8 requiere Node.js 22 o superior)
- Membresía de Apple Developer Program con permisos de admin en el equipo
- Una cuenta de Capgo y una clave API con permiso de upload
- 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 provisioning profile, y te pedirá una clave API de App Store Connect:
bunx @capgo/cli@latest login
bunx @capgo/cli@latest build init --platform ios
Si no tienes un Mac a mano, aún puedes crear el certificate signing request en el navegador con el generador de certificados iOS, o sigue Compila una app de iOS desde Windows con Capgo Build.
Ejecuta un build desde tu laptop antes de usar CI. La CI no es el mejor lugar para debuguear el primer build:
bun run build
bunx cap sync ios
bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release
Exportar las credenciales para Bitbucket
El CLI guarda las credenciales en tu máquina. Para pasarlas 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ío si no hay) |
CAPGO_IOS_PROVISIONING_MAP_BASE64 |
Mapeo del provisioning profile generado por 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 API |
APP_STORE_CONNECT_TEAM_ID |
Tu Apple team ID |
Añade cada línea como una variable segura en Bitbucket en Configuración del repositorio > Pipelines > Variables del repositorio, además de CAPGO_TOKEN con tu clave Capgo API. Las variables seguras se enmascaran en los logs y no se pueden leer desde la UI.
Si prefieres usar el API, este bucle las crea de una vez. Utiliza un token API de Bitbucket con el scope de escritura en 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"
Borra el archivo después y añade .env.capgo.* a .gitignore. Contiene material de firma en texto plano.
Para credenciales de producción, considera usar variables de despliegue en lugar de variables de repositorio. Se limitan a un entorno de despliegue (por ejemplo, Production), y en planes Premium puedes restringir ese entorno a admins o ramas específicas.
El archivo del pipeline
Bitbucket inyecta las variables de repositorio y despliegue como variables de entorno, así que Capgo CLI las detecta sin mapeo adicional. Este archivo ejecuta tests en cada push, genera un build de iOS ad hoc bajo demanda y lo envía a TestFlight con los 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 puro
node:22. Nada en este archivo requiere macOS. - En Linux,
cap sync ioscopia los assets web y actualiza la config nativa, pero no puede ejecutar CocoaPods. No hay problema; el Mac que ejecuta el build instala los pods o resuelve los Swift packages por su cuenta. - Capgo lee el último número de build de App Store Connect y lo incrementa, para que nunca recibas el error “bundle version must be higher than the previously uploaded version”. Úsalo si
--skip-build-number-bumpprefieres gestionarlo tú mismo. - El build ad hoc requiere un perfil de aprovisionamiento Ad Hoc, que solo se instala en los dispositivos listados. Guárdalo como una
ADHOC_PROFILE_BASE64variable segura; el--ios-provisioning-profileflag sobrescribe el perfil de App Store desde el entorno solo para este build. Recopila los UDIDs de los testers con el Buscador de UDID de iOS. - Fija CLI a una versión exacta (por ejemplo,
@capgo/cli@7.104.0) cuando tu pipeline sea estable, para que un release de CLI no cambie el comportamiento a mitad del sprint.
Ejecuta el pipeline personalizado desde Pipelines > Run pipeline > ios-adhoc. El último comando imprime un link de descarga que expira tras el periodo de retención.
Aprobación manual antes de TestFlight
Bitbucket es compatible con trigger: manual en un step. Divide el release en un step de build y un step de upload con aprobación si quieres que un humano valide cada subida 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 pasar directamente a App Review tras el upload a TestFlight, añade --submit-to-store-review con --store-release-name "$BITBUCKET_TAG". Aun así, debes completar primero tu ficha de App Store, las capturas y las respuestas de cumplimiento en App Store Connect.
Añade Android en paralelo
Los builds de Android pueden ejecutarse en los runners de Linux de Bitbucket, pero también puedes enviarlos a Capgo Build para que ambas plataformas usen el mismo flujo de signing y upload. Usa un parallel block:
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 step 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 forma con --platform android.
Envía cambios web sin un Native Build
La mayoría de los commits en una app Capacitor solo afectan a HTML, CSS y JavaScript. Estos no requieren un nuevo binario. Con Live Updates de Capgo, un step de Bitbucket puede decidir entre un build para la tienda o 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 sale con code 0 cuando no se requiere un Native Build y con 1 cuando sí. Compara las versiones de los plugins nativos con lo que está live en el channel. No detecta cambios manuales en ios/, así que también fuerza la ruta nativa cuando esos archivos cambian. El patrón completo está en Elegir automáticamente entre 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 un 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 CocoaPods se compilan App.xcworkspace y necesitan pod install en el runner. Nuestra guía de GitHub Actions para iOS explica cada acción de fastlane a fondo; el lane es igual en Bitbucket.
En qué te registras
El archivo es la parte fácil. Un Mac self-hosted también significa:
- Actualizaciones de Xcode cada vez que Apple suba el SDK mínimo, probado antes del deadline.
- Estado del Keychain. Un job que falla puede dejar un keychain temporal, y el siguiente job usará la identidad incorrecta.
- Sin aislamiento. Los jobs corren en el mismo disco. DerivedData, archivos viejos y simuladores lo llenan.
- Un build a la vez por runner, a menos que compres más Macs.
- Certificados y perfiles que caducan cada año y deben renovarse en la máquina.
Es un trato razonable si tu equipo ya gestiona Macs. Si nadie quiere hacerse cargo, la Opción 1 lo 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: key. |
El step funciona en main pero no en los tags |
Las variables de deployment solo aplican a los steps que declaran ese entorno. |
Provisioning profile doesn't match bundle ID |
El profile se creó para otro app ID. Re-run build init y re-export. |
| Error de autenticación en App Store Connect | Issuer ID o key ID incorrectos, o una key revocada. Revisa la key en App Store Connect > Users and Access > Integrations. |
| Upload rechazada para la versión SDK | El build usó un Xcode más antiguo que el 26. Actualiza el Mac self-hosted. |
| Step self-hosted trabado en “Pending” | Runner offline o los labels no coinciden self.hosted y macos. |
errSecInternalComponent durante el 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 solo usa cifrado estándar. |
Más soluciones para pipelines fallidos en Cómo solucionar fallos de build en pipelines de CI/CD de Capacitor y la guía de troubleshooting de build de Capgo.
¿Qué opción deberías elegir?
Elige un Mac self-hosted si ya tienes el hardware, necesitas control total de Xcode o builds de Swift que no sean apps Capacitor. Elige un cloud build desde un step de Linux si tu app es Capacitor y quieres que Bitbucket sea el único lugar donde tu equipo configure la CI. En cualquier caso, mantén la lógica del build en bitbucket-pipelines.yml junto a tu code, guarda los certificados en variables seguras y solo ejecuta Native Build cuando el code nativo haya cambiado realmente.