← Articles

Flutter flavors in CI: building dev, staging, and production automatically

By Ann Tech · 9 May 2026

Once you have Flutter flavors set up locally, the next step is automating builds in CI so every merge to main produces signed builds for all environments without anyone touching a terminal.

The workflow structure

merge to main
  ├── build dev APK → upload to Firebase App Distribution (testers)
  ├── build staging APK → upload to Firebase App Distribution (QA)
  └── build production bundle → upload to Play Store internal track

Android signing setup

Store your keystore as a base64-encoded GitHub secret:

# Encode keystore
base64 -i android/app/my-release.keystore | pbcopy

Add these secrets in GitHub → Settings → Secrets:

  • KEYSTORE_BASE64
  • KEYSTORE_PASSWORD
  • KEY_ALIAS
  • KEY_PASSWORD

android/app/build.gradle.kts:

signingConfigs {
  create("release") {
    storeFile = file(System.getenv("KEYSTORE_PATH") ?: "keystore.jks")
    storePassword = System.getenv("KEYSTORE_PASSWORD") ?: ""
    keyAlias = System.getenv("KEY_ALIAS") ?: ""
    keyPassword = System.getenv("KEY_PASSWORD") ?: ""
  }
}
buildTypes {
  release { signingConfig = signingConfigs.getByName("release") }
}

The full CI workflow

.github/workflows/deploy.yml:

name: Deploy

on:
  push:
    branches: [main]

jobs:
  build-android:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - flavor: dev
            target: lib/main_dev.dart
            track: ''
          - flavor: staging
            target: lib/main_staging.dart
            track: ''
          - flavor: production
            target: lib/main.dart
            track: internal

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.24.0'
          cache: true

      - run: flutter pub get

      - name: Decode keystore
        run: |
          echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks

      - name: Build ${{ matrix.flavor }}
        run: |
          flutter build appbundle \
            --flavor ${{ matrix.flavor }} \
            -t ${{ matrix.target }} \
            --obfuscate \
            --split-debug-info=build/debug-info \
            --dart-define=ENVIRONMENT=${{ matrix.flavor }}
        env:
          KEYSTORE_PATH: keystore.jks
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

      - name: Upload dev/staging to Firebase
        if: matrix.flavor != 'production'
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{ matrix.flavor == 'dev' && secrets.FIREBASE_APP_ID_DEV || secrets.FIREBASE_APP_ID_STAGING }}
          token: ${{ secrets.FIREBASE_TOKEN }}
          groups: testers
          file: build/app/outputs/bundle/${{ matrix.flavor }}Release/app-${{ matrix.flavor }}-release.aab

      - name: Upload production to Play Store
        if: matrix.flavor == 'production'
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_JSON }}
          packageName: com.example.myapp
          releaseFiles: build/app/outputs/bundle/productionRelease/app-production-release.aab
          track: internal

iOS signing in CI

For iOS, use Fastlane Match to manage certificates in a git repo:

# fastlane/Fastfile
platform :ios do
  lane :build_staging do
    match(type: 'adhoc', readonly: true)
    sh "flutter build ipa --flavor staging -t lib/main_staging.dart"
    firebase_app_distribution(
      app: ENV['FIREBASE_APP_ID_IOS_STAGING'],
      groups: 'testers',
      ipa_path: 'build/ios/ipa/Runner.ipa',
    )
  end
end
# In GitHub Actions (must use macos runner for iOS):
  build-ios:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.24.0'
          cache: true
      - run: cd ios && pod install
      - run: bundle exec fastlane ios build_staging
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_AUTH }}
          FIREBASE_APP_ID_IOS_STAGING: ${{ secrets.FIREBASE_APP_ID_IOS_STAGING }}

Common pitfalls

Hardcoding version numbers. Use pubspec.yaml as the single source of truth for version and build number. Extract them in the CI script: flutter build appbundle --build-number=$GITHUB_RUN_NUMBER.

Not uploading debug symbols. --split-debug-info writes symbols to build/debug-info/. Upload these to Crashlytics or Sentry after the build so crashes are symbolicated in the dashboard.

Building all flavors on every PR. Full builds are slow (5-10 minutes each). Run only tests and analysis on PRs; trigger flavor builds only on merges to main or via manual dispatch.

Sign in to like, dislike, or report.

Flutter flavors in CI: building dev, staging, and production automatically — ANN Tech