Article

Setting Up CI/CD for Flutter iOS apps

Last updated 
Mar 27, 2025
 min read
Episode 
 min
Published 
Mar 27, 2025
 min read
Published 
Mar 27, 2025
 min

Continuous Integration and Continuous Deployment (CI/CD) can dramatically improve the way you build, test, and ship your Flutter iOS apps. Instead of manually archiving builds, uploading to TestFlight, or emailing testers every time you make a change, you can automate the entire process. This not only saves time—it reduces human error and keeps your release process consistent.

In this guide, we'll walk through how to set up a robust CI/CD pipeline using Fastlane and GitHub Actions. By the end, you'll be able to automatically distribute development builds to Firebase App Distribution and push production builds straight to TestFlight with just a git push.

What you'll need

Before we dive into the setup, make sure you have the following ready:

  • A Flutter project with iOS support
    If you've already run flutter create and selected iOS as a platform, you're good to go.
  • A macOS machine with Xcode installed
    Since Flutter relies on Xcode for building iOS apps, you'll need a Mac with Xcode set up. Make sure you’ve opened Xcode at least once to accept the license agreement and installed the command line tools.
  • A GitHub repository
    Your Flutter project should be version-controlled and pushed to GitHub. This will allow GitHub Actions to detect changes and trigger your workflows.
  • An Apple Developer Account
    You'll need an active Apple Developer account (paid, not free) to generate signing certificates and deploy builds to TestFlight.
  • A Firebase Project
    Set up a Firebase project so you can distribute builds to testers via Firebase App Distribution. This is useful for internal QA, external beta testers, or early feedback before submitting to the App Store.

Step 1: Install Ruby on Mac

Flutter iOS development requires Ruby, and although it comes preinstalled with Xcode, the version is often outdated. Let's install the latest stable version:

brew install ruby
echo 'export PATH="/usr/local/opt/ruby/bin:$PATH"' >> ~/.zshrc

After installation, restart your terminal or run source ~/.zshrc to apply the changes.

Step 2: Install fastlane for Flutter CI/CD iOS

Fastlane is a tool that automates building and releasing iOS and Android apps. Install it using Homebrew:

brew install fastlane

Step 3: Initialize Fastlane in your project

Navigate to your project's iOS directory and initialize Fastlane:

cd ios
fastlane init

During initialization:

  • Select "Automate beta distribution to TestFlight."
  • Log in with your Apple ID
  • Follow the prompts to set up your app on App Store Connect

After completion, you should see a new fastlane folder Gemfile in your iOS directory, and your app should be registered on App Store Connect.

Step 4: Configure your Fastfile

Replace the default Fastfile content with the code below. This setup supports multiple environments (development and production):

1default_platform(:ios)
2platform :ios do
3  desc "Push a new iOS development build"
4  lane :development do |options|
5    # Setup the keychain and match to work with CI
6    setup_ci()
7    
8    # Increase build timeout to reduce failures on CI
9    ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "120"
10    ENV["FASTLANE_XCODEBUILD_SETTINGS_RETRIES"] = "6"
11    
12    # Get required certificates and profiles
13    match(app_identifier: "<<identifier for development flavor>>", type: "adhoc")
14    
15    # Handle input parameters
16    version = options[:version] || "1.0.0" 
17    build_number = options[:build_number] || 1
18    release_notes = options[:release_notes] || "Lots of amazing new features to test out!"
19    groups = options[:groups] || "<<Default tester group name>>"
20    testers = options[:testers] || ""
21    
22    # Update version and build number
23    increment_version_number(version_number: version)
24    increment_build_number(build_number: build_number)
25    
26    # Build iOS app
27    build_app(
28      workspace: "Runner.xcworkspace",
29      scheme: "development",
30      export_method: "ad-hoc",
31    )
32    
33    # Upload to Firebase Distribution
34    firebase_app_distribution(
35      app: "<<APP ID from Firebase app>>",
36      service_credentials_file: "fastlane/service-account.json",
37      testers: testers,
38      groups: groups,
39      release_notes: release_notes,
40    )
41  end
42
43  desc "Push a new iOS production build"
44  lane :production do |options|
45    # Setup the keychain and match to work with CI
46    setup_ci()
47    
48    # Handle input parameters
49    release_notes = options[:release_notes] || "Lots of amazing new features to test out!"
50    groups = options[:groups]
51    
52    # Increase build timeout to reduce failures on CI
53    ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "120"
54    ENV["FASTLANE_XCODEBUILD_SETTINGS_RETRIES"] = "6"
55    
56    # Get required certificates and profiles
57    match(app_identifier: "<<identifier>>", type: "appstore")
58    
59    # Auto-increment build number based on TestFlight
60    updated_version_number = bump_build_number()
61    increment_build_number(build_number: updated_version_number)
62    
63    # Build iOS app
64    build_app(
65      workspace: "Runner.xcworkspace",
66      scheme: "production",
67      export_method: "app-store",
68    )
69    
70    # Upload to TestFlight
71    pilot(
72      api_key_path: "fastlane/store.json", 
73      skip_waiting_for_build_processing: true, 
74      changelog: release_notes,
75      groups: groups,
76    )
77  end
78
79  # Helper function to get the next build number
80  def bump_build_number()
81    latest_build_number = latest_testflight_build_number(initial_build_number: 0)
82    return (latest_build_number + 1)
83  end
84end

