Skip to content
Learni
View all tutorials
CI/CD

How to Automate Mobile CI/CD Pipelines with Fastlane in 2026

Lire en français

Introduction

Fastlane is the essential open-source tool for automating mobile workflows in 2026, handling builds, tests, screenshots, and deployments without manual intervention. For senior devs, it goes beyond basic scripts with custom lanes, modular plugins, and native CI/CD integrations, shrinking release cycles from days to minutes. Imagine: a commit push triggers unit/UI tests, localized screenshot generation, signed builds, and uploads to TestFlight or Google Play—all with secret management and automated rollbacks.

This advanced tutorial targets pros: multi-platform setup (iOS/Android), performance tweaks (caching certs/provisioning), and CI scaling. You'll save 80% on release time, eliminate human errors (like expired profiles), and scale to 10+ builds/day. Ready to turn your deployments into a well-oiled machine? Let's follow a real project: 'MyMobileApp' with weekly betas and monthly prod releases.

Prerequisites

  • macOS Ventura+ with Xcode 16+ (for iOS) and Android Studio Iguana+ (for Android).
  • Accounts: Apple Developer Program ($99/year), Google Play Console ($25 one-time).
  • Ruby 3.2+ and Bundler 2.4+ installed (gem install bundler).
  • Existing project: Git repo with iOS Xcodeproj and Android app-level build.gradle.
  • GitHub repo with secrets: MATCH_PASSWORD, APPSTORE_CONNECT_KEY_ID, GOOGLE_SERVICE_ACCOUNT_JSON.
  • Knowledge: Basic Ruby, Fastlane actions (gym, fastlane/match).

Fastlane Installation and Initialization

terminal
#!/bin/bash

# Install Fastlane via Bundler (recommended for isolation)
gem install bundler
bundle init

# Add Fastlane and advanced plugins
cat > Gemfile << EOF
source "https://rubygems.org"
gem "fastlane"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
EOF

bundle install

# Initialize for iOS and Android
fastlane init --ios
fastlane init --android

# Configure Appfile for iOS
cat > fastlane/Appfile << EOF
app_identifier "com.example.mymobileapp"
apple_id "votre@apple.id"
team_id "ABC123DEF4"
EOF

# Configure Appfile for Android
cat > ./android/fastlane/Appfile << EOF
package_name "com.example.mymobileapp"
json_key_file "path/to/google-service-account.json"
EOF

This script sets up an isolated environment with Bundler, avoiding gem conflicts. The init creates fastlane/ folders for iOS and ./android/fastlane/ for Android, generating skeleton Fastfiles. Appfiles centralize app identifiers, catching package/bundle ID mismatches early.

Certificate Setup with Match

Match centralizes profiles and certs in a private Git repo, making CI clones reproducible. For advanced use, set readonly: true in CI and generate locally. Pair it with 1Password or GitHub Secrets for automatic rotation.

Match Setup for Shared Certificates

fastlane/Matchfile
storage_mode("git")
git_url("git@github.com:yourorg/fastlane-certs.git")
app_identifier(["com.example.mymobileapp", "com.example.mymobileapp.Staging"])
username("votre@apple.id")
team_id("ABC123DEF4")
readonly(true)

# Advanced: specific types
# Development certs auto-generated
# AdHoc for beta/TestFlight
# AppStore for production

The Matchfile configures a dedicated Git repo for certs, supporting multi-environments (Staging/Prod). readonly(true) protects against accidental overwrites in CI. Pitfall: always commit/push after match init, or clones will fail with 'no profiles found'.

Gemfile with Advanced Plugins

Gemfile
source "https://rubygems.org"

gem "fastlane", "~> 2.220"

# Advanced plugins for screenshots, performance, and analytics
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
gem "fastlane-plugin-screenshot_timer"
gem "fastlane-plugin-match_custom"
gem "fastlane-plugin-appcenter_distribute"

# CI optimizations
gem "cocoapods"
gem "jazzy" # Auto docs
group :test do
  gem "slather" # Coverage
end

# Lock versions for CI reproducibility
eval_gemfile "fastlane/Pluginfile"

This Gemfile locks versions and adds plugins for screenshot timers (optimizes slow builds), custom Match, and AppCenter (TestFlight alternative). Pluginfile is auto-generated by bundle exec fastlane install_plugins. Avoid global gems: Bundler ensures isolation and GitHub Actions caching.

iOS Fastfile: Advanced Lanes

Progressive structure: Modular lanes for test → build → screenshots → deploy. Use before_all/after_all for shared setup/teardown, like CocoaPods caching.

Complete iOS Fastfile with Screenshots and Beta

fastlane/Fastfile
default_platform(:ios)

