CI/CD & DevOps
Irbisa · cheatsheetSeptember 13, 2026

CI/CD & DevOps

GitHub Actions, Fastlane, Firebase Distribution, flavors

Senior Developer20 itemscompressed for a skim
  1. 01

    How do you set up CI/CD for Flutter with GitHub Actions?

    Hard

    A 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]
    …
  2. 02

    What are Flutter flavors and how do you set up dev/staging/production environments?

    Hard

    Flavors 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');
    …
  3. 03

    How do you sign Android and iOS builds in CI without leaking the keys?

    Medium

    Signing 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
    …
  4. 04

    How do you manage app versioning in Flutter — version, build number, semver?

    Medium

    Three 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
    …
  5. 05

    How do you automate Play Store and App Store releases from CI?

    Medium

    Both 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
    …
  6. 06

    How do you set up beta distribution with Firebase App Distribution and TestFlight?

    Medium

    Beta 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]
    …
  7. 07

    How do you avoid flaky integration tests in Flutter CI?

    Hard

    Integration 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
    …
  8. 08

    What is fastlane and what does it actually buy you over a hand-rolled CI script?

    Medium

    fastlane 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')
    …
  9. 09

    How do you do staged rollouts and quick rollbacks safely?

    Hard

    Mobile 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. 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?

    Medium

    Read 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. 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?

    Hard

    A 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. 12

    Production is broken and the fix is three lines of Dart. What are your options for shipping it today?

    Medium

    Rank 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. 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

    --flavor only 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. 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?

    Medium

    Both 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. 15

    riverpod_lint flags a mistake in the IDE, CI stays green, and the bug ships — what is a flutter analyze step in your pipeline actually checking?

    Medium

    Only the analyzer's own diagnostics: flutter analyze does 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 run dart run custom_lint as 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. 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?

    Medium

    The release binary itself: flutter test executes 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. 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?

    Hard

    The iOS project in the repository is only half of a project: flutter pub get writes ios/Flutter/Generated.xcconfig and .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 to pod install, xcodebuild or 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. 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?

    Hard

    Filter 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. 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?

    Hard

    Nothing 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 .xctestrun bundle 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. 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?

    Hard

    That 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
    …