Getting your Flutter app to production shouldn't involve manual clicks and crossed fingers. A well-designed CI/CD pipeline catches bugs early, ensures consistent builds, and gets your app in front of testers and users reliably. In this practical guide, we'll walk through setting up CI/CD for Flutter: a practical guide for developers that automates everything from code quality checks through store uploads, saving your team hours each release cycle.
Understanding the CI/CD Pipeline
A production-ready CI/CD pipeline for Flutter follows a logical sequence of gates, each one blocking broken code from progressing further. Think of it like a quality funnel: code enters at the top, gets filtered by automated checks, and only reaches your users if it passes every stage.
The pipeline we recommend for most teams has five distinct phases:
- Analyze and format check: Lint your code, check formatting, and run static analysis to catch style issues and potential bugs before they compile.
- Unit and widget tests: Run your test suite to verify business logic and UI behavior. This is where you catch functional regressions.
- Build Android and iOS: Compile your app for both platforms. If the build fails, the pipeline stops—no point signing code that doesn't compile.
- Sign and upload to stores: Add your release signatures and push to Google Play and App Store, or to internal distribution channels for closed testing.
- Distribute to testers: Notify your QA team or release a beta to early adopters via TestFlight or Google Play Internal Testing.
Each stage typically runs only if the previous one succeeds. This prevents wasting compute time on signing a build that failed tests, or notifying testers about a broken app.
Choosing Your Tools
You have three main options for running Flutter CI/CD, each with different trade-offs in terms of cost, customization, and ease of setup.
GitHub Actions
If your repository lives on GitHub, GitHub Actions is the quickest path to a working pipeline. It's free for public repos and includes 2,000 free minutes per month on private repos. You define workflows in YAML files committed to your repository, giving you version control over your pipeline definition.
The downside is that GitHub Actions VMs have limited storage and can be slow for large mobile builds, especially the first time you build without caches. For a small team building once or twice daily, it's usually adequate.
Codemagic
Codemagic is purpose-built for Flutter and mobile development. It handles iOS signing, provisioning profiles, and Android signing certificates more transparently than generic CI systems. Codemagic also has macOS runners built in, which you need for iOS builds—GitHub Actions makes you pay extra for macOS time.
Codemagic's free tier gives you enough builds for a solo developer or small team. The web interface for managing secrets and signing certificates is smoother than YAML configuration alone.
Fastlane for Automation
Fastlane is not a CI system itself, but a Ruby tool that automates signing, building, and uploading to app stores. You'll typically run Fastlane inside your CI system—GitHub Actions or Codemagic triggers a script that calls Fastlane, which handles the complex mobile-specific steps.
Fastlane shines when you need consistent behavior across local and CI environments. A developer can run fastlane release locally to build and sign exactly the same way CI does, reducing "works on my machine" surprises.
Building a Pipeline Step by Step
Let's walk through a concrete GitHub Actions workflow for a Flutter app targeting both Android and iOS.
Analyze and Format
Start early with static analysis. Add this job to your workflow:
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- run: flutter pub get
- run: flutter analyze
- run: dart format --set-exit-if-changed .This catches unused imports, potential null errors, and formatting drift before anything else runs. If the team has agreed on a code style via a linter config (usually in analysis_options.yaml), enforce it here.
Unit and Widget Tests
After analysis passes, run your test suite. Widget tests are slower than unit tests, so run both but separate them if your suite is large:
test:
runs-on: ubuntu-latest
needs: analyze
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- run: flutter pub get
- run: flutter test --coverage
- uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.infoThe --coverage flag generates coverage reports you can upload to Codecov or similar services to track which parts of your codebase lack tests. This data helps prioritize testing efforts in high-risk areas.
Build for Android and iOS
Compile for both platforms. These jobs can run in parallel since they don't depend on each other:
build-android:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- run: flutter pub get
- run: flutter build apk --release
build-ios:
runs-on: macos-latest
needs: test
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- run: flutter pub get
- run: flutter build ios --releaseNote that iOS builds require a macOS runner, which costs more than Ubuntu. If you're on a budget, consider building iOS only on tagged commits (when releasing) rather than on every push to main.
Signing and Uploading
After successful builds, sign your artifacts and push to stores. This is where Fastlane or direct API calls come in. Here's a simplified example using Fastlane for Android:
deploy-android:
runs-on: ubuntu-latest
needs: build-android
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- run: flutter pub get
- run: |
echo "${{ secrets.PLAY_STORE_KEY }}" > key.json
fastlane android deployStore your Google Play signing key securely as a GitHub secret. Fastlane will use it to upload to Google Play. For iOS, you'll need provisioning profiles and certificates stored similarly—this is where Codemagic's built-in certificate management can simplify your life.
Caching Dependencies for Speed
Mobile builds are slow. The first time your pipeline runs, it downloads all Dart packages and Gradle dependencies, which can take 5–10 minutes. Caching slashes this to 1–2 minutes on subsequent runs.
Pub Cache
Cache your Flutter packages like this:
- uses: actions/cache@v3
with:
path: ~/.pub-cache
key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}
restore-keys: |
${{ runner.os }}-pub-This caches based on your pubspec.lock file. If dependencies change, GitHub Actions automatically creates a new cache entry. Old caches expire after 7 days of non-use, so you won't bloat storage.
Gradle Cache
For Android builds, cache Gradle and its downloaded dependencies:
- uses: gradle/gradle-build-action@v2
with:
cache-read-only: falseThe gradle-build-action handles caching automatically. It caches the Gradle wrapper, downloaded dependencies, and build artifacts intelligently.
Pod Cache for iOS
iOS dependencies (CocoaPods) should also be cached:
- uses: actions/cache@v3
with:
path: ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-With all three caches in place, your pipeline's second run will be roughly 60–70% faster than the first, which translates to real time savings when you're pushing fixes on a Friday afternoon.
Automating Versioning
Manual version bumps are error-prone. Most teams don't increment versions by hand anymore—let your CI system do it.
Build Number from CI Run Number
Your pubspec.yaml contains a version line like version: 1.2.0+42. The part after the + is the build number. Increment it automatically using your CI environment variables:
- run: |
VERSION=$(grep "version:" pubspec.yaml | cut -d' ' -f2)
NEW_BUILD_NUMBER=${{ github.run_number }}
NEW_VERSION="${VERSION%+*}+${NEW_BUILD_NUMBER}"
sed -i "s/^version: .*/version: ${NEW_VERSION}/" pubspec.yaml
flutter pub getNow every build has a unique, incrementing build number tied to your CI run. App stores use this to ensure you never upload a build with a lower version number than the previous release.
Semantic Versioning for Releases
For semantic versioning (1.2.0), trigger a manual version bump when you tag a release in git. Tools like github-action-auto-commit can commit version changes back to your repository after a release, creating a clean audit trail.
Common Mistakes
Storing secrets in code: Never commit API keys, signing certificates, or credentials. Always use your CI system's secret management (GitHub Secrets, Codemagic environment variables, etc.). Even in a private repository, this is a bad habit.
No cache invalidation strategy: If a dependency is corrupted or you need a clean build, your cache might prevent a fix. Include a mechanism to manually flush caches or to re-run with cache disabled when needed.
Slow feedback loops: If your pipeline takes 20 minutes to run, developers won't wait—they'll keep pushing while it builds. Aim for 5–10 minutes for a full Android + iOS build. Parallelize jobs, use incremental builds, and cache aggressively.
Forgetting test coverage on CI: Your CI pipeline should fail if test coverage drops below a threshold, not just if tests fail. This prevents slow degradation of code quality over time.
Manual signing setup: If your CI signing process requires manual steps (uploading keys to a secure server, managing certificates outside your pipeline definition), you'll get signing failures at 11 PM when on-call engineers are asleep. Automate everything.
Key Takeaways
- Build a CI/CD pipeline with five stages: analyze, test, build (Android and iOS), sign, and distribute. Each stage is a gate that blocks broken code.
- Choose GitHub Actions for free and simplicity, Codemagic for mobile-specific features, and Fastlane to automate mobile builds and signing consistently.
- Cache Pub packages, Gradle dependencies, and CocoaPods to cut build times in half after the first run.
- Automate versioning by using CI run numbers for build numbers, so every artifact is uniquely identifiable.
- Store all secrets (keys, certificates, credentials) in your CI system's environment, never in code or configuration files.
- Run analysis and tests before building, and parallelize Android and iOS builds to minimize total pipeline time.






Comments
Loading comments…