Question #425MediumAutomationTools & DevOps

How to automate fastlane in iOS?

#fastlane#automation#ios#ci-cd#deployment#app-store#testflight

Answer

Overview

Fastlane is a powerful open-source tool that automates mobile app deployment. For Flutter developers targeting iOS, Fastlane can handle building, signing, version validation, TestFlight uploads, and even App Store submission โ€” all with a single command.

If you're tired of manually exporting .ipa files from Xcode and uploading them through App Store Connect, this guide will help you fully automate your iOS release workflow.

Key Benefits:

  • โšก Automate repetitive tasks - Build, sign, and upload automatically
  • โฑ๏ธ Save time - No more manual Xcode exports
  • ๐Ÿ”’ Prevent duplicate builds - Validates version + build numbers
  • ๐Ÿค– CI/CD friendly - Works with GitHub Actions, Bitrise, Jenkins
  • ๐Ÿ“ Centralized configuration - Everything is version-controlled

Prerequisites

Before setting up Fastlane for iOS, ensure you have:

Apple Requirements:

  • โœ… Active Apple Developer subscription ($99/year)
  • โœ… App has completed initial verification
  • โœ… App has been released at least once on App Store Connect

System Requirements:

  • macOS with Xcode installed (latest stable version)
  • Ruby 2.5 or higher (check with
    text
    ruby --version
    )
  • Homebrew installed (for macOS)
  • Flutter SDK installed and configured

Project Requirements:

  • Flutter project with iOS folder
  • App already registered in App Store Connect
  • Bundle identifier configured in Xcode

โš ๏ธ Important: Fastlane requires your app to have completed the first manual release. You cannot use Fastlane for the very first App Store submission.


Step 1: Install & Initialize Fastlane

1๏ธโƒฃ Install Fastlane

macOS (Recommended):

bash
brew install fastlane

Alternative (RubyGems):

bash
sudo gem install fastlane -NV

2๏ธโƒฃ Navigate to Your Flutter Project

bash
cd your_flutter_project
cd ios

3๏ธโƒฃ Initialize Fastlane

bash
fastlane init

When prompted:

text
What would you like to use fastlane for?
1. ๐Ÿ“ธ  Automate screenshots
2. ๐Ÿ‘ฉโ€โœˆ๏ธ  Automate beta distribution to TestFlight
3. ๐Ÿš€  Automate App Store distribution
4. ๐Ÿ›   Manual setup

Choose Option 3 โ†’ Fully automated deployment

You'll be prompted to:

  • Enter your Apple ID (App Store Connect email)
  • Enter your password
  • Complete 2FA verification if enabled

After initialization, Fastlane will generate:

text
ios/
 โ”œโ”€โ”€ Gemfile
 โ””โ”€โ”€ fastlane/
      โ”œโ”€โ”€ Appfile
      โ””โ”€โ”€ Fastfile

These files will control your entire deployment pipeline.


Step 2: Managing Dependencies (Gemfile Setup)

Open the generated

text
Gemfile
and update it to ensure consistent Ruby and gem versions:

ruby
# ios/Gemfile
source "https://rubygems.org"

ruby "~> 3.4.7"

gem "fastlane", ">= 2.232.0"
gem "cocoapods", ">= 1.16.2"
gem "abbrev"

Install dependencies:

bash
bundle install

Why use Bundler?

  • โœ… Ensures all developers use the same fastlane version
  • โœ… Prevents "works on my machine" issues
  • โœ… Required for CI/CD consistency

Always run fastlane via Bundler:

bash
bundle exec fastlane beta

Step 3: Create Environment Configuration

Create a

text
.env
file inside
text
ios/fastlane/
:

bash
cd fastlane
touch .env

Add the following configuration:

bash
# ios/fastlane/.env

# App Bundle Identifier
APP_IDENTIFIER=com.example.app

# Apple Developer Account Email
APPLE_ID=your@email.com

# App Store Connect Team ID
ITC_TEAM_ID=

# Developer Portal Team ID
TEAM_ID=

# App Store Connect API Key
APP_STORE_CONNECT_KEY_ID=
APP_STORE_CONNECT_ISSUER_ID=
APP_STORE_CONNECT_KEY_PATH=./AuthKey.p8

