DEV Community

Cover image for Minha Experiência Estruturando uma Pipeline Mobile CI/CD Flutter
Marcos Vilela
Marcos Vilela

Posted on

Minha Experiência Estruturando uma Pipeline Mobile CI/CD Flutter

Recentemente, enfrentei o desafio de estruturar uma pipeline de CI/CD completa para um aplicativo corporativo desenvolvido em Flutter. O objetivo era claro: automatizar, proteger e agilizar o deploy para Android e iOS. Neste artigo, compartilho os aprendizados, as soluções para os obstáculos encontrados e os exemplos práticos que formaram a espinha dorsal dessa implementação.

Por que investir em CI/CD mobile?

Em um cenário de desenvolvimento ágil, automatizar os processos de build, teste e deploy não é um luxo, mas uma necessidade. Para aplicações mobile, essa urgência é amplificada: os builds são demorados e consomem recursos, as dependências evoluem rapidamente e cada loja de aplicativos (Google Play e App Store) impõe seu próprio conjunto de regras e exigências. Uma pipeline bem arquitetada garante entregas rápidas, seguras e com a consistência que um ambiente corporativo exige.

Estrutura dos Workflows

Para organizar o processo, a pipeline foi modularizada em workflows distintos no GitHub Actions. Cada workflow é uma sequência de jobs que rodam em ambientes específicos, conhecidos como runners.

Runners: São as máquinas virtuais que executam seus jobs. Para builds Android, utilizamos os runners ubuntu-latest fornecidos pelo GitHub, que são eficientes e econômicos. Para iOS, a compilação exige o macOS. Como os runners macOS do GitHub têm um custo associado, optamos por configurar um runner self-hosted em uma máquina Mac local, garantindo controle total sobre o ambiente e otimização de custos.

Dividimos nossa esteira em workflows específicos, cada um com sua responsabilidade:

  • feature-workflow.yml: Valida código novo, executa lint, testes unitários e análise SAST (Static Application Security Testing), garantindo qualidade e segurança desde o início.
  • staging-workflow.yml: Prepara builds de homologação, rodando validações e testes automatizados.
  • mobile-workflow.yml: Geram builds de produção e fazem o deploy para Google Play e App Store.

Controle dos Fluxos

Para garantir que cada etapa só rode no momento certo e que a pipeline de produção só seja executada após a validação em staging, utilizei gatilhos condicionais. Isso orquestra a transição entre ambientes de forma segura.

on:
  workflow_dispatch: # Permite disparo manual
  workflow_run:
    workflows: ["staging-workflow"] # Nome do workflow que deve terminar
    types:
      - completed # Apenas quando ele for concluído

jobs:
  build:
    # A condição abaixo garante que o job só rode se o disparo for manual
    # ou se o workflow de staging anterior tiver sido concluído com sucesso.
    if: >
      github.event_name == 'workflow_dispatch' ||
      (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'staging')
    runs-on: ubuntu-latest
    ...
Enter fullscreen mode Exit fullscreen mode

Isso permite que o workflow seja disparado manualmente ou automaticamente após a conclusão bem-sucedida do workflow de staging. O uso do workflow_dispatch também foi essencial para execuções manuais e testes pontuais.

Configuração do Ambiente

Antes de qualquer build, preparei o ambiente instalando Flutter, Java e aplicando cache para dependências, acelerando o processo e evitando downloads desnecessários:

- name: Setup Flutter
  uses: subosito/flutter-action@v2
  with:
    flutter-version: ${{ env.FLUTTER_VERSION }}
    channel: stable
    cache: true

- name: Cache Flutter pub dependencies
  uses: actions/cache@v4
  with:
    path: ~/.pub-cache
    key: ${{ runner.os }}-pub-${{ env.FLUTTER_VERSION }}-${{ hashFiles('**/pubspec.lock') }}

- name: Setup Java
  uses: actions/setup-java@v4
  with:
    distribution: 'temurin'
    java-version: '17'
Enter fullscreen mode Exit fullscreen mode

No iOS, também utilizei cache para CocoaPods e limpei o ambiente antes de cada build:

- name: Cache CocoaPods
  uses: actions/cache@v4
  with:
    path: ios/Pods
    key: ${{ runner.os }}-pods-${{ hashFiles('**/pubspec.lock') }}
Enter fullscreen mode Exit fullscreen mode

Assinatura dos Apps

A assinatura digital é obrigatória para publicação nas lojas. No Android, trabalhei com keystore e no iOS, com certificados e provisioning profiles:

- name: Setup iOS certificates
  uses: apple-actions/import-codesign-certs@v2
  with:
    p12-file-base64: ${{ secrets.CERTIFICATE_BASE64 }}
    p12-password: ${{ secrets.CERTIFICATE_PASSWORD }}

- name: Download Provisioning Profiles
  uses: apple-actions/download-provisioning-profiles@v2
  with:
    bundle-id: com.seuapp.id
    profile-type: IOS_APP_STORE
    issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
    api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
    api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
Enter fullscreen mode Exit fullscreen mode

O GitHub trabalha de uma forma bem peculiar com certificados, exigindo que o sejam salvos como secret em base64 e decodificado em tempo de execução no runner.

No Android, o keystore foi salvo via secret em base64 e decodificado no runner:

- name: Setup Android signing
  run: |
    echo "$KEYSTORE_BASE64" | base64 -d > android/app/keystore.jks
Enter fullscreen mode Exit fullscreen mode

Jobs de Qualidade

Implementei jobs de lint, testes unitários e análise estática de segurança (SAST) para garantir qualidade e segurança:

- name: Lint Flutter
  run: flutter analyze

- name: Testes Unitários
  run: flutter test --coverage

- name: SAST com MobSF
  uses: MobSF/mobsfscan@main
  with:
    args: .
Enter fullscreen mode Exit fullscreen mode

O SAST foi crucial para identificar vulnerabilidades cedo, especialmente em apps que lidam com dados sensíveis. Cada job gera relatórios que ajudam a manter a qualidade do código alta.

Deploy Automatizado

Com os builds assinados, o último passo é o deploy, utilizei actions específicas para upload dos builds:

  • Android: r0adkll/upload-google-play@v1 para enviar o bundle diretamente à Google Play.
  • iOS: apple-actions/upload-testflight-build@v3 para subir o IPA ao TestFlight.

Exemplo de upload para TestFlight:

- name: Upload app to TestFlight
  uses: apple-actions/upload-testflight-build@v3
  with: 
    app-path: ${{ steps.find-ipa.outputs.ipa-path }}
    issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
    api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
    api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
Enter fullscreen mode Exit fullscreen mode

Dificuldades e Aprendizados

Um dos maiores desafios foi adaptar o pipeline iOS para rodar em runner Mac self-hosted, já que o GitHub Actions não oferece MacOS gratuito. Também precisei ajustar versões de Java e Gradle para evitar conflitos e falhas silenciosas nos builds Android.

No fim, consegui uma esteira automatizada, segura e eficiente, que valida, testa, assina e publica os apps nas lojas com mínimo esforço manual. Se você está montando sua pipeline mobile, recomendo investir tempo nos jobs de qualidade, na configuração de secrets e no controle dos fluxos. O resultado compensa!

Top comments (0)