Lesson 37 of 77 intermediate

CI/CD Basics: Build Pipelines, Artifacts & Secrets

Automating builds, tests, and deployments — from push to production without manual steps

Open interactive version (quiz + challenge)

Real-world analogy

CI/CD is like a factory assembly line for your app. Every time you add a new part (commit), the conveyor belt automatically runs quality checks (lint, test), assembles the product (build), and ships it to the store (deploy). Without it, every developer manually assembles and ships — slowly and inconsistently.

What is it?

CI/CD (Continuous Integration / Continuous Delivery) automates the process of validating, building, and distributing your Flutter app every time code is pushed. It replaces manual build and test steps with a reliable, repeatable pipeline that runs in the cloud.

Real-world relevance

On a SaaS collaboration app, adding a GitHub Actions pipeline caught that a developer had accidentally committed a hardcoded API key in a config file. The secret scanning step flagged it before the PR was merged, preventing a potential security breach.

Key points

Code example

# .github/workflows/flutter_ci.yml
name: Flutter CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    name: Lint, Test & Build
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.19.0'
          channel: 'stable'
          cache: true

      - name: Install dependencies
        run: flutter pub get

      - name: Run lint / analyze
        run: flutter analyze --fatal-infos

      - name: Run unit and widget tests
        run: flutter test --coverage

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          file: coverage/lcov.info

      - name: Build Android APK (debug)
        run: flutter build apk --debug

      - name: Upload APK artifact
        uses: actions/upload-artifact@v4
        with:
          name: debug-apk
          path: build/app/outputs/flutter-apk/app-debug.apk
          retention-days: 7

  build-release:
    name: Build Release APK
    runs-on: ubuntu-latest
    needs: test  # Only runs if 'test' job passes
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.19.0'

      - name: Decode keystore
        run: |
          echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks

      - name: Create key.properties
        run: |
          cat > android/key.properties <<EOF
          storePassword=${{ secrets.KEY_STORE_PASSWORD }}
          keyPassword=${{ secrets.KEY_PASSWORD }}
          keyAlias=${{ secrets.KEY_ALIAS }}
          storeFile=keystore.jks
          EOF

      - name: Build release APK
        run: flutter build apk --release

Line-by-line walkthrough

  1. 1. on: push: branches: [main, develop] — triggers the pipeline on every push to these branches
  2. 2. on: pull_request: branches: [main] — also triggers on PRs targeting main, enabling PR status checks
  3. 3. uses: subosito/flutter-action@v2 with cache: true — downloads and caches the Flutter SDK, reducing run time from ~3 min to ~20 sec
  4. 4. flutter analyze --fatal-infos — fails the pipeline on any analysis warning, not just errors; enforces code quality
  5. 5. flutter test --coverage — runs all tests and generates coverage/lcov.info
  6. 6. needs: test — the release build job only starts after the test job passes; enforces quality gates
  7. 7. echo ${{ secrets.KEYSTORE_BASE64 }} | base64 --decode > keystore.jks — decodes a base64-encoded keystore stored as a CI secret into a file
  8. 8. cat > android/key.properties — writes signing config from secrets into a file that Gradle reads; the file never exists in the repo

Spot the bug

# .github/workflows/ci.yml
name: Flutter CI

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
      - run: flutter pub get
      - run: flutter build apk --release
      - run: flutter test
Need a hint?
There are two ordering problems and one security problem.
Show answer
Bug 1: flutter build apk --release runs BEFORE flutter test — the pipeline builds a release APK even when tests would fail. Tests should always run before builds. Bug 2: flutter analyze is missing entirely — lint/analyze should run between pub get and test. Bug 3: Building a release APK on CI without signing credentials will fail or produce an unsigned APK. Either use --debug for CI validation builds or add keystore secrets. Correct order: checkout → flutter-action → pub get → analyze → test → build.

Explain like I'm 5

Imagine every time you add a brick to a Lego building, a robot automatically checks: is the brick the right colour? Does it fit? Does the whole building still stand? If yes, the robot adds it to the official building. If no, the robot says 'stop, fix this first.' CI/CD is that robot for your app code.

Fun fact

GitHub Actions has over 15,000 community-built actions in the marketplace. The subosito/flutter-action alone has been used in over 50,000 repositories and handles Flutter SDK caching, significantly reducing pipeline run time from 3 minutes to under 30 seconds.

Hands-on challenge

Write a GitHub Actions YAML workflow (or describe it in detail) that: (1) runs on PRs to main, (2) runs flutter analyze and flutter test, (3) blocks merge if either fails, (4) stores the test coverage report as an artifact.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery