⚡
Milan.dev
>Home>Projects>Experience>Blog
GitHubLinkedIn
status: building
>Home>Projects>Experience>Blog
status: building

Connect

Let's collaborate on infrastructure challenges

Open to discussing DevOps strategies, cloud architecture optimization, security implementations, and interesting infrastructure problems.

send a message→

Find me elsewhere

GitHub
@milandangol
LinkedIn
/in/milan-dangol
Email
milandangol57@gmail.com
Forged with& code

© 2026 Milan Dangol — All systems reserved

back to blog
cicd

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, including code signing, version management, and release automation.

M

Milan Dangol

Sr DevOps & DevSecOps Engineer

Jul 3, 2025
6 min read

Introduction

Mobile app deployment is notoriously painful. Between code signing certificates, provisioning profiles, version bumping, and store submissions, a single release can eat up hours of manual work. Fastlane eliminates this friction by automating the entire release pipeline.

In this post, I'll walk through a production-ready Fastlane setup that handles:

  • iOS: Automatic code signing with Match, build number incrementing, and TestFlight deployment
  • Android: Google Play Store internal track releases with automatic version code management
  • CI/CD: GitHub Actions workflow for fully automated releases on push

Architecture Overview

flowchart TB subgraph "Developer Workflow" A[Push to master/fastlane branch] --> B[GitHub Actions Triggered] end subgraph "GitHub Actions Runner" B --> C[Checkout Code] C --> D[Setup Environment] D --> D1[Java 17] D --> D2[Node.js 20] D --> D3[Ruby 3.4.5] D --> D4[Yarn] D1 & D2 & D3 & D4 --> E[Install Dependencies] E --> E1[yarn install] E --> E2[bundle install] E --> E3[gem install fastlane] E1 & E2 & E3 --> F[Decode Secrets] F --> F1[Service Account JSON] F --> F2[Keystore File] end subgraph "Fastlane Android Deploy" F1 & F2 --> G[fetch_and_increment_build_number] G --> G1[Query Play Console for latest version code] G1 --> G2[Increment version code in build.gradle] G2 --> H[gradle clean bundleRelease] H --> I[supply - Upload to Internal Track] end subgraph "Fastlane iOS Beta" J[Push to iOS branch] --> K[setup_ci] K --> L[match - Fetch Certificates] L --> L1[Clone git repo with certs] L1 --> L2[Install provisioning profiles] L2 --> M[get_version_number] M --> N[latest_testflight_build_number] N --> O[increment_build_number] O --> P[build_app] P --> Q[pilot - Upload to TestFlight] end subgraph "Outputs" I --> R[Google Play Internal Testing] Q --> S[App Store TestFlight] H --> T[Artifacts: APK + AAB] end

Android Fastlane Configuration

Appfile

The Appfile defines your app's identity and authentication:

json_key_file("playstore-sa.json") # Google Play service account
package_name("com.acme.mobileapp")

Fastfile

The Android Fastfile has two key lanes:

default_platform(:android)

platform :android do
  desc "Runs all the tests"
  lane :test do
    gradle(task: "test")
  end

  lane :deploy do
    fetch_and_increment_build_number
    gradle(task: "clean bundleRelease")
    supply(
      track: "internal",
      aab: "./app/build/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true,
      release_status: "completed",
    )
  end

  desc "Fetches the latest build number from Play Console and increments by 1"
  lane :fetch_and_increment_build_number do
    app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:package_name)
    codes = google_play_track_version_codes(
      package_name: app_identifier,
      json_key: "./playstore-sa.json",
      track: "internal"
    )
    next_version_code = (Array(codes).map(&:to_i).max || 0) + 1
    increment_version_code(
      gradle_file_path: "app/build.gradle",
      version_code: next_version_code
    )
  end
end

Key Concepts

Action Purpose
google_play_track_version_codes Queries Play Console API for existing version codes
increment_version_code Modifies build.gradle with new version code
gradle Executes Gradle tasks (clean, bundleRelease)
supply Uploads AAB to Google Play Store

iOS Fastlane Configuration

Appfile

app_identifier("com.acme.mobileapp")
apple_id("dev@acme.com")
itc_team_id("123456789")  # App Store Connect Team ID
team_id("ABCD1234XY")     # Developer Portal Team ID

Matchfile

Match stores certificates in a private Git repo for team sharing:

git_url("git@github.com:acme-org/fastlane-certificates.git")
storage_mode("git")
git_full_name("Your Name")
git_user_email("dev@acme.com")
type("appstore")

Fastfile

default_platform(:ios)

platform :ios do
  desc "Push a new beta build to TestFlight"
  lane :beta do
    setup_ci if ENV["CI"]
    match(
      app_identifier: "com.acme.mobileapp",
      type: "appstore",
      git_private_key: "./fastlane/deploy_key",
      readonly: true
    )
    version = get_version_number(
      xcodeproj: "MyApp.xcodeproj",
      target: "MyApp"
    )
    latest = latest_testflight_build_number(
      app_identifier: "com.acme.mobileapp",
      version: version
    ) || 0
    increment_build_number(
      xcodeproj: "MyApp.xcodeproj",
      build_number: latest + 1
    )
    build_app(workspace: "MyApp.xcworkspace", scheme: "MyApp")
    pilot(
      api_key_path: "fastlane/store.json",
      skip_waiting_for_build_processing: true
    )
  end
end

iOS Flow Breakdown

