Bitbucket Pipelines kann keine iOS-App auf den gehosteten Runnern bauen, da jeder Schritt in einem Linux-Container läuft und Xcode nur auf macOS funktioniert. Du hast zwei funktionierende Optionen: Registriere deinen eigenen Mac als selbst gehosteten Bitbucket-Runner oder behalte den Schritt auf Linux und überlasse das iOS-Kompilieren, Signieren und TestFlight-Upload einem Cloud-Build-Dienst. Dieser Guide zeigt beides, mit vollständigen bitbucket-pipelines.yml Dateien für eine Capacitor-App.
Wenn dein Team bereits in Bitbucket für code-Reviews, Jira-Links und Deployments arbeitet, brauchst du kein zweites CI-Tool nur für iOS. Die Pipeline bleibt in Bitbucket. Nur der Teil, der einen Mac benötigt, wird ausgelagert.
Warum iOS-Builds in Bitbucket Pipelines scheitern
Bitbucket Pipelines führt jeden Schritt in einem von dir gewählten Docker-Image aus (node:22, cimg/androidund so weiter) auf der Linux-Infrastruktur von Atlassian. Das ist in Ordnung für bun install, Unit-Tests, Linting und sogar vollständige Android-Builds. Bei iOS endet es jedoch:
xcodebuild,codesign, und die iOS-SDK existieren nur auf macOS.- Die Apple-Lizenz erlaubt nur, macOS auf Apple-Hardware auszuführen, daher gibt es kein Linux-Image, das eine signierte
.ipa. - Seit dem 28. April 2026 lehnt App Store Connect Uploads ab, die nicht mit Xcode 26 und dem iOS-26-SDK gebaut wurden. Der verwendete Mac muss daher stets aktuell sein. Siehe Apples Xcode-26-Anforderung für Capacitor Apps.
Bitbucket unterstützt Self-Hosted-Runner auf Linux, Windows und macOS. Atlassian berechnet keine Build-Minuten für Schritte, die auf eigenen Runnern ausgeführt werden, aber man zahlt für Hardware und Wartung.
Deine drei Optionen
| Option | Wo der iOS-Build läuft | Was du pflegen musst | Ideal geeignet |
|---|---|---|---|
| Self-hosted macOS-Runner | Dein Mac mini oder ein gemieteter Mac | macOS, Xcode, Keychain, Zertifikate, Speicherplatz, Uptime | Teams, die bereits Macs besitzen und jemanden haben, der sie betreibt |
| Zweite CI nur für Mobile | Macs eines anderen Anbieters | Zwei Pipelines, zwei Sätze an Secrets und Triggern | Teams, die ohnehin eine dedizierte Mobile-CI wünschen |
| Cloud-Build, aufgerufen aus einem Linux-Step | Die Mac-Flotte von Capgo Build | Ein CLI-Aufruf und einige gesicherte Variablen | Capacitor-Teams, die Bitbucket als einzige CI behalten wollen |
Der Rest dieses Guides behandelt die erste und dritte Option im Detail. Für einen breiteren Überblick über die Plattformen lesen Sie CI/CD-Plattformen für iOS-Apps im Vergleich.
Option 1: iOS von einem Linux-Step mit Capgo Build bauen
Capgo Build kompiliert und signiert Capacitor-Apps auf von Capgo verwalteten Macs. Ihr Bitbucket-Step macht, was er auf Linux ohnehin gut kann: Abhängigkeiten installieren, die Web-Schicht bauen und cap sync iosausführen. Danach lädt der Capgo CLI das vorbereitete Native-Projekt hoch (nicht Ihre Web-Quellcode oder node_modules, führt den Xcode-Build aus, signiert ihn mit deinem Zertifikat und kann das Ergebnis zu TestFlight hochladen. Die Logs werden zurück in den Bitbucket-Schritt gestreamt, und der Befehl endet mit einem Exit-Code ungleich null, wenn der Build fehlschlägt – so scheitert die Pipeline, wenn der iOS-Build fehlschlägt.
Da dein Runner das Projekt an Capgo pusht, benötigt Capgo nie Zugriff auf dein Bitbucket-Repository. Es gibt keine Repository-Verbindung, die autorisiert werden muss, und keine OAuth-App, die genehmigt werden muss.
Voraussetzungen
- Eine Capacitor-App, die lokal gebaut wird (Capacitor 8 benötigt Node.js 22 oder neuer)
- Eine Mitgliedschaft im Apple Developer Program mit Admin-Rechten im Team
- Ein Capgo-Konto und ein API-Key mit Upload-Berechtigung
- Die in Capgo registrierte App:
bunx @capgo/cli@latest app add
Einmaliges Setup auf deinem Rechner
Führe das Onboarding einmal aus. Auf iOS erstellt oder wiederverwendet es dein Distribution-Zertifikat und dein Provisioning-Profil und fragt nach einem App Store Connect API-Key:
bunx @capgo/cli@latest login
bunx @capgo/cli@latest build init --platform ios
Falls kein Mac zur Hand ist, lässt sich die Zertifikatsignierungsanfrage auch im Browser mit dem iOS-Zertifikatsgenerator, oder folge iOS-App unter Windows mit Capgo Build erstellen.
Führe vor dem CI einen Build von deinem Laptop aus. CI ist der falsche Ort, um einen ersten Build zu debuggen:
bun run build
bunx cap sync ios
bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release
Exportiere die Credentials für Bitbucket
Die CLI speichert Credentials auf deinem Rechner. Um sie in Bitbucket zu übertragen, exportiere sie in eine dotenv-Datei:
bunx @capgo/cli@latest build credentials manage --appId com.example.app --platform ios
# choose "Export to .env" in the menu
Das schreibt .env.capgo.com.example.app.ios mit dem Modus 0600. Sie enthält diese Keys:
| Variabel | Inhalt |
|---|---|
BUILD_CERTIFICATE_BASE64 |
Base64 von deinem .p12 Distributionszertifikat |
P12_PASSWORD |
Zertifikat-Passwort (leer, falls keines) |
CAPGO_IOS_PROVISIONING_MAP_BASE64 |
Provisioning-Profil-Mapping, erstellt von CLI |
APPLE_KEY_ID |
App Store Connect API Key-ID |
APPLE_ISSUER_ID |
App Store Connect Issuer-ID |
APPLE_KEY_CONTENT |
Base64 von .p8 API Key |
APP_STORE_CONNECT_TEAM_ID |
Ihre Apple-Team-ID |
Fügen Sie jede Zeile als gesicherte Variable in Bitbucket unter Repository-Einstellungen > Pipelines > Repository-Variablenhinzu, sowie CAPGO_TOKEN mit Ihrem Capgo API-Schlüssel. Gesicherte Variablen werden in Logs maskiert und können nicht über die UI abgerufen werden.
Wenn du die API bevorzugst, legt diese Schleife sie in einem Durchgang an. Sie verwendet einen Bitbucket-API-Token mit Write-Scope für Pipeline-Variablen:
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"
Lösche die Datei anschließend und füge hinzu .env.capgo.* zu .gitignore. Sie enthält Signing-Material im Klartext.
Für Produktions-Credentials erwäge Deployment-Variablen anstelle von Repository-Variablen. Sie sind an eine Deployment-Umgebung gebunden (zum Beispiel Production), und du kannst diese Umgebung in Premium-Plänen auf Admins oder bestimmte Branches beschränken.
Die Pipeline-Datei
Bitbucket injiziert Repository- und Deployment-Variablen als Umgebungsvariablen, sodass die Capgo CLI sie ohne Mapping übernimmt. Diese Datei führt bei jedem Push Tests aus, baut bei Bedarf einen Ad-hoc-iOS-Build und lädt ihn bei Version-Tags zu TestFlight hoch:
# 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
Ein paar Details, die zählen:
- Das Image ist ein normales Linux
node:22. Nichts in dieser Datei braucht macOS. - Unter Linux,
cap sync ioskopiert Web-Assets und aktualisiert die Native-Konfiguration, kann aber CocoaPods nicht ausführen. Das ist in Ordnung. Der Mac, auf dem der Build läuft, installiert die Pods oder löst Swift-Pakete selbst auf. - Capgo liest die neueste Build-Nummer aus App Store Connect und erhöht sie, damit du nie auf „Bundle-Version muss höher sein als die zuvor hochgeladene Version“ stößt. Übergib
--skip-build-number-bump, wenn du es selbst verwaltest. - Der Ad-hoc-Build benötigt ein Ad-hoc-Provisioning-Profil, das nur auf den darin aufgeführten Geräten installiert wird. Speichere es als gesicherte
ADHOC_PROFILE_BASE64Variable; das--ios-provisioning-profileFlag überschreibt das App-Store-Profil aus der Umgebung nur für diesen Build. Sammle die Tester-UDIDs mit dem iOS UDID Finder. - Pinne CLI auf eine exakte Version (zum Beispiel
@capgo/cli@7.104.0), sobald deine Pipeline stabil ist, damit ein CLI-Release das Verhalten nicht mitten im Sprint ändert.
Führe die benutzerdefinierte Pipeline von Pipelines > Pipeline ausführen > ios-adhoc. Der letzte Befehl gibt einen Download-Link aus, der nach Ablauf der Aufbewahrungsfrist abläuft.
Manuelle Freigabe vor TestFlight
Bitbucket unterstützt trigger: manual auf einem Step. Teile die Release in einen Build-Step und einen gated Upload-Step auf, wenn du möchtest, dass ein Mensch jeden TestFlight-Upload freigibt:
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
Um direkt nach dem TestFlight-Upload zur App Review zu gelangen, füge hinzu --submit-to-store-review mit --store-release-name "$BITBUCKET_TAG". Deine App-Store-Eintragung, Screenshots und Compliance-Antworten müssen zunächst in App Store Connect vollständig sein.
Android parallel hinzufügen
Android-Builds können direkt auf Bitbuckets Linux-Runnern ausgeführt werden, du kannst sie aber auch an Capgo Build senden, damit beide Plattformen denselben Signing- und Upload-Flow nutzen. Verwende ein 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
Der Android-Step benötigt ANDROID_KEYSTORE_FILE, KEYSTORE_KEY_ALIAS, KEYSTORE_KEY_PASSWORD, KEYSTORE_STORE_PASSWORD, und PLAY_CONFIG_JSON als gesicherte Variablen. Exportiere sie auf dieselbe Weise mit --platform android.
Web-Änderungen ohne Native Build veröffentlichen
Die meisten Commits in einer Capacitor-App betreffen nur HTML, CSS und JavaScript. Dafür ist kein neues Binary nötig. Mit Capgo Live Updateskann ein Bitbucket-Schritt zwischen einem Store-Build und einem Over-the-Air-Bundle entscheiden:
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 endet mit code 0, wenn kein Native Build erforderlich ist, und mit 1, wenn er es ist. Es vergleicht die Versionen der Native-Plugins mit dem, was auf dem Channel live ist. Es erkennt manuelle Änderungen unter ios/nicht, daher den Native-Pfad auch erzwingen, wenn sich diese Dateien ändern. Das vollständige Muster findest du in Automatisch live update oder Native Build wählen.
Option 2: Ein selbst gehosteter macOS-Runner
Wenn du bereits Macs besitzt, kannst du einen als Bitbucket-Runner registrieren und Xcode selbst ausführen.
Runner registrieren
- In Bitbucket öffnen Repository-Einstellungen > Runners (oder Workspace-Einstellungen > Workspace runners zum Teilen) und klicken auf Runner hinzufügen.
- Wähle MacOS als das System. Bitbucket zeigt einen Startbefehl mit einer Runner-UUID und OAuth-Zugangsdaten an.
- Installiere auf dem Mac eine unterstützte JDK (der Runner ist ein Java-Programm), Xcode 26 und fastlane. Füge dann den Startbefehl ein.
- Halte den Runner als Launch-Agenten am Laufen, damit er Neustarts übersteht.
Ein Step adressiert den Runner über Labels:
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
macOS-Runner werden direkt auf dem Host in deiner User-Umgebung ausgeführt, nicht in einem Container. Installiere Bun und Ruby direkt auf dem Mac.
Eine minimale Fastfile
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
Neue Capacitor 8-Projekte nutzen Swift Package Manager, daher baust du App.xcodeproj. Ältere Projekte, die noch CocoaPods verwenden, bauen App.xcworkspace und benötigen pod install auf dem Runner. Unser GitHub Actions iOS Guide erklärt jede fastlane-Aktion im Detail; die Lane ist auf Bitbucket identisch.
Wofür du dich anmeldest
Das Archivieren ist der einfache Teil. Ein selbst gehosteter Mac bedeutet auch:
- Xcode-Updates sobald Apple das Minimum für SDK anhebt, getestet vor der Deadline.
- Keychain-Zustand. Ein abgestürzter Job kann einen temporären Keychain hinterlassen, und der nächste Job übernimmt die falsche Identität.
- Keine Isolation. Jobs laufen auf derselben Festplatte. DerivedData, alte Archive und Simulatoren füllen sie auf.
- Nur ein Build zur Zeit pro Runner, es sei denn, man kauft weitere Macs.
- Zertifikate und Profile die jedes Jahr ablaufen und auf dem Gerät erneuert werden müssen.
Das ist ein fairer Kompromiss, wenn euer Team bereits Macs verwaltet. Wenn niemand diese Box besitzen will, übernimmt Option 1 das.
Fehlerbehebung
| Symptom | Wahrscheinliche Ursache und Lösung |
|---|---|
CAPGO_TOKEN is not set |
Die Variable fehlt oder es handelt sich um eine Deployment-Variable, aber der Schritt hat keine deployment: Key. |
Schritt läuft auf main aber nicht auf Tags |
Deployment-Variablen gelten nur für Schritte, die diese Deployment-Umgebung deklarieren. |
Provisioning profile doesn't match bundle ID |
Das Profil wurde für eine andere App-ID erstellt. Führe aus build init und exportiere es neu. |
| App Store Connect-Authentifizierung fehlgeschlagen | Falsche Issuer-ID oder Key-ID oder ein widerrufenes Schlüssel. Prüfen Sie den Schlüssel unter App Store Connect > Benutzer und Zugriff > Integrationen. |
| Upload für Version SDK abgelehnt | Der Build verwendete ein Xcode älter als 26. Aktualisiere den selbst gehosteten Mac. |
| Selbst gehosteter Schritt hängt in „Pending“ fest | Runner offline oder Labels stimmen nicht überein self.hosted und macos. |
errSecInternalComponent während des Codesigning |
Der Keychain ist in einer nicht-interaktiven Sitzung gesperrt. Verwende setup_ci und importiere das Zertifikat in den temporären Keychain. |
| Build erfolgreich, aber nichts in TestFlight | Die Verarbeitung dauert einige Minuten, und die Export Compliance kann ausstehen. Füge hinzu ITSAppUsesNonExemptEncryption zu Info.plist falls deine App nur Standard-Verschlüsselung verwendet. |
Weitere Fixes für fehlerhafte Pipelines findest du in Build-Fehler in Capacitor CI/CD-Pipelines beheben und Capgo Build-Troubleshooting-Leitfaden.
Welche Option solltest du wählen?
Wähle einen selbst gehosteten Mac, wenn du die Hardware bereits besitzt, die volle Kontrolle über die Xcode-Umgebung benötigst oder native Swift-Ziele baust, die keine Capacitor-Apps sind. Wähle einen Cloud-Build aus einem Linux-Schritt, wenn deine App eine Capacitor-App ist und du möchtest, dass Bitbucket der einzige Ort bleibt, an dem dein Team CI konfiguriert. In beiden Fällen behalte die Build-Logik in bitbucket-pipelines.yml neben deinem code, speichere Signaturmaterial in gesicherten Variablen und führe native Builds nur aus, wenn sich native code tatsächlich geändert haben.