๐Ÿ”Ž Where to Find These Values

1. APP_IDENTIFIER

Your app's bundle identifier (e.g.,

text
com.yourcompany.appname
)

Find it in:

  • Xcode: Open
    text
    ios/Runner.xcodeproj
    โ†’ General โ†’ Bundle Identifier
  • Or in
    text
    ios/Runner/Info.plist
    โ†’
    text
    CFBundleIdentifier

2. APPLE_ID

Your Apple Developer account email address (used to sign in to App Store Connect)


3. ITC_TEAM_ID (App Store Connect Team ID)

Step-by-step:

  1. Go to App Store Connect
  2. Click on your username (top right)
  3. Select View Membership
  4. Copy the Team ID (e.g.,
    text
    123456789
    )

4. TEAM_ID (Developer Portal Team ID)

Step-by-step:

  1. Go to Apple Developer Portal
  2. Click on Membership (left sidebar)
  3. Find Team ID under your account details
  4. Copy the Team ID (e.g.,
    text
    ABC123XYZ
    )

Note:

text
ITC_TEAM_ID
and
text
TEAM_ID
are often the same, but not always. Use the correct values from each portal.


5. API Key Values (See Step 4 below)

  • text
    APP_STORE_CONNECT_KEY_ID
    โ†’ Key ID from App Store Connect
  • text
    APP_STORE_CONNECT_ISSUER_ID
    โ†’ Issuer ID from App Store Connect
  • text
    APP_STORE_CONNECT_KEY_PATH
    โ†’ Path to .p8 file (e.g.,
    text
    ./AuthKey.p8
    )

๐Ÿ” Secure Your .env File

Add to

text
.gitignore
:

bash
# ios/fastlane/.gitignore
.env
*.p8

Never commit:

  • โŒ
    text
    .env
    file (contains sensitive credentials)
  • โŒ
    text
    .p8
    files (API keys)

For CI/CD:

  • Use GitHub Secrets, Bitrise Environment Variables, etc.
  • Never hardcode credentials in code

Step 4: Create App Store Connect API Key

Why Use API Keys?

  • โœ… More secure - No passwords stored in CI/CD
  • โœ… More reliable - Doesn't expire like app-specific passwords
  • โœ… No 2FA issues - Works seamlessly in automation
  • โœ… Recommended by Apple - Modern authentication method

Create API Key

1. Go to App Store Connect:

Visit: https://appstoreconnect.apple.com

2. Navigate to API Keys:

  • Click on Users and Access (in the top navigation)
  • Click on Integrations tab (formerly "Keys")
  • Click the + button to create a new key

3. Configure the Key:

FieldValue
Name
text
Fastlane API Key
(or any descriptive name)
AccessApp Manager (minimum required)
or Admin (full access)

4. Download the Key:

  • Click Generate
  • Download the
    text
    .p8
    file immediately (โš ๏ธ only downloadable once!)
  • Note down the Key ID (e.g.,
    text
    ABC123XYZ
    )
  • Note down the Issuer ID (e.g.,
    text
    12345678-1234-1234-1234-123456789012
    )

5. Store the Key:

bash
# Move the downloaded .p8 file to fastlane directory
mv ~/Downloads/AuthKey_ABC123XYZ.p8 ios/fastlane/AuthKey.p8

# Secure the file
chmod 600 ios/fastlane/AuthKey.p8

6. Update .env file:

bash
# ios/fastlane/.env
APP_STORE_CONNECT_KEY_ID=ABC123XYZ
APP_STORE_CONNECT_ISSUER_ID=12345678-1234-1234-1234-123456789012
APP_STORE_CONNECT_KEY_PATH=./AuthKey.p8

API Key Permissions

RoleCan Upload to TestFlight?Can Submit to App Store?
DeveloperโŒ NoโŒ No
App Managerโœ… Yesโœ… Yes
Adminโœ… Yesโœ… Yes

Recommendation: Use App Manager role (least privilege principle).


Step 5: Configure Fastlane Files

๐Ÿ“„ Update Appfile

ruby
# ios/fastlane/Appfile

app_identifier(ENV["APP_IDENTIFIER"])
apple_id(ENV["APPLE_ID"])
itc_team_id(ENV["ITC_TEAM_ID"])
team_id(ENV["TEAM_ID"])