Replace the placeholders (<<identifier>> , <<Default tester group name>> and <<APP ID from Firebase app>>.) with your actual app information.

Step 5: Configure match for certificate management

Match is a Fastlane tool that helps manage iOS certificates and provisioning profiles.

fastlane match init

Create a private Git repository to store your certificates and profiles securely. Once created, set up your certificates:

For TestFlight production release:

fastlane run match type:"appstore" app_identifier:"<<identifier>>"

For Firebase Distribution (development):

fastlane run match type:"adhoc" app_identifier:"<<identifier for development flavor>>"

Note: You'll be prompted to create a password to encrypt the certificates. Remember this password, as you'll need it for your GitHub Actions secrets.

Step 6: Configure Firebase app distribution for Flutter CI/CD

Add the Firebase App Distribution plugin to Fastlane:

fastlane add_plugin firebase_app_distribution

Next:

  1. Create and configure a Firebase project for your iOS app
  2. Generate a service account file
  3. Save the service account JSON file to /ios/fastlane/service-account.json

Step 7: Create API key for TestFlight uploads

Create a file called store.json in the ios/fastlane folder with the following structure:

1{
2    "key_id": "<<KEY ID>>",
3    "issuer_id": "<<ISSUER ID>>",
4    "key": "-----BEGIN PRIVATE KEY-----\n<<REPLACE WITH YOUR KEY>>\n-----END PRIVATE KEY-----"
5}

To get the required information:

  1. Go to App Store Connect → Integrations → App Store Connect API
  2. Navigate to "Keys" and generate a new API key
  3. Download the API key (.p8 file)
  4. Replace the placeholders in the JSON file with your actual key information

Step 8: Set up tester groups

  1. Add testers to Firebase App Distribution and create groups for your development builds
  2. Add testers to App Store Connect and create groups for your TestFlight builds

Step 9: Project configuration checks

Ensure your Xcode project is properly configured:

  • Disable "Automatically manage signing" in your Xcode project settings:
Screenshot 2025-02-27 at 1.26.09 PM.png
  • Verify that the Flutter target is set in your Xcode configuration:
Screenshot 2025-02-27 at 1.16.13 PM.png

Step 10: Test locally

Before setting up GitHub Actions, test your Fastlane setup locally:

cd ios
fastlane production version:"1.0.0" build_number:1 groups:"<<tester_group>>" release_notes:"First release"
fastlane development version:"1.0.0" build_number:1 testers:"email@example.com" groups:"<<tester_group>>" release_notes:"First release"

Step 11: Set Up GitHub actions workflows for Flutter CI/CD iOS

Create the following workflow files in your repository's .github/workflows directory:

Main Workflow (main.yaml)

This workflow runs tests when code is pushed to the main branch or when a pull request is created:

1name: Flutter CI Tests
2on:
3  push:
4    branches:
5      - main
6  pull_request:
7    branches:
8      - main
9jobs:
10  test:
11    name: Run Flutter Tests
12    runs-on: ubuntu-latest
13    steps:
14      - uses: actions/checkout@v3
15      - uses: subosito/flutter-action@v2
16        with:
17          flutter-version: "3.24.5"
18          channel: "stable"
19      - run: flutter --version
20      - name: Get Packages
21        run: flutter pub get
22      - name: Run Tests
23        run: flutter test

Development deployment workflow (deploy_ios_development.yaml)

This workflow deploys your app to Firebase App Distribution for testing:

