How to automate fastlane in Android?
Answer
Overview
Fastlane is an open-source automation tool designed to streamline mobile app delivery. For Flutter developers targeting Android, Fastlane can automate building, version validation, and publishing to the Play Store, reducing manual effort and preventing release mistakes.
If you're tired of manually uploading AAB files and managing version codes, this guide will help you fully automate your Android release process using Fastlane.
Key Benefits:
- š¤ Automation of repetitive tasks - Build, sign, and upload automatically
- ā±ļø Saves time - Release in minutes instead of manually navigating Play Console
- ā Improves consistency - Avoid human errors like duplicate version codes
- š CI/CD friendly - Works smoothly with GitHub Actions, Jenkins, Bitrise
- š Centralized configuration - Keep entire release process version-controlled
Prerequisites
Before setting up Fastlane for Android, ensure you have:
Google Requirements:
- ā Active Google Play Console account
- ā App has completed initial review
- ā App has been released at least once to Play Store
System Requirements:
- macOS, Linux, or Windows
- Ruby 2.5 or higher (check with )text
ruby --version - Flutter SDK installed and configured
- Android SDK and build tools
Project Requirements:
- Flutter project with Android folder
- App already registered in Play Console
- Package name configured (e.g., )text
com.example.app
ā ļø Important: Fastlane requires your app to have completed the first manual release. You cannot use Fastlane for the very first Play Store submission.
Step 1: Install & Initialize Fastlane
1ļøā£ Install Fastlane
macOS / Linux (Recommended):
bashbrew install fastlane
Windows:
bashsudo gem install fastlane
2ļøā£ Navigate to Your Flutter Project
bashcd your_flutter_project cd android
3ļøā£ Initialize Fastlane
bashfastlane init
When prompted:
- Enter your app package name (e.g., )text
com.example.app - JSON path: Press Enter and confirm with Yes
After setup, you'll see:
textandroid/ āāā Gemfile āāā Gemfile.lock āāā fastlane/ āāā Appfile āāā Fastfile
4ļøā£ Update the Gemfile
Open
android/Gemfileruby# android/Gemfile source "https://rubygems.org" ruby "3.3.7" gem "fastlane", "2.232.0" gem "abbrev" gem "ostruct"
Install dependencies:
bashbundle install
Why these versions?
- - Stable Ruby versiontext
ruby "3.3.7" - - Latest stable fastlanetext
fastlane "2.232.0" - andtext
abbrev- Required dependenciestextostruct
Step 2: Configure Google Play API (Supply Setup)
Fastlane uses the Google Play Developer API to upload builds automatically.
1ļøā£ Create a Google Cloud Project
Go to: Google Cloud Console
- Click Select a project (top navigation bar)
- Click New Project
- Enter project name: text
Fastlane Automation - Click Create
- Wait for project creation to complete
2ļøā£ Enable the Required API
Navigate to: APIs & Services ā Enable APIs and Services
- Search for: text
Google Play Android Developer API - Click on the API
- Click Enable
- Wait for activation (may take a few seconds)
3ļøā£ Create a Service Account
Navigate to: IAM & Admin ā Service Accounts
- Click Create Service Account
- Enter details:
- Service account name: text
fastlane-uploader - Service account ID: (auto-filled)text
fastlane-uploader - Description: text
Fastlane automation for Play Store uploads
- Service account name:
- Click Create and Continue
- Grant role (optional): Skip this step ā Click Continue
- Click Done
Copy the service account email:
- Format: text
fastlane-uploader@your-project-id.iam.gserviceaccount.com - Save this email - you'll need it in Step 3
4ļøā£ Generate JSON Key
- Click on the created service account ()text
fastlane-uploader - Go to Keys tab
- Click Add Key ā Create new key
- Choose JSON format
- Click Create
- A JSON file will be downloaded (e.g., )text
your-project-123456-abc123def456.json
ā ļø Important:
- This JSON file contains sensitive credentials
- Store it securely
- Never commit it to version control
Step 3: Grant Access in Play Console
Now connect the service account to your Play Console.
Steps:
- Go to: Google Play Console
- Click Users and Permissions (left sidebar)
- Click Invite new users
- Paste the service account email:
text
fastlane-uploader@your-project-id.iam.gserviceaccount.com - Grant permissions:
- Select Admin (all permissions) for full automation
- Or select specific permissions:
- ā View app information
- ā Create and edit draft apps
- ā Release apps to testing tracks
- ā Release apps to production
- ā Manage testing tracks
- Click Invite user
- No email will be sent (service accounts don't have email)
- The service account will appear in the user list
Step 4: Add JSON File to Project
Place the downloaded JSON file inside your project:
bash# Move JSON file to fastlane directory mv ~/Downloads/your-project-123456-abc123def456.json android/fastlane/play-store-credentials.json
Secure the file:
bash# Set appropriate permissions chmod 600 android/fastlane/play-store-credentials.json
Add to .gitignore:
bash# android/fastlane/.gitignore *.json play-store-credentials.json .env .env.local
ā ļø Critical: Never commit JSON credentials to version control!
Step 5: Create Environment File
Create a
.envandroid/fastlane/bashcd android/fastlane touch .env
Add configuration:
bash# android/fastlane/.env # Android app package name PACKAGE_NAME=com.example.app # Path to Google Play service account JSON PLAY_STORE_JSON_PATH=fastlane/play-store-credentials.json
Why use .env file?
- ā Keeps sensitive data out of code
- ā Easy to change between environments
- ā Can be overridden in CI/CD
For CI/CD: Create
.env.localStep 6: Configure Fastlane Files
š Update Appfile
ruby# android/fastlane/Appfile json_key_file(ENV['PLAY_STORE_JSON_PATH']) package_name(ENV['PACKAGE_NAME'])
This file:
- Reads values from filetext
.env - Configures Play Store authentication
- Sets default package name
š Update Fastfile
Production-ready Fastfile for Flutter Android:
ruby# android/fastlane/Fastfile default_platform(:android) platform :android do # Prevent Fastlane from regenerating README.md on every run skip_docs # Load environment variables from .env (absolute path for reliability) env_file = File.expand_path('.env', __dir__) Dotenv.overload(env_file) if File.exist?(env_file) # Lane: Build & upload to Play Store Internal Testing desc "Build Flutter AAB & upload to Play Store Internal Testing" lane :beta do # 1. Read version from pubspec.yaml require 'yaml' pubspec_path = "../../pubspec.yaml" pubspec = YAML.load_file(pubspec_path) version_string = pubspec['version'] UI.user_error!("Missing 'version' in pubspec.yaml") if version_string.nil? || version_string.empty? parts = version_string.split('+') version = parts[0] UI.user_error!("pubspec.yaml version '#{version_string}' is missing build number (expected format: x.y.z+n)") unless parts.length == 2 build_number = parts[1].to_i UI.message("Using version from pubspec.yaml: #{version}+#{build_number}") # 2. Validate version (prevent duplicate uploads) existing_versions = begin google_play_track_version_codes(track: "internal") rescue => e UI.user_error!("ā Could not verify existing versions on Play Store: #{e.message}") end if existing_versions.include?(build_number) UI.user_error!("ā Version code #{build_number} already exists on Internal track. Please increment the build number in pubspec.yaml") end UI.success("ā Version validation passed - #{version}+#{build_number}") # 3. Build AAB sh("cd ../.. && flutter build appbundle --release") # 4. Prepare Changelog changelog_path = "./metadata/android/en-US/release_notes.txt" changelog_text = File.exist?(changelog_path) ? File.read(changelog_path) : "Beta build v#{version} (#{build_number})" # Write changelog to the path 'supply' expects dest_changelog_dir = "./metadata/android/en-US/changelogs" FileUtils.mkdir_p(dest_changelog_dir) File.write("#{dest_changelog_dir}/#{build_number}.txt", changelog_text) # 5. Upload to Play Store Internal track upload_to_play_store( track: "internal", aab: "../build/app/outputs/bundle/release/app-release.aab", skip_upload_metadata: true, skip_upload_images: true, skip_upload_screenshots: true ) UI.success("ā Uploaded build #{build_number} (v#{version}) to Internal Track š") end # Lane: Promote beta build to Production desc "Promote existing Internal build to Production" lane :production do |options| # Read from pubspec.yaml if no explicit params provided if options[:version].nil? || options[:build].nil? require 'yaml' pubspec_path = "../../pubspec.yaml" pubspec = YAML.load_file(pubspec_path) version_string = pubspec['version'] UI.user_error!("Missing 'version' in pubspec.yaml") if version_string.nil? || version_string.empty? parts = version_string.split('+') version = parts[0] UI.user_error!("pubspec.yaml version '#{version_string}' is missing build number (expected format: x.y.z+n)") unless parts.length == 2 build_number = parts[1].to_i else version = options[:version] build_number = options[:build].to_i end UI.important("ā ļø Ensure this matches the Internal build you want to promote!") UI.message("Promoting: v#{version} (#{build_number})") # Validate the build exists on Internal track existing_versions = begin google_play_track_version_codes(track: "internal") rescue => e UI.user_error!("ā Could not verify build on Internal track: #{e.message}") end if existing_versions unless existing_versions.include?(build_number) UI.user_error!("ā Build #{build_number} not found on Internal track.") end UI.success("ā Build #{build_number} verified on Internal track") end # Promote to production upload_to_play_store( track: "internal", track_promote_to: "production", version_code: build_number, skip_upload_aab: true, skip_upload_metadata: true, skip_upload_images: true, skip_upload_screenshots: true ) UI.success("ā Successfully promoted build #{build_number} (v#{version}) to Production š") end # Lane: Build only (no upload) desc "Build Flutter AAB without uploading" lane :build_only do # 1. Read version from pubspec.yaml require 'yaml' pubspec_path = "../../pubspec.yaml" pubspec = YAML.load_file(pubspec_path) version_string = pubspec['version'] UI.user_error!("Missing 'version' in pubspec.yaml") if version_string.nil? || version_string.empty? parts = version_string.split('+') version = parts[0] UI.user_error!("pubspec.yaml version '#{version_string}' is missing build number (expected format: x.y.z+n)") unless parts.length == 2 build_number = parts[1].to_i UI.message("Using version from pubspec.yaml: #{version}+#{build_number}") # 2. Validate version existing_versions = begin google_play_track_version_codes(track: "internal") rescue => e UI.user_error!("ā Could not verify existing versions on Play Store: #{e.message}") end if existing_versions.include?(build_number) UI.user_error!("ā Version code #{build_number} already exists on Internal track. Please increment the build number in pubspec.yaml") end UI.success("ā Version validation passed - #{version}+#{build_number}") # 3. Build AAB sh("cd ../.. && flutter build appbundle --release") # 4. Prepare Changelog changelog_path = "./metadata/android/en-US/release_notes.txt" changelog_text = File.exist?(changelog_path) ? File.read(changelog_path) : "Beta build v#{version} (#{build_number})" dest_changelog_dir = "./metadata/android/en-US/changelogs" FileUtils.mkdir_p(dest_changelog_dir) File.write("#{dest_changelog_dir}/#{build_number}.txt", changelog_text) UI.success("ā Build #{build_number} (v#{version}) completed successfully") end # Error handling error do |lane, exception| UI.error("ā Error in lane #{lane}: #{exception.message}") end end
Step 7: Available Commands
š Upload to Internal Testing
bashcd android bundle exec fastlane beta
What happens:
- Reads version from text
pubspec.yaml - Validates version code against Play Store
- Builds Flutter AAB ()text
flutter build appbundle --release - Prepares changelog
- Uploads to Play Store Internal Testing track
š Promote to Production
bashcd android bundle exec fastlane production
What happens:
- Reads version from text
pubspec.yaml - Verifies build exists on Internal track
- Promotes existing build to Production (no rebuild)
šØ Build Only (No Upload)
bashcd android bundle exec fastlane build_only
What happens:
- Validates version
- Builds AAB file
- Does not upload to Play Store
Step 8: Important Notes
Before running Fastlane, ensure:
1. App is Buildable ā
Test your build process first:
bashflutter build appbundle --release
Ensure it completes without errors.
2. Signing Configs Are Set ā
Create signing key:
bashkeytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
Configure android/key.properties
propertiesstorePassword=YourStorePassword keyPassword=YourKeyPassword keyAlias=upload storeFile=/Users/yourname/upload-keystore.jks
Update android/app/build.gradle
groovydef keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } android { ... signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null storePassword keystoreProperties['storePassword'] } } buildTypes { release { signingConfig signingConfigs.release } } }
3. Increment Version in pubspec.yaml ā
Before each release:
yaml# pubspec.yaml # WRONG - Same as previous release version: 1.0.0+1 # CORRECT - Incremented build number version: 1.0.0+2 # OR increment version version: 1.1.0+1
Rules:
- Increment version code (+1, +2) for internal builds
- Increment version (1.0.0 ā 1.1.0) for production releases
4. First Manual Release Completed ā
Fastlane requirements:
- ā Cannot handle the very first Play Store submission
- ā Can handle all updates after the first manual release
First release checklist:
- Build AAB manually
- Upload to Play Console manually
- Complete store listing (screenshots, description)
- Submit for review
- After approval, use Fastlane for all future releases
CI/CD Integration
GitHub Actions Example
Create
.github/workflows/android-release.ymlyamlname: Android Release on: push: tags: - 'v*' # Trigger on version tags (e.g., v1.0.0) jobs: release: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Setup Flutter uses: subosito/flutter-action@v2 with: flutter-version: '3.16.0' channel: 'stable' - name: Install dependencies run: flutter pub get - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: '3.0' bundler-cache: true - name: Install fastlane run: | cd android bundle install - name: Create .env file run: | cd android/fastlane cat << EOF > .env PACKAGE_NAME=${{ secrets.PACKAGE_NAME }} PLAY_STORE_JSON_PATH=fastlane/play-store-credentials.json EOF - name: Decode Play Store credentials env: PLAY_STORE_CREDENTIALS: ${{ secrets.PLAY_STORE_CREDENTIALS }} run: | cd android/fastlane echo "$PLAY_STORE_CREDENTIALS" | base64 --decode > play-store-credentials.json chmod 600 play-store-credentials.json - name: Decode signing key env: SIGNING_KEY: ${{ secrets.SIGNING_KEY }} run: | echo "$SIGNING_KEY" | base64 --decode > android/app/upload-keystore.jks - name: Create key.properties env: KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} KEY_ALIAS: ${{ secrets.KEY_ALIAS }} run: | cd android cat << EOF > key.properties storePassword=$KEY_STORE_PASSWORD keyPassword=$KEY_PASSWORD keyAlias=$KEY_ALIAS storeFile=../app/upload-keystore.jks EOF - name: Build and deploy to Internal Testing run: | cd android bundle exec fastlane beta
Required GitHub Secrets:
| Secret Name | Value |
|---|---|
text | Your app's package name (e.g., text |
text | Base64 encoded JSON credentials file |
text | Base64 encoded upload-keystore.jks |
text | Keystore password |
text | Key password |
text | Key alias (e.g., text |
To create base64 secrets:
bash# Encode Play Store credentials base64 -i play-store-credentials.json | pbcopy # Encode signing key base64 -i upload-keystore.jks | pbcopy
Troubleshooting
Issue: "Version code already exists on Internal track"
Error:
textā Version code 42 already exists on Internal track. Please increment the build number in pubspec.yaml
Solution:
Update
pubspec.yamlyaml# Before version: 1.0.0+42 # After version: 1.0.0+43
Issue: "Could not authenticate with Play Store"
Error:
textGoogle Play API error: Invalid Credentials
Solution:
-
Verify JSON file path in
:text.envbashPLAY_STORE_JSON_PATH=fastlane/play-store-credentials.json -
Ensure JSON file exists:
bashls -la android/fastlane/play-store-credentials.json -
Verify service account has permissions in Play Console
Issue: "Signing configuration not found"
Error:
textExecution failed for task ':app:validateSigningRelease'
Solution:
-
Verify
exists:textkey.propertiesbashcat android/key.properties -
Ensure paths are correct in
textkey.properties -
Verify keystore file exists:
bashls -la ~/upload-keystore.jks
Issue: "AAB file not found"
Error:
textCould not find AAB file at ../build/app/outputs/bundle/release/app-release.aab
Solution:
Build the AAB manually first:
bashflutter build appbundle --release
Verify it exists:
bashls -la build/app/outputs/bundle/release/app-release.aab
Best Practices
1. Always Use Bundler ā
bash# WRONG fastlane beta # CORRECT bundle exec fastlane beta
2. Version Control Fastlane Files ā
Commit to Git:
- ā
text
Gemfile - ā
text
Gemfile.lock - ā
text
Fastfile - ā
text
Appfile
Add to .gitignore:
- ā text
.env - ā (credentials)text
*.json - ā text
key.properties - ā (keystore files)text
*.jks
3. Test Locally First ā
Before CI/CD:
- Test locallytext
bundle exec fastlane beta - Verify upload to Play Console
- Test internal track installation
- Then configure GitHub Actions
4. Use Play Tracks Properly ā
Recommended workflow:
textDevelopment ā Internal Testing (fastlane beta) ā Closed Beta ā Open Beta ā Production (fastlane production)
5. Create Release Checklist ā
markdownBefore release: - [ ] Update version in pubspec.yaml - [ ] Test app thoroughly - [ ] Run fastlane beta - [ ] Test on Internal track - [ ] Promote to production
Summary
| Task | Command | Description |
|---|---|---|
| Install Fastlane | text | Install via Homebrew |
| Initialize | text | Setup Fastlane in project |
| Install Dependencies | text | Install Ruby gems |
| Upload to Internal | text | Build and upload |
| Promote to Production | text | Promote existing build |
| Build Only | text | Build AAB only |
Final Thoughts
With Fastlane configured, your Android release process becomes:
bash# Update version in pubspec.yaml version: 1.2.0+43 # One command for Internal Testing bundle exec fastlane beta # One command for Production bundle exec fastlane production
That's it. Clean. Automated. Reliable. š
What You've Automated
ā No more manual AAB uploads ā No duplicate version code mistakes ā Version validation from pubspec.yaml ā One command = Full Internal release ā One command = Production promotion ā CI/CD ready with GitHub Actions
Your Flutter Android deployment is now fully automated!
Official Documentation: