Salta al contenuto principale

Build e deploy di app iOS con Bitbucket Pipelines

Build, firma e rilascia app iOS su TestFlight da Bitbucket Pipelines senza runner macOS ospitati: build cloud da uno step Linux o da un Mac self-hosted.

Bitbucket Pipelines per la creazione e la pubblicazione di un'app iOS su TestFlight

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 ios copia 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-bump se 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_BASE64 protetta; il --ios-provisioning-profile flag 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

  1. In Bitbucket, apri Impostazioni repository > Runner (o Impostazioni workspace > Runner del workspace per condividerlo) e clicca su Aggiungi runner.
  2. Usa MacOS come sistema. Bitbucket mostra un comando di avvio con un UUID del runner e le credenziali OAuth.
  3. Sul Mac, installa un JDK supportato (il runner è un programma Java), Xcode 26 e fastlane. Quindi incolla il comando di avvio.
  4. 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.

Live updates per le app Capacitor

Quando un bug nel web-layer è live, rilascia la fix tramite Capgo invece di aspettare giorni l'approvazione degli store. Gli utenti ricevono l'update in background, mentre le modifiche native restano nel normale processo di review.

Ultimi articoli dal blog

Capgo ti offre gli insight migliori che ti servono per creare un'app mobile davvero professionale.