This file:

  • Reads values from
    text
    .env
    file
  • Sets default app identifier and team IDs
  • Used by all Fastlane lanes

๐Ÿ“„ Update Fastfile

Production-ready Fastfile for Flutter iOS automation:

ruby
# ios/fastlane/Fastfile

default_platform(:ios)

# Load environment variables
Dotenv.load('.env')

platform :ios do
  # API Key helper method
  def api_key
    key_path = File.expand_path(ENV["APP_STORE_CONNECT_KEY_PATH"], __dir__)
    app_store_connect_api_key(
      key_id: ENV["APP_STORE_CONNECT_KEY_ID"],
      issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
      key_filepath: key_path
    )
  end

  # Lane: Push a new iOS beta build to TestFlight
  desc "Push a new iOS beta build to TestFlight"
  lane :beta do
    require 'yaml'

    # Read version from pubspec.yaml
    pubspec = YAML.load_file("../../pubspec.yaml")
    version, build_number = pubspec['version'].split('+')

    # Check if build already exists on TestFlight
    latest_build = app_store_build_number(
      api_key: api_key,
      app_identifier: ENV["APP_IDENTIFIER"],
      live: false,
      version: version
    ) rescue nil

    # Prevent duplicate builds
    if latest_build && latest_build.to_i >= build_number.to_i
      UI.user_error!("โŒ Build #{build_number} already exists on TestFlight. Increment build number in pubspec.yaml.")
    end

    # Build Flutter app
    sh("cd ../.. && flutter build ipa --release")

    # Build iOS app
    build_app(
      scheme: "Runner",
      workspace: "Runner.xcworkspace",
      export_method: "app-store",
      xcargs: "-allowProvisioningUpdates"
    )

    # Upload to TestFlight
    testflight(
      api_key: api_key,
      app_identifier: ENV["APP_IDENTIFIER"]
    )

    UI.success("โœ… Uploaded to TestFlight successfully ๐Ÿš€")
  end

  # Lane: Promote existing TestFlight build to App Store
  desc "Promote existing TestFlight build to App Store"
  lane :production do |options|
    require 'yaml'

    # Read version from pubspec.yaml
    pubspec = YAML.load_file("../../pubspec.yaml")
    version, build = pubspec['version'].split('+')

    # Promote to App Store
    deliver(
      api_key: api_key,
      app_version: version,
      build_number: build,
      submit_for_review: false,  # Set to true for auto-submit
      automatic_release: false,  # Set to true for auto-release
      force: true,
      skip_metadata: false,
      skip_screenshots: false
    )

    UI.success("โœ… Submitted to App Review ๐ŸŽ")
  end

  # Lane: Run tests
  desc "Run iOS unit tests"
  lane :test do
    run_tests(
      scheme: "Runner",
      device: "iPhone 14 Pro",
      clean: true
    )
  end

  # Error handling
  error do |lane, exception|
    UI.error("โŒ Error in lane #{lane}: #{exception.message}")
  end
end

๐Ÿ” Key Features of This Fastfile

1. Version Validation โœ…

ruby
latest_build = app_store_build_number(...)
if latest_build && latest_build.to_i >= build_number.to_i
  UI.user_error!("Build already exists. Increment build number.")
end

Prevents:

  • โŒ Uploading duplicate builds
  • โŒ Wasting build time
  • โŒ App Store Connect errors

2. Reads Version from pubspec.yaml โœ…

ruby
pubspec = YAML.load_file("../../pubspec.yaml")
version, build_number = pubspec['version'].split('+')

Example pubspec.yaml:

yaml
version: 1.2.0+42
  • text
    version
    =
    text
    1.2.0
  • text
    build_number
    =
    text
    42

3. Flutter Build Integration โœ…

ruby
sh("cd ../.. && flutter build ipa --release")

Runs:

bash
flutter build ipa --release

Generates:

  • text
    build/ios/ipa/your_app.ipa

4. Production Lane โœ…

ruby
lane :production do
  deliver(
    api_key: api_key,
    app_version: version,
    build_number: build
  )
end

Purpose:

  • Promotes an existing TestFlight build to App Store
  • No need to rebuild
  • Just promotes the version you specify

Step 6: Running Fastlane Lanes

