skip to content

Pipeline Automation

A Flutter pipeline pins the SDK, caches pub, runs analyze and tests, then builds and signs both platforms from CI secrets. Interviewers ask where signing keys live and how builds reach testers.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

4

In a Flutter CI pipeline run on every merge, which Flutter and Dart commands form the stages, and what fails each one?

level: juniorimportance: must knowfreq 55%

answer

  1. cheapest checks first
  2. pub get with --enforce-lockfile
  3. format with --set-exit-if-changed
  4. flutter analyze fails on infos
  5. test, then build per platform

basics

~20 s

A Flutter pipeline runs flutter pub get --enforce-lockfile, dart format --output=none --set-exit-if-changed, flutter analyze, flutter test, then flutter build appbundle and flutter build ipa; each exits non-zero on drift, formatting, analyzer issues, failures or build errors.

solid answer

~40 s

The usual order goes cheapest first. `flutter pub get --enforce-lockfile` fails if `pubspec.lock` does not exactly resolve `pubspec.yaml` or a package's content hash changed. `dart format --output=none --set-exit-if-changed .` fails when any file would be reformatted. `flutter analyze` fails on warnings **and on info-level lints by default**, since its `--fatal-infos` defaults to true, unlike `dart analyze`. `flutter test` fails on any failing test and can write `coverage/lcov.info` with `--coverage`. Finally `flutter build appbundle` on a Linux or macOS runner and `flutter build ipa` on macOS produce the signed artefacts. For the car-wash booking app, the build stages run only after the checks pass, so a lint does not cost a 20-minute iOS build.

code

bash · 10 lines
bash
set -e
flutter --version
flutter pub get --enforce-lockfile
dart format --output=none --set-exit-if-changed .
flutter analyze
flutter test --coverage
flutter build appbundle
# on the macOS job:
flutter build ipa
test -n "$(ls build/ios/ipa/*.ipa 2>/dev/null)"

go deeper

for a junior

Recall the stage order: pub get, format check, analyze, test, then build for each platform, and that each command's exit code gates the next.

for a middle

Explain the exact flags: --enforce-lockfile, --output=none with --set-exit-if-changed, flutter analyze's fatal-infos default, and --coverage output.

for a senior

Design the job layout: cheap checks shared, Android and iOS builds in parallel on the right runners, and guards for silent failures such as a missing IPA.

for a principal

Decide which checks gate merges versus nightly runs, and how analyzer and coverage policies are owned across teams without slowing every merge.

## The car-wash booking app pipeline A car-wash booking app has a Flutter client for Android and iOS. The team wants every merge to `main` to produce signed builds of both platforms, but only if the code is healthy. The pipeline is a chain of Flutter and Dart commands, each of which **exits non-zero** on a specific problem; the CI system only has to stop at the first failure. ## The stages, cheapest first 1. **Resolve dependencies exactly.** `flutter pub get --enforce-lockfile` refuses to change the lock file: it fails if `pubspec.lock` does not exactly specify a valid resolution of `pubspec.yaml`, or if a hosted package's content hash changed. The pub docs call this useful for CI and production deploys. 2. **Check formatting.** `dart format --output=none --set-exit-if-changed .` writes nothing and returns a non-zero exit code if any file would change. 3. **Static analysis.** `flutter analyze` runs the analyzer with the project's `analysis_options.yaml`. 4. **Tests.** `flutter test` runs unit and widget tests; `--coverage` writes `coverage/lcov.info` for a coverage report. 5. **Build.** `flutter build appbundle` for Google Play and `flutter build ipa` for App Store Connect, each signed with credentials the pipeline injects. ## What fails each stage | Stage | Command | Fails when | |---|---|---| | Dependencies | `flutter pub get --enforce-lockfile` | lock file does not match `pubspec.yaml`, or a content hash changed | | Format | `dart format --output=none --set-exit-if-changed .` | any file is not formatted | | Analyze | `flutter analyze` | any warning or info-level issue, by default | | Test | `flutter test` | any test fails or fails to load | | Build | `flutter build appbundle` / `flutter build ipa` | compile, Gradle or Xcode errors, missing signing | ## The analyzer default that surprises people `flutter analyze` has `--fatal-infos` and `--fatal-warnings` both **defaulting to true**, so a single info-level lint, such as a preferred-const suggestion from `flutter_lints`, fails the stage. `dart analyze` treats warnings as fatal by default but not infos. Teams moving a project between the two commands are often surprised in one direction or the other. Pass `--no-fatal-infos` to `flutter analyze` only as a deliberate policy decision. ## Why the order matters - Formatting and analysis take seconds; an iOS archive takes many minutes on a more expensive macOS machine. - Tests run before builds so a broken merge never produces an artefact that could reach testers. - The Android build can run on Linux, while `flutter build ipa` requires macOS, so the two builds usually run as parallel jobs after the shared checks. - Keep the commands identical to what developers run locally, so a red pipeline can be reproduced with the same command. ## Checks that are easy to forget - **Generated code.** If the app uses code generation, the pipeline must run it before `flutter analyze`, or the analyzer reports missing files. - **`flutter build ipa` returns success when only the export fails**, so add a check that `build/ios/ipa` contains a file before uploading. - **Log the toolchain.** `flutter --version` at the top of the job records the exact Flutter and Dart versions each run used. ## Splitting the work across jobs For the car-wash app the checks and the two builds map naturally onto three jobs: - **checks** on Linux: `pub get`, format, analyze, test. Fast and cheap, and the only job that runs on pull requests. - **android** on Linux, after checks: `flutter build appbundle`, signed from CI secrets, uploaded as an artefact. - **ios** on macOS, after checks: `flutter build ipa`, signed through a temporary keychain, uploaded as an artefact. Each build job repeats `flutter pub get --enforce-lockfile` because jobs start on fresh machines. Both build jobs pass the same `--build-number`, so the Android and iOS builds from one merge share an identifier. Delivery to testers or stores is a further job that consumes those artefacts rather than rebuilding them, so what testers install is exactly what the checks approved. ## Common mistakes - Running `flutter pub get` without `--enforce-lockfile`, so the pipeline silently resolves newer packages than the developer tested. - Running `dart format .` in CI, which rewrites files and always passes. - Building before testing, which wastes macOS minutes on commits that will be rejected anyway.

  • A lint that is only an info in the IDE fails the CI analyze stage; why?
    `flutter analyze` treats info-level issues as fatal by default because its `--fatal-infos` flag defaults to true. Either fix the lint, change its severity in `analysis_options.yaml`, or pass `--no-fatal-infos` as an explicit team decision.
  • Why use --enforce-lockfile in CI instead of plain flutter pub get?
    Plain `pub get` may update `pubspec.lock` when it no longer matches `pubspec.yaml`, so CI could build with dependencies nobody tested. `--enforce-lockfile` fails instead, and also fails when a hosted package's content hash changed.

saying these in an interview costs you the question

  • Running dart format . in CI is enough to enforce formatting.
  • flutter analyze only fails on errors, never on lints.
  • Plain flutter pub get in CI always reproduces the developer's dependency versions.
  • Both platform builds can run on one Linux runner.
  • It is fine to build and upload first and run tests afterwards.
open as a page

For a Flutter car-wash booking app built on every merge, how does CI get the Android upload keystore and the iOS distribution certificate without committing them?

level: seniorimportance: must knowfreq 50%

basics

~20 s

Store the keystore and certificate as encrypted CI secrets, materialise them only during the job: decode the base64 keystore and write key.properties for Gradle; import the .p12 and profiles into a temporary keychain on the macOS runner, then sign.

open as a page

What does a Flutter-aware CI service like Codemagic or Bitrise handle that GitHub Actions with subosito/flutter-action leaves to you?

level: middleimportance: should knowfreq 35%

basics

~20 s

Codemagic and Bitrise are all-in-one Flutter CI services: they select the Flutter version, provide macOS machines, manage signing files and publish to stores or Firebase App Distribution. With GitHub Actions and subosito/flutter-action you script those steps yourself, often with fastlane.

open as a page

In Flutter CI, how do you pin the SDK and reuse the pub cache, and why is pubspec's environment: flutter constraint not a pin?

level: middleimportance: should knowfreq 40%

basics

~20 s

Pin the Flutter SDK by installing an exact release in the pipeline, for example through FVM or an exact version on the setup step; environment: flutter only enforces a lower bound. Cache the pub cache ($HOME/.pub-cache or PUB_CACHE) between runs.

open as a page