Salta al contenuto principale

CI/CD per Capacitor: errori comuni e soluzioni

Le insidie CI/CD che rompono i build di Capacitor: signing iOS, runner macOS, Xcode 26, cap sync non aggiornato, version code, rifiutazioni dagli store e le soluzioni per ciascuna.

identità di firma Code e step CI falliti in una pipeline Capacitor

La maggior parte dei fallimenti CI/CD di Capacitor deriva sempre dalle stesse poche cause: signing iOS code, tool macOS mancanti o obsoleti, una build web che non è mai finita nel progetto nativo, build number riutilizzati e requisiti degli store che falliscono solo al momento dell'upload. Ognuna ha una causa nota e una correzione da applicare una volta sola. Questa guida raggruppa le insidie per fase della pipeline, con l'errore che vedrai, perché succede e cosa cambiare.

Se stai configurando una pipeline da zero, leggi Configurare CI/CD per app Capacitor prima, poi usa questa lista per rafforzarlo.

Fase 1: Build del layer web

Trappola: l'app nativa distribuisce una build web vecchia

Sintomo: CI va a buon fine, l'app si installa, ma mostra l'interfaccia di ieri.

Causa: cap sync copia ciò che si trova in webDir in quel momento. Se la pipeline esegue cap sync prima della web build, o la web build scrive in una cartella diversa da webDir nella capacitor.config.ts, il progetto nativo riceve file obsoleti.

Soluzione: esegui sempre i passaggi in questo ordine e fallisci se la cartella di output è vuota.

bun install --frozen-lockfile
bun run build
test -f dist/index.html || { echo "web build missing"; exit 1; }
bunx cap sync

Assicurati che webDir corrisponde all'output del bundler (dist per Vite, www per Angular con Ionic, build per alcune configurazioni React).

Trappola: URL del dev server lasciato nella configurazione

Sintomo: la release build mostra uno schermo vuoto o tenta di caricare http://192.168.x.x:5173.

Causa: server.url nella capacitor.config.ts era stato impostato per il live reload e committato.

Soluzione: non commettere mai server.url. Leggilo da una variabile d'ambiente che CI non imposta mai:

import type { CapacitorConfig } from '@capacitor/cli'

const config: CapacitorConfig = {
  appId: 'com.example.app',
  appName: 'Example',
  webDir: 'dist',
  ...(process.env.LIVE_RELOAD_URL && {
    server: { url: process.env.LIVE_RELOAD_URL, cleartext: true },
  }),
}

export default config

Trappola: variabili d'ambiente incorporate al momento sbagliato

Sintomo: l'app di produzione comunica con l'ambiente di staging API.

Causa: Vite, webpack e Angular incorporano le variabili d'ambiente al momento del build. Il valore presente quando bun run build ran è quello presente nel binario e in qualsiasi live update generato dallo stesso job.

Soluzione: Imposta le variabili specifiche dell'ambiente prima del web build, per job, e genera bundle separati per staging e produzione. Non provare a sostituirle in seguito. cap sync.

Errore comune: lockfile drift

Sintomo: la versione di un plugin in CI differisce da quella locale e la compilazione nativa fallisce per simboli mancanti.

Soluzione: fai il commit del lockfile e installa con bun install --frozen-lockfile (o npm ci). Fissa @capacitor/core, @capacitor/ios, @capacitor/android, e @capacitor/cli alla stessa versione. Le versioni non corrispondenti causano spesso errori nativi; vedi Fix Capacitor version mismatch errors.

Fase 2: La toolchain nativa

Errore comune: versione Node, JDK o Xcode errata

Sintomo: Unsupported class file major version, The engine "node" is incompatible, o errori Xcode relativi alle funzionalità di SDK.

Causa: le immagini dei runner hosted cambiano, e Capacitor 8 ha requisiti minimi rigidi.

Strumento Capacitor 8 requisiti
Node.js 22 o superiore
JDK 21
Xcode 26 o superiore
Target di deployment iOS 15.0
Android minSdkVersion / targetSdkVersion 24 / 36

Soluzione: fissa esplicitamente ogni versione nella pipeline invece di fidarti di latest:

- uses: actions/setup-node@v6
  with:
    node-version-file: .nvmrc
- uses: actions/setup-java@v4
  with:
    distribution: temurin
    java-version: '21'
- uses: maxim-lobanov/setup-xcode@v1
  with:
    xcode-version: '26'

Pitfall: fare build con un Xcode che Apple non accetta più

Sintomo: l'upload fallisce con un messaggio che indica che l'app è stata compilata con un SDK non supportato.

Causa: Dal 28 aprile 2026, App Store Connect richiede Xcode 26 e il SDK iOS 26. I Mac self-hosted e le immagini runner più vecchie usano ancora Xcode 16 di default.

Soluzione: seleziona un macos-26 immagine o installa Xcode 26 sui tuoi runner. Dettagli in Il requisito Xcode 26 di Apple per le app Capacitor.

Trappola: confusione tra CocoaPods e SPM

Sintomo: xcodebuild: error: 'App.xcworkspace' does not exist, o pods non trovati.

Causa: I nuovi Capacitor 8 progetti usano Swift Package Manager e build ios/App/App.xcodeproj. I progetti più vecchi usano CocoaPods e build ios/App/App.xcworkspace dopo pod install. Le pipeline copiate da tutorial datati presuppongono il workspace.

Soluzione: controlla quale usa il tuo progetto e genera il file corretto. Se stai migrando, vedi Come migrare la tua app Capacitor a SPM.

Problema: i build dei plugin Android si rompono dopo un aggiornamento di Gradle

Sintomo: Namespace not specified, package attribute is deprecated, o errori da un plugin build.gradle dopo aver aggiornato Android Studio.

Soluzione: fissa la versione dell'Android Gradle Plugin in android/build.gradle, aggiorna su un branch e aggiorna prima i plugin. Gli errori specifici sono trattati in Fix degli errori di build del plugin Capacitor con AGP 9.

Fase 3: firma Code

Errore comune: la firma iOS funziona solo sul tuo Mac

Sintomo: No signing certificate "iOS Distribution" found, No profiles for 'com.example.app' were found, o errSecInternalComponent.

Causa: il keychain del tuo Mac contiene il certificato e Xcode scarica i profili per te. Un runner CI non ha nessuno dei due e il suo keychain è bloccato in una sessione non interattiva.

Soluzione: importa il certificato in un keychain temporaneo sbloccato e installa il profilo dove Xcode 16 e versioni successive lo cercano:

security create-keychain -p "$KEYCHAIN_PASSWORD" ci.keychain
security set-keychain-settings -lut 21600 ci.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" ci.keychain
security import dist.p12 -k ci.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" ci.keychain
security list-keychains -d user -s ci.keychain login.keychain

PROFILES="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
mkdir -p "$PROFILES"
cp app.mobileprovision "$PROFILES/"

Usa il nome dell'identity Apple Distribution, non quello legacy iOS Distribution. fastlane’s setup_ci più match automatizza tutto questo. Capgo Build prende il certificato e il profilo come variabili d'ambiente e gestisce il keychain sulle proprie macchine, così il job Linux non deve toccare security.

Trappola: certificati scaduti o revocati

Sintomo: una pipeline che funzionava da un anno fallisce improvvisamente.

Causa: I certificati di distribuzione Apple e i profili di provisioning scadono dopo un anno. Un collega che crea un nuovo certificato in Xcode può anche invalidare il profilo utilizzato dalla tua CI.

Soluzione: metti le date di scadenza nel calendario del team, mantieni un certificato di distribuzione condiviso per la CI e verifica prima di fare il build. bunx @capgo/cli@latest build prescan --platform ios verifica la scadenza del certificato, la password e l'abbinamento del profilo prima di caricare qualsiasi cosa.

Trappola: login Apple ID e prompt di autenticazione a due fattori

Sintomo: fastlane si blocca in attesa di un codice a 6 cifre code.

Soluzione: usa una chiave API di App Store Connect (.p8, key ID, issuer ID) invece di ID Apple e password. Non richiede l'autenticazione a due fattori e può essere limitata al ruolo App Manager. Se l'autenticazione fallisce ancora con una chiave corretta, controlla l'orologio del runner: il token è firmato con l'ora locale e Apple rifiuta i token con timestamp non allineati.

Trappola: segreti base64 che non vengono decodificati

Sintomo: MAC verification failed, invalid keystore format, o base64: invalid input.

Causa: a capo aggiunti durante la copia del valore base64, oppure un .p12 creato con i valori predefiniti di OpenSSL 3, non leggibili da macOS.

Soluzione: codifica su una riga e usa -legacy quando crei un .p12 con OpenSSL 3:

base64 -i dist.p12 | tr -d '\n' > dist.p12.b64
openssl pkcs12 -export -legacy -inkey key.pem -in cert.pem -out dist.p12

Trappola: keystore Android perso

Sintomo: non puoi firmare un aggiornamento perché nessuno ha il keystore.

Soluzione: con Play App Signing, il keystore è la chiave di upload e il supporto di Play Console può registrarne una nuova. Conserva il keystore nei segreti della tua CI e in un backup offline. Non lasciarlo mai solo su un laptop. Il Generatore di keystore per Android ne crea uno nuovo se parti da zero.

Fase 4: design della pipeline

Trappola: build native a ogni pull request

Sintomo: check PR lenti e una bolletta elevata per i minuti macOS.

Causa: Le build iOS su runner macOS hosted sono i minuti più costosi nella maggior parte dei piani CI, e la maggior parte dei commit tocca solo JavaScript.

Soluzione: esegui lint, test e la build web su ogni PR. Esegui le build native sui tag di release o sui merge in main. Per le anteprime PR, pubblica il bundle web su un canale Capgo invece di compilare un binario, come descritto in Confronto tra piattaforme CI/CD per app Capacitor.

Trappola: build di iOS e Android in sequenza

Soluzione: usa una matrix così entrambe le piattaforme buildano in parallelo, e imposta fail-fast: false così un problema di signing su iOS non annulla una build Android valida.

strategy:
  fail-fast: false
  matrix:
    platform: [ios, android]

Trappola: nessuna cache, o quella sbagliata

Sintomo: ogni build scarica da zero le dipendenze Gradle e CocoaPods, oppure una build di staging porta config di produzione da una cache obsoleta.

Soluzione: cache ~/.gradle/caches, ~/.gradle/wrapper, e ios/App/Pods basata sui lockfile. Dividi le cache per ambiente. Con Capgo Build, la cache di build per app può essere divisa con --cache-key prod e --cache-key staging, o saltato con --no-cache per una build pulita.

Errore comune: percorsi monorepo

Sintomo: could not find capacitor.config o plugin mancanti nel progetto nativo.

Soluzione: esegui i comandi Capacitor dal pacchetto dell'app e punta gli strumenti ai file hoisted node_modules. Il Capgo CLI accetta --path e --node-modules per questo.

Fase 5: Pubblicazione sullo Store

Trappola: numeri di build riutilizzati

Sintomo: “The bundle version must be higher than the previously uploaded version” su iOS, oppure “Version code has already been used” su Google Play.

Soluzione: genera il numero in CI. Con fastlane, leggi l'ultimo build di TestFlight e aggiungi uno. Con Capgo Build, questo è il comportamento predefinito: recupera l'ultimo numero di build da App Store Connect o il più alto versionCode da Google Play e lo incrementa.

Problema: build bloccate in TestFlight

Sintomo: l'upload ha successo ma i tester non vedono mai la build.

Causa: risposta mancante sulla conformità all'export.

Soluzione: dichiaralo una volta in ios/App/App/Info.plist se usi solo crittografia standard:

<key>ITSAppUsesNonExemptEncryption</key>
<false/>

Problema: rifiuti del privacy manifest

Sintomo: email da Apple relativa a dichiarazioni API obbligatorie mancanti (ITMS-91053).

Soluzione: aggiungi un PrivacyInfo.xcprivacy al target dell'app e aggiorna i plugin che ne includono uno proprio. Consulta il guida al privacy manifest per app iOS.

Trappola: tipo di artefatto errato

Soluzione: Google Play richiede un AAB (bundleRelease), non un APK. bundleRelease comunque riesce senza una release di firma e genera un AAB non firmato che Play rifiuta, quindi configura signingConfigs.release nella android/app/build.gradle prima. iOS richiede un export per l'App Store, non un IPA di sviluppo o ad hoc. Verifica il metodo di export nel tuo step di build.

Fase 6: Live updates

Trappola: pubblicare un live update che richiede una nuova build nativa

Sintomo: dopo un aggiornamento over-the-air, l'app crasha chiamando un metodo di un plugin che non esiste nel binario installato.

Causa: il web bundle dipende da una versione di un plugin più recente di quella compilata nell'app sui dispositivi degli utenti.

Soluzione: lascia che sia la pipeline a decidere. build needed esce con 0 quando le dipendenze native corrispondono a quelle live sul canale e con 1 quando serve un nuovo binario:

if bunx @capgo/cli@latest build needed com.example.app --channel production; then
  bunx @capgo/cli@latest bundle upload com.example.app --channel production
else
  bunx cap sync
  bunx @capgo/cli@latest build request com.example.app --platform ios
  bunx @capgo/cli@latest build request com.example.app --platform android
fi

Forza anche il percorso nativo quando i file sotto ios/, android/, o capacitor.config.* cambiano. Il pattern completo è in Scegli automaticamente live update o Native Build, e le regole di compatibilità sono in compatibilità nativa.

La soluzione che elimina il maggior numero di problemi

Se devi cambiare una sola cosa, sposta la compilazione e la firma iOS fuori dai tuoi CI runner. Configurazione Keychain, aggiornamenti Xcode, costi macOS e installazione profili spariscono dalla tua pipeline quando un job Linux passa il progetto preparato a Build Capgo:

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

Le criticità delle fasi 1, 5 e 6 restano valide, perché riguardano il progetto e non il runner. Per saperne di più sul debugging dei job falliti, vedi Risolvere gli errori di build nelle pipeline CI/CD di Capacitor.

Riferimento rapido

Errore Trappola Sezione
UI vecchia in una nuova build cap sync prima della web build Fase 1
No profiles for ... were found Profilo non installato o non corrispondente Fase 3
errSecInternalComponent Keychain bloccata Fase 3
MAC verification failed Password errata o OpenSSL 3 .p12 Fase 3
Unsupported class file major version JDK errato Fase 2
SDK troppo vecchio sull'upload Xcode precedente alla versione 26 Fase 2
La versione del bundle deve essere superiore Numero di build riutilizzato Fase 5
Versione code già utilizzata Riutilizzato versionCode Fase 5
Crash dopo live update Modifica nativa distribuita over the air Fase 6
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.