Shipping a Flutter app once is straightforward. Shipping it consistently, safely, and fast, every time a developer pushes code, is a different problem entirely.
That is what a CI/CD pipeline solves. CI stands for Continuous Integration: automatically building and testing your code on every push. CD stands for Continuous Delivery or Continuous Deployment: automatically packaging and releasing builds to testers or app stores without manual steps.
Without a pipeline, shipping a Flutter app means someone manually runs tests, builds the APK or IPA, signs it, uploads it, and fills out release notes. That process takes time, introduces human error, and slows down every release cycle.
With a proper Flutter CI/CD pipeline setup, all of that happens automatically. A developer merges a pull request, and the pipeline does the rest. This guide walks through exactly how to build that pipeline, which tools to use, and what each stage should do.
The development team at FBIP runs CI/CD pipelines on Flutter production apps and the patterns here come from what actually works at that level.
Why Flutter CI/CD Pipeline Setup Matters for Production
Let’s break it down.
A Flutter app targets at least two platforms: Android and iOS. Each has its own build toolchain, signing requirements, and distribution process. Without automation, every release requires:
- Running flutter test manually and hoping nothing was skipped
- Building the Android APK or AAB and signing it with the correct keystore
- Building the iOS IPA on a macOS machine and signing it with the right provisioning profile
- Uploading to Google Play and the App Store separately
- Writing release notes for both stores
That is thirty to sixty minutes of error-prone manual work per release. Teams that release frequently — multiple times a week — burn enormous time on this. And when a step is missed, a broken or unsigned build goes out.
A CI/CD pipeline compresses all of that into a single automated workflow that runs the same way every time.
Choosing a CI/CD Platform for Flutter
Several platforms support Flutter builds well. Here is how they compare.
GitHub Actions
GitHub Actions is the default choice for teams already hosting code on GitHub. It is free for public repositories and offers generous minutes for private ones. Flutter workflows run on Linux (for Android), macOS (for iOS), and Windows runners. The Flutter team publishes an official action that installs Flutter with a single step.
Best for: Teams on GitHub who want a free, tightly integrated option with a large library of community actions.
Source: “GitHub Actions documentation,” GitHub, 2024, https://docs.github.com/en/actions
Codemagic
Codemagic was built specifically for Flutter and mobile apps. It handles Flutter builds, signing, and App Store / Play Store uploads out of the box with a graphical interface. The free tier covers 500 build minutes per month on macOS machines, which is enough for small teams.
Best for: Teams that want a Flutter-native CI/CD experience with minimal YAML configuration.
Source: “Codemagic documentation,” Codemagic, 2024, https://docs.codemagic.io
Bitrise
Bitrise is a mobile-focused CI/CD platform with pre-built steps for Flutter, signing, and store uploads. It costs more than Codemagic at equivalent tiers but has stronger enterprise features for large teams.
Best for: Enterprise teams that need fine-grained access controls and integration with multiple project management tools.
Source: “Bitrise documentation,” Bitrise, 2024, https://devcenter.bitrise.io
GitLab CI/CD
Teams using GitLab get a built-in CI/CD system. Flutter support requires a bit more setup than GitHub Actions but the pipeline configuration is powerful and runs on self-hosted or shared runners.
Best for: Teams already on GitLab, or organizations that self-host their code and want CI/CD in the same system.
The Stages of a Flutter CI/CD Pipeline
A production-ready Flutter CI/CD pipeline setup runs through these stages in order.
Stage 1: Environment Setup
Before any Flutter command runs, the pipeline needs the right versions of Flutter, the JDK, and Xcode (for iOS). Using consistent, pinned versions across CI and local development prevents “works on my machine” failures.
# GitHub Actions example
– name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ‘3.22.0’
channel: ‘stable’
– name: Install dependencies
run: flutter pub get
Pin your Flutter version explicitly. Using stable channel without a version number means the pipeline uses whatever stable happens to be today, which changes without warning.
Source: “flutter-action,” subosito, GitHub, 2024, https://github.com/subosito/flutter-action
Stage 2: Code Analysis
Run static analysis before tests. Catching type errors, unused imports, and style violations early is cheaper than discovering them in a test failure.
– name: Run analysis
run: flutter analyze
– name: Check formatting
run: dart format –output=none –set-exit-if-changed .
The –set-exit-if-changed flag makes the pipeline fail if any file is not formatted correctly. This enforces consistent formatting across the team without arguments.
Stage 3: Testing
This is the heart of the CI stage. The pipeline runs your full test suite and fails the build if any test fails.
– name: Run unit and widget tests
run: flutter test –coverage
– name: Upload coverage report
uses: codecov/codecov-action@v3
with:
file: coverage/lcov.info
The –coverage flag generates a coverage report. Uploading it to a service like Codecov tracks coverage trends over time and lets you set minimum thresholds that fail the build if coverage drops below them.
For integration tests that require a device or emulator, run them in a separate job to keep your fast unit test job short.
Stage 4: Build
After tests pass, build the release artifacts. Android and iOS builds are separate jobs because iOS requires a macOS runner.
Android build:
– name: Build Android release AAB
run: |
flutter build appbundle –release \
–build-number=${{ github.run_number }}
Use appbundle instead of apk for Play Store submissions. Google Play uses the AAB format to generate optimized APKs for each device configuration, reducing download sizes for users.
iOS build:
– name: Build iOS release IPA
run: |
flutter build ipa –release \
–export-options-plist=ios/ExportOptions.plist
The ExportOptions.plist file defines your distribution method (App Store, ad-hoc, enterprise) and signing configuration. Keep this file in version control.
Stage 5: Code Signing
Code signing is where most Flutter CI/CD setups get complicated. Both platforms require signed artifacts for distribution, and signing requires private keys that must never be committed to a repository.
Android signing:
Store your keystore file and signing credentials as CI secrets. Reference them during the build:
– name: Decode keystore
run: |
echo “${{ secrets.KEYSTORE_BASE64 }}” | base64 –decode > android/app/keystore.jks
– name: Build signed AAB
run: flutter build appbundle –release
env:
KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
In android/app/build.gradle, reference these environment variables in your signing config.
iOS signing:
iOS signing on CI requires a distribution certificate and provisioning profile. The recommended approach is to use Fastlane Match, which stores encrypted certificates in a private Git repository and syncs them to the CI machine at build time.
# Fastlane Matchfile
git_url(“https://github.com/your-org/certificates”)
type(“appstore”)
app_identifier(“com.yourcompany.yourapp”)
– name: Install certificates via Fastlane Match
run: bundle exec fastlane match appstore –readonly
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Source: “Fastlane Match,” Fastlane, 2024, https://docs.fastlane.tools/actions/match/
Stage 6: Distribution
After a successful signed build, distribute automatically.
Internal testing (Firebase App Distribution):
– name: Upload to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
token: ${{ secrets.FIREBASE_TOKEN }}
groups: internal-testers
file: build/app/outputs/bundle/release/app-release.aab
Firebase App Distribution sends the build to your tester group immediately, with no manual upload steps.
Production release (Google Play):
– name: Upload to Google Play
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.yourcompany.yourapp
releaseFiles: build/app/outputs/bundle/release/app-release.aab
track: production
For App Store uploads, Fastlane’s deliver action or Transporter handles the submission.
A Complete GitHub Actions Workflow File
Here is what a production Flutter CI/CD pipeline setup looks like in a single workflow file, structured for clarity:
name: Flutter CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v4
– name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ‘3.22.0’
– name: Install dependencies
run: flutter pub get
– name: Analyze
run: flutter analyze
– name: Test
run: flutter test –coverage
build-android:
needs: test
runs-on: ubuntu-latest
if: github.ref == ‘refs/heads/main’
steps:
– uses: actions/checkout@v4
– name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ‘3.22.0’
– name: Install dependencies
run: flutter pub get
– name: Decode keystore
run: echo “${{ secrets.KEYSTORE_BASE64 }}” | base64 –decode > android/app/keystore.jks
– name: Build release AAB
run: flutter build appbundle –release
env:
KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
– name: Upload to Play Store
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.yourcompany.yourapp
releaseFiles: build/app/outputs/bundle/release/app-release.aab
track: internal
build-ios:
needs: test
runs-on: macos-latest
if: github.ref == ‘refs/heads/main’
steps:
– uses: actions/checkout@v4
– name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ‘3.22.0’
– name: Install dependencies
run: flutter pub get
– name: Install certificates
run: bundle exec fastlane match appstore –readonly
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
– name: Build iOS IPA
run: flutter build ipa –release
– name: Upload to TestFlight
run: bundle exec fastlane pilot upload –ipa build/ios/ipa/*.ipa
env:
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
The needs: test field means the build jobs only run if the test job passes. Android and iOS builds run in parallel after tests succeed, reducing total pipeline time.
Managing Environment-Specific Configuration
Production apps need different API endpoints, feature flags, and keys for development, staging, and production environments. Hard-coding these values or committing them to version control are both bad approaches.
Here is a clean pattern using –dart-define:
flutter build appbundle –release \
–dart-define=API_URL=https://api.production.com \
–dart-define=ENVIRONMENT=production
In your Dart code:
const apiUrl = String.fromEnvironment(‘API_URL’);
const environment = String.fromEnvironment(‘ENVIRONMENT’);
Store these values as CI secrets and pass them through the pipeline. Different branches or workflow triggers can pass different values, giving you separate development and production builds from the same codebase.
Build Number Automation
Every build uploaded to the App Store or Play Store needs a unique build number. Using the CI run number keeps this automatic:
– name: Build AAB with auto build number
run: |
flutter build appbundle –release \
–build-number=${{ github.run_number }}
github.run_number increments by one for every workflow run, ensuring every build has a number higher than the last. No manual version bumping required.
Flutter CI/CD Pipeline Setup Checklist
Use this before configuring your pipeline:
- Pin Flutter version explicitly in CI configuration
- Store all signing credentials as encrypted CI secrets, never in version control
- Run flutter analyze and dart format checks before tests
- Use flutter build appbundle for Android Play Store submissions
- Set up Fastlane Match for iOS certificate management across CI machines
- Pass environment-specific values with –dart-define, not hardcoded in source
- Use github.run_number or equivalent for automatic build number increments
- Run Android and iOS builds as parallel jobs after a shared test job passes
- Distribute internal builds to Firebase App Distribution before promoting to production tracks
FAQs About Flutter CI/CD Pipeline Setup
Q: Which CI/CD platform is best for Flutter apps?
GitHub Actions works well for most teams, especially those already on GitHub. It is free for public repositories, has an official Flutter setup action, and supports both Linux and macOS runners for Android and iOS builds. Codemagic is a strong alternative if you want Flutter-native tooling with less YAML configuration.
Q: How do I handle iOS code signing in a Flutter CI/CD pipeline?
Fastlane Match is the standard approach. It stores your distribution certificate and provisioning profiles in an encrypted private Git repository. The CI machine downloads and installs them at build time using a password stored as a CI secret. This keeps certificates out of your main codebase and makes rotating them straightforward.
Q: Can I run Flutter integration tests in a CI/CD pipeline?
Yes, though it requires more setup than unit tests. For Android, you can run integration tests on an emulator using the emulator action in GitHub Actions. For iOS, you need a macOS runner with a simulator. These tests take significantly longer than unit and widget tests, so run them as a separate job that does not block your main build pipeline.
Q: How do I separate development, staging, and production builds in Flutter CI?
Use –dart-define flags to pass environment-specific values at build time. Store the values as CI secrets and pass them from your workflow file. Trigger different environment configurations based on the branch being built, using main for production and develop or staging for lower environments.
Q: How long should a Flutter CI/CD pipeline take to run?
A well-structured pipeline with unit tests and both Android and iOS builds typically completes in twelve to twenty minutes when Android and iOS jobs run in parallel. The test job alone should finish in two to four minutes for most apps. If your pipeline takes longer, look at caching Flutter dependencies and pub packages between runs.





