Flutter en producción: CI/CD con GitHub Actions, Fastlane y distribución automática

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

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP