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
- What CI is — Continuous Integration: every code push triggers an automated pipeline that runs lint, tests, and builds. The goal is to detect integration problems immediately, not days later during a manual release.
- What CD is — Continuous Delivery/Deployment: the pipeline automatically delivers build artifacts (APK, IPA) to distribution channels (Play Store internal track, TestFlight, Firebase App Distribution) after CI passes.
- CI/CD platforms for Flutter — GitHub Actions (free tier, YAML, tightly integrated with GitHub), Codemagic (Flutter-specific, easy setup, free tier), Bitrise (mobile-focused, visual pipeline editor). Most teams use GitHub Actions or Codemagic.
- Pipeline stages — Typical Flutter pipeline: checkout → install Flutter → get packages → lint/analyze → unit tests → widget tests → build APK/IPA → upload artifact → (optional) deploy to distribution. Each stage gates the next.
- Environment variables and secrets — API keys, signing credentials, Firebase configs must NEVER be committed to git. Store them as CI secrets (GitHub Secrets, Codemagic environment groups). Access them in YAML as ${{ secrets.MY_SECRET }}.
- Artifacts — The output files from a CI build — APKs, IPAs, test coverage reports, screenshots. Artifacts are stored in CI for download or passed to deployment stages. Without artifacts, you cannot distribute a CI-built app.
- GitHub Actions for Flutter — Create .github/workflows/ci.yml. Key actions: actions/checkout@v4, subosito/flutter-action@v2. Triggers: on push to main and on pull_request. Jobs define the steps that run on ubuntu-latest or macos-latest runners.
- Matrix builds — Test across multiple Flutter versions or OS combinations using a matrix strategy. Ensures your app works on stable and beta channels and on both Android and iOS.
- Fail fast — Lint and analyze should run before tests. Tests before builds. If lint fails, do not waste 10 minutes building. The goal is the fastest possible signal that something is wrong.
- Branch protection rules — Configure GitHub to require CI to pass before merging a PR. This enforces that no broken code enters main. Combined with required reviewers, this is the minimum viable quality gate for a production team.
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 --releaseLine-by-line walkthrough
- 1. on: push: branches: [main, develop] — triggers the pipeline on every push to these branches
- 2. on: pull_request: branches: [main] — also triggers on PRs targeting main, enabling PR status checks
- 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. flutter analyze --fatal-infos — fails the pipeline on any analysis warning, not just errors; enforces code quality
- 5. flutter test --coverage — runs all tests and generates coverage/lcov.info
- 6. needs: test — the release build job only starts after the test job passes; enforces quality gates
- 7. echo ${{ secrets.KEYSTORE_BASE64 }} | base64 --decode > keystore.jks — decodes a base64-encoded keystore stored as a CI secret into a file
- 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 testNeed 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
- GitHub Actions for Flutter (Flutter Docs)
- subosito/flutter-action (GitHub)
- Codemagic for Flutter (Codemagic Docs)
- GitHub Encrypted Secrets (GitHub Docs)