ITSkillsCenter
Développement Web

CI/CD pour applications mobile — GitHub Actions et Codemagic

17 دقائق للقراءة

Ce tutoriel met en place un pipeline d’intégration et de livraison continues pour une application mobile, depuis le commit jusqu’à un binaire signé prêt à être uploadé sur Google Play ou TestFlight. Vous comparerez d’abord les trois principaux services en 2026 — GitHub Actions, Codemagic, EAS Build — pour choisir celui qui correspond à votre projet, puis vous configurerez pas-à-pas un workflow GitHub Actions qui build une app Flutter pour Android, suivi d’un workflow Codemagic visuel pour iOS. À la fin, chaque push sur la branche principale déclenche un build cloud, signe le binaire avec vos clés stockées en secret, et publie l’artefact sur Firebase App Distribution ou TestFlight.

📍 Guide principal de la série : Développement mobile 2026 : Flutter 3, React Native, Expo, stores

Pour le panorama complet, lire d’abord ce guide.

Prérequis

Ce tutoriel suppose que vous avez déjà un projet mobile fonctionnel (Flutter ou React Native), capable de produire un build release localement. Si ce n’est pas votre cas, commencez par les tutoriels associés sur Flutter ou React Native + Expo.

  • Un projet Flutter ou React Native fonctionnel, avec un build release qui fonctionne localement
  • Un dépôt GitHub (gratuit) ou Bitbucket pour héberger le code
  • Un keystore Android valide (upload-keystore.jks) et son mot de passe
  • Pour iOS : un compte Apple Developer payant (99 $/an) avec un App ID enregistré et un certificat de distribution
  • Comptes : Codemagic gratuit, Firebase gratuit, EAS gratuit selon le service choisi
  • Notions de base sur YAML et la ligne de commande

Niveau attendu : développeur intermédiaire qui a déjà fait au moins une release locale signée. Temps total : deux à trois heures pour configurer GitHub Actions Android, plus une heure pour Codemagic iOS.

Étape 1 — Choisir le bon service de CI/CD mobile

Avant d’écrire un fichier YAML, il faut choisir où le faire tourner. Les trois services dominants en 2026 répondent à des besoins légèrement différents, et un mauvais choix initial coûte plusieurs heures de migration plus tard.

GitHub Actions est le plus polyvalent. Il intègre nativement votre workflow Git, gère les builds Android et iOS sur la même plateforme, et offre 3 000 minutes Linux par mois en free tier — soit largement de quoi couvrir un petit projet. Le piège est le multiplicateur des runners macOS : chaque minute de macOS facture 10 minutes du quota, ce qui ramène votre budget iOS à 300 minutes effectives par mois. Pour un projet iOS actif, il faut prévoir un plan payant ou une facturation à la consommation.

Codemagic est spécialisé mobile, avec une UX visuelle qui guide les nouveaux venus. Le free tier offre 500 minutes par mois sur des Mac M2 sur compte personnel — généreux pour iOS. Codemagic excelle sur Flutter (l’éditeur est lui-même un contributeur Flutter actif) et propose des templates prêts pour tous les frameworks principaux.

EAS Build est l’option par défaut si votre projet est en React Native + Expo. Il lit directement votre eas.json, gère les credentials, et publie sur les stores via eas submit. Le free tier donne 30 builds mensuels (15 Android + 15 iOS), avec un timeout de 45 minutes. Pour un projet uniquement Expo, c’est l’outil le plus intégré, mais il ne gère pas Flutter.

Le tableau ci-dessous résume les arbitrages. Choisissez en fonction de votre stack et de votre volume de builds.

Critère GitHub Actions Codemagic EAS Build
Free tier Android 3 000 min Linux 500 min M2 15 builds / mois
Free tier iOS 300 min effectives 500 min M2 15 builds / mois
Stack supportée Tout Tout, focus Flutter React Native / Expo
Configuration YAML YAML ou GUI JSON + GUI
Soumission stores intégrée Via fastlane Native Native (eas submit)
Adapté au CI custom ★★★★★ ★★★★ ★★★

