Automating Flutter builds with Fastlane
By John · 18 October 2024
Fastlane automates the most tedious parts of mobile deployment: building, signing, testing, taking screenshots, and submitting to stores. Here is how to set it up for a Flutter project and automate common workflows.
Setup
Fastlane is a Ruby tool:
# Install
gem install fastlane
# or via Homebrew:
brew install fastlane
# Initialize in your Flutter project root
fastlane init
# Choose "Manual setup"
This creates:
fastlane/
├── Appfile # App identifiers
├── Fastfile # Your lanes (automation scripts)
└── Matchfile # Certificate management config (if using Match)
Appfile
# fastlane/Appfile
app_identifier "com.example.myapp"
apple_id "[email protected]"
team_id "XXXXXXXXXX" # Apple Team ID
package_name "com.example.myapp" # Android package name
json_key_file "fastlane/google-play-key.json" # Google Play service account
Basic lanes
# fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc 'Run tests'
lane :test do
sh 'flutter test'
end
desc 'Build and upload to TestFlight'
lane :beta do
increment_build_number(
xcodeproj: 'ios/Runner.xcodeproj',
build_number: latest_testflight_build_number + 1,
)
build_ios_app(
scheme: 'Runner',
workspace: 'ios/Runner.xcworkspace',
configuration: 'Release',
export_method: 'app-store',
output_directory: './build',
)
upload_to_testflight(
api_key_path: 'fastlane/app_store_connect_api_key.json',
skip_waiting_for_build_processing: true,
)
end
desc 'Submit to App Store'
lane :release do
deliver(
api_key_path: 'fastlane/app_store_connect_api_key.json',
submit_for_review: true,
automatic_release: false,
force: true, # Skip HTML report
)
end
end
platform :android do
desc 'Build and upload to Play Store internal track'
lane :internal do
sh 'flutter build appbundle --release'
supply(
track: 'internal',
aab: '../build/app/outputs/bundle/release/app-release.aab',
)
end
desc 'Promote from internal to production with 10% rollout'
lane :promote_to_production do
supply(
track: 'internal',
track_promote_to: 'production',
rollout: '0.1',
)
end
end
Running lanes
bundle exec fastlane ios beta
bundle exec fastlane android internal
bundle exec fastlane ios test
Match: certificate management
Match stores certificates and provisioning profiles in a private Git repo. Every team member and CI machine gets the same certs automatically.
# First time: create the private certs repo, then:
bundle exec fastlane match init
# Enter the Git URL of your private certs repo
# Generate distribution certs
bundle exec fastlane match appstore
# In CI (read-only mode)
bundle exec fastlane match appstore --readonly
# fastlane/Matchfile
git_url "[email protected]:yourorg/mobile-certs.git"
type "appstore"
app_identifier "com.example.myapp"
username "[email protected]"
Automating screenshots
lane :screenshots do
capture_ios_screenshots # Uses UI tests to take screenshots
frame_screenshots # Add device frames
upload_to_app_store(skip_binary_upload: true, skip_metadata: true)
end
GitHub Actions integration
name: Deploy to TestFlight
on:
push:
branches: [main]
jobs:
deploy:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.22.0'
cache: true
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: Setup Fastlane Match
run: bundle exec fastlane match appstore --readonly
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
- name: Deploy to TestFlight
run: bundle exec fastlane ios beta
env:
APP_STORE_CONNECT_API_KEY_PATH: fastlane/api_key.json
- name: Create API key file
run: echo '${{ secrets.APP_STORE_CONNECT_API_KEY }}' > fastlane/api_key.json
Gemfile for reproducible builds
# Gemfile
source 'https://rubygems.org'
gem 'fastlane'
gem 'cocoapods'
bundle install
bundle exec fastlane # Always run via bundle exec
Common pitfalls
Not using bundle exec. Running fastlane directly uses the system Ruby and Fastlane, which may differ from what your project expects. Always use bundle exec fastlane for reproducible behaviour.
Match with write access in CI. If two CI jobs run simultaneously, one can corrupt the other's certificate write. Always use --readonly in CI.
Hardcoding build numbers. If you call increment_build_number with a hardcoded value, two builds will conflict. Compute from latest_testflight_build_number + 1 or from the CI run number.
Sign in to like, dislike, or report.