CI/CD with GitHub Actions in Flutter: solved exercise
CI/CD with GitHub Actions in Flutter: solved exercise
Without CI/CD, code quality depends on every developer remembering to run tests and analysis before each push. With GitHub Actions, those checks run automatically on every PR and every merge to main, with no manual intervention.
Problem statement
Create a GitHub Actions pipeline for a Flutter project that:
Analysis and tests: runs on every push and PR to main.
APK build: generates a release APK on every merge to main.
Pub cache: reduces setup time from ~2 min to ~15 s.
Version matrix: tests against Flutter stable and beta.
# .github/workflows/ci.ymlname:CIon:push:branches:[main, develop]pull_request:branches:[main]jobs:analyze_and_test:name:Analyze & Test (Flutter ${{ matrix.flutter_channel }})runs-on:ubuntu-lateststrategy:matrix:flutter_channel:[stable, beta]fail-fast:false# does not cancel the other channel if one failssteps:- name:Checkout codeuses:actions/checkout@v4- name:Set up Flutteruses:subosito/flutter-action@v2with:flutter-version:''# uses the latest for the channelchannel:${{ matrix.flutter_channel }}cache:true# caches the Flutter SDK- name:Cache pub packagesuses:actions/cache@v4with:path:| ~/.pub-cache
.dart_toolkey:pub-${{ runner.os }}-${{ hashFiles('**/pubspec.lock') }}restore-keys:| pub-${{ runner.os }}-- name:Install dependenciesrun:flutter pub get- name:Check formattingrun:dart format --set-exit-if-changed .- name:Static analysisrun:dart analyze --fatal-warnings- name:Unit and widget testsrun:flutter test --coverage- name:Upload coverage to Codecovif:matrix.flutter_channel == 'stable'uses:codecov/codecov-action@v4with:token:${{ secrets.CODECOV_TOKEN }}files:coverage/lcov.info
dart analyze passes locally but fails in CI: different Dart version. Pin the version in flutter-action with flutter-version: '3.32.0' or use the same channel as locally.
Tests fail in CI but not locally: usually tests using DateTime.now() or timezone-dependent logic. Use fixed dates in tests.
APK too large: add --split-debug-info=build/debug-info and --obfuscate to reduce size and obfuscate code.
Practical use
With this pipeline: every PR goes through automatic analysis and tests, reviewers see the result before approving, and every merge to main produces an APK ready for internal distribution or Play Store upload.
How many GitHub Actions minutes does the free plan include?
2,000 minutes/month on Linux for private repositories. For public repositories, Linux runners are free and unlimited. macOS runners consume 10× more minutes than Linux.
Can I deploy automatically to the Play Store?
Yes, using fastlane or the r0adkll/upload-google-play action. You need a Google Play API key and must upload an AAB instead of an APK.
How do I prevent a PR from reaching main without passing tests?
In Settings → Branches → Branch protection rules, enable “Require status checks to pass before merging” and select the CI job as a required check.