CI/CD Pipeline Setup for Flutter
This skill configures automated build, test, and deployment pipelines for Flutter mobile apps. It produces three workflows that cover the full release lifecycle.
Three Workflows
Every Flutter CI/CD setup needs these three workflows, regardless of platform:
- PR Quality Gate — runs on pull requests, blocks merge if quality checks fail
- Android Pipeline — builds, signs, and distributes Android artifacts per environment
- iOS Pipeline — builds, signs, and distributes iOS artifacts per environment
Platform Selection
GitHub Actions + Custom Runners (recommended when you already have CI/CD infra):
- Read
references/github-actions.mdfor complete workflow files - If your org already uses custom runners (Blacksmith, Buildjet, self-hosted), use them for Flutter too
- Add Flutter via
subosito/flutter-action@v2— works on any Ubuntu runner - Path filtering for monorepo setups
- Same dashboard, secrets, and billing as your existing CI/CD
- Signing: manual (keystore in secrets, key.properties generated in CI)
GitHub Actions + GitHub-hosted runners (starting from scratch, budget-conscious):
- Read
references/github-actions.mdfor complete workflow files - 2000 free Linux minutes/month, 200 macOS minutes
- Larger action ecosystem but slower than custom runners
- Good for small projects without existing CI infra
Codemagic (recommended for iOS releases or teams without CI/CD):
- Read
references/codemagic.mdfor the completecodemagic.yamlconfiguration - Native Flutter support — no Docker images or custom setup
- Built-in code signing UI for both Android and iOS
- Mac Mini M2 instances for iOS builds — no macOS runner management
- 500 free build minutes/month on the free tier
- Best value when you need iOS builds without managing macOS infrastructure
Common Architecture
Both platforms share the same logical architecture. The differences are in YAML syntax and platform-specific features.
Environment Detection
Pipelines detect the target environment from the branch name:
| Branch | Environment | Distribution | Signing | Obfuscation |
|---|---|---|---|---|
develop |
dev | Firebase App Distribution | Debug or unsigned | No |
staging |
staging | Firebase App Distribution / TestFlight | Release signed | No |
production |
production | Google Play / App Store | Release signed | Yes |
Config Injection Pattern
Never commit secrets to the repository. Use template-based injection:
Template file (committed): lib/core/env/env_ci.dart
class EnvConfig {
static const String baseUrl = '<<BASE_URL>>';
static const String apiKey = '<<API_KEY>>';
static const String environment = '<<ENV_NAME>>';
}
Generator script (committed): scripts/generate_config.sh
#!/usr/bin/env bash
set -euo pipefail
ENV_NAME=${1:?Usage: $0 <env> <base-url> <api-key>}
BASE_URL=${2:?}
API_KEY=${3:?}
TEMPLATE="lib/core/env/env_ci.dart"
OUT="lib/core/env/env_ci.g.dart"
sed -e "s|<<BASE_URL>>|$BASE_URL|g" \
-e "s|<<API_KEY>>|$API_KEY|g" \
-e "s|<<ENV_NAME>>|$ENV_NAME|g" \
"$TEMPLATE" > "$OUT"
echo "Config generated for $ENV_NAME"
Generated file (in .gitignore): lib/core/env/env_ci.g.dart
Quality Checks Script
Reusable across both CI platforms: scripts/quality_checks.sh
#!/usr/bin/env bash
set -euo pipefail
echo "=== Formatting ==="
dart format --output=none --set-exit-if-changed .
echo "=== Static analysis ==="
flutter analyze --fatal-infos
echo "=== Tests ==="
flutter test --no-pub --coverage
echo "=== Coverage threshold ==="
THRESHOLD=${COVERAGE_THRESHOLD:-70}
if command -v lcov >/dev/null 2>&1; then
COVERAGE=$(lcov --summary coverage/lcov.info 2>&1 | grep 'lines' | awk '{print $2}' | sed 's/%//')
if [ "$(echo "$COVERAGE < $THRESHOLD" | bc)" -eq 1 ]; then
echo "Coverage $COVERAGE% is below threshold $THRESHOLD%"
exit 1
fi
echo "Coverage: $COVERAGE% (threshold: $THRESHOLD%)"
fi
Sentry Symbol Upload Script
For production builds with obfuscation: scripts/upload_symbols.sh
#!/usr/bin/env bash
set -euo pipefail
RELEASE=${1:?Usage: $0 <release-id>}
if ! command -v sentry-cli >/dev/null 2>&1; then
echo "sentry-cli not found, skipping symbol upload"
exit 0
fi
sentry-cli releases new "$RELEASE" || true
sentry-cli upload-dif build/symbols || true
sentry-cli releases finalize "$RELEASE" || true
echo "Symbols uploaded for $RELEASE"
Build Variations by Environment
Dev builds:
flutter build appbundle --release # Android
flutter build ios --release --no-codesign # iOS
Staging builds:
flutter build appbundle --release # Android (signed via key.properties)
flutter build ipa --release # iOS (signed via profile)
Production builds:
flutter build appbundle --release \
--obfuscate \
--split-debug-info=build/symbols # Android
flutter build ipa --release \
--obfuscate \
--split-debug-info=build/symbols # iOS
Distribution Channels
| Environment | Android | iOS |
|---|---|---|
| Dev | Firebase App Distribution | TestFlight (internal) or none |
| Staging | Firebase App Distribution | TestFlight (external) |
| Production | Google Play Store | App Store Connect |
Required Secrets
Organize secrets into logical groups in your CI platform:
Signing:
ANDROID_KEYSTORE_BASE64— base64-encoded.jksfileKEYSTORE_PASSWORD— keystore passwordKEY_ALIAS— key alias nameKEY_PASSWORD— key passwordIOS_CERTIFICATE_BASE64— base64-encoded.p12distribution certificateIOS_CERTIFICATE_PASSWORD— certificate passwordIOS_PROVISIONING_PROFILE_BASE64— base64-encoded.mobileprovision
Distribution:
FIREBASE_TOKEN— fromfirebase login:ciFIREBASE_ANDROID_APP_ID— Firebase console app IDFIREBASE_IOS_APP_ID— Firebase console app IDFIREBASE_GROUPS— comma-separated tester group namesGOOGLE_PLAY_SERVICE_ACCOUNT_JSON— Play Console service accountAPP_STORE_CONNECT_API_KEY_ID— App Store Connect key IDAPP_STORE_CONNECT_API_ISSUER_ID— issuer IDAPP_STORE_CONNECT_API_KEY_CONTENT—.p8key content
Environment:
STAGING_BASE_URL,STAGING_API_KEYPROD_BASE_URL,PROD_API_KEY
Monitoring:
SENTRY_AUTH_TOKEN,SENTRY_ORG,SENTRY_PROJECT
Implementation
After understanding the architecture above, read the reference file for the chosen platform:
- Codemagic:
references/codemagic.md— completecodemagic.yamlwith all three workflows - GitHub Actions:
references/github-actions.md— complete.github/workflows/*.ymlfiles
Both references produce the same outcome: signed artifacts distributed to the right channel per environment.