sequenceDiagram participant Dev as Developer participant GH as GitHub Actions participant Match as Match (Git Repo) participant ASC as App Store Connect participant TF as TestFlight Dev->>GH: Push to branch GH->>GH: setup_ci (keychain setup) GH->>Match: Fetch certificates & profiles Match-->>GH: Install to keychain GH->>ASC: Query latest TestFlight build number ASC-->>GH: Return build number (e.g., 42) GH->>GH: increment_build_number (43) GH->>GH: build_app (xcodebuild) GH->>TF: pilot (upload IPA) TF-->>Dev: Build available for testing

GitHub Actions Workflow

The CI/CD pipeline ties everything together:

name: Android APK Build and Deploy

on:
  push:
    branches:
      - master
      - fastlane

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Set up Java
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20 

      - name: Enable Corepack (Yarn)
        run: |
          corepack enable
          corepack prepare yarn@stable --activate

      - name: Yarn install
        working-directory: android
        run: yarn install

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.4.5

      - name: Install Fastlane
        run: gem install fastlane

      - name: Install Ruby dependencies
        working-directory: android
        run: |
          gem install bundler -N
          bundle install --path ../vendor/bundle

      - name: Decode Service Account Key
        uses: timheuer/base64-to-file@v1
        id: service_account_json_file
        with:
          fileName: "playstore-sa.json"
          FileDir: "./android"
          encodedString: ${{ secrets.GPLAY_SERVICE_ACCOUNT_KEY_JSON }}

      - name: Validate Service Account Key
        run: |
          cd android
          fastlane run validate_play_store_json_key json_key:./playstore-sa.json

      - name: Decode Keystore File
        uses: timheuer/base64-to-file@v1
        id: android_keystore
        with:
          fileName: "my-upload-key.keystore"
          fileDir: "./android/app"
          encodedString: ${{ secrets.KEYSTORE_FILE }}

      - name: Build & Deploy to Internal Testing
        working-directory: android
        run: fastlane android deploy

      - name: Collect artifacts
        uses: actions/upload-artifact@v4
        with:
          name: android-build-artifacts
          path: |
            android/app/build/outputs/apk/release/app-release.apk
            android/app/build/outputs/bundle/release/app-release.aab

Secrets Management

Store these as GitHub repository secrets:

Secret Description
GPLAY_SERVICE_ACCOUNT_KEY_JSON Base64-encoded Google Play service account JSON
KEYSTORE_FILE Base64-encoded Android signing keystore
MATCH_PASSWORD Password for Match certificate encryption (iOS)
APP_STORE_CONNECT_API_KEY Base64-encoded App Store Connect API key (iOS)

Encoding secrets for GitHub:

# Encode service account JSON
base64 -i playstore-sa.json | pbcopy

# Encode keystore
base64 -i release-key.keystore | pbcopy

Complete Release Flow

flowchart LR subgraph "Trigger" A[git push master] end subgraph "Build" B[Install deps] C[Decode secrets] D[Build AAB/IPA] end subgraph "Version" E[Query store API] F[Increment build #] end subgraph "Deploy" G[Upload to store] H[Internal/TestFlight] end subgraph "Artifacts" I[Save APK/AAB] end A --> B --> C --> E --> F --> D --> G --> H D --> I

Best Practices

  1. Use readonly: true for Match in CI - Prevents accidental certificate regeneration
  2. Skip metadata/screenshot uploads - Speeds up releases when only updating the binary
  3. Validate credentials before build - Fail fast with validate_play_store_json_key
  4. Use internal/TestFlight tracks - Test before promoting to production
  5. Store artifacts - Always save build outputs for debugging

Troubleshooting

Common Issues

"Version code already exists"

  • The fetch_and_increment_build_number lane should prevent this
  • Ensure you're querying the correct track (internal vs production)

"Code signing failed"

  • Verify Match repo access with SSH key
  • Check setup_ci is called in CI environment

"Service account permission denied"

  • Ensure service account has "Release Manager" role in Play Console
  • Verify JSON key is correctly base64-decoded

Conclusion

Fastlane transforms mobile releases from a multi-hour manual process into a single git push. The combination of:

  • Match for iOS certificate management
  • Supply for Android Play Store uploads
  • GitHub Actions for CI/CD orchestration

Creates a robust, repeatable deployment pipeline that scales with your team. No more "works on my machine" certificate issues or manual version bumping—just push and ship.

Share this article

Tags

#fastlane#mobile#ios#android#github-actions#automation

Related Articles

system-design13 min read

Payment Processing System at Scale: Stripe/Adyen Integration with AWS EventBridge, Lambda, and DynamoDB

Building a payment processing system handling millions of daily transactions - featuring EventBridge for event-driven orchestration, Lambda for serverless processing, DynamoDB for transaction state, idempotency guarantees, and real-time fraud detection with Kinesis.

system-design12 min read

AI Chatbot System Architecture: WhatsApp Business API, Facebook Messenger, and AWS Bedrock Integration

Designing a multi-channel AI chatbot system handling 5M+ conversations monthly - featuring AWS Bedrock for conversational AI, SQS for message queuing, DynamoDB for conversation state, and Lambda for serverless processing across WhatsApp and Facebook Messenger.

cloud9 min read

Multi-Region AWS Infrastructure for Resilience: A Terraform Deep Dive

Learn how to architect highly available, multi-region AWS infrastructure using Terraform, Transit Gateway, Network Load Balancers, and intelligent routing strategies for enterprise-grade applications.