CI/CD & App Store Deployment

Automating App Store Releases with Fastlane Deliver

16 min Lesson 11 of 12

Automating App Store Releases with Fastlane Deliver

Manually uploading an iOS app to App Store Connect is a repetitive, error-prone workflow: export the archive, fill in release notes, set the build number, toggle TestFlight groups, and finally click Submit for Review. Fastlane Deliver (the deliver action) automates every one of these steps from the command line, making it trivial to wire the entire process into a CI/CD pipeline triggered by a Git tag or a merge to your release branch.

What Fastlane Deliver Does

The deliver action is Fastlane's official tool for interacting with App Store Connect. It can:

  • Upload a signed .ipa file to App Store Connect using the Transporter protocol
  • Push localized app metadata (descriptions, keywords, release notes, support URLs)
  • Upload screenshots and preview videos for every device size and locale
  • Distribute the build to internal or external TestFlight groups automatically
  • Submit the build for App Review with a single flag
  • Skip submission and only upload, letting a human trigger review from the web UI
Note: Fastlane Deliver communicates with App Store Connect exclusively through the App Store Connect API (JWT-based key authentication). The older Apple ID / password flow is deprecated. You must create an API key in App Store Connect under Users and Access → Integrations → App Store Connect API and download the .p8 private key file.

Authenticating with an App Store Connect API Key

Before calling deliver, you must tell Fastlane which API key to use. The cleanest approach is the app_store_connect_api_key action, which returns a reusable key object you pass to subsequent actions.

Configuring the API Key in a Fastlane Lane

# fastlane/Fastfile

lane :upload_to_store do
  # Load the App Store Connect API key from CI environment variables
  api_key = app_store_connect_api_key(
    key_id:        ENV["ASC_KEY_ID"],        # e.g. "ABCD1234EF"
    issuer_id:     ENV["ASC_ISSUER_ID"],     # UUID from ASC portal
    key_content:   ENV["ASC_KEY_CONTENT"],   # Contents of the .p8 file (base64 or raw)
    is_key_content_base64: true,
    duration:      1200,                     # Token TTL in seconds (max 1200)
    in_house:      false,                    # true only for Apple Developer Enterprise
  )

  deliver(
    api_key:              api_key,
    ipa:                  "build/Runner.ipa",
    skip_metadata:        false,
    skip_screenshots:     true,
    submit_for_review:    false,
    automatic_release:    false,
    force:                true,              # Skip HTML report preview
    precheck_include_in_app_purchases: false,
  )
end
Tip: Never hard-code the .p8 key content in your Fastfile. Store it as a base64-encoded CI secret (e.g. base64 AuthKey_ABCD1234EF.p8) and set is_key_content_base64: true. On GitHub Actions, add it as an encrypted repository secret and reference it via ENV["ASC_KEY_CONTENT"].

Distributing to External TestFlight Groups

After the binary passes Apple's processing queue (typically 10–15 minutes), you can automatically add it to an external TestFlight group. Use the pilot action (also known as testflight) or pass distribution parameters directly to deliver.

Full CI Lane: Build, Sign, Upload, and Distribute to TestFlight

# fastlane/Fastfile

lane :beta do
  api_key = app_store_connect_api_key(
    key_id:              ENV["ASC_KEY_ID"],
    issuer_id:           ENV["ASC_ISSUER_ID"],
    key_content:         ENV["ASC_KEY_CONTENT"],
    is_key_content_base64: true,
  )

  # Increment build number from latest TestFlight build
  increment_build_number(
    build_number: app_store_build_number(
      api_key:    api_key,
      live:       false,
    ) + 1,
    xcodeproj: "ios/Runner.xcodeproj",
  )

  # Build and sign the Flutter IPA
  sh "flutter build ipa --release " \
     "--export-options-plist=ios/ExportOptions.plist"

  # Upload to TestFlight and distribute to external group
  pilot(
    api_key:                         api_key,
    ipa:                             "build/ios/ipa/Runner.ipa",
    distribute_external:             true,
    groups:                          ["External Beta Testers"],
    notify_external_testers:         true,
    changelog:                       ENV["RELEASE_NOTES"] || "Bug fixes and improvements.",
    beta_app_review_info:            {
      contact_email:        "qa@example.com",
      contact_first_name:   "QA",
      contact_last_name:    "Team",
      contact_phone:        "+1-555-0100",
      demo_account_name:    "demo@example.com",
      demo_account_password: "DemoPass123!",
      notes:                "Use demo account to test all flows.",
    },
    skip_waiting_for_build_processing: false,
  )
end

Submitting for App Review

To go straight from CI to the App Review queue, set submit_for_review: true inside the deliver call. Combine this with automatic_release: false to keep a human in the loop for the final publish decision, or set it to true for a fully automated release once Apple approves.

Warning: Setting submit_for_review: true in CI means every successful build on that lane will enter the App Review queue. Gate this lane behind a protected tag pattern (e.g. v*.*.*-rc) or a manual CI trigger to avoid accidentally submitting development builds for review.

Wiring the Lane into a GitHub Actions CI Pipeline

A typical GitHub Actions workflow triggers the release lane when a version tag is pushed. The .p8 content and other secrets are stored as repository secrets and injected at runtime.

GitHub Actions Workflow (.github/workflows/release.yml)

# .github/workflows/release.yml
name: App Store Release

on:
  push:
    tags:
      - "v[0-9]+.[0-9]+.[0-9]+"   # triggers on e.g. v1.4.0

jobs:
  release:
    runs-on: macos-14              # Xcode 15 image
    steps:
      - uses: actions/checkout@v4

      - uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.22.0"
          channel: stable

      - name: Install Ruby & Fastlane
        run: |
          gem install bundler --no-document
          bundle install

      - name: Install Flutter dependencies
        run: flutter pub get

      - name: Install CocoaPods
        run: cd ios && pod install --repo-update

      - name: Run Fastlane beta lane
        env:
          ASC_KEY_ID:       ${{ secrets.ASC_KEY_ID }}
          ASC_ISSUER_ID:    ${{ secrets.ASC_ISSUER_ID }}
          ASC_KEY_CONTENT:  ${{ secrets.ASC_KEY_CONTENT }}
          MATCH_PASSWORD:   ${{ secrets.MATCH_PASSWORD }}
          RELEASE_NOTES:    "Version ${{ github.ref_name }} released."
        run: bundle exec fastlane beta

Fastlane Deliver Metadata Structure

When skip_metadata: false, Fastlane expects a fastlane/metadata/ folder with a specific layout. This lets you version-control all App Store content alongside your code:

  • metadata/en-US/name.txt — App name for the locale
  • metadata/en-US/description.txt — Full App Store description
  • metadata/en-US/keywords.txt — Comma-separated keywords
  • metadata/en-US/release_notes.txt — What’s new in this version
  • metadata/en-US/support_url.txt — Support URL
  • metadata/review_information/ — App Review contact and demo credentials
Tip: Run fastlane deliver init once to download your existing App Store metadata into the correct folder structure. Commit the entire fastlane/metadata/ directory to version control so release notes and descriptions are reviewed in pull requests just like code changes.

Summary

Fastlane Deliver transforms the App Store submission process from a manual, multi-step GUI workflow into a single terminal command. The key points to remember are: use an App Store Connect API key (never username/password) stored as CI secrets; use deliver for metadata and binary upload; use pilot for TestFlight distribution with external tester groups; and gate the submit_for_review flag behind protected tags or manual triggers to prevent accidental submissions. With this setup, every Git tag automatically produces a TestFlight build with no manual intervention required.