Automatizzare i Deploy Android su Google Play Store
Configurare build e deploy Android automatizzati — dalla configurazione Gradle alla pubblicazione su Play Store tramite CI/CD.

Distribuire un'app Android sul Play Store sembra semplice — si crea un APK, lo si carica, e il gioco è fatto. In pratica, il processo coinvolge configurazioni di firma, varianti di build, formati di bundle, tracce del Play Store, rollout graduali e una configurazione Gradle sufficiente a far dubitare chiunque delle proprie scelte di carriera. Dopo aver automatizzato i deploy Android su diversi progetti, incluse app Flutter che necessitano di pipeline sia iOS che Android, ho una configurazione che gestisce tutto, dal commit al rollout in produzione, senza intervento manuale.
Il Sistema di Build Android
Prima di automatizzare qualsiasi cosa, è necessario avere una solida comprensione di come funzionano le build Android. A differenza di iOS, dove Xcode gestisce la maggior parte della complessità tramite una GUI, il sistema di build di Android è interamente basato su Gradle e configurato tramite codice. Questo è in realtà un vantaggio per l'automazione — tutto è esplicito e versionato.
Configurazione Gradle per le Build di Rilascio
Il tuo android/app/build.gradle (o build.gradle.kts se sei migrato a Kotlin DSL) è il file di configurazione centrale. Per le build di rilascio, devi configurare la firma, la minificazione e l'ottimizzazione.
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 configurazione della firma legge prima dalle variabili d'ambiente, poi ricade su un file di proprietà locale. Questo schema consente agli sviluppatori di firmare le build localmente utilizzando un file key.properties mentre la CI utilizza variabili d'ambiente — lo stesso file Gradle funziona in entrambi i contesti senza modifiche.
Il File di Proprietà Locale
Per lo sviluppo locale, crea un file key.properties nella tua directory android/ (e aggiungilo immediatamente a .gitignore):
storePassword=your_store_password
keyPassword=your_key_password
keyAlias=your_key_alias
keystorePath=../keys/release-keystore.jks
Quindi referenzialo nel tuo build.gradle:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
Configurazione della Firma
La firma delle app Android è più semplice della firma del codice iOS, ma comporta un rischio permanente: se perdi la tua chiave di upload, perdi la capacità di aggiornare la tua app. Google ha introdotto Play App Signing per mitigare questo problema, e ne raccomando vivamente l'uso.
Play App Signing
Con Play App Signing abilitato, Google detiene la chiave di distribuzione effettiva. Tu firmi i tuoi upload con una chiave di upload, e Google rifirma con la chiave di distribuzione prima di consegnare agli utenti. I vantaggi:
- Recupero della chiave persa: Se perdi la tua chiave di upload, Google può resettarla. Senza Play App Signing, perdere la chiave significa creare una nuova lista di app.
- APK più piccoli: Google può ottimizzare l'app per ogni configurazione del dispositivo utilizzando la chiave di distribuzione.
- Rotazione della chiave: Puoi ruotare la tua chiave di upload senza influenzare gli utenti che hanno già installato l'app.
Abilitalo nella Play Console sotto Configurazione > Firma dell'app. Per le nuove app, è abilitato di default.
Generazione di un Keystore
Se hai bisogno di creare un nuovo keystore:
keytool -genkey -v \
-keystore release-keystore.jks \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-alias your_key_alias
Conserva questo file keystore in modo sicuro. Per la CI, lo codifico in base64 e lo memorizzo come segreto di GitHub Actions:
base64 -i release-keystore.jks | pbcopy
Quindi decodificalo nel workflow della CI prima di costruire:
- name: Decode keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks
AAB vs APK
Google ora richiede Android App Bundles (AAB) per tutte le nuove app sul Play Store. La differenza è importante per la tua pipeline di build.
APK (Android Package) è un singolo file contenente tutto per tutte le configurazioni del dispositivo. È più grande, ma universale — qualsiasi dispositivo Android può installare qualsiasi APK. Gli APK sono ancora utili per la distribuzione interna, il testing su dispositivi fisici e la distribuzione al di fuori del Play Store.
AAB (Android App Bundle) contiene tutto il codice e le risorse ma permette a Google Play di generare APK ottimizzati per ogni dispositivo. Un utente con un telefono Pixel scarica solo le risorse di cui ha bisogno, non gli asset per ogni densità di schermo e architettura CPU. I bundle sono tipicamente dal 15 al 30% più piccoli degli APK universali.
Per la tua pipeline CI, costruisci entrambi:
# Per il Play Store
flutter build appbundle --release
# Per il testing interno / installazione diretta
flutter build apk --release --split-per-abi
Il flag --split-per-abi genera APK separati per ogni architettura CPU (arm64-v8a, armeabi-v7a, x86_64), il che riduce la dimensione del file per la distribuzione diretta.
Fastlane Supply per il Play Store
L'azione supply di Fastlane gestisce gli upload sul Play Store e la gestione dei metadati, rispecchiando ciò che deliver fa per iOS.
Configurazione di Supply
Innanzitutto, hai bisogno di un account di servizio Google Play con i permessi corretti.
- Vai alla Google Cloud Console e crea un account di servizio.
- Scarica il file della chiave JSON.
- Nella Play Console, vai a Impostazioni > Accesso API e collega l'account di servizio.
- Concedigli i permessi di "Gestore delle release" per la tua app.
# android/fastlane/Appfile
json_key_file(ENV["PLAY_STORE_JSON_KEY"] || "path/to/service-account.json")
package_name("com.yourcompany.yourapp")
Upload di Base
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
Per i progetti Flutter, preferisco eseguire direttamente il comando di build di Flutter piuttosto che utilizzare il task Gradle tramite Fastlane, perché il processo di build di Flutter include passaggi di compilazione Dart che Gradle da solo non gestisce correttamente:
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
Tracce del Play Store
Il Play Store ha quattro tracce, e capire quando usarle è importante per la tua strategia di rilascio.
Testing Interno
- Fino a 100 tester.
- Nessuna revisione richiesta — le build sono disponibili quasi istantaneamente.
- Ideale per: testing del team di sviluppo, cicli di QA, controllo delle build prima della promozione.
Testing Chiuso (Alpha)
- Tester illimitati, ma gestisci tu la lista.
- Richiede una breve revisione da parte di Google (solitamente poche ore).
- Ideale per: programmi beta con utenti invitati, anteprime per i clienti.
Testing Aperto (Beta)
- Chiunque può unirsi dalla pagina del Play Store.
- Richiede revisione.
- Ideale per: programmi beta pubblici, raccolta di feedback prima di un rilascio ampio.
Produzione
- Disponibile per tutti gli utenti.
- Richiede una revisione completa.
- Supporta rollout graduali.
Promozione tra le Tracce
Fastlane rende la promozione tra le tracce semplice:
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
Rollout Graduali
I rollout graduali sono uno dei maggiori vantaggi di Android rispetto a iOS. Puoi rilasciare a una percentuale di utenti e aumentare gradualmente, monitorando i tassi di crash e il feedback degli utenti in ogni fase.
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 tipico programma di rollout:
- Giorno 1: 10% — monitora i tassi di crash in Firebase Crashlytics.
- Giorni 2-3: 25% — controlla il feedback degli utenti e i ticket di supporto.
- Giorni 4-5: 50% — verifica l'assenza di regressioni di performance su larga scala.
- Giorni 6-7: 100% — rilascio completo.
Se qualcosa va storto in qualsiasi fase, puoi interrompere il rollout e rilasciare una correzione senza influenzare tutti gli utenti.
Workflow di GitHub Actions
Ecco un workflow CI/CD completo per 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
Alcune cose da notare su questo workflow:
I runner Ubuntu funzionano bene per Android. A differenza di iOS, che richiede macOS, le build Android vengono eseguite su Linux. I runner Ubuntu sono più economici e si avviano più velocemente su GitHub Actions.
Java 17 è richiesto per le versioni attuali dell'Android Gradle Plugin. Se il tuo progetto utilizza una versione AGP più vecchia, potresti aver bisogno di Java 11, ma questo è sempre più raro.
Il passaggio di pulizia rimuove i segreti decodificati anche se la build fallisce. Questa è una misura di difesa in profondità — i runner di GitHub Actions sono effimeri, ma è una buona pratica.
Gestione delle Versioni
Android utilizza due identificatori di versione: versionCode (un numero intero che deve aumentare ad ogni upload) e versionName (la stringa di versione leggibile dall'uomo).
Per i progetti Flutter, entrambi provengono da pubspec.yaml:
version: 1.2.3+45
# 1.2.3 = versionName
# 45 = versionCode
Automatizzo l'incremento del codice di versione utilizzando il numero di esecuzione della 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
Questo assicura che il codice di versione aumenti sempre, anche tra i branch. Se hai bisogno di maggiore controllo, puoi recuperare l'ultimo codice di versione dal 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
Gestione delle Schede del Play Store
Supply può gestire l'intera scheda del tuo Play Store da file versionati:
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
Per scaricare la tua scheda attuale come punto di partenza:
fastlane supply init
Quindi aggiorna i file e invia le modifiche:
lane :update_listing do
supply(
skip_upload_aab: true,
skip_upload_apk: true
)
end
Configurazione di ProGuard e R8
Quando minifyEnabled è true, R8 (il successore di ProGuard) riduce, offusca e ottimizza il tuo codice. Questo è essenziale per le build di produzione — riduce la dimensione dell'APK e rende più difficile l'ingegneria inversa — ma può anche causare problemi se non configurato correttamente.
Problemi comuni e le loro regole 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.**
Testa accuratamente la tua build di rilascio — i problemi di R8 spesso si manifestano come crash a runtime che non appaiono nelle build di debug. I flag --obfuscate e --split-debug-info in Flutter abilitano ulteriori ottimizzazioni:
flutter build appbundle --release --obfuscate --split-debug-info=build/symbols
Conserva la directory build/symbols — ne hai bisogno per simbolizzare le tracce di stack dai rapporti di crash.
Il Fastfile Completo
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
Differenze dall'Automazione iOS
Avendo automatizzato entrambe le piattaforme, ci sono diverse differenze chiave degne di nota.
La firma è più semplice su Android. Un singolo file keystore rispetto alla danza di certificati e profili di provisioning su iOS. Non c'è bisogno di match o di qualsiasi strumento di gestione dei certificati.
Nessun requisito obbligatorio di macOS. Le build Android vengono eseguite su Linux, il che significa runner CI più economici e veloci.
Tempi di revisione più rapidi. Il processo di revisione di Google è tipicamente più veloce di quello di Apple, e gli upload sulla traccia interna non richiedono alcuna revisione.
I rollout graduali sono nativi. Su iOS, puoi fare rilasci a fasi, ma non puoi controllare la percentuale o interrompere a metà rollout con la stessa granularità.
L'API del Play Store è più permissiva. Puoi gestire quasi tutto programmaticamente, inclusi esperimenti di listing dello store e prezzi. L'API di App Store Connect di Apple ha più restrizioni.
Lo svantaggio principale sono i tempi di build di Gradle. Una build Android pulita con ottimizzazione R8 può richiedere molto più tempo di una build iOS equivalente. La cache aiuta — sia la cache integrata di Gradle che la cache a livello di CI della directory .gradle.
Come si Presenta una Pipeline Matura
Dopo tutta la configurazione, l'esperienza quotidiana dovrebbe essere invisibile. Un push su main, una build viene eseguita, e pochi minuti dopo i tester hanno una nuova versione nella traccia interna. Un tag di rilascio, e una build di produzione procede con un rollout graduale.
La pipeline rimuove un'intera categoria di problemi "funziona sulla mia macchina", assicura che ogni build sia riproducibile e dà al team la fiducia che il rilascio sia un non-evento. Quest'ultima parte è l'obiettivo reale — rendere il deploy così di routine che nessuno ci pensi.