Back to Blog

Mobile CI/CD: Fastlane, GitHub Actions for iOS and Android, TestFlight and Play Store

Automate mobile app delivery — Fastlane setup for iOS and Android, GitHub Actions workflows, code signing with Match, TestFlight beta distribution, Play Store d

Viprasol Tech Team
May 24, 2026
13 min read

Mobile CI/CD: Fastlane, GitHub Actions for iOS and Android, TestFlight and Play Store

Mobile CI/CD is harder than web CI/CD. Code signing certificates, provisioning profiles, Apple Developer portal quirks, Android keystore management, and the app store review process all add complexity that web deployments don't have.

This guide covers the setup that automates iOS and Android builds from commit to beta distribution.


The Mobile Deployment Challenge

ChallengeiOSAndroid
Code signingCertificates + provisioning profilesKeystore file
DistributionApp Store Connect / TestFlightPlay Store / APK direct
Review time1–3 days (App Store)1–3 days (Play Store)
Beta testingTestFlight (up to 10,000 testers)Internal track → Closed/Open testing
Build machineRequires macOS for iOSAny OS for Android

Fastlane: The Automation Layer

Fastlane is the standard tool for mobile deployment automation. It wraps common tasks (building, signing, uploading) into reusable "lanes."

# Install Fastlane
gem install fastlane
# or
brew install fastlane

# Initialize in your project root
cd YourApp
fastlane init

🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript — production-grade stack
  • Fixed-price contracts — no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

iOS: Fastfile Configuration

# ios/fastlane/Fastfile

default_platform(:ios)

platform :ios do
  # Before any lane: ensure we have the right environment
  before_all do
    ensure_git_status_clean
    xcversion(version: "~> 16.0")  # Ensure correct Xcode version
  end

  # Lane: Sync certificates and provisioning profiles
  lane :certificates do
    match(
      type: "appstore",          # or "adhoc" for TestFlight, "development" for dev
      app_identifier: "com.yourcompany.yourapp",
      git_url: "https://github.com/yourorg/ios-certificates",  # Private repo for certs
      git_branch: "main",
      readonly: is_ci,           # CI only reads; devs can rotate
    )
  end

  # Lane: Run tests
  lane :test do
    run_tests(
      scheme: "YourApp",
      device: "iPhone 16",
      code_coverage: true,
      xcargs: "-maximum-test-execution-time-allowance 300"
    )
  end

  # Lane: Build and upload to TestFlight
  lane :beta do
    certificates  # Sync signing assets

    # Increment build number (must be unique and incrementing for App Store)
    increment_build_number(
      build_number: ENV["BUILD_NUMBER"] || latest_testflight_build_number + 1
    )

    # Build the app
    build_app(
      scheme: "YourApp",
      workspace: "YourApp.xcworkspace",
      configuration: "Release",
      export_method: "app-store",
      export_options: {
        provisioningProfiles: {
          "com.yourcompany.yourapp" => "match AppStore com.yourcompany.yourapp"
        }
      }
    )

    # Upload to TestFlight
    upload_to_testflight(
      api_key_path: "fastlane/app_store_connect_api_key.json",
      skip_waiting_for_build_processing: false,  # Wait for processing
      distribute_external: true,
      groups: ["Beta Testers"],
      changelog: changelog_from_git_commits(
        commits_count: 10,
        pretty: "- %s"
      )
    )

    # Notify Slack
    slack(
      message: "New iOS beta uploaded to TestFlight! Build #{get_build_number}",
      slack_url: ENV["SLACK_WEBHOOK_URL"],
      success: true
    )
  end

  # Lane: Release to App Store
  lane :release do
    certificates

    build_app(
      scheme: "YourApp",
      workspace: "YourApp.xcworkspace",
      configuration: "Release",
      export_method: "app-store"
    )

    upload_to_app_store(
      api_key_path: "fastlane/app_store_connect_api_key.json",
      submit_for_review: true,
      automatic_release: false,   # Manually release after approval
      force: true,
      precheck_include_in_app_purchases: false
    )
  end

  error do |lane, exception|
    slack(
      message: "iOS #{lane} lane failed: #{exception.message}",
      slack_url: ENV["SLACK_WEBHOOK_URL"],
      success: false
    )
  end
end

iOS Code Signing: Match

Match stores all certificates and provisioning profiles encrypted in a Git repository, solving the "it works on my machine" signing problem.

# Setup Match (one-time, by team lead)
fastlane match init
# Choose: git (recommended)
# Enter: private repo URL for certificates

# Generate and store certificates
fastlane match development
fastlane match appstore
fastlane match adhoc

# On CI: just read existing certificates
fastlane match appstore --readonly
# Fastlane Match creates:
# - Development certificate + provisioning profile
# - AppStore certificate + provisioning profile
# All encrypted with MATCH_PASSWORD, stored in private git repo
# Any team member runs `fastlane match` to install locally

🚀 Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Android: Fastfile Configuration

# android/fastlane/Fastfile

default_platform(:android)

platform :android do
  lane :test do
    gradle(
      task: "test",
      project_dir: "android/"
    )
  end

  lane :beta do
    # Increment version code (must be higher than current Play Store version)
    android_set_version_code(
      version_code: ENV["BUILD_NUMBER"].to_i,
      gradle_file: "android/app/build.gradle"
    )

    # Build signed AAB (Android App Bundle — required for Play Store)
    gradle(
      task: "bundle",
      build_type: "Release",
      project_dir: "android/",
      properties: {
        "android.injected.signing.store.file" => ENV["KEYSTORE_FILE"],
        "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 Play Store internal testing track
    upload_to_play_store(
      track: "internal",           # internal → alpha → beta → production
      aab: "android/app/build/outputs/bundle/release/app-release.aab",
      json_key: "fastlane/play_store_api_key.json",
      release_status: "completed",
      skip_upload_apk: true
    )
  end

  lane :release do |options|
    gradle(
      task: "bundle",
      build_type: "Release",
      project_dir: "android/",
      properties: {
        "android.injected.signing.store.file" => ENV["KEYSTORE_FILE"],
        # ... signing properties
      }
    )

    upload_to_play_store(
      track: options[:track] || "production",
      rollout: "0.1",            # 10% staged rollout
      aab: "android/app/build/outputs/bundle/release/app-release.aab",
      json_key: "fastlane/play_store_api_key.json",
    )
  end
end

GitHub Actions: Full CI/CD Pipeline

# .github/workflows/mobile-ci.yml
name: Mobile CI/CD

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

env:
  BUILD_NUMBER: ${{ github.run_number }}

jobs:
  # ─────────────────────────────────────────
  # iOS Beta Build
  # ─────────────────────────────────────────
  ios-beta:
    name: iOS Beta  TestFlight
    runs-on: macos-15          # Must use macOS runner for iOS builds
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v4

      - name: Setup Ruby (for Fastlane)
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.3'
          bundler-cache: true

      - name: Setup Node.js (for React Native)
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Setup SSH key for Match certificates repo
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.MATCH_REPO_SSH_KEY }}

      - name: Run iOS Fastlane beta lane
        run: bundle exec fastlane ios beta
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
          APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
          APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.ASC_API_KEY_CONTENT }}
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
          BUILD_NUMBER: ${{ env.BUILD_NUMBER }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}

  # ─────────────────────────────────────────
  # Android Beta Build
  # ─────────────────────────────────────────
  android-beta:
    name: Android Beta  Play Store Internal
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v4

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.3'
          bundler-cache: true

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Setup Java (for Android build)
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Install dependencies
        run: npm ci

      # Decode keystore from base64 secret
      - name: Decode Android Keystore
        run: |
          echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode \
            > android/app/release.keystore

      - name: Run Android Fastlane beta lane
        run: bundle exec fastlane android beta
        env:
          KEYSTORE_FILE: "app/release.keystore"
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
          PLAY_STORE_JSON_KEY: ${{ secrets.PLAY_STORE_JSON_KEY }}
          BUILD_NUMBER: ${{ env.BUILD_NUMBER }}

Version Management

// scripts/bump-version.ts
// Bump version numbers across both platforms from one place
import { execSync } from 'child_process';
import { readFileSync, writeFileSync } from 'fs';

const newVersion = process.argv[2];  // e.g., "2.4.1"
if (!newVersion) throw new Error('Usage: ts-node bump-version.ts 2.4.1');

// Update package.json
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
pkg.version = newVersion;
writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');

// iOS: update version in Info.plist via Fastlane
execSync(`cd ios && bundle exec fastlane run increment_version_number version_number:${newVersion}`, { stdio: 'inherit' });

// Android: update versionName in build.gradle
let gradle = readFileSync('android/app/build.gradle', 'utf8');
gradle = gradle.replace(/versionName "[\d.]+"/, `versionName "${newVersion}"`);
writeFileSync('android/app/build.gradle', gradle);

console.log(`Version bumped to ${newVersion}`);

Working With Viprasol

We set up mobile CI/CD pipelines for React Native and Flutter apps — Fastlane configuration, code signing automation with Match, GitHub Actions workflows, and TestFlight/Play Store delivery. Automated mobile builds eliminate manual release day operations.

Talk to our team about mobile CI/CD automation.


See Also

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms — we build it all with Next.js and React.

Free consultation • No commitment • Response within 24 hours

Viprasol · Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.