CI/CD & DevOps
GitHub Actions, Fastlane, Firebase Distribution, flavors
- 01
How do you set up CI/CD for Flutter with GitHub Actions?
HardA Flutter pipeline splits into a CI half that runs on every pull request and a CD half that runs only on protected branches or version tags.
# .github/workflows/ci.yml name: Flutter CI on: pull_request: branches: [main, develop] … - 02
What are Flutter flavors and how do you set up dev/staging/production environments?
HardFlavors are build variants compiled from one codebase — the same Dart source shipped as separate, side-by-side installable apps for dev, staging and production.
// ── Dart side: environment config via --dart-define ── // Run: flutter run --dart-define=FLAVOR=dev // Or: flutter run --dart-define-from-file=config/dev.json class AppConfig { static const flavor = String.fromEnvironment('FLAVOR', defaultValue: 'dev'); … - 03
How do you sign Android and iOS builds in CI without leaking the keys?
MediumSigning is the single highest-stakes secret in mobile CI.
# .github/workflows/release.yml — Android signing jobs: build-android: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 … - 04
How do you manage app versioning in Flutter — version, build number, semver?
MediumThree numbers travel together in every build: the public version, the build number, and the semantic intent of the change.
# pubspec.yaml version: 1.4.2+87 # major.minor.patch+build_number # ↓ ↓ # Android versionName versionCode = 87 # iOS CFBundleShortVer CFBundleVersion … - 05
How do you automate Play Store and App Store releases from CI?
MediumBoth stores expose APIs for unattended uploads.
# fastlane/Fastfile default_platform(:android) platform :android do desc 'Upload AAB to Play Console internal track' lane :internal do … - 06
How do you set up beta distribution with Firebase App Distribution and TestFlight?
MediumBeta channels are how you get a build into the hands of real users without going through the public store review and ramp.
# .github/workflows/beta.yml name: Beta on: push: branches: [main] … - 07
How do you avoid flaky integration tests in Flutter CI?
HardIntegration tests on emulators in CI are notorious for flakes that block green builds for reasons unrelated to your code.
void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); setUp(() async { await wipeStorage(); // ✅ deterministic starting state fakeServer.reset(); // ✅ in-process fake API … - 08
What is fastlane and what does it actually buy you over a hand-rolled CI script?
Mediumfastlane is a Ruby tool that turns the long, error-prone sequences of cert/profile/build/upload commands into named lanes.
# fastlane/Fastfile default_platform(:ios) before_all do ensure_git_status_clean ensure_git_branch(branch: 'main') … - 09
How do you do staged rollouts and quick rollbacks safely?
HardMobile releases ship to thousands of devices in minutes once you push the button.
# Play Console — staged rollout via fastlane lane :promote do |opts| rollout = opts[:rollout] || '0.05' # 5% by default upload_to_play_store( track: 'production', track_promote_to: nil, # already on production from internal … - 10
Every pull request waits 40 minutes on CI. Where does the time actually go in a Flutter pipeline, and what do you cut first?
MediumRead the per-step timings before changing anything — in almost every slow Flutter pipeline the answer is cold caches plus a macOS runner doing work that a Linux runner could have done.
name: ci on: [pull_request, push] concurrency: # cancel superseded runs on the same ref group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true … - 11
Golden tests pass on your Mac and fail on the Linux CI runner with a few pixels of difference. What is going on and how do you make them trustworthy?
HardA golden is a rendered image, so it encodes the machine that produced it — its fonts, its text shaping, its engine build — not only your widget.
// test/flutter_test_config.dart — runs once for every test in this tree import 'dart:async'; import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; … - 12
Production is broken and the fix is three lines of Dart. What are your options for shipping it today?
MediumRank the options by how much of the release pipeline each one skips, because the fastest fixes never involve a new binary at all.
#!/usr/bin/env bash set -euo pipefail # 1) Kill switch — no build, no review, effective immediately curl -sS -X PATCH "$CONFIG_API/flags/new_checkout" \ -H "Authorization: Bearer $CONFIG_TOKEN" \ … - 13
The build signed for production ships with the staging API URL and nobody changed the code — where does a flavored Flutter build lose its Dart config?
Medium--flavoronly selects the native variant; the Dart side learns nothing from it unless the same command also carries the matching--dart-define, so the two halves can disagree silently.// lib/config/app_config.dart class AppConfig { // Const, and no defaultValue: a missing define is empty, never "dev". static const flavor = String.fromEnvironment('FLAVOR'); static const apiUrl = String.fromEnvironment('API_URL'); static const isComplete = … - 14
Your repo generates .freezed.dart and .g.dart with build_runner — do those files belong in git, and what does CI have to do in either case?
MediumBoth policies work, but only if CI enforces the one you picked: committed generated code needs a drift check, ignored generated code needs codegen before every analyze, test and build.
jobs: codegen: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 … - 15
riverpod_lint flags a mistake in the IDE, CI stays green, and the bug ships — what is a
flutter analyzestep in your pipeline actually checking?MediumOnly the analyzer's own diagnostics:
flutter analyzedoes not load analyzer plugins, so every custom_lint-based rule you installed — riverpod_lint, bloc_lint, your own — exists in the IDE and nowhere in CI until you rundart run custom_lintas a separate step.jobs: quality: runs-on: ubuntu-latest # never spend macOS minutes on lint steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 … - 16
Every test is green and the debug build works, yet the release APK from CI has no network and crashes on a plugin call — what did the pipeline never run?
MediumThe release binary itself:
flutter testexecutes your Dart in a JIT VM on the host with a headless engine, so it never produces an AOT snapshot, never merges the release manifest and never runs R8 — which is exactly where those three failures live.#!/usr/bin/env bash set -euo pipefail # 1) The artifact CI must actually produce — `flutter test` never builds one. flutter build apk --release --flavor prod \ --dart-define-from-file=config/prod.json \ … - 17
The iOS job dies with "Generated.xcconfig must exist" on a runner that has the same Xcode as your Mac — what is a fresh checkout missing, and in what order should the job run?
HardThe iOS project in the repository is only half of a project:
flutter pub getwritesios/Flutter/Generated.xcconfigand.flutter-plugins-dependencies, the Podfile reads that xcconfig to find the SDK and the plugin pods, and both files are gitignored — so a clean checkout that jumps straight topod install,xcodebuildor fastlane has nothing to read.#!/usr/bin/env bash set -euo pipefail sudo xcode-select -s /Applications/Xcode_16.4.app # images ship several # WRONG — a clean checkout has no Generated.xcconfig yet: … - 18
A monorepo with an app and fifteen packages runs every test for a one-line change in one of them — how do you make CI test only what that change can break?
HardFilter by the dependency graph, not by the folder someone touched: Melos can list the packages changed since a git ref and pull in every package that depends on them, and that set is what CI analyses, tests and builds.
# melos.yaml — Dart 3.6+ resolves these as a single pub workspace name: acme packages: - apps/* - packages/* … - 19
Your integration_test suite passes on the CI emulator and the team wants it on twenty real devices in Firebase Test Lab — what actually has to change?
HardNothing in the Dart tests and everything about how they are packaged: a device farm has no host machine running
flutter test, so you hand it the two APKs that Gradle builds or the.xctestrunbundle Xcode builds, and your suite runs as an ordinary instrumentation test.#!/usr/bin/env bash set -euo pipefail TEST=integration_test/app_test.dart # ---- Android ------------------------------------------------------------- # The farm runs a plain instrumentation test, so Gradle — not `flutter test` — … - 20
Your internal packages are published to pub.dev from CI using a credentials file kept in a repository secret — what is wrong with that, and what replaced it?
HardThat file holds a long-lived refresh token for the whole publisher account, so anyone who can read the secret — or land a workflow that prints it — can publish any package you own; pub.dev's automated publishing replaces it with a short-lived OIDC token minted for one workflow run.
name: publish on: pull_request: push: tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] # must match the pattern set on pub.dev …