Étape 2 — Stocker le keystore Android dans les secrets GitHub

Avant de configurer le workflow, il faut sécuriser les fichiers sensibles. Le keystore Android, son mot de passe et l’alias ne doivent jamais apparaître dans le dépôt Git. La méthode standard consiste à encoder le keystore en base64, le stocker comme secret GitHub, et le décoder à la volée pendant le build.

Sur votre machine locale, encodez le keystore en base64. Sur macOS et Linux, la commande base64 est intégrée. Sur Windows, utilisez PowerShell.

# macOS / Linux
base64 -i upload-keystore.jks -o keystore.b64

# Windows PowerShell
$bytes = [IO.File]::ReadAllBytes("upload-keystore.jks")
[Convert]::ToBase64String($bytes) | Out-File -Encoding ASCII keystore.b64

Le fichier keystore.b64 contient le contenu binaire du keystore sous forme de chaîne ASCII. Ouvrez-le dans un éditeur de texte et copiez l’intégralité du contenu. Ne mettez pas ce fichier sur Git — il a la même valeur que le keystore lui-même.

Sur GitHub, allez dans les paramètres de votre dépôt → Secrets and variables → Actions → New repository secret. Créez quatre secrets dans cet ordre, en collant les valeurs correspondantes :

  • ANDROID_KEYSTORE_BASE64 : le contenu du fichier keystore.b64
  • ANDROID_KEYSTORE_PASSWORD : le mot de passe du keystore
  • ANDROID_KEY_PASSWORD : le mot de passe de la clé (souvent identique au précédent)
  • ANDROID_KEY_ALIAS : upload (ou l’alias choisi à la création)

Ces secrets sont chiffrés au repos par GitHub et exposés au workflow uniquement au moment de l’exécution. Ils n’apparaissent jamais dans les logs (GitHub les masque automatiquement par ***).

Étape 3 — Écrire le workflow GitHub Actions Android

Un workflow GitHub Actions est un fichier YAML placé dans .github/workflows/. Le nom du fichier importe peu mais doit se terminer en .yml. Créez .github/workflows/android-build.yml avec le contenu ci-dessous, qui build une app Flutter en release et upload l’AAB comme artefact.

name: Android Build

on:
  push:
    branches: [ main ]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          channel: 'stable'

      - name: Install dependencies
        run: flutter pub get

      - name: Decode keystore
        run: |
          echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/upload-keystore.jks

      - name: Create key.properties
        run: |
          cat > android/key.properties << EOF
          storeFile=upload-keystore.jks
          storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
          keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
          keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}
          EOF

      - name: Build AAB release
        run: flutter build appbundle --release

      - name: Upload AAB artifact
        uses: actions/upload-artifact@v4
        with:
          name: app-release-aab
          path: build/app/outputs/bundle/release/app-release.aab

Ce workflow se déclenche à chaque push sur la branche main et au déclenchement manuel via workflow_dispatch. Il installe Java 17 (Temurin), installe Flutter en canal stable, télécharge les dépendances Dart, décode le keystore depuis le secret base64, écrit le fichier key.properties avec les credentials, build l’AAB, et upload le résultat en artefact GitHub. L’artefact est téléchargeable depuis l’onglet « Actions » du dépôt après la fin du run.

Commitez le fichier et poussez sur main. Allez dans l’onglet Actions de votre dépôt : un nouveau run apparaît avec le statut « queued » puis « in progress ». La première exécution prend cinq à huit minutes le temps que Gradle télécharge ses dépendances. Les exécutions suivantes utilisent le cache et tournent en deux à trois minutes.

Étape 4 — Optimiser avec le cache Gradle

La première fois qu’un workflow s’exécute, Gradle télécharge environ 800 Mo de dépendances Maven. Sans cache, ce téléchargement se reproduit à chaque run et consomme inutilement votre quota et votre temps. GitHub Actions propose un mécanisme de cache trivial à activer.

Ajoutez l’étape suivante juste après le checkout, avant le setup Flutter.

      - name: Cache Gradle
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

