
Modern app development requires automated testing and deployment pipelines. Manual testing and deployment are time-consuming, error-prone, and don't scale with growing teams.
This guide will show you how to set up robust CI/CD pipelines for Flutter apps using GitHub Actions. You'll learn to automate testing, code quality checks, builds, and deployments to both Google Play Store and Apple App Store.
GITHUB ACTIONS FOUNDATION
GitHub Actions provides free CI/CD for public repositories and generous limits for private ones. It integrates seamlessly with your Flutter development workflow.
# .github/workflows/flutter_ci.yml
name: Flutter CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
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.24.0'
channel: 'stable'
cache: true
- name: Install dependencies
run: flutter pub get
- name: Verify formatting
run: dart format --output=none --set-exit-if-changed .
- name: Analyze project source
run: flutter analyze --fatal-infos
- name: Run tests
run: flutter test --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
fail_ci_if_error: true
build_android:
needs: test
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.24.0'
channel: 'stable'
cache: true
- name: Install dependencies
run: flutter pub get
- name: Build Android APK
run: flutter build apk --release
- name: Build Android App Bundle
run: flutter build appbundle --release
- name: Upload APK artifact
uses: actions/upload-artifact@v3
with:
name: android-apk
path: build/app/outputs/flutter-apk/app-release.apk
- name: Upload AAB artifact
uses: actions/upload-artifact@v3
with:
name: android-aab
path: build/app/outputs/bundle/release/app-release.aab
build_ios:
needs: test
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
channel: 'stable'
cache: true
- name: Install dependencies
run: flutter pub get
- name: Build iOS (no signing)
run: flutter build ios --release --no-codesign
- name: Upload iOS artifact
uses: actions/upload-artifact@v3
with:
name: ios-build
path: build/ios/iphoneos/Runner.app✅ WORKFLOW BENEFITS
- • Runs on every push and PR
- • Parallel Android and iOS builds
- • Automated code quality checks
- • Coverage reporting
🎯 KEY FEATURES
- • Flutter version caching
- • Dependency caching
- • Artifact uploads
- • Multi-platform support
COMPREHENSIVE TESTING PIPELINE
A robust testing pipeline includes unit tests, widget tests, integration tests, and code quality checks. This ensures your app is reliable before deployment.
# .github/workflows/comprehensive_testing.yml
name: Comprehensive Testing
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
quality_checks:
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.24.0'
channel: 'stable'
cache: true
- name: Install dependencies
run: flutter pub get
# Code formatting check
- name: Check code formatting
run: dart format --output=none --set-exit-if-changed .
# Static analysis
- name: Run static analysis
run: flutter analyze --fatal-infos --fatal-warnings
# Import sorting check
- name: Check import sorting
run: |
flutter pub global activate import_sorter
import_sorter --check-only
# Unused dependencies check
- name: Check for unused dependencies
run: |
flutter pub global activate dependency_validator
dependency_validator
# Security audit
- name: Security audit
run: |
flutter pub deps --json > deps.json
# Add security scanning tool here
unit_tests:
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.24.0'
channel: 'stable'
cache: true
- name: Install dependencies
run: flutter pub get
- name: Run unit tests
run: |
flutter test \
--coverage \
--test-randomize-ordering-seed random \
--reporter expanded
- name: Generate coverage report
run: |
sudo apt-get update
sudo apt-get install lcov
genhtml coverage/lcov.info -o coverage/html
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
fail_ci_if_error: true
verbose: true
widget_tests:
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.24.0'
channel: 'stable'
cache: true
- name: Install dependencies
run: flutter pub get
- name: Run widget tests
run: flutter test test/widget_test/
integration_tests:
runs-on: macos-latest
strategy:
matrix:
device:
- "iPhone 14 Pro Simulator (16.0)"
- "iPad Pro (11-inch) (4th generation) Simulator (16.0)"
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
channel: 'stable'
cache: true
- name: Install dependencies
run: flutter pub get
- name: Start iOS Simulator
run: |
xcrun simctl list devices
xcrun simctl boot "${{ matrix.device }}" || true
- name: Run integration tests
run: |
flutter test integration_test/ \
--device-id="${{ matrix.device }}"
- name: Upload test results
uses: actions/upload-artifact@v3
if: failure()
with:
name: integration-test-results
path: test_results/🧪 Testing Strategy:
- • Unit tests for business logic (70% of tests)
- • Widget tests for UI components (20% of tests)
- • Integration tests for user flows (10% of tests)
- • Code coverage threshold enforcement
AUTOMATED DEPLOYMENT PIPELINE
Deploy your Flutter app to Google Play Store and Apple App Store automatically when tests pass. This includes app signing, version management, and release notes.
# .github/workflows/deploy.yml
name: Deploy to App Stores
on:
push:
tags:
- 'v*' # Deploy when version tags are pushed
jobs:
deploy_android:
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.24.0'
channel: 'stable'
cache: true
- name: Install dependencies
run: flutter pub get
# Setup Android signing
- name: Setup Android signing
run: |
echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app/keystore.jks
echo "storeFile=keystore.jks" >> android/key.properties
echo "keyAlias=$ANDROID_KEY_ALIAS" >> android/key.properties
echo "storePassword=$ANDROID_STORE_PASSWORD" >> android/key.properties
echo "keyPassword=$ANDROID_KEY_PASSWORD" >> android/key.properties
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
- name: Build signed App Bundle
run: flutter build appbundle --release
- name: Deploy to Google Play Console
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
packageName: com.onon.yourapp
releaseFiles: build/app/outputs/bundle/release/app-release.aab
track: production
status: completed
inAppUpdatePriority: 2
userFraction: 0.33
whatsNewDirectory: distribution/whatsnew
mappingFile: build/app/outputs/mapping/release/mapping.txt
deploy_ios:
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
channel: 'stable'
cache: true
- name: Install dependencies
run: flutter pub get
# Setup iOS certificates and provisioning profiles
- name: Install Apple Certificate
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
p12-password: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
- name: Install Provisioning Profile
uses: apple-actions/download-provisioning-profiles@v1
with:
bundle-id: com.onon.yourapp
issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
- name: Build iOS app
run: |
flutter build ios --release --no-codesign
cd ios
xcodebuild -workspace Runner.xcworkspace \
-scheme Runner \
-configuration Release \
-destination generic/platform=iOS \
-archivePath $PWD/build/Runner.xcarchive \
archive
xcodebuild -exportArchive \
-archivePath $PWD/build/Runner.xcarchive \
-exportOptionsPlist ExportOptions.plist \
-exportPath $PWD/build
- name: Deploy to TestFlight
uses: apple-actions/upload-testflight-build@v1
with:
app-path: ios/build/Runner.ipa
issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
create_release:
needs: [deploy_android, deploy_ios]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: |
🚀 New release deployed to app stores!
## What's New
- Check CHANGELOG.md for detailed changes
## Deployment Status
- ✅ Android: Google Play Store
- ✅ iOS: TestFlight & App Store
draft: false
prerelease: false🚀 Deployment Features:
- • Automated app signing for both platforms
- • Staged rollout for risk mitigation
- • Automatic release notes from changelogs
- • Version management and tagging
ENVIRONMENT & SECRET MANAGEMENT
Proper secret management and environment configuration ensure secure deployments while supporting different stages like development, staging, and production.
# .github/workflows/multi_environment.yml
name: Multi-Environment Deployment
on:
push:
branches:
- develop # Deploy to staging
- main # Deploy to production
pull_request:
branches:
- main # Test on PR
jobs:
determine_environment:
runs-on: ubuntu-latest
outputs:
environment: ${{ steps.env.outputs.environment }}
api_url: ${{ steps.env.outputs.api_url }}
steps:
- name: Determine environment
id: env
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "environment=production" >> $GITHUB_OUTPUT
echo "api_url=https://api.yourapp.com" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == "refs/heads/develop" ]]; then
echo "environment=staging" >> $GITHUB_OUTPUT
echo "api_url=https://staging-api.yourapp.com" >> $GITHUB_OUTPUT
else
echo "environment=test" >> $GITHUB_OUTPUT
echo "api_url=https://test-api.yourapp.com" >> $GITHUB_OUTPUT
fi
build_and_test:
needs: determine_environment
runs-on: ubuntu-latest
environment: ${{ needs.determine_environment.outputs.environment }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
channel: 'stable'
cache: true
- name: Create environment config
run: |
cat > lib/config/environment.dart << EOF
class Environment {
static const String apiUrl = '${{ needs.determine_environment.outputs.api_url }}';
static const String environment = '${{ needs.determine_environment.outputs.environment }}';
static const String apiKey = '${{ secrets.API_KEY }}';
static const bool isProduction = ${{ needs.determine_environment.outputs.environment == 'production' }};
}
EOF
- name: Install dependencies
run: flutter pub get
- name: Run tests
run: flutter test
- name: Build for environment
run: |
if [[ "${{ needs.determine_environment.outputs.environment }}" == "production" ]]; then
flutter build apk --release --flavor production -t lib/main_production.dart
elif [[ "${{ needs.determine_environment.outputs.environment }}" == "staging" ]]; then
flutter build apk --release --flavor staging -t lib/main_staging.dart
else
flutter build apk --debug --flavor development -t lib/main_development.dart
fi
# GitHub Repository Secrets Setup:
# ANDROID_KEYSTORE_BASE64 - Base64 encoded Android keystore
# ANDROID_KEY_ALIAS - Android key alias
# ANDROID_STORE_PASSWORD - Android keystore password
# ANDROID_KEY_PASSWORD - Android key password
# IOS_CERTIFICATE_BASE64 - Base64 encoded iOS certificate
# IOS_CERTIFICATE_PASSWORD - iOS certificate password
# APPSTORE_ISSUER_ID - App Store Connect issuer ID
# APPSTORE_KEY_ID - App Store Connect key ID
# APPSTORE_PRIVATE_KEY - App Store Connect private key
# GOOGLE_PLAY_SERVICE_ACCOUNT_JSON - Google Play Console service account
# API_KEY - Your app's API key
# FIREBASE_CONFIG_ANDROID - Firebase config for Android
# FIREBASE_CONFIG_IOS - Firebase config for iOS
# Environment-specific secrets:
# For each environment (production, staging, test), create:
# - API_URL
# - DATABASE_URL
# - ANALYTICS_KEY
# - Feature flags configuration🔐 Security Best Practices:
- • Store all sensitive data in GitHub Secrets
- • Use environment-specific configurations
- • Rotate secrets regularly
- • Limit secret access to necessary workflows only
CI/CD BEST PRACTICES
Fast Feedback Loop: Keep build times under 10 minutes. Use caching, parallel jobs, and incremental builds to speed up the pipeline.
Fail Fast: Run static analysis and unit tests first. Don't waste time on expensive integration tests if basic quality checks fail.
Staged Deployments: Deploy to staging first, then production. Use feature flags and gradual rollouts to minimize risk.
Monitor Everything: Track build success rates, test coverage, deployment frequency, and lead time for changes.
Rollback Strategy: Always have a plan to quickly rollback deployments. Keep previous versions available and test rollback procedures.