๐Ÿš€ Upload to TestFlight

bash
cd ios
bundle exec fastlane beta

What happens:

  1. Reads version from
    text
    pubspec.yaml
  2. Checks if build already exists on TestFlight
  3. Builds Flutter app (
    text
    flutter build ipa --release
    )
  4. Builds iOS IPA file
  5. Uploads to TestFlight
  6. Shows success message

๐ŸŽ Submit to App Store

bash
cd ios
bundle exec fastlane production

What happens:

  1. Reads version from
    text
    pubspec.yaml
  2. Promotes existing TestFlight build to App Store
  3. Submits for review (if
    text
    submit_for_review: true
    )

๐Ÿงช Run Tests

bash
cd ios
bundle exec fastlane test

โš ๏ธ Important Notes

Before running Fastlane, ensure:

1. App Exists in App Store Connect โœ…

  • Create your app in App Store Connect first
  • Set up app information, screenshots, description
  • Configure pricing and availability

2. Complete First Manual Release โœ…

  • Fastlane cannot handle the very first App Store submission
  • You must manually release version 1.0.0 through Xcode + App Store Connect
  • After first release, Fastlane can handle all future updates

3. Increment Version in pubspec.yaml โœ…

Before each release:

yaml
# pubspec.yaml

# WRONG - Same as previous release
version: 1.0.0+1

# CORRECT - Incremented build number
version: 1.0.0+2

# OR increment version
version: 1.1.0+1

Rules:

  • Increment build number (+1, +2, +3) for TestFlight builds
  • Increment version (1.0.0 โ†’ 1.1.0) for App Store releases

4. Ensure Signing & Certificates are Configured โœ…

Option 1: Automatic Signing (Recommended for beginners)

In Xcode:

  1. Open
    text
    ios/Runner.xcworkspace
  2. Select Runner target
  3. Signing & Capabilities tab
  4. Enable Automatically manage signing
  5. Select your team

Option 2: Manual Signing (Recommended for teams)

Use Fastlane Match (see Advanced Setup below)


CI/CD Integration

GitHub Actions Example

Create

text
.github/workflows/ios-release.yml
:

yaml
name: iOS Release

on:
  push:
    tags:
      - 'v*'  # Trigger on version tags (e.g., v1.0.0)

jobs:
  release:
    runs-on: macos-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.16.0'
          channel: 'stable'

      - name: Install dependencies
        run: flutter pub get

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.0'
          bundler-cache: true

      - name: Install fastlane
        run: |
          cd ios
          bundle install

      - name: Create .env file
        run: |
          cd ios/fastlane
          cat << EOF > .env
          APP_IDENTIFIER=${{ secrets.APP_IDENTIFIER }}
          APPLE_ID=${{ secrets.APPLE_ID }}
          ITC_TEAM_ID=${{ secrets.ITC_TEAM_ID }}
          TEAM_ID=${{ secrets.TEAM_ID }}
          APP_STORE_CONNECT_KEY_ID=${{ secrets.APP_STORE_CONNECT_KEY_ID }}
          APP_STORE_CONNECT_ISSUER_ID=${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
          APP_STORE_CONNECT_KEY_PATH=./AuthKey.p8
          EOF

      - name: Decode App Store Connect API Key
        env:
          APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}
        run: |
          cd ios/fastlane
          echo "$APP_STORE_CONNECT_API_KEY_CONTENT" | base64 --decode > AuthKey.p8
          chmod 600 AuthKey.p8

      - name: Build and deploy to TestFlight
        run: |
          cd ios
          bundle exec fastlane beta

Required GitHub Secrets:

Secret NameValue
text
APP_IDENTIFIER
Your app's bundle identifier
text
APPLE_ID
Your Apple ID email
text
ITC_TEAM_ID
App Store Connect Team ID
text
TEAM_ID
Developer Portal Team ID
text
APP_STORE_CONNECT_KEY_ID
Key ID from API key
text
APP_STORE_CONNECT_ISSUER_ID
Issuer ID from API key
text
APP_STORE_CONNECT_API_KEY_CONTENT
Base64 encoded .p8 file

To create APP_STORE_CONNECT_API_KEY_CONTENT:

bash
# Base64 encode the .p8 file
base64 -i AuthKey.p8 | pbcopy

