Skip to main content
mobile8 février 202612 min de lecture

Automatiser les déploiements Android sur le Google Play Store

Mettre en place des builds et déploiements Android automatisés — de la configuration Gradle à la publication sur le Play Store via CI/CD.

androidgradlecicd
Automatiser les déploiements Android sur le Google Play Store

Publier une application Android sur le Play Store semble simple — construire un APK, le téléverser, terminé. En pratique, le processus implique des configurations de signature, des variantes de build, des formats de bundle, des tracks du Play Store, des déploiements progressifs, et suffisamment de configuration Gradle pour faire douter quiconque de ses choix de carrière. Après avoir automatisé les déploiements Android sur plusieurs projets, y compris des applications Flutter nécessitant des pipelines iOS et Android, j'ai un setup qui gère tout, du commit au déploiement en production, sans intervention manuelle.

Le système de build Android

Avant d'automatiser quoi que ce soit, il faut bien comprendre comment fonctionnent les builds Android. Contrairement à iOS, où Xcode gère la majeure partie de la complexité derrière une interface graphique, le système de build Android est entièrement basé sur Gradle et configuré par le code. C'est en fait un avantage pour l'automatisation — tout est explicite et versionné.

Configuration Gradle pour les builds de release

Votre fichier android/app/build.gradle (ou build.gradle.kts si vous avez migré vers Kotlin DSL) est le fichier de configuration central. Pour les builds de release, vous devez configurer la signature, la minification et l'optimisation.

android {
    compileSdkVersion 35

    defaultConfig {
        applicationId "com.yourcompany.yourapp"
        minSdkVersion 24
        targetSdkVersion 35
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

    signingConfigs {
        release {
            keyAlias System.getenv("KEY_ALIAS") ?: properties["keyAlias"]
            keyPassword System.getenv("KEY_PASSWORD") ?: properties["keyPassword"]
            storeFile file(System.getenv("KEYSTORE_PATH") ?: properties["keystorePath"] ?: "keystore.jks")
            storePassword System.getenv("STORE_PASSWORD") ?: properties["storePassword"]
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

La configuration de signature lit d'abord les variables d'environnement, puis se rabat sur un fichier de propriétés local. Ce pattern permet aux développeurs de signer les builds localement avec un fichier key.properties tandis que la CI utilise les variables d'environnement — le même fichier Gradle fonctionne dans les deux contextes sans modification.

Le fichier de propriétés local

Pour le développement local, créez un fichier key.properties dans votre répertoire android/ (et ajoutez-le immédiatement au .gitignore) :

storePassword=your_store_password
keyPassword=your_key_password
keyAlias=your_key_alias
keystorePath=../keys/release-keystore.jks

Puis référencez-le dans votre build.gradle :

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

Configuration de la signature

La signature d'application Android est plus simple que la signature de code iOS, mais elle comporte un risque permanent : si vous perdez votre clé de téléversement, vous perdez la capacité de mettre à jour votre application. Google a introduit Play App Signing pour atténuer ce risque, et je recommande fortement de l'utiliser.

Play App Signing

Avec Play App Signing activé, Google détient la clé de distribution réelle. Vous signez vos téléversements avec une clé de téléversement, et Google re-signe avec la clé de distribution avant la livraison aux utilisateurs. Les avantages :

  • Récupération de clé : Si vous perdez votre clé de téléversement, Google peut la réinitialiser. Sans Play App Signing, perdre votre clé signifie créer une nouvelle fiche d'application.
  • APK plus petits : Google peut optimiser l'application pour chaque configuration d'appareil en utilisant la clé de distribution.
  • Rotation de clé : Vous pouvez faire tourner votre clé de téléversement sans affecter les utilisateurs installés.

Activez-le dans la Play Console sous Configuration > Signature de l'application. Pour les nouvelles applications, c'est activé par défaut.

Générer un keystore

Si vous devez créer un nouveau keystore :

keytool -genkey -v \
  -keystore release-keystore.jks \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000 \
  -alias your_key_alias

Stockez ce fichier keystore de manière sécurisée. Pour la CI, je l'encode en base64 et le stocke comme secret GitHub Actions :

base64 -i release-keystore.jks | pbcopy

Puis je le décode dans le workflow CI avant le build :

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

AAB vs APK

Google exige maintenant les Android App Bundles (AAB) pour toutes les nouvelles applications sur le Play Store. La différence compte pour votre pipeline de build.

APK (Android Package) est un fichier unique contenant tout pour toutes les configurations d'appareils. Il est plus gros, mais universel — tout appareil Android peut installer n'importe quel APK. Les APK sont toujours utiles pour la distribution interne, les tests sur appareils physiques et la distribution en dehors du Play Store.

AAB (Android App Bundle) contient tout le code et les ressources mais permet à Google Play de générer des APK optimisés pour chaque appareil. Un utilisateur avec un Pixel ne télécharge que les ressources dont il a besoin, pas les assets pour chaque densité d'écran et architecture CPU. Les bundles sont généralement 15-30 % plus petits que les APK universels.

Pour votre pipeline CI, construisez les deux :

# Pour le Play Store
flutter build appbundle --release

# Pour les tests internes / installation directe
flutter build apk --release --split-per-abi

Le flag --split-per-abi génère des APK séparés pour chaque architecture CPU (arm64-v8a, armeabi-v7a, x86_64), ce qui réduit la taille des fichiers pour la distribution directe.

Fastlane Supply pour le Play Store

L'action supply de Fastlane gère les téléversements et la gestion des métadonnées du Play Store, comme deliver le fait pour iOS.

Configurer Supply

D'abord, vous avez besoin d'un compte de service Google Play avec les bonnes permissions.

  1. Allez dans la Google Cloud Console et créez un compte de service.
  2. Téléchargez le fichier de clé JSON.
  3. Dans la Play Console, allez dans Paramètres > Accès API et liez le compte de service.
  4. Accordez-lui les permissions "Release manager" pour votre application.
# android/fastlane/Appfile
json_key_file(ENV["PLAY_STORE_JSON_KEY"] || "path/to/service-account.json")
package_name("com.yourcompany.yourapp")

Téléversement basique

default_platform(:android)

platform :android do
  desc "Deploy to Play Store Internal Testing"
  lane :internal do
    gradle(
      task: "bundle",
      build_type: "Release",
      project_dir: "./"
    )

    supply(
      track: "internal",
      aab: "../build/app/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end
end

Pour les projets Flutter, je préfère exécuter la commande de build Flutter directement plutôt que d'utiliser la tâche Gradle via Fastlane, car le processus de build Flutter inclut des étapes de compilation Dart que Gradle seul ne gère pas correctement :

lane :internal do
  Dir.chdir("..") do
    sh("flutter build appbundle --release")
  end

  supply(
    track: "internal",
    aab: "../build/app/outputs/bundle/release/app-release.aab",
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

Les tracks du Play Store

Le Play Store a quatre tracks, et comprendre quand utiliser chacun est important pour votre stratégie de release.

Test interne

  • Jusqu'à 100 testeurs.
  • Aucune revue requise — les builds sont disponibles presque instantanément.
  • Idéal pour : tests de l'équipe de développement, cycles de QA, vérification des builds avant promotion.

Test fermé (Alpha)

  • Testeurs illimités, mais vous gérez la liste.
  • Nécessite une brève revue de Google (généralement quelques heures).
  • Idéal pour : programmes bêta avec utilisateurs invités, aperçus clients.

Test ouvert (Beta)

  • N'importe qui peut rejoindre depuis la fiche Play Store.
  • Nécessite une revue.
  • Idéal pour : programmes bêta publics, recueil de feedback avant un lancement large.

Production

  • Disponible pour tous les utilisateurs.
  • Revue complète requise.
  • Supporte les déploiements progressifs.

Promotion entre tracks

Fastlane rend la promotion entre tracks simple :

lane :promote_to_beta do
  supply(
    track: "internal",
    track_promote_to: "beta",
    skip_upload_changelogs: false,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

lane :promote_to_production do
  supply(
    track: "beta",
    track_promote_to: "production",
    rollout: "0.1",  # 10% staged rollout
    skip_upload_changelogs: false,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

Déploiements progressifs

Les déploiements progressifs sont l'un des plus grands avantages d'Android par rapport à iOS. Vous pouvez déployer à un pourcentage d'utilisateurs et augmenter progressivement, en surveillant les taux de crash et les retours utilisateurs à chaque étape.

lane :staged_rollout do |options|
  percentage = options[:percentage] || "0.1"

  supply(
    track: "production",
    rollout: percentage,
    aab: "../build/app/outputs/bundle/release/app-release.aab"
  )
end

lane :increase_rollout do |options|
  percentage = options[:percentage] || "0.5"

  supply(
    track: "production",
    rollout: percentage,
    skip_upload_aab: true,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

lane :complete_rollout do
  supply(
    track: "production",
    rollout: "1.0",
    skip_upload_aab: true,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

Un calendrier de déploiement typique :

  1. Jour 1 : 10 % — surveiller les taux de crash dans Firebase Crashlytics.
  2. Jour 2-3 : 25 % — vérifier les retours utilisateurs et les tickets de support.
  3. Jour 4-5 : 50 % — vérifier l'absence de régressions de performance à grande échelle.
  4. Jour 6-7 : 100 % — release complète.

Si quelque chose tourne mal à n'importe quelle étape, vous pouvez arrêter le déploiement et pousser un correctif sans affecter tous les utilisateurs.

Workflow GitHub Actions

Voici un workflow CI/CD complet pour Android :

name: Deploy to Play Store

on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      track:
        description: 'Play Store track'
        required: true
        default: 'internal'
        type: choice
        options:
          - internal
          - alpha
          - beta
          - production

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4

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

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.27.x'
          channel: stable
          cache: true

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.2'
          bundler-cache: true
          working-directory: android

      - name: Flutter dependencies
        run: flutter pub get

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

      - name: Create service account file
        run: echo '${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}' > android/play-store-key.json

      - name: Build and deploy
        working-directory: android
        env:
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
          STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
          KEYSTORE_PATH: app/keystore.jks
          PLAY_STORE_JSON_KEY: play-store-key.json
        run: bundle exec fastlane ${{ github.event.inputs.track || 'internal' }}

      - name: Cleanup secrets
        if: always()
        run: |
          rm -f android/app/keystore.jks
          rm -f android/play-store-key.json

Quelques points à noter sur ce workflow :

Les runners Ubuntu fonctionnent très bien pour Android. Contrairement à iOS, qui nécessite macOS, les builds Android tournent sur Linux. Les runners Ubuntu sont moins chers et démarrent plus vite sur GitHub Actions.

Java 17 est requis pour les versions actuelles du Android Gradle Plugin. Si votre projet utilise une version plus ancienne de l'AGP, vous pourriez avoir besoin de Java 11, mais c'est de plus en plus rare.

L'étape de nettoyage supprime les secrets décodés même si le build échoue. C'est une mesure de défense en profondeur — les runners GitHub Actions sont éphémères, mais c'est une bonne pratique.

Gestion des versions

Android utilise deux identifiants de version : versionCode (un entier qui doit augmenter à chaque téléversement) et versionName (la chaîne de version lisible par l'humain).

Pour les projets Flutter, les deux viennent du pubspec.yaml :

version: 1.2.3+45
# 1.2.3 = versionName
# 45 = versionCode

J'automatise l'incrémentation du code de version en utilisant le numéro d'exécution CI :

lane :set_version_code do
  build_number = ENV["GITHUB_RUN_NUMBER"] || "1"

  Dir.chdir("..") do
    current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
    sh("sed -i 's/^version: .*/version: #{current_version}+#{build_number}/' pubspec.yaml")
  end
end

Cela garantit que le code de version augmente toujours, même entre les branches. Si vous avez besoin de plus de contrôle, vous pouvez récupérer le dernier code de version depuis le Play Store :

lane :smart_version_code do
  current = google_play_track_version_codes(track: "internal").max || 0
  new_code = current + 1

  Dir.chdir("..") do
    current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
    sh("sed -i 's/^version: .*/version: #{current_version}+#{new_code}/' pubspec.yaml")
  end
end

Gestion des fiches Play Store

Supply peut gérer l'intégralité de votre fiche Play Store à partir de fichiers versionnés :

android/fastlane/metadata/android/
├── en-US/
│   ├── title.txt
│   ├── short_description.txt
│   ├── full_description.txt
│   ├── changelogs/
│   │   ├── default.txt
│   │   └── 45.txt          # Changelog for versionCode 45
│   └── images/
│       ├── phoneScreenshots/
│       │   ├── 1_home.png
│       │   └── 2_detail.png
│       ├── featureGraphic.png
│       └── icon.png
└── ru-RU/
    ├── title.txt
    ├── short_description.txt
    └── full_description.txt

Pour télécharger votre fiche actuelle comme point de départ :

fastlane supply init

Puis mettez à jour les fichiers et poussez les changements :

lane :update_listing do
  supply(
    skip_upload_aab: true,
    skip_upload_apk: true
  )
end

Configuration ProGuard et R8

Quand minifyEnabled est true, R8 (le successeur de ProGuard) réduit, obfusque et optimise votre code. C'est essentiel pour les builds de production — cela réduit la taille de l'APK et rend la rétro-ingénierie plus difficile — mais cela peut aussi casser des choses si ce n'est pas correctement configuré.

Problèmes courants et leurs règles ProGuard :

# Keep Flutter's classes
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }

# Keep Firebase classes
-keep class com.google.firebase.** { *; }

# Keep models used with JSON serialization
-keep class com.yourcompany.yourapp.models.** { *; }

# Keep classes referenced via reflection
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes InnerClasses

# Common crash fix: OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**

Testez votre build de release minutieusement — les problèmes R8 se manifestent souvent comme des crashs runtime qui n'apparaissent pas dans les builds debug. Les flags --obfuscate et --split-debug-info dans Flutter permettent des optimisations supplémentaires :

flutter build appbundle --release --obfuscate --split-debug-info=build/symbols

Conservez le répertoire build/symbols — vous en avez besoin pour symboliquer les stack traces des rapports de crash.

Le Fastfile complet

default_platform(:android)

platform :android do
  desc "Deploy to internal testing"
  lane :internal do
    set_version_code
    build_release
    supply(
      track: "internal",
      aab: "../build/app/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end

  desc "Deploy to closed beta"
  lane :beta do
    set_version_code
    build_release
    supply(
      track: "beta",
      aab: "../build/app/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end

  desc "Deploy to production with staged rollout"
  lane :production do
    set_version_code
    build_release
    supply(
      track: "production",
      rollout: "0.1",
      aab: "../build/app/outputs/bundle/release/app-release.aab"
    )
  end

  desc "Promote internal to beta"
  lane :promote_to_beta do
    supply(
      track: "internal",
      track_promote_to: "beta"
    )
  end

  desc "Increase production rollout"
  lane :increase_rollout do |options|
    supply(
      track: "production",
      rollout: options[:percentage] || "0.5",
      skip_upload_aab: true
    )
  end

  private_lane :build_release do
    Dir.chdir("..") do
      sh("flutter build appbundle --release --obfuscate --split-debug-info=build/symbols")
    end
  end

  private_lane :set_version_code do
    build_number = ENV["GITHUB_RUN_NUMBER"] || Time.now.strftime("%Y%m%d%H%M")
    Dir.chdir("..") do
      current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
      sh("sed -i '' 's/^version: .*/version: #{current_version}+#{build_number}/' pubspec.yaml")
    end
  end
end

Différences avec l'automatisation iOS

Ayant automatisé les deux plateformes, il y a plusieurs différences clés à noter.

La signature est plus simple sur Android. Un seul fichier keystore contre la danse des certificats et profils de provisionnement sur iOS. Pas besoin de match ou d'un autre outil de gestion de certificats.

Pas d'exigence macOS obligatoire. Les builds Android tournent sur Linux, ce qui signifie des runners CI moins chers et plus rapides.

Des temps de revue plus rapides. Le processus de revue de Google est généralement plus rapide que celui d'Apple, et les téléversements sur le track interne ne nécessitent aucune revue du tout.

Les déploiements progressifs sont natifs. Sur iOS, vous pouvez faire des releases par phases, mais vous ne pouvez pas contrôler le pourcentage ou arrêter en cours de déploiement avec la même granularité.

L'API du Play Store est plus permissive. Vous pouvez gérer presque tout de manière programmatique, y compris les expériences de fiche et la tarification. L'API d'App Store Connect d'Apple a plus de restrictions.

Le principal inconvénient est les temps de build Gradle. Un build Android propre avec optimisation R8 peut prendre significativement plus de temps qu'un build iOS équivalent. Le cache aide — à la fois le cache intégré de Gradle et le cache au niveau CI du répertoire .gradle.

A quoi ressemble un pipeline mature

Après toute cette configuration, l'expérience quotidienne devrait être invisible. Poussez sur main, un build s'exécute, et quelques minutes plus tard les testeurs ont une nouvelle version dans le track interne. Taguez une release, et un build de production passe avec un déploiement progressif.

Le pipeline élimine toute une catégorie de problèmes "ça marche sur ma machine", garantit que chaque build est reproductible, et donne à l'équipe la confiance que le déploiement est un non-événement. Cette dernière partie est le vrai objectif — rendre le déploiement si routinier que personne n'y pense.

DU

Danil Ulmashev

Full Stack Developer

Intéressé par une collaboration ?