Install GitHub Actions + Fastlane CI/CD into Flutter projects for Android (Play Store) and iOS (TestFlight).
- PowerShell 7+
- GitHub CLI authenticated (
gh auth login) - Ruby + Bundler (optional; used for
bundle lockon target project) - For iOS first-time setup: macOS with Xcode for
fastlane match appstore
git clone https://github.com/osama188/flutter-cicd-installer.git
cd flutter-cicd-installer
.\install.ps1 -TargetPath "C:\path\to\flutter_app" -Platform Both-Platform |
Scaffolds |
|---|---|
Android |
deploy-android.yml, android/fastlane/* |
iOS |
deploy-ios.yml, ios/fastlane/* |
Both |
All of the above (default) |
Copy install.config.example.json (no dart-defines) or install.config.with-dart-defines.example.json (Supabase example), fill in paths and values, then:
.\install.ps1 -TargetPath "C:\path\to\flutter_app" `
-ConfigFile ".\install.config.local.json" `
-Platform Both `
-UpdateSecretsMany Flutter apps need no compile-time --dart-define values in CI. The installer supports both cases.
Use this when your app does not read String.fromEnvironment(...) at build time (no Supabase keys, API URLs baked in at compile time, etc.).
Interactive: press Enter at the prompt:
Dart-define keys (comma-separated) [none]:
Config file:
"dartDefineKeys": [],
"dartDefineValues": {}Generated CI behavior:
| Platform | What happens |
|---|---|
| Android | No dart_defines.json step; flutter test / flutter build appbundle run without --dart-define-from-file |
| iOS | No dart_defines.json step; Fastlane runs flutter build ios without --dart-define-from-file |
| Secrets | No extra GitHub secrets beyond signing / store credentials |
Use when your app expects compile-time values, e.g. String.fromEnvironment('API_URL').
Interactive: enter comma-separated key names:
Dart-define keys (comma-separated) [none]: API_URL,API_KEY
Config file:
"dartDefineKeys": ["API_URL", "API_KEY"],
"dartDefineValues": {
"API_URL": "https://api.example.com",
"API_KEY": "your-key"
}The installer will:
- Add a
Create dart_defines.jsonworkflow step (one GitHub secret per key) - Pass
--dart-define-from-file=dart_defines.jsonon Android test + build - On iOS, pass the same file in Fastlane before archive and fail the build if
DART_DEFINESis missing fromGenerated.xcconfig
Key names are fully configurable — not limited to Supabase. See install.config.with-dart-defines.example.json for a Supabase-shaped example.
| Field | Default | Description |
|---|---|---|
ios.inHouse |
false |
Set in_house on the App Store Connect API key in the generated Fastfile. Use false for standard App Store / TestFlight accounts; set true only for Apple Enterprise (in-house) accounts. |
In interactive mode, you are prompted: Apple Enterprise (in-house) account? [y/N] (default: no).
| Target | Change |
|---|---|
.github/workflows/deploy-ios.yml |
FlutterFire CLI install step (for Crashlytics symbol upload) |
ios/fastlane/Fastfile |
in_house: false by default; full flutter build ios before archive; reads dart_defines.json from project root |
ios/Podfile |
IPHONEOS_DEPLOYMENT_TARGET aligned to platform :ios version in post_install |
| Flag | Description |
|---|---|
-TargetPath |
Flutter project root (required) |
-Platform |
Android, iOS, or Both (default: Both) |
-ConfigFile |
JSON config for non-interactive install |
-Force |
Overwrite existing scaffolded files |
-UpdateSecrets |
Set GitHub secrets without prompting |
-SkipSecrets |
Scaffold files only |
-WhatIf |
Preview changes without writing |
After the installer runs, commit the scaffolded files (.github/workflows/, android/fastlane/, ios/fastlane/) and push to GitHub. Each platform has its own workflow and can be released independently.
| Platform | Workflow | Tag prefix |
|---|---|---|
| Android | Deploy to Play Store |
android-v* |
| iOS | Deploy to TestFlight |
ios-v* |
Version format is {version}+{build} — same as pubspec.yaml (e.g. version: 1.0.4+13 → tag android-v1.0.4+13 or ios-v1.0.4+13).
1. Bump version in pubspec.yaml
version: 1.0.4+132. Commit and push your branch
git add pubspec.yaml
git commit -m "chore: bump version to 1.0.4+13"
git push origin mainReplace main with your default branch if different (master, etc.).
3. Create the tag
Android:
git tag android-v1.0.4+13iOS:
git tag ios-v1.0.4+134. Push the tag (this starts the GitHub Action)
git push origin android-v1.0.4+13or for iOS:
git push origin ios-v1.0.4+13One-liner (create + push tag in one step):
# Android
git tag android-v1.0.4+13 && git push origin android-v1.0.4+13
# iOS
git tag ios-v1.0.4+13 && git push origin ios-v1.0.4+13Tag must match exactly: android-v<major>.<minor>.<patch>+<build> or ios-v<major>.<minor>.<patch>+<build>.
- GitHub → Actions → Deploy to Play Store or Deploy to TestFlight
- Run workflow → enter version name (
1.0.4) and build number (13) → Run workflow
Use this for ad-hoc deploys without a tag. Keep pubspec.yaml in sync with the values you enter.
Actions tab on your repo (e.g. https://github.com/you/your-app/actions). Workflows use the production environment for secrets.
| Platform | Tag format | Example |
|---|---|---|
| Android | android-v{version}+{build} |
android-v1.0.4+13 |
| iOS | ios-v{version}+{build} |
ios-v1.0.4+13 |
Platforms deploy independently — push only the tag for the platform you want to release.
| Secret | Description |
|---|---|
KEYSTORE_BASE64 |
Base64 upload keystore |
KEY_ALIAS, KEY_PASSWORD, STORE_PASSWORD |
Signing |
PLAY_STORE_JSON_KEY_BASE64 |
Base64 Play service account JSON |
| Secret | Description |
|---|---|
ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_CONTENT |
App Store Connect API key |
MATCH_PASSWORD |
Match encryption passphrase |
MATCH_GIT_BASIC_AUTHORIZATION |
Base64 username:PAT for certs repo |
The generated ios/fastlane/Fastfile sets in_house: false by default (configurable via ios.inHouse in the install config). Set inHouse to true only if you use an Apple Enterprise account.
| Secret | Description |
|---|---|
One per key in dartDefineKeys |
Value passed as --dart-define at build time (e.g. API_URL, SUPABASE_ANON_KEY) |
Omit this section entirely for apps with "dartDefineKeys": [].
The installer scaffolds files and sets secrets but cannot automate:
-
Create App Store Connect API key (role: App Manager or Admin)
-
Create private
ios-certificatesGitHub repo -
Run
fastlane match appstoreon a Mac (from the Flutter project root):cd ios bundle install bundle exec fastlane match appstore
Set
matchGitUrlin your install config to your real certs repo (e.g.https://github.com/you/ios-certificates.git), not the exampleownerplaceholder. -
Xcode: Runner → Release → manual signing → commit
project.pbxproj
The installer does automatically:
- Install
flutterfire_cliin CI (required ifflutterfire configureadded the Crashlytics Xcode build phase) - Patch
ios/Podfilepod deployment targets to match yourplatform :iosversion - Generate
in_house: falsein the Fastfile for standard TestFlight deploys
| Symptom | Likely cause | Fix |
|---|---|---|
Archive fails on flutterfire upload-crashlytics-symbols |
flutterfire CLI missing on CI |
Re-run installer with -Force -Platform iOS (v2.1.0+ includes the workflow step) |
| App shows "Something went wrong" after splash on TestFlight | --dart-define values not compiled into release build |
Verify production secrets are non-empty; use v2.1.1+ (full flutter build ios in Fastlane); v2.1.3+ fixes dart_defines.json path from repo root |
upload_to_testflight auth error |
in_house: true on a standard App Store account |
Set ios.inHouse to false and re-scaffold Fastfile |
Pod IPHONEOS_DEPLOYMENT_TARGET warnings |
Pods target older iOS than Xcode supports | Re-run installer to patch Podfile, or add the post_install block manually |
| Match clone fails with exit 128 / "could not read Username" | MATCH_GIT_BASIC_AUTHORIZATION uses an expired or invalid PAT |
Re-run installer with -UpdateSecrets (v2.1.4+ falls back to gh auth token if config PAT is invalid), or create a new PAT with read access to ios-certificates |
| Match works but upload fails | ASC API key lacks App Manager role, or wrong ASC_* secrets |
Verify secrets in production environment; re-encode .p8 as base64 |
To refresh an existing project after upgrading the installer:
.\install.ps1 -TargetPath "C:\path\to\flutter_app" -Platform iOS -Force -SkipSecretsInvoke-Pester -Path tests/
.\install.ps1 -TargetPath "C:\path\to\flutter_app" -WhatIf -SkipSecrets -Platform Both- Repo renamed from
flutter-android-cicd-installertoflutter-cicd-installer - Android tags changed from
v*toandroid-v* - Installer v2.0.0 adds iOS support
- Installer v2.1.0 ports production iOS CI fixes: FlutterFire CLI step,
in_house: falsedefault, Podfile deployment-target patch - Installer v2.1.1 fixes dart-define propagation: full
flutter build iosin Fastlane instead of workflow--config-only - Installer v2.1.2 defaults interactive dart-define prompt to none; documents optional dart-defines in README
- Installer v2.1.3 fixes
dart_defines.jsonlookup from repo root in generated iOS Fastfile (notios/dart_defines.json)