# Paste into GitHub Secrets

Advanced: Code Signing with Match

Why Use Match?

Without Match:

  • โŒ Each developer manages their own certificates
  • โŒ "Provisioning profile doesn't match" errors
  • โŒ Manual certificate renewal
  • โŒ Complex CI/CD setup

With Match:

  • โœ… Single source of truth for certificates
  • โœ… Shared across team and CI/CD
  • โœ… Automatic renewal
  • โœ… Zero configuration after setup

Setup Match

1. Create Private Git Repository:

bash
# On GitHub, create a private repo: "ios-certificates"
# NEVER make this public - contains signing certificates

2. Initialize Match:

bash
cd ios
fastlane match init

Select git storage and enter repository URL.

3. Generate Certificates:

bash
fastlane match appstore

4. Update Fastfile:

Add to your

text
beta
lane:

ruby
lane :beta do
  # Sync certificates
  match(
    type: "appstore",
    readonly: true,
    api_key: api_key
  )

  # Rest of your lane...
end

Troubleshooting

Issue: "Build already exists on TestFlight"

Error:

text
โŒ Build 42 already exists on TestFlight. Increment build number in pubspec.yaml.

Solution:

Update

text
pubspec.yaml
:

yaml
# Before
version: 1.0.0+42

# After
version: 1.0.0+43

Issue: "Could not find workspace"

Error:

text
Could not find workspace Runner.xcworkspace

Solution:

bash
cd ios
pod install

This generates

text
Runner.xcworkspace
.


Issue: "No provisioning profile found"

Error:

text
No provisioning profile matching 'com.example.app' found

Solution:

Option 1: Enable automatic signing in Xcode

Option 2: Use Match

bash
fastlane match appstore

Issue: "Authentication failure"

Error:

text
Could not authenticate with App Store Connect

Solution:

Verify your

text
.env
file:

  • Check
    text
    APP_STORE_CONNECT_KEY_ID
  • Check
    text
    APP_STORE_CONNECT_ISSUER_ID
  • Verify
    text
    .p8
    file exists at
    text
    APP_STORE_CONNECT_KEY_PATH

Best Practices

1. Always Use Bundler โœ…

bash
# WRONG
fastlane beta

# CORRECT
bundle exec fastlane beta

2. Version Control Fastlane Files โœ…

Commit to Git:

  • โœ…
    text
    Gemfile
  • โœ…
    text
    Gemfile.lock
  • โœ…
    text
    Fastfile
  • โœ…
    text
    Appfile

Add to .gitignore:

  • โŒ
    text
    .env
  • โŒ
    text
    *.p8
  • โŒ
    text
    fastlane/report.xml

3. Increment Version Before Each Release โœ…

Create a checklist:

markdown
Before release:
- [ ] Update version in pubspec.yaml
- [ ] Test the app
- [ ] Run fastlane beta
- [ ] Test on TestFlight
- [ ] Run fastlane production

4. Test Locally First โœ…

Before setting up CI/CD:

  1. Test
    text
    bundle exec fastlane beta
    locally
  2. Verify upload to TestFlight
  3. Then configure GitHub Actions

Summary

TaskCommandDescription
Install Fastlane
text
brew install fastlane
Install via Homebrew
Initialize
text
fastlane init
Setup Fastlane in project
Install Dependencies
text
bundle install
Install Ruby gems
Upload to TestFlight
text
bundle exec fastlane beta
Build and upload
Submit to App Store
text
bundle exec fastlane production
Promote to App Store
Run Tests
text
bundle exec fastlane test
Run unit tests

Final Thoughts

With Fastlane configured, your iOS release process becomes:

bash
# Update version in pubspec.yaml
version: 1.2.0+43

# One command for TestFlight
bundle exec fastlane beta

# One command for App Store
bundle exec fastlane production

Clean. Automated. Production-ready. ๐Ÿš€๐ŸŽ


What You've Automated

โœ… No more manual IPA uploads โœ… No duplicate version mistakes โœ… Version validation from pubspec.yaml โœ… One command = Full TestFlight release โœ… One command = Production submission โœ… CI/CD ready with GitHub Actions

Your Flutter iOS deployment is now fully automated!

Official Documentation: