Bitbucket Pipelines ne peut pas compiler une app iOS sur ses runners hébergés, car chaque étape s’exécute dans un conteneur Linux et Xcode ne tourne que sur macOS. Deux options viables s’offrent à vous : enregistrer votre propre Mac comme runner Bitbucket auto-hébergé, ou garder l’étape sur Linux et confier la compilation iOS, la signature et l’upload TestFlight à un service de build cloud. Ce guide présente les deux, avec des fichiers complets bitbucket-pipelines.yml pour une app Capacitor.
Si votre équipe vit déjà dans Bitbucket pour la revue de code, les liens Jira et les déploiements, pas besoin d’un second outil de CI juste pour iOS. Le pipeline reste dans Bitbucket. Seule la partie qui a besoin d’un Mac est déplacée.
Pourquoi les builds iOS échouent sur Bitbucket Pipelines
Bitbucket Pipelines exécute chaque étape dans une image Docker de votre choix (node:22, cimg/android, etc.) sur l’infrastructure Linux d’Atlassian. C’est parfait pour bun install, les tests unitaires, le linting et même des builds Android complets. Ça s’arrête à iOS :
xcodebuild,codesign, et les SDK iOS n’existent que sur macOS.- La licence d’Apple n’autorise macOS que sur du matériel Apple, il n’existe donc aucune image Linux capable de produire un
.ipa. - Depuis le 28 avril 2026, App Store Connect rejette les uploads qui n’ont pas été compilés avec Xcode 26 et le SDK iOS 26 : le Mac utilisé doit donc rester à jour. Voir Xcode 26 désormais exigé par Apple pour les apps Capacitor.
Bitbucket prend bien en charge les runners auto-hébergés sur Linux, Windows et macOS. Atlassian ne facture pas de minutes de build pour les étapes exécutées sur vos propres runners, mais vous en payez le coût en matériel et en maintenance.
Vos trois options
| Option | Où s'exécute le build iOS | Éléments à maintenir | Bonne option |
|---|---|---|---|
| Runner macOS auto-hébergé | Votre Mac mini ou un Mac loué | macOS, Xcode, keychain, certificats, espace disque, uptime | Équipes qui possèdent déjà des Mac et quelqu'un pour les gérer |
| Second CI dédié au mobile | Les Mac d'un autre fournisseur | Deux pipelines, deux ensembles de secrets et de déclencheurs | Les équipes qui veulent une CI mobile dédiée de toute façon |
| Build cloud appelé depuis une étape Linux | La flotte de Mac de Capgo Build | Un appel CLI et quelques variables sécurisées | Les équipes Capacitor qui veulent que Bitbucket reste leur seul CI |
Le reste de ce guide détaille la première et la troisième option. Pour une vue d'ensemble des plateformes, lisez Comparaison des plateformes CI/CD pour les apps iOS.
Option 1 : compiler iOS depuis une étape Linux avec Capgo Build
Build Capgo compile et signe les apps Capacitor sur des Mac gérés par Capgo. Votre étape Bitbucket fait ce qu'elle fait déjà bien sur Linux : installer les dépendances, compiler la couche web et exécuter cap sync ios. Ensuite, le Capgo CLI effectue l'upload du projet natif préparé (pas votre code source web ou node_modules), lance le build Xcode, le signe avec votre certificat et peut envoyer le résultat sur TestFlight. Les logs remontent dans l'étape Bitbucket, et la commande se termine avec un code non nul si le build échoue, donc un build iOS en échec fait échouer le pipeline.
Comme votre runner pousse le projet vers Capgo, Capgo n'a jamais besoin d'accéder à votre dépôt Bitbucket. Aucune connexion de dépôt à autoriser ni aucune application OAuth à approuver.
Prerequisites
- Une application Capacitor qui compile localement (Capacitor 8 nécessite Node.js 22 ou plus récent)
- Un abonnement Apple Developer Program avec des droits d’administrateur sur l’équipe
- Un compte Capgo et une clé API avec la permission d’upload
- L’application enregistrée dans Capgo :
bunx @capgo/cli@latest app add
Configuration unique sur votre machine
Lancez l’onboarding une seule fois. Sur iOS, il crée ou réutilise votre certificat de distribution et votre profil de provisionnement, et demande une clé API App Store Connect :
bunx @capgo/cli@latest login
bunx @capgo/cli@latest build init --platform ios
Si vous n’avez pas de Mac sous la main, vous pouvez tout de même créer la demande de signature de certificat dans le navigateur avec le Générateur de certificats iOS, ou suivez Compilez une app iOS depuis Windows avec Capgo Build.
Lancez un build depuis votre laptop avant de toucher à la CI. La CI est un mauvais endroit pour déboguer un premier build :
bun run build
bunx cap sync ios
bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release
Exporter les identifiants pour Bitbucket
Le CLI conserve les identifiants sur votre machine. Pour les transférer dans Bitbucket, exportez-les dans un fichier dotenv :
bunx @capgo/cli@latest build credentials manage --appId com.example.app --platform ios
# choose "Export to .env" in the menu
Cela écrit .env.capgo.com.example.app.ios avec le mode 0600. Il contient ces clés :
| Variable | Contenu |
|---|---|
BUILD_CERTIFICATE_BASE64 |
Base64 de votre .p12 certificat de distribution |
P12_PASSWORD |
Mot de passe du certificat (vide si aucun) |
CAPGO_IOS_PROVISIONING_MAP_BASE64 |
Mapping des profils de provisionnement généré par CLI |
APPLE_KEY_ID |
ID de clé API d'App Store Connect |
APPLE_ISSUER_ID |
ID de l'émetteur d'App Store Connect |
APPLE_KEY_CONTENT |
Base64 de la .p8 clé API |
APP_STORE_CONNECT_TEAM_ID |
Votre ID d'équipe Apple |
Ajoutez chaque ligne en tant que sécurisée variable dans Bitbucket sous Paramètres du dépôt > Pipelines > Variables du dépôt, ainsi que CAPGO_TOKEN avec votre clé Capgo API. Les variables sécurisées sont masquées dans les logs et ne peuvent pas être relues depuis l'interface.
Si vous préférez l’API, cette boucle les crée en une seule fois. Elle utilise un token API Bitbucket avec le scope d'écriture des variables de 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"
Supprimez ensuite le fichier et ajoutez .env.capgo.* vers .gitignore. Il contient des données de signature en clair.
Pour les identifiants de production, privilégiez les variables de déploiement plutôt que les variables de dépôt. Elles sont limitées à un environnement de déploiement (par exemple Production), et vous pouvez restreindre cet environnement aux admins ou à des branches spécifiques sur les plans Premium.
Le fichier de pipeline
Bitbucket injecte les variables de dépôt et de déploiement en tant que variables d'environnement, donc le Capgo CLI les récupère sans aucun mapping. Ce fichier exécute des tests à chaque push, génère un build iOS ad hoc à la demande et déploie sur TestFlight lors des tags de version :
# 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
Quelques détails importants :
- L'image est un Linux de base
node:22. Rien dans ce fichier ne nécessite macOS. - Sur Linux,
cap sync ioscopie les assets web et met à jour la config native mais ne peut pas exécuter CocoaPods. Ce n'est pas un problème. Le Mac qui lance le build installe les pods ou résout les packages Swift lui-même. - Capgo récupère le dernier numéro de build depuis App Store Connect et l’incrémente, pour ne jamais tomber sur l’erreur « bundle version must be higher than the previously uploaded version ». Passez
--skip-build-number-bumpsi vous le gérez vous-même. - Le build ad hoc nécessite un profil de provisionnement Ad Hoc, qui ne s’installe que sur les appareils qu’il liste. Enregistrez-le comme
ADHOC_PROFILE_BASE64variable sécurisée ; le--ios-provisioning-profileflag remplace le profil App Store de l’environnement pour ce build uniquement. Récupérez les UDID des testeurs avec le Recherche d'UDID iOS. - Figez CLI sur une version exacte (par exemple
@capgo/cli@7.104.0) une fois votre pipeline stabilisé, afin qu’une release CLI ne modifie pas le comportement en cours de sprint.
Lancez le pipeline personnalisé depuis Pipelines > Run pipeline > ios-adhoc. La dernière commande affiche un lien de téléchargement qui expire après la période de rétention.
Approbation manuelle avant TestFlight
Bitbucket prend en charge trigger: manual sur une étape. Divisez la release en une étape de build et une étape d'upload avec validation si vous voulez qu'un humain valide chaque upload 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
Pour passer directement à l'App Review après l'upload TestFlight, ajoutez --submit-to-store-review par --store-release-name "$BITBUCKET_TAG". Votre fiche App Store, vos captures d'écran et vos réponses de conformité doivent d'abord être complétées dans App Store Connect.
Ajoutez Android en parallèle
Les builds Android peuvent s'exécuter directement sur les runners Linux de Bitbucket, mais vous pouvez aussi les envoyer vers Capgo Build pour que les deux plateformes utilisent le même flux de signature et d'upload. Utilisez un parallel bloc :
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
L'étape Android nécessite ANDROID_KEYSTORE_FILE, KEYSTORE_KEY_ALIAS, KEYSTORE_KEY_PASSWORD, KEYSTORE_STORE_PASSWORD, et PLAY_CONFIG_JSON en tant que variables sécurisées. Exportez-les de la même manière avec --platform android.
Déployez des changements web sans build natif
La plupart des commits d'une application Capacitor ne touchent qu'au HTML, CSS et JavaScript. Cela ne nécessite pas de nouveau binaire. Avec les live updates Capgo, une étape Bitbucket peut choisir entre un build pour les stores et un bundle OTA :
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 se termine avec le code code 0 lorsqu'aucun build natif n'est requis et 1 lorsqu'il l'est. Il compare les versions des plugins natifs à ce qui est live sur le canal. Il ne détecte pas les modifications manuelles sous ios/, donc forcez aussi le chemin natif quand ces fichiers changent. Le pattern complet se trouve dans Choisir automatiquement live update ou un build natif.
Option 2 : un runner macOS auto-hébergé
Si vous possédez déjà des Mac, vous pouvez en enregistrer un comme runner Bitbucket et lancer Xcode vous-même.
Enregistrer le runner
- Dans Bitbucket, ouvrez Paramètres du dépôt > Runners (ou Workspace settings > Workspace runners pour le partager) et cliquez sur Ajouter un runner.
- Choisissez MacOS en tant que système. Bitbucket affiche une commande de démarrage avec un UUID de runner et des identifiants OAuth.
- Sur le Mac, installez un JDK pris en charge (le runner est un programme Java), Xcode 26 et fastlane. Collez ensuite la commande de démarrage.
- Gardez le runner actif en tant qu’agent de lancement pour qu’il survive aux redémarrages.
Une étape cible le runner à l’aide de 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
Les runners macOS s'exécutent directement sur l'hôte avec l'environnement de votre utilisateur, et non dans un conteneur. Installez Bun et Ruby sur le Mac lui-même.
Un Fastfile minimal
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
Les nouveaux projets Capacitor 8 utilisent Swift Package Manager, vous compilez donc App.xcodeproj. Les anciens projets qui utilisent encore CocoaPods compilent App.xcworkspace et nécessitent pod install sur le runner. Notre guide iOS GitHub Actions explique chaque action fastlane en détail ; la lane est identique sur Bitbucket.
Ce que vous acceptez
L'archive est la partie facile. Un Mac auto-hébergé, c'est aussi :
- Mises à niveau Xcode dès qu'Apple relève la version minimale de SDK, testées avant l'échéance.
- État du Keychain. Un job en échec peut laisser un Keychain temporaire derrière lui, et le job suivant récupère la mauvaise identité.
- Pas d'isolation. Les jobs s'exécutent sur le même disque. DerivedData, les anciens archives et les simulateurs le remplissent.
- Un build à la fois par runner, sauf si vous achetez plus de Macs.
- Certificats et profils qui expirent chaque année et doivent être renouvelés sur la machine.
C'est un compromis raisonnable si votre équipe gère déjà des Macs. Si personne ne veut s'occuper de cette machine, l'option 1 la supprime.
Troubleshooting
| Symptôme | Cause probable et solution |
|---|---|
CAPGO_TOKEN is not set |
La variable est manquante, ou c'est une variable de déploiement et l'étape n'a pas de deployment: clé. |
L'étape fonctionne sur main mais pas sur les tags |
Les variables de déploiement ne s'appliquent qu'aux étapes qui déclarent cet environnement de déploiement. |
Provisioning profile doesn't match bundle ID |
Le profil a été créé pour un autre ID d'application. Relancez build init et réexportez. |
| Échec de l'authentification App Store Connect | ID d'émetteur ou ID de clé incorrect, ou clé révoquée. Vérifiez la clé dans App Store Connect > Users and Access > Integrations. |
| Upload rejeté pour la version SDK | Le build a utilisé une version d'Xcode plus ancienne que 26. Mettez à jour le Mac auto-hébergé. |
| L'étape auto-hébergée est bloquée sur « Pending » | Runner hors ligne ou étiquettes non correspondantes self.hosted et macos. |
errSecInternalComponent pendant le codesign |
Le trousseau de clés est verrouillé dans une session non interactive. Utilisez setup_ci et importez le certificat dans le trousseau de clés temporaire. |
| Le build passe, mais rien n’apparaît dans TestFlight | Le traitement prend quelques minutes et la conformité à l’export peut être en attente. Ajouter ITSAppUsesNonExemptEncryption vers Info.plist si votre application n’utilise que des chiffrement standard. |
Plus de correctifs pour les pipelines en échec sont dans Corriger les échecs de build dans les pipelines CI/CD Capacitor et le Guide de dépannage des builds Capgo.
Quelle option choisir ?
Choisissez un Mac auto-hébergé si vous possédez déjà le matériel, si vous avez besoin d'un contrôle total de l'environnement Xcode, ou si vous compilez des cibles Swift natives qui ne sont pas des applications Capacitor. Choisissez un build cloud depuis une étape Linux si votre application est une application Capacitor et que vous voulez que Bitbucket reste le seul endroit où votre équipe configure la CI. Dans les deux cas, gardez la logique de build dans bitbucket-pipelines.yml à côté de votre code, placez les éléments de signature dans des variables sécurisées et ne lancez les builds natifs que lorsque le code natif a réellement changé.