← back to blog

Automating Mobile App Releases with Fastlane: iOS & Android CI/CD

A comprehensive guide to setting up Fastlane for automated iOS TestFlight and Android Play Store deployments with GitHub Actions, code signing, and version management.

SN
Sabin Joshi
DevOps Engineer

Why Fastlane?

Mobile releases are notoriously painful. Code signing certificates expire, provisioning profiles get out of sSNc, version numbers need manual bumping, screenshots need regenerating, and deploying to TestFlight takes 15 manual steps. Fastlane automates all of it.

Before Fastlane, our mobile team spent 4-6 hours per release on manual tasks. After: a merge to main triggers a full release to TestFlight and Play Store in under 45 minutes, fully unattended.

Pipeline Architecture

Fastlane CI/CD Pipeline
PR Merge → main branch GitHub Actions macOS runner (iOS) ubuntu runner (Android) parallel jobs secrets from AWS SSM Fastlane iOS match (code signing) increment_build_number gym (build .ipa) pilot (TestFlight) Fastlane Android gradle (build .aab) sign_apk supply (Play Store) TestFlight internal testers Play Store internal track Slack Notify build summary

Project Setup

# Install Fastlane
gem install fastlane

# Initialize in your project root
cd ios && fastlane init
cd android && fastlane init

Code Signing with Match

Code signing is the #1 source of iOS CI failures. Fastlane's match action solves this by storing all certificates and profiles in a private Git repository, encrypted with a passphrase. Every machine — developer laptops and CI runners — fetches signing credentials from the same source.

# ios/fastlane/Matchfile
git_url("https://github.com/yourorg/ios-signing")
storage_mode("git")
type("appstore")
app_identifier(["com.yourcompany.app"])
username("apple@yourcompany.com")

# ios/fastlane/Fastfile
lane :release do
  match(type: "appstore", readonly: true)
  increment_build_number
  gym(
    scheme: "YourApp",
    export_method: "app-store",
    output_directory: "./build"
  )
  pilot(
    skip_waiting_for_build_processing: true,
    changelog: changelog_from_git_commits
  )
  slack(
    message: "🚀 iOS build #{lane_context[SharedValues::BUILD_NUMBER]} uploaded!"
  )
end

Android Lane

# android/fastlane/Fastfile
lane :release do
  gradle(
    task: "bundle",
    build_type: "Release",
    properties: {
      "android.injected.signing.store.file" => ENV["KEYSTORE_PATH"],
      "android.injected.signing.key.alias"  => ENV["KEY_ALIAS"],
      "android.injected.signing.store.password" => ENV["STORE_PASSWORD"]
    }
  )
  supply(
    track: "internal",
    aab: "app/build/outputs/bundle/release/app-release.aab"
  )
end

GitHub Actions Workflow

# .github/workflows/release.yml
on:
  push:
    branches: [main]

jobs:
  ios-release:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - name: Install Fastlane
        run: gem install fastlane
      - name: Run Fastlane release
        run: cd ios && fastlane release
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_KEY }}

  android-release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Fastlane release
        run: cd android && bundle exec fastlane release
        env:
          KEYSTORE_PATH: ${{ secrets.KEYSTORE_PATH }}
          STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
💡Store your Android keystore in AWS Secrets Manager and download it at runtime using the AWS CLI in your GitHub Actions workflow. Never commit it to the repository, even an encrypted one.

The Result

What used to take half a day of an engineer's time now runs every time code merges. Our mobile team ships to TestFlight and the internal Play Store track on every merge to main. Full production releases happen weekly with a single label on the release PR. Engineering time saved: approximately 20 hours per month.