Home / Blog / Fastlane release automation: the pipeline I run across 12 apps

Fastlane release automation: the pipeline I run across 12 apps

An iOS release should be one command. Here's the repeatable pipeline I use on 12 shipped apps.

Shipping an iOS release by hand: build, archive, dSYM upload, TestFlight, metadata update, screenshots, release notes, submit. Every step is a separate click. A release takes 3 to 4 hours. When something goes wrong, a day or two disappears.

With Fastlane all of that runs on one command. This is the pipeline I use across 12 apps.

What Fastlane is, briefly

Fastlane is a Ruby-based automation tool for iOS and Android. You define “lanes” in a Fastfile, each lane is a workflow.

Install:

brew install fastlane
# or
gem install fastlane

Fastfile (in fastlane/Fastfile at the project root):

platform :ios do
  lane :beta do
    # TestFlight release
  end
  
  lane :release do
    # App Store release
  end
end

Run it: fastlane beta or fastlane release.

My basic beta pipeline

The beta lane from my Fastfile:

lane :beta do
  # 1. Git check
  ensure_git_status_clean
  
  # 2. Certificates
  match(type: "appstore", readonly: true)
  
  # 3. Build
  build_app(
    scheme: "MyApp",
    configuration: "Release",
    export_method: "app-store",
    output_directory: "./builds",
    clean: true
  )
  
  # 4. TestFlight upload
  upload_to_testflight(
    skip_waiting_for_build_processing: true,
    changelog: File.read("./CHANGELOG.md")
  )
  
  # 5. dSYM upload
  upload_symbols_to_crashlytics(
    dsym_path: lane_context[SharedValues::DSYM_OUTPUT_PATH],
    gsp_path: "./GoogleService-Info.plist"
  )
  
  # 6. Slack notification
  slack(
    message: "Beta build uploaded to TestFlight",
    success: true
  )
  
  # 7. Git tag
  add_git_tag(tag: "beta-#{get_build_number}")
  push_git_tags
end

Those seven steps take 2 to 3 hours by hand. With Fastlane, fastlane beta is one command, 10 to 15 minutes.

Production release pipeline

lane :release do
  # Pre-release checks
  ensure_git_branch(branch: "main")
  ensure_git_status_clean
  
  # Version bump (minor)
  increment_version_number(bump_type: "minor")
  increment_build_number
  
  # Build
  match(type: "appstore", readonly: true)
  build_app(scheme: "MyApp", configuration: "Release")
  
  # Metadata update (App Store Connect)
  deliver(
    submit_for_review: false,
    automatic_release: false,
    force: true,
    metadata_path: "./fastlane/metadata",
    screenshots_path: "./fastlane/screenshots"
  )
  
  # Crashlytics dSYM
  upload_symbols_to_crashlytics(
    dsym_path: lane_context[SharedValues::DSYM_OUTPUT_PATH]
  )
  
  # Commit version changes
  version = get_version_number(target: "MyApp")
  build = get_build_number
  commit_version_bump(message: "Release #{version} (#{build})")
  add_git_tag(tag: "v#{version}")
  push_to_git_remote
  
  # Slack notification
  slack(
    message: "New version #{version} submitted to App Store",
    channel: "#releases"
  )
end

With this pipeline every production release is 20 to 30 minutes. The risk of getting it wrong is close to zero.

Match: code signing automation

Fastlane’s most powerful tool is match. Automated certificate and provisioning profile management.

Setup:

fastlane match init

You pick a Git repo (private). Match stores certificates there, encrypted. Everyone on the team uses the same cert.

Usage (Fastfile):

match(type: "appstore", readonly: true)

readonly: true doesn’t generate new certs, it uses existing ones. Always readonly in CI.

Without match, certificate management is manual for every developer, onboarding takes 1 to 2 hours. With match, 5 minutes.

Metadata management

Entering metadata (description, keywords, what’s new, screenshots) in App Store Connect for every release is slow.

Fastlane deliver automates it:

fastlane/metadata/
  en-US/
    description.txt
    keywords.txt
    release_notes.txt
  tr-TR/
    description.txt
    ...
  [36 languages]

A text file per language. fastlane deliver uploads to App Store Connect.

You update the release notes and run fastlane beta. Metadata updates in every language.

Screenshots automation (snapshot)