platform :ios do
  before_all do
    # Shared setup: certs, pods
    match(type: "appstore", readonly: true)
    cocoapods
  end

  desc "Tests unitaires et UI"
  lane :test do
    scan(
      scheme: "MyMobileApp",
      devices: ["iPhone 15 Pro"],
      clean: true
    )
    slather
  end

  desc "Générer screenshots localisés"
  lane :screenshots do
    snapshot(
      locale: ["en-US", "fr-FR"],
      dark_mode: true,
      ios_multiplier: 3
    )
    frameit
  end

  desc "Beta TestFlight"
  lane :beta do
    increment_build_number(build_number: latest_testflight_build_number + 1)
    gym(
      scheme: "MyMobileApp",
      export_method: "app-store",
      output_directory: "builds"
    )
    upload_to_testflight(
      skip_submission: true,
      notify_tester: true
    )
    appcenter_distribute
  end

  desc "Release App Store"
  lane :release do
    screenshots
    beta
    upload_to_app_store
  end

after_all do
  clean_build_artifacts
end
end

This iOS Fastfile handles everything: scan for parallel tests, snapshot/frameit for auto marketing assets (13 iOS sizes), optimized gym AppStore export. latest_testflight_build_number avoids collisions. Pitfall: use skip_waiting_for_build_processing in CI for faster feedback.

Complete Android Fastfile with Play Store

android/fastlane/Fastfile
default_platform(:android)

platform :android do
  before_all do
    # Gradle clean
    gradle(task: "clean")
  end

  desc "Tests Android"
  lane :test do
    gradle(task: "test")
    gradle(task: "connectedAndroidTest")
  end

  desc "Build et deploy beta (Internal Testing)"
  lane :beta do
    gradle(
      task: "clean bundleRelease",
      properties: {
        "android.injected.signing.store.file": "keystore.jks",
        "android.injected.signing.store.password": ENV["KEYSTORE_PASSWORD"]
      }
    )
    upload_to_play_store(
      track: "internal",
      aab: "app/build/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end

  desc "Release Production"
  lane :release do
    increment_version_code
    increment_version_name(append_version_code: false)
    beta
    upload_to_play_store(track: "production")
  end

after_all do |lane|
  sh("../gradlew clean")
end
end

Android Fastfile uses gradle for custom tasks with ENV-injected signing (secure). upload_to_play_store with tracks (internal/production) for beta/GA. Advanced: auto increment_version_* bumps. Avoid hardcoded paths: use vars for multi-module scalability.

CI/CD Integration with GitHub Actions

Run fastlane beta on main pushes. Cache Bundler/Match for <5min builds. Use GitHub Secrets for MATCH_GIT_URL (private).

Multi-Platform GitHub Actions Workflow

.github/workflows/fastlane.yml
name: CI/CD Fastlane

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test-ios:
    runs-on: macos-14
    steps:
    - uses: actions/checkout@v4
    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: '3.2'
        bundler-cache: true
    - name: Install dependencies
      run: bundle install
    - name: Run tests
      run: bundle exec fastlane test
      env:
        MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
        MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
        APPSTORE_CONNECT_KEY_ID: ${{ secrets.APPSTORE_CONNECT_KEY_ID }}

  beta-android:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: nttld/setup-ndk@v1
      id: setup-ndk
      with:
        ndk-version: "r26b"
    - name: Install Java
      uses: actions/setup-java@v4
      with:
        distribution: 'temurin'
        java-version: '17'
    - name: Bundle & Beta
      run: |
        bundle install
        bundle exec fastlane beta
      env:
        KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
        GOOGLE_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_JSON }}

  deploy-beta:
    needs: [test-ios, beta-android]
    runs-on: macos-14
    if: github.ref == 'refs/heads/main'
    steps:
    - uses: actions/checkout@v4
    - uses: ruby/setup-ruby@v1
      with: bundler-cache: true
    - run: bundle exec fastlane beta
      env:
        FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 1200

This workflow parallelizes iOS/Android tests and deploys beta on main. bundler-cache and setup-ndk optimize (<10min total). Conditional jobs (needs/if) ensure proper gating. Pitfall: Xcode timeouts → set FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT=1200.

Best Practices

  • Modularity: One lane per responsibility (test/screenshots/deploy), reuse via lane(:sub_lane).
  • Secrets management: Never hardcode; use ENV + GitHub/1Password CLI for auto-rotation.
  • Aggressive caching: Pods/DerivedData in CI, readonly Match for 90% speedup.
  • Observability: Slack/Teams notifications via notify, Slather for >80% coverage.
  • Rollback: reset_git_repo + tag backup before release.

Common Errors to Avoid

  • Expired certs: match nuke distribution + regenerate; monitor with GitHub cron.
  • Screenshot failures: Ensure simulators launch (snapshot_launch_timer: 30) and locales installed.
  • CI timeouts: Bump FASTLANE_UPDATE_METHOD=global and cache Bundler.
  • Version bump races: Lock increment_build_number with rescue and retry.

Next Steps

Dive into Learni trainings on mobile CI/CD for Fastlane + GitLab/Terraform. Read Fastlane docs and contribute on GitHub. Advanced: integrate Firebase App Distribution and custom Ruby plugins.