La plupart des échecs CI/CD Capacitor viennent de la même courte liste : signature code iOS, outils macOS manquants ou obsolètes, un build web jamais intégré au projet natif, des numéros de build réutilisés et des exigences des stores qui n'échouent qu'au moment de l'upload. Chacun a une cause connue et un correctif à appliquer une fois pour toutes. Ce guide regroupe les pièges par étape de pipeline, avec l'erreur rencontrée, sa cause et ce qu'il faut changer.
Si vous mettez en place un pipeline de zéro, lisez Configurer la CI/CD pour les apps Capacitor d'abord, puis utilisez cette liste pour la durcir.
Étape 1 : build de la couche web
Piège : l'app native embarque un ancien build web
Symptôme : La CI réussit, l'app s'installe, mais elle affiche l'UI d'hier.
Cause : cap sync copie tout ce qui est dans webDir à ce moment-là. Si le pipeline s'exécute cap sync avant le build web, ou si le build web écrit dans un autre dossier que webDir dans capacitor.config.ts, le projet natif contient des fichiers obsolètes.
Solution : exécutez toujours les étapes dans cet ordre et échouez si le dossier de sortie est vide.
bun install --frozen-lockfile
bun run build
test -f dist/index.html || { echo "web build missing"; exit 1; }
bunx cap sync
Assurez-vous que webDir correspond à la sortie de votre bundler (dist pour Vite, www pour Angular avec Ionic, build pour certaines configurations React).
Piège : l'URL du serveur de dev est restée dans la config
Symptôme : le build de release affiche un écran blanc ou tente de charger http://192.168.x.x:5173.
Cause : server.url dans capacitor.config.ts était configuré pour le live reload et a été commité.
Solution : ne jamais commiter server.url. Lisez-le depuis une variable d’environnement que la CI ne définit jamais :
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
Piège : variables d’environnement intégrées au mauvais moment
Symptôme : l’application de production communique avec le staging API.
Cause : Vite, webpack et Angular inlinent les variables d’environnement au moment du build. La valeur présente au bun run build ran est celui présent dans le binaire, et dans tout live update généré depuis le même job.
Solution : définissez les variables spécifiques à l’environnement avant le build web, par job, et générez des bundles distincts pour staging et production. Ne tentez pas de les échanger a posteriori cap sync.
Piège : dérive du lockfile
Symptôme : la version d’un plugin en CI diffère de celle sur votre machine, et la compilation native échoue en raison de symboles manquants.
Solution : committez le lockfile et installez avec bun install --frozen-lockfile (ou npm ci). Épinglez @capacitor/core, @capacitor/ios, @capacitor/android, et @capacitor/cli à la même version. Les versions non alignées sont une source fréquente d’erreurs natives ; voir Corriger les erreurs de décalage de version Capacitor.
Étape 2 : la chaîne d’outils native
Piège : mauvaise version de Node, JDK ou Xcode
Symptôme : Unsupported class file major version, The engine "node" is incompatible, or Xcode errors about SDK features.
Cause : les images des runners hébergés changent, et Capacitor 8 impose des minimums stricts.
| Outil | Exigence Capacitor 8 |
|---|---|
| Node.js | 22 ou plus récent |
| JDK | 21 |
| Xcode | 26 ou plus récent |
| Cible de déploiement iOS | 15.0 |
Android minSdkVersion / targetSdkVersion |
24 / 36 |
Solution : épinglez explicitement chaque version dans le pipeline plutôt que de vous fier à 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'
Piège : compiler avec un Xcode qu'Apple n'accepte plus
Symptôme : l'upload échoue avec un message indiquant que l'app a été compilée avec un SDK non pris en charge.
Cause : depuis le 28 avril 2026, App Store Connect exige Xcode 26 et le SDK iOS 26. Les Mac auto-hébergés et les anciennes images de runner utilisent encore Xcode 16 par défaut.
Solution : sélectionnez une macos-26 image ou installez Xcode 26 sur vos runners. Détails dans Xcode 26 désormais exigé par Apple pour les apps Capacitor.
Piège : confusion entre CocoaPods et SPM
Symptôme : xcodebuild: error: 'App.xcworkspace' does not exist, ou des pods introuvables.
Cause : Les nouveaux projets Capacitor 8 utilisent Swift Package Manager et compilent ios/App/App.xcodeproj. Les anciens projets utilisent CocoaPods et compilent ios/App/App.xcworkspace après pod install. Les pipelines copiés d'anciens tutoriels supposent le workspace.
Solution : vérifiez celui que votre projet utilise et compilez le bon fichier. Si vous migrez, consultez Comment migrer votre application Capacitor vers SPM.
Piège : les builds de plugins Android cassent après une mise à jour de Gradle
Symptôme : Namespace not specified, package attribute is deprecated, ou des erreurs provenant d’un plugin build.gradle après la mise à jour d’Android Studio.
Solution : verrouiller la version d’Android Gradle Plugin dans android/build.gradle, upgrade sur une branche, et mettez à jour les plugins en premier. Les erreurs spécifiques sont abordées dans Corriger les erreurs de build du plugin Capacitor avec AGP 9.
Étape 3 : signature Code
Piège : la signature iOS ne fonctionne que sur votre Mac
Symptôme : No signing certificate "iOS Distribution" found, No profiles for 'com.example.app' were found, ou errSecInternalComponent.
Cause : le keychain de votre Mac contient le certificat et Xcode télécharge les profils pour vous. Un runner CI n’a ni l’un ni l’autre, et son keychain est verrouillé dans une session non interactive.
Solution : importez le certificat dans un keychain temporaire déverrouillé et installez le profil à l’endroit où Xcode 16 et les versions ultérieures le recherchent :
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/"
Utilisez le nom d’identité Apple Distribution, et non l’ancienne iOS Distribution. fastlane’s setup_ci , ce qui match automatise cela. Capgo Build prend le certificat et le profil en variables d’environnement et gère le keychain sur ses propres machines, de sorte que le job Linux ne touche jamais security.
Piège : certificats expirés ou révoqués
Symptôme : un pipeline qui fonctionnait depuis un an échoue du jour au lendemain.
Cause : Les certificats de distribution Apple et les profils de provisionnement expirent au bout d'un an. Un coéquipier qui crée un nouveau certificat dans Xcode peut aussi invalider le profil utilisé par votre CI.
Solution : ajoutez les dates d'expiration au calendrier de votre équipe, gardez un certificat de distribution partagé pour la CI et vérifiez avant de compiler. bunx @capgo/cli@latest build prescan --platform ios vérifie l'expiration du certificat, le mot de passe et l'appairage du profil avant tout upload.
Piège : connexions Apple ID et demandes de double authentification
Symptôme : fastlane reste bloqué en attendant un code à 6 chiffres.
Solution : utilisez une clé API App Store Connect (.p8, l'ID de clé, l'ID de l'émetteur) au lieu d'un Apple ID et d'un mot de passe. Cela ne nécessite pas de double authentification et peut être restreint au rôl’App Manager. Si l'authentification échoue malgré une clé correcte, vérifiez l'horloge du runner : le token est signé avec l'heure locale, et Apple rejette les tokens dont le timestamp est décalé.
Piège : secrets base64 impossibles à décoder
Symptôme : MAC verification failed, invalid keystore format, ou base64: invalid input.
Cause : des sauts de ligne ajoutés lors de la copie de la valeur base64, ou un .p12 créé avec les valeurs par défaut d’OpenSSL 3, illisibles par macOS.
Solution : encodé sur une seule ligne, et utilisez -legacy lors de la création d’un .p12 avec 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
Piège : keystore Android perdu
Symptôme : vous ne pouvez pas signer une mise à jour car personne ne possède le keystore.
Solution : avec Play App Signing, le keystore est la clé d’upload, et le support de Play Console peut en enregistrer une nouvelle. Stockez le keystore dans vos secrets CI et dans une sauvegarde hors ligne. Il ne doit jamais exister uniquement sur un seul ordinateur portable. Le générateur de keystore Android en ligne en crée un nouveau si vous partez de zéro.
Étape 4 : conception du pipeline
Piège : builds natifs à chaque pull request
Symptôme : des checks de PR lents et une facture de minutes macOS élevée.
Cause : Les builds iOS sur des runners macOS hébergés représentent les minutes les plus coûteuses de la plupart des plans CI, alors que la majorité des commits ne touchent que du JavaScript.
Solution : lancer le lint, les tests et le build web à chaque PR. Lancer les builds natifs sur les tags de release ou les merges vers main. Pour les previews de PR, publiez le bundle web sur un canal Capgo plutôt que de compiler un binaire, comme décrit dans Comparaison des plateformes CI/CD pour les apps Capacitor.
Piège : compiler iOS et Android séquentiellement
Solution : utilisez une matrice pour compiler les deux plateformes en parallèle, et configurez fail-fast: false pour qu'un problème de signature iOS n'annule pas un bon build Android.
strategy:
fail-fast: false
matrix:
platform: [ios, android]
Piège : pas de cache, ou un mauvais cache
Symptôme : chaque build télécharge les dépendances Gradle et CocoaPods de zéro, ou un build de staging embarque une config de prod issue d'un cache obsolète.
Solution : cache ~/.gradle/caches, ~/.gradle/wrapper, et ios/App/Pods indexé sur les lockfiles. Partitionnez les caches par environnement. Avec Capgo Build, le cache de build par application peut être divisé avec --cache-key prod et --cache-key staging, ou ignoré avec --no-cache pour un build propre.
Piège : chemins dans un monorepo
Symptôme : could not find capacitor.config ou des plugins manquants dans le projet natif.
Solution : run Capacitor commands from the app package, and point tools at hoisted node_modules. Le Capgo CLI accepte --path et --node-modules pour cela.
Étape 5 : publication sur les stores
Piège : numéros de build réutilisés
Symptôme : « The bundle version must be higher than the previously uploaded version » sur iOS, ou « Version code has already been used » sur Google Play.
Solution : générer le numéro en CI. Avec fastlane, lire le dernier build TestFlight et ajouter un. Avec Capgo Build, c’est le comportement par défaut : il récupère le dernier numéro de build depuis App Store Connect ou le plus élevé versionCode depuis Google Play et l’incrémente.
Piège : builds bloqués dans TestFlight
Symptôme : l’upload réussit, mais les testeurs ne voient jamais le build.
Cause : réponse de conformité à l'exportation manquante.
Solution : déclarez-l’une seule fois dans ios/App/App/Info.plist si vous n'utilisez qu'un chiffrement standard :
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
Piège : rejets liés au privacy manifest
Symptôme : e-mail d'Apple concernant l'absence de déclarations API à raison obligatoire (ITMS-91053).
Solution : ajoutez un PrivacyInfo.xcprivacy à la cible de l'app et mettez à jour les plugins qui incluent le leur. Voir le guide sur les privacy manifests pour les apps iOS.
Piège : mauvais type d'artefact
Solution : Google Play nécessite un AAB (bundleRelease), pas un APK. bundleRelease réussit toujours sans release config de signature et génère un AAB non signé que Play rejette. Configurez donc signingConfigs.release dans android/app/build.gradle en premier. iOS nécessite un export App Store, pas un IPA de développement ou ad hoc. Vérifiez la méthode d’export dans votre étape de build.
Étape 6 : live updates
Piège : déployer un live update nécessitant un nouveau build natif
Symptôme : après une mise à jour OTA, l’app plante en appelant une méthode de plugin qui n’existe pas dans le binaire installé.
Cause : le bundle web dépend d'une version de plugin plus récente que celle compilée dans l'app sur les appareils des utilisateurs.
Solution : laissez le pipeline décider. build needed renvoie 0 lorsque les dépendances natives correspondent à ce qui est en ligne sur le canal, et 1 lorsqu'un nouveau binaire est requis :
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
Forcez également le chemin natif lorsque les fichiers sous ios/, android/, ou capacitor.config.* changent. Le pattern complet se trouve dans Choisir automatiquement live update ou un build natif, et les règles de compatibilité sont dans compatibilité native.
La solution qui élimine la plupart des pièges
Si vous ne changez qu’une seule chose, sortez la compilation et la signature iOS de vos runners CI. La configuration du Keychain, les mises à niveau Xcode, les coûts macOS et l’installation des profils disparaissent de votre pipeline lorsqu’un job Linux transmet le projet préparé à 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
Les pièges des étapes 1, 5 et 6 s’appliquent toujours, car ils concernent votre projet, pas le runner. Pour en savoir plus sur le débogage des jobs en échec, voir Corriger les échecs de build dans les pipelines CI/CD de Capacitor.
Récapitulatif rapide
| Erreur | Piège | Section |
|---|---|---|
| Ancienne interface dans un nouveau build | cap sync avant le build web |
Étape 1 |
No profiles for ... were found |
Profil non installé ou non conforme | Étape 3 |
errSecInternalComponent |
Keychain verrouillé | Étape 3 |
MAC verification failed |
Mot de passe incorrect ou OpenSSL 3 .p12 |
Étape 3 |
Unsupported class file major version |
JDK incorrect | Étape 2 |
| SDK trop ancien lors de l'upload | Xcode antérieur à la version 26 | Étape 2 |
| La version du bundle doit être supérieure | Numéro de build déjà utilisé | Étape 5 |
| La version code est déjà utilisée | Réutilisé versionCode |
Étape 5 |
| Crash après live update | Changement natif déployé en OTA | Étape 6 |