The App Store wants 36 languages * 6 screenshots * 3 devices = 648 screenshots. Manually impossible.

Fastlane snapshot:

  1. Create a UI test target
  2. Snapfile config: supported devices, languages
  3. UI tests capture screens
  4. fastlane snapshot generates all 648
# Snapfile
devices([
  "iPhone 15 Pro",
  "iPhone SE (3rd generation)",
  "iPad Pro 12.9-inch"
])
languages([
  "en-US", "tr-TR", "de-DE", ... # 36 languages
])

A one to two hour job. I run it weekly so fresh screenshots are ready before a release.

CI/CD integration

Fastlane runs locally but should be automated in CI/CD:

GitHub Actions example:

# .github/workflows/release.yml
name: Release
on:
  push:
    tags: ['v*']

jobs:
  release:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.1'
          bundler-cache: true
      
      - name: Run Fastlane
        env:
          APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
        run: bundle exec fastlane release

Push a Git tag (git tag v2.3.0 && git push origin v2.3.0) and the release pipeline runs.

Secrets management

The secrets Fastlane needs:

  • Apple ID password / App Store Connect API key
  • Match password (cert decryption)
  • GitHub API token (for git push)
  • Firebase API key (for Crashlytics)
  • Slack webhook URL

Pass them as environment variables:

env FASTLANE_PASSWORD=xxx MATCH_PASSWORD=yyy fastlane beta

GitHub Secrets in CI, .env locally (in gitignore).

Don’t:
– Hard-code secrets in the Fastfile
– Leave secrets in shell history
– Commit a secrets.yml to Git

Do:
.env.default with dummy values committed, .env with real values gitignored
– Secrets management in CI (GitHub Secrets, AWS Secrets Manager)
– Rotation discipline: quarterly secret rotation

Error handling

What happens when the Fastlane pipeline fails?

error do |lane, exception|
  slack(
    message: "Fastlane fail: #{exception.message}",
    success: false,
    channel: "#alerts"
  )
end

The error do block runs on failure. Slack alert fires, the team knows immediately.

Also rollback logic:

lane :release do
  # ... pipeline steps ...
  
  rescue => e
    # Revert the version
    git_revert
    UI.user_error!("Release failed: #{e}")
end

Prevents half-done deploys.

Plugin ecosystem

Fastlane has 400+ plugins. Most popular:

  • fastlane-plugin-firebase_app_distribution: Firebase test distribution
  • fastlane-plugin-sentry: Sentry dSYM upload
  • fastlane-plugin-versioning: advanced version bumping
  • fastlane-plugin-badge: add a badge to the icon (beta, staging)
  • fastlane-plugin-changelog: changelog generation from git

Install:

fastlane add_plugin sentry

Common pitfalls

1. Fastfile too long. 500 lines is unreadable. Keep lanes modular, extract shared logic to methods.

2. Wrong version bump. Build number must increase monotonically. Sync with git tags by discipline.

3. Typo in metadata. App Store Connect rejects it. Pre-upload validation:

fastlane run deliver --metadata_only --submit_for_review false

4. Expired certificates. Match readonly uses them but fails if they’ve expired. Check quarterly.

5. TestFlight beta expiry. Builds expire after 90 days. Add an expiry warning to your lanes.

Bundle versioning

Lock gems in Gemfile:

# Gemfile
source "https://rubygems.org"
gem "fastlane", "~> 2.215.0"

Commit the lockfile. Team members and CI use the same Fastlane version, no surprise breaking changes.

Continuous improvement

The pipeline evolves over time. My refinements across 12 apps:

  • Initial: basic build + TestFlight
  • +1 month: dSYM upload automation
  • +2 months: Slack notifications
  • +3 months: automated screenshots (snapshot)
  • +6 months: multi-environment pipelines (staging, production)
  • +12 months: complex metadata management

After every release I find something to add to the pipeline. Continuous improvement mindset.

Bottom line

Fastlane is the gold standard for iOS release automation. One to two days to set up initially, but every release after drops from 3 to 4 hours to 20 to 30 minutes.

Match for code signing, deliver for metadata, snapshot for screenshots, CI/CD integration. All production ready.

Not overkill for a solo developer. Running a 12-app portfolio alone without Fastlane would be impossible. Even a 2 to 3 app portfolio earns the investment back.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch