Perché una pipeline CI/CD per Flutter
Quando un progetto Flutter cresce, eseguire manualmente test, analisi statica e build diventa lento e soggetto a errori. Una pipeline di Continuous Integration / Continuous Delivery (CI/CD) automatizza queste operazioni ad ogni push o pull request, garantendo che il codice sia sempre analizzato, testato e pronto per la distribuzione.
In questo articolo vedremo come costruire una pipeline completa con GitHub Actions, uno strumento gratuito (nei limiti del piano) e integrato direttamente nei repository GitHub.
Struttura di un workflow
I workflow di GitHub Actions vivono nella cartella .github/workflows/ sotto forma di file YAML. Ogni workflow è composto da:
- trigger (
on): quando eseguire la pipeline (push, pull_request, tag, ecc.) - jobs: gruppi di step eseguiti su una macchina virtuale (runner)
- steps: le singole azioni (checkout, setup, comandi shell)
Analisi statica e test automatici
Partiamo dal job più importante: garantire la qualità del codice. Creiamo il file .github/workflows/ci.yml:
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
analyze-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
channel: 'stable'
cache: true
- name: Installa dipendenze
run: flutter pub get
- name: Verifica formattazione
run: dart format --set-exit-if-changed .
- name: Analisi statica
run: flutter analyze
- name: Esegui i test
run: flutter test --coverage
- name: Carica coverage
uses: codecov/codecov-action@v4
with:
files: coverage/lcov.info
Alcuni dettagli utili:
subosito/flutter-actionè l'action de facto standard per installare Flutter; l'opzionecache: truevelocizza notevolmente le esecuzioni successive.dart format --set-exit-if-changedfa fallire il job se il codice non è formattato correttamente.- Il flag
--coveragegenera il filelcov.infoche possiamo caricare su servizi come Codecov.
Build dell'APK e dell'App Bundle Android
Una volta superati i controlli, possiamo automatizzare la generazione degli artefatti. Aggiungiamo un job che si attiva solo quando pubblichiamo un tag di versione:
build-android:
needs: analyze-and-test
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
channel: 'stable'
cache: true
- name: Decodifica keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/upload-keystore.jks
- name: Crea key.properties
run: |
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/key.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties
echo "storeFile=upload-keystore.jks" >> android/key.properties
- name: Build App Bundle
run: flutter build appbundle --release
- name: Carica artefatto
uses: actions/upload-artifact@v4
with:
name: release-aab
path: build/app/outputs/bundle/release/app-release.aab
La chiave di firma non deve mai essere committata nel repository. La soluzione è codificarla in Base64 e salvarla come secret di GitHub (Settings > Secrets and variables > Actions):
base64 -i upload-keystore.jks | pbcopy # macOS
base64 -w 0 upload-keystore.jks # Linux
Build iOS su runner macOS
La compilazione iOS richiede un runner macos-latest. La firma del codice è più complessa perché servono certificati e provisioning profile. Ecco un esempio semplificato per una build non firmata (utile per i test), spesso combinata con strumenti come fastlane per la firma completa:
build-ios:
needs: analyze-and-test
if: startsWith(github.ref, 'refs/tags/v')
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
channel: 'stable'
cache: true
- run: flutter pub get
- name: Build iOS (no codesign)
run: flutter build ios --release --no-codesign
Attenzione ai minuti: i runner macOS consumano crediti a un fattore moltiplicativo più alto rispetto a quelli Linux. Riserva la build iOS ai soli tag di release.
Best practice per pipeline affidabili
- Fissa la versione di Flutter: evita
channel: stablesenzaflutter-version, altrimenti aggiornamenti del canale potrebbero rompere la build. - Usa la cache:
cache: truesull'action Flutter e la cache dipubriducono drasticamente i tempi. - Separa i job: analisi/test da un lato, build dall'altro, sfruttando
needsper creare dipendenze. - Proteggi i segreti: keystore, password e API key sempre nei GitHub Secrets, mai in chiaro.
- Limita i trigger costosi: le build native solo su tag o branch di release.
Distribuzione automatica
Per chiudere il cerchio puoi collegare la pipeline a servizi di distribuzione:
- Firebase App Distribution per i tester interni tramite l'action ufficiale.
- Google Play con
r0adkll/upload-google-playper il caricamento automatico dell'AAB. - TestFlight / App Store tramite fastlane e le action Apple.
Conclusioni
Una pipeline CI/CD ben strutturata trasforma il rilascio da un'operazione manuale rischiosa a un processo affidabile e ripetibile. Con GitHub Actions puoi partire da un semplice job di analisi e test, per poi aggiungere gradualmente build multipiattaforma e distribuzione automatica, mantenendo il codice sempre pronto per la produzione.
