Publicar una app Flutter en producción manualmente es un proceso tedioso y propenso a errores: firmar el APK, subir a Google Play, archivar en Xcode, subir a App Store Connect... Hacerlo a mano una vez es tolerable. Hacerlo en cada release no lo es. Automatizar ese pipeline con GitHub Actions y Fastlane es el estándar en proyectos Flutter serios.
La arquitectura del pipeline
Un pipeline de CI/CD para Flutter tiene dos partes:
- GitHub Actions: orquesta los pasos, gestiona los runners y dispara el pipeline en cada push o pull request.
- Fastlane: se encarga de la firma del código, el versionado y la distribución a las tiendas. Funciona en el runner de GitHub Actions.
La división tiene sentido porque Fastlane tiene soporte nativo para Google Play y App Store Connect con todas sus particularidades (tokens de autenticación, perfiles de provisioning, tracks de distribución), y GitHub Actions se encarga de la infraestructura.
Workflow básico de GitHub Actions para Flutter
# .github/workflows/ci.yml
name: CI Flutter
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.0'
channel: 'stable'
cache: true
- name: Instalar dependencias
run: flutter pub get
- name: Analizar código
run: flutter analyze
- name: Ejecutar tests
run: flutter test --coverage
- name: Subir cobertura
uses: codecov/codecov-action@v3
with:
file: coverage/lcov.info
El action subosito/flutter-action es el estándar para instalar Flutter en GitHub Actions. Con cache: true cachea el SDK de Flutter entre ejecuciones, lo que reduce el tiempo del job en varios minutos.
Build de Android con firma automática
Para publicar en Google Play, el APK o AAB tiene que estar firmado. La clave de firma es un secreto que no puede ir en el repositorio; en su lugar, se almacena como secret en GitHub y se decodifica en el runner:
# .github/workflows/deploy-android.yml
name: Deploy Android
on:
push:
tags:
- 'v*'
jobs:
build-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.0'
channel: 'stable'
- name: Decodificar keystore
run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks
- name: Crear key.properties
run: |
cat > android/key.properties << EOF
storeFile=keystore.jks
storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}
keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
EOF
- name: Build AAB
run: flutter build appbundle --release
- name: Deploy a Google Play con Fastlane
run: bundle exec fastlane android deploy
working-directory: android
env:
GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
Fastlane para Android
# android/fastlane/Fastfile
default_platform(:android)
platform :android do
desc "Deploy a Google Play (track interno)"
lane :deploy do
upload_to_play_store(
track: 'internal',
aab: '../build/app/outputs/bundle/release/app-release.aab',
json_key_data: ENV['GOOGLE_PLAY_JSON_KEY'],
skip_upload_screenshots: true,
skip_upload_images: true,
)
end
end
Build de iOS: el runner macOS
El build de iOS requiere un runner macOS, que en GitHub Actions tiene un coste mayor que Ubuntu. Si el presupuesto es limitado, considera usar runners macOS solo para los tags de release y no en cada pull request:
# .github/workflows/deploy-ios.yml
jobs:
build-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.0'
- name: Instalar certificados con Fastlane Match
run: bundle exec fastlane ios certificates
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_TOKEN }}
- name: Build IPA
run: flutter build ipa --release
- name: Deploy a App Store Connect
run: bundle exec fastlane ios deploy
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_PRIVATE_KEY }}
Fastlane Match para certificados de iOS
Fastlane Match es la forma recomendada de gestionar certificados y perfiles de provisioning de iOS en equipo. Almacena los certificados cifrados en un repositorio Git privado y los sincroniza en cada build:
# ios/fastlane/Fastfile
default_platform(:ios)
platform :ios do
lane :certificates do
match(
type: 'appstore',
app_identifier: 'com.ejemplo.myapp',
readonly: true,
)
end
lane :deploy do
build_app(
workspace: 'Runner.xcworkspace',
scheme: 'Runner',
export_method: 'app-store',
)
upload_to_app_store(
skip_metadata: true,
skip_screenshots: true,
submit_for_review: false,
)
end
end
Versionado automático
Con GitHub Actions puedes automatizar el versionado usando los tags de Git. El número de build puede derivarse del número de ejecución del workflow:
# En el step de build, antes de flutter build
- name: Actualizar versión
run: |
VERSION=$(git describe --tags --abbrev=0 | sed 's/v//')
BUILD_NUMBER=${{ github.run_number }}
flutter build appbundle --release
--build-name=$VERSION
--build-number=$BUILD_NUMBER
Este pipeline cubre el ciclo completo: cada push a main ejecuta los tests, y cada tag nuevo dispara el build y la distribución. Para un equipo pequeño es suficiente. Para equipos más grandes, suele añadirse una etapa de distribución a testers con Firebase App Distribution o TestFlight antes del despliegue a producción.
Para que el pipeline detecte regresiones de rendimiento, conviene complementarlo con los integration tests que se mencionan en el artículo sobre testing en Flutter. Y para entender qué pasa dentro del motor de renderizado una vez que la app está en manos de los usuarios, el artículo sobre Flutter DevTools y el rendimiento explica las herramientas de diagnóstico disponibles.
Imagen: Pexels / Myburgh Roux
