Mobile App Deployment Automation with Fastlane
Deploying a mobile app should not take an entire afternoon. Yet many teams still follow manual processes: bump the version number, update screenshots, build the archive, wait for code signing to cooperate, upload to App Store Connect, fill in release notes, submit for review, then repeat the entire dance for Google Play.
Fastlane eliminates this pain. It is an open-source automation tool that handles everything from code signing to store submission. After setting up Fastlane across dozens of projects, I can tell you that the initial investment of a few hours saves hundreds of hours over a project’s lifetime.
Why Fastlane in 2023
Fastlane has been the de facto mobile deployment tool for years, and its position is stronger than ever. With over 40,000 GitHub stars and active maintenance, it integrates with every major CI/CD platform (GitHub Actions, Bitrise, CircleCI, GitLab CI) and handles both iOS and Android from a single configuration.
The alternatives — manual deployment, Xcode Cloud, and custom scripts — each have limitations. Xcode Cloud is Apple-only and limited in customisation. Custom shell scripts become unmaintainable as complexity grows. Fastlane provides a structured, tested, and well-documented approach that scales with your team.
Setting Up Fastlane
Install Fastlane using Bundler (recommended) to lock versions across your team:
# Gemfile
source "https://rubygems.org"
gem "fastlane", "~> 2.212"
gem "cocoapods", "~> 1.12" # if using CocoaPods
bundle install
bundle exec fastlane init
Fastlane creates a fastlane/ directory with two key files: Appfile (app identifiers) and Fastfile (automation lanes).
Appfile Configuration
# fastlane/Appfile
# iOS
app_identifier("com.au.yourcompany.app")
apple_id("[email protected]")
team_id("ABCDE12345")
itc_team_id("123456789")
# Android
json_key_file("./fastlane/google-play-key.json")
package_name("com.au.yourcompany.app")
iOS Deployment Lane
Her
e is a production-ready iOS deployment lane that handles versioning, code signing, building, and uploading:
# fastlane/Fastfile
platform :ios do
desc "Deploy a new version to the App Store"
lane :release do |options|
# Ensure we are on a clean branch
ensure_git_status_clean
# Run tests first
run_tests(
scheme: "MyApp",
devices: ["iPhone 14"],
clean: true
)
# Increment version
increment_build_number(
build_number: latest_testflight_build_number + 1
)
if options[:bump]
increment_version_number(bump_type: options[:bump])
end
# Handle code signing with Match
sync_code_signing(
type: "appstore",
readonly: is_ci
)
# Build the app
build_app(
scheme: "MyApp",
export_method: "app-store",
output_directory: "./build",
output_name: "MyApp.ipa",
clean: true,
include_bitcode: false
)
# Upload to App Store Connect
upload_to_app_store(
skip_metadata: false,
skip_screenshots: true,
submit_for_review: false,
automatic_release: false,
precheck_include_in_app_purchases: false,
force: true,
metadata_path: "./fastlane/metadata",
release_notes: {
"en-AU" => options[:notes] || changelog_from_git_commits
}
)
# Tag the release
add_git_tag(
tag: "ios/v#{get_version_number}"
)
# Notify the team
slack(
message: "iOS #{get_version_number} submitted to App Store Connect",
channel: "#releases",
success: true
)
commit_version_bump(
message: "Bump iOS version to #{get_version_number}"
)
push_to_git_remote
end
desc "Deploy to TestFlight for beta testing"
lane :beta do
sync_code_signing(type: "appstore", readonly: is_ci)
increment_build_number(
build_number: latest_testflight_build_number + 1
)
build_app(
scheme: "MyApp",
export_method: "app-store"
)
upload_to_testflight(
skip_waiting_for_build_processing: true,
distribute_external: false
)
slack(
message: "New iOS beta build uploaded to TestFlight",
channel: "#releases"
)
end
end
Code Signing with Match
Code signing is the most painful part of iOS deployment. Fastlane’s match tool solves this by storing certificates and profiles in a Git repository or cloud storage:
# fastlane/Matchfile
git_url("https://github.com/yourcompany/certificates.git")
storage_mode("git")
type("appstore")
app_identifier(["com.au.yourcompany.app"])
username("[email protected]")
Initial setup creates and stores the certificates:
bundle exec fastlane match appstore
bundle exec fastlane match development
After initial setup, new team members and CI servers pull certificates automatically. No more “missing provisioning profile” nightmares.
Android Deployment Lane
The An
droid lane handles signing, building, and uploading to Google Play:
platform :android do
desc "Deploy a new version to Google Play"
lane :release do |options|
ensure_git_status_clean
# Run tests
gradle(
task: "test",
project_dir: "./android"
)
# Increment version code
android_set_version_code(
version_code: google_play_track_version_codes(
track: "production"
).max + 1,
gradle_file: "./android/app/build.gradle"
)
if options[:version]
android_set_version_name(
version_name: options[:version],
gradle_file: "./android/app/build.gradle"
)
end
# Build the AAB (Android App Bundle)
gradle(
task: "bundle",
build_type: "Release",
project_dir: "./android",
properties: {
"android.injected.signing.store.file" => ENV["KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["KEY_PASSWORD"]
}
)
# Upload to Google Play
upload_to_play_store(
track: options[:track] || "production",
aab: "./android/app/build/outputs/bundle/release/app-release.aab",
skip_upload_metadata: false,
skip_upload_images: true,
skip_upload_screenshots: true,
release_status: "draft",
metadata_path: "./fastlane/metadata/android"
)
add_git_tag(tag: "android/v#{android_get_version_name}")
slack(
message: "Android #{android_get_version_name} uploaded to Google Play",
channel: "#releases",
success: true
)
end
desc "Deploy to internal testing track"
lane :beta do
gradle(
task: "bundle",
build_type: "Release",
project_dir: "./android"
)
upload_to_play_store(
track: "internal",
aab: "./android/app/build/outputs/bundle/release/app-release.aab",
release_status: "completed"
)
end
end
Automating Screenshots
App Store screenshots are a marketing necessity but a maintenance headache. Fastlane’s snapshot (iOS) and screengrab (Android) automate this process:
# fastlane/Snapfile
devices([
"iPhone 14 Pro Max",
"iPhone 14",
"iPhone SE (3rd generation)",
"iPad Pro (12.9-inch) (6th generation)"
])
languages(["en-AU"])
scheme("MyAppScreenshots")
output_directory("./fastlane/screenshots")
clear_previous_screenshots(true)
Write UI tests that navigate to each screenshot-worthy screen:
class ScreenshotTests: XCTestCase {
func testScreenshots() {
let app = XCUIApplication()
setupSnapshot(app)
app.launch()
snapshot("01_HomeScreen")
app.buttons["Browse Products"].tap()
snapshot("02_ProductList")
app.cells.firstMatch.tap()
snapshot("03_ProductDetail")
app.buttons["Add to Cart"].tap()
app.tabBars.buttons["Cart"].tap()
snapshot("04_ShoppingCart")
}
}
Run bundle exec fastlane snapshot and Fastlane generates screenshots across all configured devices and languages.
CI/CD Integration with GitHub Actions
Here is a production GitHub Actions workflow for both platforms:
name: Deploy Mobile App
on:
push:
tags:
- 'release/*'
jobs:
ios-release:
runs-on: macos-13
steps:
- uses: actions/checkout@v3
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.1'
bundler-cache: true
- name: Install CocoaPods
run: bundle exec pod install
working-directory: ios
- name: Deploy to App Store
run: bundle exec fastlane ios release
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_AUTH }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}
android-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.1'
bundler-cache: true
- name: Decode Keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks
- name: Deploy to Google Play
run: bundle exec fastlane android release
env:
KEYSTORE_PATH: ${{ github.workspace }}/android/app/keystore.jks
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
SUPPLY_JSON_KEY_DATA: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
Managing Metadata
Store your App Store and Google Play metadata in version control alongside your code:
fastlane/
metadata/
en-AU/
name.txt
subtitle.txt
description.txt
keywords.txt
privacy_url.txt
release_notes.txt
android/
en-AU/
full_description.txt
short_description.txt
title.txt
changelogs/
default.txt
This approach ensures metadata changes go through code review, are traceable in version control, and deploy automatically with your app updates.
Error Handling and Recovery
Production deployments fail. Network timeouts, expired certificates, and API changes all cause issues. Build resilience into your lanes:
lane :release do
begin
# ... deployment steps
rescue => e
slack(
message: "Deployment failed: #{e.message}",
channel: "#releases",
success: false
)
raise e
ensure
# Clean up build artefacts
clean_build_artifacts
end
end
For transient failures, Fastlane supports retries on individual actions. For persistent issues, the error messages are clear enough to diagnose and fix quickly.
Getting Started
If you are currently deploying manually, here is your migration path:
- Week 1: Install Fastlane, configure Appfile, set up Match for iOS code signing
- Week 2: Create beta lanes for both platforms, test with internal builds
- Week 3: Create release lanes, automate metadata management
- Week 4: Integrate with your CI/CD platform, document the process
The first automated deployment is a revelation. What used to take an afternoon takes minutes, and the consistency eliminates an entire category of human errors. Your team can ship with confidence, every time.
Need help setting up mobile deployment automation? Our team at eawesome builds robust CI/CD pipelines for Australian app development teams.