Zum Hauptinhalt springen

iOS Apps mit Bitbucket Pipelines builden und deployen

iOS-Apps bauen, signieren und über Bitbucket Pipelines an TestFlight ausliefern – ohne gehostete macOS-Runner: Cloud-Builds aus einem Linux-Step oder von einem Self-hosted Mac.

Bitbucket Pipelines: iOS-App bauen und auf TestFlight veröffentlichen

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 ios kopiert 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_BASE64 Variable; das --ios-provisioning-profile Flag ü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

  1. In Bitbucket öffnen Repository-Einstellungen > Runners (oder Workspace-Einstellungen > Workspace runners zum Teilen) und klicken auf Runner hinzufügen.
  2. Wähle MacOS als das System. Bitbucket zeigt einen Startbefehl mit einer Runner-UUID und OAuth-Zugangsdaten an.
  3. Installiere auf dem Mac eine unterstützte JDK (der Runner ist ein Java-Programm), Xcode 26 und fastlane. Füge dann den Startbefehl ein.
  4. 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.

Live-Updates für Capacitor-Apps

Wenn ein Bug im Web-Layer live ist, liefere den Fix über Capgo aus, statt Tage auf die App-Store-Freigabe zu warten. Nutzer bekommen das Update im Hintergrund, während native Änderungen im normalen Review-Prozess bleiben.

Neues aus unserem Blog

Capgo gibt dir die besten Insights, um eine wirklich professionelle Mobile App zu erstellen.