Cette étape vérifie si un cache existe déjà pour le hash actuel des fichiers Gradle. Si oui, elle restaure les dépendances de la précédente exécution. Si non, elle laisse Gradle faire son travail puis sauvegarde le résultat pour le run suivant. La clé inclut un hash des fichiers .gradle pour invalider le cache automatiquement quand une dépendance change.

L’effet est mesurable : un run sans cache prend 6 à 8 minutes, un run avec cache 2 à 3 minutes. Sur 50 builds par mois, c’est 4 heures de runner économisées.

Étape 5 — Workflow iOS sur runner macOS

Pour build une app iOS, le workflow doit tourner sur un runner macOS. GitHub fournit des runners macos-latest qui exécutent macOS Sequoia 15 avec Xcode 26 préinstallé en 2026. Le multiplicateur ×10 sur ces runners impose de les utiliser avec parcimonie.

Créez un nouveau fichier .github/workflows/ios-build.yml. Cette fois on cible un runner macOS et on délègue la signature à fastlane match, qui stocke les certificats dans un dépôt Git privé.

name: iOS Build

on:
  push:
    branches: [ main ]
  workflow_dispatch:

jobs:
  build:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-ruby@v1
        with:
          ruby-version: '3.3'

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          channel: 'stable'

      - name: Install dependencies
        run: |
          flutter pub get
          cd ios && pod install && cd ..

      - name: Setup fastlane
        run: gem install fastlane

      - name: Build IPA
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
        run: |
          cd ios
          fastlane match appstore --readonly
          cd ..
          flutter build ipa --release

      - name: Upload IPA
        uses: actions/upload-artifact@v4
        with:
          name: app-release-ipa
          path: build/ios/ipa/*.ipa

Ce workflow installe Ruby 3.3, Flutter, fastlane, et utilise match pour récupérer les certificats de distribution depuis un dépôt Git privé que vous avez créé au préalable (voir le tutoriel sur le déploiement aux stores pour la procédure de mise en place de match). Le mot de passe de match est stocké dans le secret MATCH_PASSWORD, et l’URL du dépôt dans MATCH_GIT_URL.

La sortie est un IPA stocké dans build/ios/ipa/. Un build iOS standard prend entre 12 et 18 minutes sur macos-latest en 2026, ce qui équivaut à 120 à 180 minutes de quota avec le multiplicateur ×10. Sur le free tier de 3 000 minutes, vous pouvez vous offrir 16 à 25 builds iOS par mois — calcul à garder en tête.

Étape 6 — Pipelines conditionnels par branche

Un seul workflow déclenché à chaque push n’est pas viable à long terme. Vous voulez des comportements différents selon la branche : build complet seulement sur main, tests unitaires sur les pull requests, déploiement automatique seulement sur les tags. GitHub Actions permet ces conditions via la clé on et les expressions if.

on:
  push:
    branches: [ main ]
    tags: [ 'v*' ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - run: flutter test

  build:
    needs: test
    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - run: flutter build appbundle --release

Ce workflow exécute les tests à chaque PR et à chaque push sur main, mais ne déclenche le build release que pour les tags qui commencent par v. Cela fait que git tag v1.0.0 && git push --tags déclenche un build de production, alors qu’un push de feature ne consomme que les minutes de tests.

Étape 7 — Codemagic pour Flutter en interface visuelle

Si configurer du YAML vous fatigue ou si votre équipe préfère une interface, Codemagic est l’alternative la plus aboutie pour les projets Flutter. Connectez votre dépôt Git, choisissez le projet, et l’assistant initial génère un workflow par défaut en quelques clics.

Créez un compte sur codemagic.io, autorisez l’accès à votre organisation GitHub, et sélectionnez le dépôt mobile. Codemagic détecte automatiquement qu’il s’agit d’un projet Flutter (présence du fichier pubspec.yaml) et propose une configuration initiale. Cliquez sur « Start your first build » pour lancer un build de démonstration sans configuration supplémentaire.

Une fois le premier build réussi, allez dans l’onglet « Workflow Editor » pour personnaliser. L’éditeur visuel permet de cocher les variables d’environnement, les certificats à utiliser, les artefacts à exporter, les notifications. Pour la signature Android, uploadez votre upload-keystore.jks dans la section « Code signing identities » : Codemagic le chiffre et l’expose au build via des variables d’environnement nommées.

Le grand avantage par rapport à GitHub Actions est l’intégration native des stores. Cochez « Google Play Submit » dans l’éditeur, fournissez votre service account JSON, et chaque build de production est automatiquement uploadé sur la piste de votre choix (interne, alpha, bêta, production). Idem pour TestFlight côté iOS.

Étape 8 — Distribuer sur Firebase App Distribution

Avant de soumettre à un store, l’étape utile est de distribuer le binaire à des testeurs internes. Firebase App Distribution est un service gratuit de Google qui partage des APK ou IPA par email à des groupes de testeurs, sans passer par Play Console ni TestFlight.

Sur la console Firebase, créez un projet (ou utilisez un projet existant), puis dans la section « App Distribution », uploadez votre app pour la première fois. Récupérez l’App ID Firebase. Côté CI, l’action GitHub officielle est wzieba/Firebase-Distribution-Github-Action. Ajoutez l’étape suivante après le build.

      - name: Distribute to Firebase
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{ secrets.FIREBASE_APP_ID }}
          serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
          groups: testeurs-internes
          file: build/app/outputs/flutter-apk/app-release.apk

Le secret FIREBASE_SERVICE_ACCOUNT contient le JSON de credentials de service Firebase, généré dans les paramètres Firebase. Le secret FIREBASE_APP_ID est l’identifiant de votre app affiché dans la console. Le groupe testeurs-internes doit exister dans Firebase App Distribution avec les emails de vos testeurs ajoutés.

À chaque push sur main, vos testeurs reçoivent un email avec un lien direct. Sur Android, ils cliquent et installent. Sur iOS, ils acceptent l’invitation Firebase puis installent un profil de provisioning ad hoc — l’expérience est similaire à TestFlight, en plus simple à mettre en place pour les bêtas privées.

Étape 9 — Notifications de build sur Slack ou email

Un pipeline qui tourne dans le vide ne sert à rien. Vous voulez savoir immédiatement quand un build échoue, quand un build est prêt à tester, ou quand une soumission a réussi. GitHub Actions et Codemagic offrent tous les deux des intégrations natives avec Slack, Discord et email.

Pour Slack via GitHub Actions, créez un webhook entrant dans votre Slack workspace, stockez l’URL dans un secret SLACK_WEBHOOK, et ajoutez l’action slackapi/slack-github-action. La notification se déclenche conditionnellement avec if: failure() ou if: success().

      - name: Notify Slack on failure
        if: failure()
        uses: slackapi/slack-github-action@v1
        with:
          webhook: ${{ secrets.SLACK_WEBHOOK }}
          payload: |
            {
              "text": "❌ Build Android failed on ${{ github.ref }}"
            }

L’équipe reçoit un message Slack à chaque échec, avec lien vers le run incriminé. Pour les succès, ajoutez une étape similaire avec if: success() et un message différent. Codemagic propose le même mécanisme via son éditeur visuel : cochez « Slack notifications » et collez l’URL du webhook.

Étape 10 — Rotation des secrets et bonnes pratiques de sécurité

Un pipeline mobile manipule des fichiers de signature qui valent autant que le code source de votre app. Une fuite accidentelle peut autoriser un attaquant à publier des updates malveillantes sous votre identité. Trois pratiques minimales s’imposent.

Premièrement, rotation périodique des credentials. Le mot de passe du keystore et le mot de passe de match doivent changer tous les six mois minimum. La procédure est simple : modifier les secrets dans GitHub / Codemagic, mettre à jour les fichiers correspondants chez vous, retester un build complet. La rotation oblige aussi à vérifier qui a accès aux secrets — un développeur parti peut encore lire les valeurs s’il avait des droits administrateur.

Deuxièmement, séparation des environnements. Ne mélangez pas vos secrets de développement et de production dans le même dépôt. Sur GitHub, utilisez les environments pour scoper les secrets : un environnement production avec les credentials de release, un environnement staging avec des credentials de bêta. Le workflow déclare l’environnement requis et n’a accès qu’aux secrets correspondants.

Troisièmement, audit des logs. GitHub masque automatiquement les valeurs de secrets dans les logs, mais cette protection se contourne facilement par accident — un echo $SECRET | base64 par exemple affiche la version base64 du secret en clair. Avant de fusionner un workflow, relisez chaque étape pour vous assurer qu’aucune commande n’affiche un secret dérivé.

Erreurs fréquentes

Erreur Cause Solution
« keystore signature mismatch » Mauvais keystore décodé en CI Vérifier le secret ANDROID_KEYSTORE_BASE64 et le ré-encoder
Build iOS échoue à pod install Cocoapods cache obsolète Ajouter une étape pod repo update avant pod install
« Provisioning profile doesn’t include device » UDID du device de test manquant dans le profil Ajouter le device dans Apple Developer puis régénérer profil via match
Quota macOS épuisé en milieu de mois Multiplicateur ×10 mal anticipé Migrer iOS vers Codemagic free tier
Workflow ne se déclenche pas sur tag Tag poussé avec git push sans --tags git push --tags ou configurer push automatique
« Resource not accessible by integration » Firebase Service account sans rôle « Firebase App Distribution Admin » Ajouter rôle dans Google Cloud IAM
Secret « *** » affiché dans logs malgré masquage Secret transformé (echo $SECRET | base64) Ne jamais transformer un secret avant de l’utiliser
Build cache Gradle corrompu après upgrade Hash de cache reste valide alors que dépendances ont changé Invalider manuellement via re-run avec --no-cache

Tutoriels associés

Pour aller plus loin

FAQ

Q : Combien de temps ajoute la signature au build par rapport à un build non signé ?

R : Quelques secondes seulement. La signature elle-même est une opération rapide (chiffrement RSA / SHA-256). Ce qui prend du temps en CI, c’est plutôt le téléchargement et la mise en cache du keystore et la génération du provisioning profile sur iOS. Une fois le cache rempli, l’overhead total est inférieur à 30 secondes.

Q : Faut-il configurer le CI iOS dès le début d’un projet ?

R : Non. La pratique pragmatique est de commencer par CI Android — moins coûteuse, plus rapide, plus permissive. La CI iOS se configure quand l’app approche de TestFlight, soit en général 4 à 8 semaines avant la première release. Configurer la CI iOS trop tôt fait perdre du temps et consomme du quota macOS sans rendement.

Q : Mon dépôt est privé, ai-je quand même accès au free tier GitHub Actions ?

R : Oui, le free tier de 3 000 minutes Linux et 300 minutes macOS effectives s’applique aux dépôts privés des comptes Free. Les dépôts publics ont des minutes illimitées sur les runners standards. Si votre dépôt est dans une organisation Pro ou Enterprise, le quota et les tarifs changent — consultez la page de billing GitHub.

Q : Peut-on faire du build matriciel pour tester plusieurs versions de Flutter en parallèle ?

R : Oui, et c’est même un cas d’usage classique. La clé YAML strategy.matrix permet de lancer le même job avec différentes valeurs (par exemple flutter-version: [3.40.x, 3.41.x, master]). Chaque combinaison s’exécute sur un runner séparé en parallèle. Utile pour valider qu’une montée de version Flutter ne casse pas votre projet avant de migrer.

Q : Comment éviter qu’un push accidentel sur main ne déclenche un déploiement en production ?

R : Trois protections à combiner. Premièrement, restreindre le déploiement aux tags v* et non aux pushes main. Deuxièmement, ajouter une protection rule sur la branche main qui exige une PR validée par un reviewer. Troisièmement, scoper le job de production à un environnement GitHub production qui exige une approbation manuelle dans l’interface Actions avant chaque exécution.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité