skip to content

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.