1name: Deploy iOS to Development
2on:
3  workflow_dispatch:
4    inputs:
5      version:
6        description: "App Version (e.g., 1.0.0)"
7        required: true
8        default: "1.0.0"
9      build_number:
10        description: "Build Number (e.g., 42)"
11        required: true
12        default: "1"
13      release_notes:
14        description: "Release Notes"
15        required: true
16        default: "Initial Release"
17      groups:
18        description: "Firebase Tester Groups"
19        required: true
20        default: "Internal"
21      testers:
22        description: "Firebase Testers"
23        required: false
24        default: ""
25jobs:
26  deployIOSDevelopment:
27    runs-on: macos-latest
28    env:
29      APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
30      FIREBASE_SERVICE_ACCOUNT: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
31    steps:
32      - name: Checkout repository
33        uses: actions/checkout@v3
34      - name: Setup Flutter
35        uses: subosito/flutter-action@v2
36        with:
37          flutter-version: "3.24.5"
38          channel: "stable"
39          cache: true
40        id: flutter
41      - name: Accept Xcode License
42        run: sudo xcodebuild -license accept
43      - name: Get Packages
44        run: flutter pub get
45      - name: Setup Fastlane
46        uses: ruby/setup-ruby@v1
47        with:
48          ruby-version: "3.3"
49          bundler-cache: true
50          working-directory: ios
51      - name: Set up Firebase authentication
52        run: echo "$FIREBASE_SERVICE_ACCOUNT" >> ./ios/fastlane/service-account.json
53      - name: Build and Deploy to Firebase App Distribution
54        env:
55          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
56          MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
57        run: |
58          cd ./ios
59          bundle exec fastlane ios development version:${{github.event.inputs.version}} build_number:${{github.event.inputs.build_number}} release_notes:${{github.event.inputs.release_notes}} groups:${{github.event.inputs.groups}} testers:${{github.event.inputs.testers}}

Production deployment workflow (deploy_ios_production.yaml)

This workflow deploys your app to TestFlight for production release:

1name: Deploy iOS to Production
2on:
3  workflow_dispatch:
4    inputs:
5      release_notes:
6        description: "Release Notes"
7        required: true
8        default: "Initial Release"
9      groups:
10        description: "TestFlight Tester Groups"
11        required: false
12jobs:
13  deployIOSProduction:
14    runs-on: macos-latest
15    env:
16      APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
17    steps:
18      - name: Checkout repository
19        uses: actions/checkout@v3
20      - name: Setup Flutter
21        uses: subosito/flutter-action@v2
22        with:
23          flutter-version: "3.24.5"
24          channel: "stable"
25          cache: true
26        id: flutter
27      - name: Accept Xcode License
28        run: sudo xcodebuild -license accept
29      - name: Get Packages
30        run: flutter pub get
31      - name: Setup Fastlane
32        uses: ruby/setup-ruby@v1
33        with:
34          ruby-version: "3.3"
35          bundler-cache: true
36          working-directory: ios
37      - name: Setup app store connect
38        run: echo "$APP_STORE_CONNECT_API_KEY" >> ./ios/fastlane/store.json
39      - name: Build and Deploy to TestFlight
40        env:
41          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
42          MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
43        run: |
44          cd ./ios
45          bundle exec fastlane ios production release_notes:"${{ github.event.inputs.release_notes }}" groups:"${{ github.event.inputs.groups }}"

Step 12: Add GitHub secrets

In your GitHub repository settings, add the following secrets:

  1. APP_STORE_CONNECT_API_KEY : The contents of your store.json  file
  2. FIREBASE_SERVICE_ACCOUNT : The contents of your Firebase service account JSON file
  3. MATCH_PASSWORD : The password you created when initializing Match
  4. MATCH_GIT_BASIC_AUTHORIZATION : A Base64-encoded GitHub authentication token

To create the MATCH_GIT_BASIC_AUTHORIZATION value:

  1. Create a GitHub personal access token with appropriate permissions
  2. Generate the Base64 authorization string: echo -n your_github_username:your_personal_access_token | base64
  3. Copy the output value to the GitHub secret

Step 13: Test your CI/CD pipeline

Commit and push your changes to GitHub, then:

  1. Go to the "Actions" tab in your GitHub repository
  2. Select the "Deploy iOS to Development" workflow
  3. Click "Run workflow" and fill out the required parameters
  4. Monitor the workflow execution

If everything is set up correctly, your app should be automatically built and deployed to Firebase App Distribution. Once you're ready for a production release, run the "Deploy iOS to Production" workflow to send the app to TestFlight.

Conclusion

At Aubergine, we specialize in building scalable, high-performance digital products, from idea to impact. Our engineering team brings deep expertise in Flutter, DevOps, and CI/CD automation to help startups and enterprises accelerate development, reduce release cycles, and ensure product quality.

Whether you're setting up your first mobile pipeline or refining an existing process, we partner with you to create robust, reliable, and maintainable systems. We believe great products are built through thoughtful design, engineering rigor, and continuous innovation, and our teams are here to bring that to life.

Authors

Yash Savsani

Senior Software Engineer
A Senior Software Engineer specializing in mobile app development, dedicated to building seamless and high-performing applications in a hybrid environment. Yash emphasizes thorough planning before execution, ensuring every project is efficient and well-structured from inception to completion. Beyond work, he has a passion for travel and exploring new destinations with friends, believing that the best memories are created on the road.

Podcast Transcript

Episode
 - 
minutes

Host

No items found.

Guests

No items found.

Tags

No items found.

Have a project in mind?

Read