Bitbucket Pipelines non può fare la build di un'app iOS sui propri runner ospitati, perché ogni step gira in un container Linux e Xcode funziona solo su macOS. Hai due opzioni che funzionano: registrare il tuo Mac come runner Bitbucket self-hosted, oppure tenere lo step su Linux e delegare compilazione, firma e upload su TestFlight a un servizio di cloud build. Questa guida le mostra entrambe, con bitbucket-pipelines.yml i file completi per un'app Capacitor.
Se il tuo team usa già Bitbucket per la revisione del code, i link a Jira e le deploy, non ti serve un secondo tool di CI solo per iOS. La pipeline resta in Bitbucket. Sposta solo la parte che richiede un Mac.
Perché le build iOS falliscono su Bitbucket Pipelines
Bitbucket Pipelines esegue ogni step all'interno di un'immagine Docker a tua scelta (node:22, cimg/android, e così via) sull'infrastruttura Linux di Atlassian. Va bene per bun install, test unitari, linting e persino build complete per Android. Si ferma con iOS:
xcodebuild,codesign, e il SDK per iOS esiste solo su macOS.- La licenza di Apple consente di eseguire macOS solo su hardware Apple, quindi non esiste un'immagine Linux in grado di produrre un
.ipa. - Dal 28 aprile 2026, App Store Connect rifiuta gli upload non compilati con Xcode 26 e il SDK iOS 26, quindi qualsiasi Mac tu usi deve essere aggiornato. Vedi Il requisito Xcode 26 di Apple per le app Capacitor.
Bitbucket supporta i runner self-hosted su Linux, Windows e macOS. Atlassian non addebita minuti di build per gli step eseguiti sui tuoi runner, ma paghi in hardware e manutenzione.
Le tue tre opzioni
| Opzione | Dove gira la build iOS | Cosa devi gestire | Ideale per |
|---|---|---|---|
| Runner macOS self-hosted | Il tuo Mac mini o un Mac a noleggio | macOS, Xcode, keychain, certificati, spazio disco, uptime | Team che hanno già dei Mac e personale per gestirli |
| Seconda CI dedicata al mobile | Mac di un altro provider | Due pipeline, due set di secret e trigger | Team che preferiscono una CI mobile dedicata |
| Build in cloud chiamata da uno step Linux | La flotta Mac di Capgo Build | Una sola chiamata a CLI e alcune variabili sicure | I team Capacitor che vogliono che Bitbucket resti l'unico CI |
Il resto di questa guida copre in dettaglio la prima e la terza opzione. Per una visione più ampia delle piattaforme, leggi Confronto tra piattaforme CI/CD per app iOS.
Opzione 1: Build iOS da uno step Linux con Capgo Build
Build Capgo compila e firma le app Capacitor su Mac gestiti da Capgo. Il tuo step di Bitbucket fa ciò che sa già fare bene su Linux: installa le dipendenze, compila il layer web ed esegue cap sync ios. Poi Capgo CLI carica il progetto nativo preparato (non il tuo sorgente web code o node_modules), esegue la build Xcode, la firma con il tuo certificato e può caricare il risultato su TestFlight. I log vengono trasmessi in streaming al passaggio di Bitbucket e il comando restituisce un codice di uscita non zero se la build fallisce, quindi una build iOS non riuscita fa fallire la pipeline.
Poiché il runner spinge il progetto su Capgo, Capgo non ha mai bisogno di accedere al tuo repository Bitbucket. Non c'è alcuna connessione al repository da autorizzare e nessuna app OAuth da approvare.
Prerequisiti
- Un'app Capacitor che viene compilata localmente (Capacitor 8 richiede Node.js 22 o versioni successive)
- Un'iscrizione al Apple Developer Program con diritti di amministratore sul team
- Un account Capgo e una chiave API con permesso di upload
- L'app registrata in Capgo:
bunx @capgo/cli@latest app add
Setup una tantum sulla tua macchina
Esegui l'onboarding una volta. Su iOS crea o riutilizza il tuo certificato di distribuzione e il profilo di provisioning, e richiede una chiave API di App Store Connect:
bunx @capgo/cli@latest login
bunx @capgo/cli@latest build init --platform ios
Se non hai un Mac a portata di mano, puoi comunque creare la richiesta di firma del certificato nel browser con il Generatore di certificati iOS, o seguire Build di un'app iOS da Windows con Capgo Build.
Esegui una build dal tuo laptop prima di toccare la CI. La CI è un pessimo posto per debuggare una prima build:
bun run build
bunx cap sync ios
bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release
Esporta le credenziali per Bitbucket
Il CLI mantiene le credenziali sulla tua macchina. Per spostarle su Bitbucket, esportale in un file .env:
bunx @capgo/cli@latest build credentials manage --appId com.example.app --platform ios
# choose "Export to .env" in the menu
Questo scrive .env.capgo.com.example.app.ios con modalità 0600. Contiene queste chiavi:
| Variabile | Contenuto |
|---|---|
BUILD_CERTIFICATE_BASE64 |
Base64 del tuo .p12 certificato di distribuzione |
P12_PASSWORD |
Password del certificato (vuota se assente) |
CAPGO_IOS_PROVISIONING_MAP_BASE64 |
Mappatura del provisioning profile generata da CLI |
APPLE_KEY_ID |
ID della chiave API di App Store Connect |
APPLE_ISSUER_ID |
Issuer ID di App Store Connect |
APPLE_KEY_CONTENT |
Base64 del .p8 Chiave API |
APP_STORE_CONNECT_TEAM_ID |
Il tuo Apple team ID |
Aggiungi ogni riga come protetta variabile in Bitbucket, in Impostazioni repository > Pipelines > Variabili del repository, e anche CAPGO_TOKEN con la tua chiave Capgo API. Le variabili protette sono mascherate nei log e non possono essere rilette dalla UI.
Se preferisci usare API, questo loop le crea in un colpo solo. Usa un token API di Bitbucket con scope di scrittura sulle variabili della 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 il file dopo e aggiungi .env.capgo.* a .gitignore. Contiene materiale di firma in chiaro.
Per le credenziali di produzione, valuta variabili di deployment invece delle variabili di repository. Sono associate a un ambiente di deployment (ad esempio Production) e puoi limitare l'accesso a quell'ambiente agli admin o a branch specifici nei piani Premium.
Il file della pipeline
Bitbucket inietta le variabili di repository e di deployment come variabili d'ambiente, quindi Capgo CLI le rileva senza bisogno di mapping. Questo file esegue i test a ogni push, crea una build iOS ad hoc su richiesta e pubblica su TestFlight per i tag di versione:
# 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
Alcuni dettagli importanti:
- L'immagine è un Linux standard
node:22. In questo file non serve macOS. - Su Linux,
cap sync ioscopia gli asset web e aggiorna la configurazione nativa, ma non può eseguire CocoaPods. Nessun problema: il Mac che esegue la build installa i pod o risolve i pacchetti Swift da solo. - Capgo legge l'ultimo build number da App Store Connect e lo incrementa, così eviti l'errore “bundle version must be higher than the previously uploaded version”. Passa
--skip-build-number-bumpse te ne occupi tu. - La build ad hoc richiede un provisioning profile Ad Hoc, che si installa solo sui dispositivi elencati. Salvalo come variabile
ADHOC_PROFILE_BASE64protetta; il--ios-provisioning-profileflag sovrascrive il profilo App Store preso dall'ambiente solo per questa build. Raccogli gli UDID dei tester con il Trova UDID iOS. - Blocca CLI a una versione esatta (ad esempio
@capgo/cli@7.104.0) una volta che la pipeline è stabile, così un rilascio CLI non può cambiare il comportamento a metà sprint.
Esegui la pipeline personalizzata da Pipelines > Run pipeline > ios-adhoc. L'ultimo comando stampa un link di download che scade dopo il periodo di retention.
Approvazione manuale prima di TestFlight
Bitbucket supporta trigger: manual su uno step. Dividi il rilascio in uno step di build e uno step di upload con approvazione se vuoi che una persona approvi ogni upload su 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
Per passare direttamente all'App Review dopo l'upload su TestFlight, aggiungi --submit-to-store-review con --store-release-name "$BITBUCKET_TAG". La scheda su App Store, gli screenshot e le risposte sulla conformità devono comunque essere già completati su App Store Connect.
Aggiungi Android in parallelo
Le build Android possono girare direttamente sui runner Linux di Bitbucket, ma puoi anche inviarle a Capgo Build per usare lo stesso flusso di signing e upload su entrambe le piattaforme. Usa un parallel blocco:
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
Lo step Android richiede ANDROID_KEYSTORE_FILE, KEYSTORE_KEY_ALIAS, KEYSTORE_KEY_PASSWORD, KEYSTORE_STORE_PASSWORD, e PLAY_CONFIG_JSON come variabili protette. Esportale allo stesso modo con --platform android.
Distribuisci modifiche solo web senza una build nativa
La maggior parte dei commit in un'app Capacitor modifica solo HTML, CSS e JavaScript. Questi non richiedono un nuovo binario. Con Live updates di Capgo, uno step di Bitbucket può scegliere tra una build per lo store e 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 esce con code 0 quando non serve una build nativa e con 1 quando serve. Confronta le versioni dei plugin nativi con quelle attive sul canale. Non rileva le modifiche manuali in ios/, quindi forza il percorso nativo anche quando questi file cambiano. Il pattern completo è in Scegli automaticamente live update o Native Build.
Opzione 2: Un runner macOS self-hosted
Se hai già dei Mac, puoi registrarne uno come runner di Bitbucket e lanciare Xcode da solo.
Registra il runner
- In Bitbucket, apri Impostazioni repository > Runner (o Impostazioni workspace > Runner del workspace per condividerlo) e clicca su Aggiungi runner.
- Usa MacOS come sistema. Bitbucket mostra un comando di avvio con un UUID del runner e le credenziali OAuth.
- Sul Mac, installa un JDK supportato (il runner è un programma Java), Xcode 26 e fastlane. Quindi incolla il comando di avvio.
- Mantieni il runner in esecuzione come launch agent così sopravvive ai riavvii.
Uno step seleziona il runner tramite le etichette:
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
I runner macOS girano direttamente sull'host con l'ambiente del tuo utente, non in un container. Installa Bun e Ruby sul Mac stesso.
Un Fastfile minimale
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
I nuovi progetti Capacitor 8 usano Swift Package Manager, quindi esegui la build App.xcodeproj. I progetti più vecchi che usano ancora CocoaPods buildano App.xcworkspace e hanno bisogno pod install sul runner. La nostra guida alle GitHub Actions per iOS approfondisce ogni action di fastlane; la lane è la stessa su Bitbucket.
Cosa ti aspetta
L'archivio è la parte facile. Un Mac self-hosted significa anche:
- Aggiornamenti di Xcode ogni volta che Apple alza il SDK minimo, testati prima della scadenza.
- Stato del Keychain. Un job andato in crash può lasciare un keychain temporaneo, e il job successivo si ritrova con l'identità sbagliata.
- Nessun isolamento. I job girano sullo stesso disco. DerivedData, archivi vecchi e simulatori lo riempiono.
- Un build alla volta per runner, a meno che non compri altri Mac.
- Certificati e profili che scadono ogni anno e vanno rinnovati sulla macchina.
È un compromesso ragionevole se il tuo team gestisce già i Mac. Se nessuno vuole occuparsi di quella macchina, l'Opzione 1 lo elimina.
Risoluzione problemi
| Sintomo | Causa probabile e soluzione |
|---|---|
CAPGO_TOKEN is not set |
La variabile è mancante, oppure è una variabile di deployment e il passaggio non ha deployment: chiave. |
Il passaggio funziona su main ma non sui tag |
Le variabili di deployment si applicano solo agli step che dichiarano quell'ambiente di deployment. |
Provisioning profile doesn't match bundle ID |
Il profilo è stato creato per un altro App ID. Riesegui build init e riesporta. |
| Autenticazione App Store Connect non riuscita | Issuer ID o Key ID errati, oppure una chiave revocata. Controlla la chiave in App Store Connect > Users and Access > Integrations. |
| Upload rifiutato per la versione SDK | La build ha usato una versione di Xcode precedente alla 26. Aggiorna il Mac self-hosted. |
| Step self-hosted bloccato in “Pending” | Runner offline o i label non corrispondono self.hosted e macos. |
errSecInternalComponent durante il codesign |
Il keychain è bloccato in una sessione non interattiva. Usa setup_ci e importa il certificato nel keychain temporaneo. |
| La build passa ma non c'è nulla in TestFlight | L'elaborazione richiede diversi minuti e la conformità all'export potrebbe essere in sospeso. Aggiungi ITSAppUsesNonExemptEncryption a Info.plist se la tua app utilizza solo crittografia standard. |
Altre correzioni per pipeline in errore sono in Risolvere gli errori di build nelle pipeline CI/CD di Capacitor e il Capgo Build troubleshooting guide.
Quale opzione scegliere?
Scegli un Mac self-hosted se possiedi già l'hardware, ti serve il pieno controllo dell'ambiente Xcode o compili target Swift nativi che non sono app Capacitor. Scegli una build cloud da uno step Linux se la tua app è un'app Capacitor e vuoi che Bitbucket resti l'unico posto in cui il team configura la CI. In entrambi i casi, tieni la logica di build in bitbucket-pipelines.yml accanto al tuo code, metti il materiale di firma in variabili protette ed esegui build native solo quando i code nativi sono davvero cambiati.