Arrow icon
Effortless iOS App Deployment to TestFlight with GitHub Actions and Fastlane

Effortless iOS App Deployment to TestFlight with GitHub Actions and Fastlane

Discover how to simplify your iOS app deployment to TestFlight using GitHub Actions and Fastlane. Our comprehensive guide walks you through each step, making the process accessible for developers of all skill levels. Save time, reduce stress, and enhance your workflow with our efficient and effective strategies.

Saiful Islam Adar

Senior Frontend Developer

August 8, 2023
Technology
Table of content

Are you tired of the tedious process of deploying your iOS app to TestFlight? Manually managing code signing, provisioning profiles, and dealing with distribution configurations can be a real headache, slowing down your release cycle and leaving room for errors. But fear not! We have a solution that will revolutionize your deployment workflow.

In this guide, we'll demonstrate how to leverage GitHub Actions and Fastlane to streamline and automate your entire iOS application deployment process to TestFlight. With these two powerful tools at your disposal, you'll save precious time, minimize stress, and fully leverage the benefits of continuous integration.

By the end of this article, you'll have a solid grasp of integrating GitHub Actions and Fastlane into your iOS development workflow. You'll be equipped with the knowledge to effortlessly build, sign, and distribute your app to TestFlight, empowering you to focus on what you do best – crafting amazing iOS experiences.

So, if you're ready to banish the deployment blues and supercharge your TestFlight releases, let's dive in! Whether you're a seasoned developer or just starting your iOS journey, this guide has got you covered. Get ready to witness the magic of automation and continuous integration as we transform your deployment process.

Note: This article assumes you have a basic understanding of iOS development, Xcode, and Git. If you're new to GitHub Actions or Fastlane, don't worry – we'll provide clear explanations and walk you through the setup from start to finish.

Setting up GitHub Actions

Now that we understand the power of automation with GitHub Actions, let's dive into setting it up for our iOS app deployment to TestFlight. Don't worry if you're new to GitHub Actions – we'll guide you through the process step by step.

A. Explaining GitHub Actions

GitHub Actions is a powerful automation platform built right into GitHub. It allows you to define custom workflows that automatically trigger actions in response to events, such as code commits or pull requests. With GitHub Actions, you can streamline your development processes and eliminate manual tasks.

B. Creating a Workflow file

To get started, let's create a Workflow file called `deploy-ios-app.yml` that defines our deployment process. This file will reside in the .github/workflows directory of your repository. Here's a breakdown of the steps involved:

1. Defining the workflow trigger


on:
  # Enable manual run
  workflow_dispatch:
    inputs:
      lane:
        description: "Fastlane lane"
        required: true
        default: "beta"
        type: choice
        options:
          - beta
          - publish

We'll define the trigger for our workflow using the `workflow_dispatch` event. This enables manual runs of the workflow, allowing us to choose the Fastlane lane to execute. We have added two options here beta and publish, for this article, we will only cover the `beta` option. But overall this gives us the option to manually trigger the workflow via selecting an option.


2. Setting up the environment


  fastlane-deploy:
    runs-on: macos-latest
    

We'll set up the environment for our workflow using the `macos-latest` runner. This ensures that the workflow runs on the latest macOS environment, which is crucial for iOS app development.


3. Checking out the repository


steps:
  - uses: actions/checkout@v2

We'll include the actions/checkout step to ensure that the workflow has access to the repository's source code. This step checks out the codebase, making it available for subsequent steps.


C. Configuring SSH Keys and known_hosts

Before we proceed, let's configure SSH Keys and known_hosts for Fastlane match. This will enable secure communication with your Git repository and facilitate the retrieval of necessary resources.

Why do we need SSH Keys?

We will use `fastlane match` command to securely store iOS certificates and profiles in a private repository. You can read about it here https://docs.fastlane.tools/actions/match/

SSH Keys are a secure way to authenticate and establish a secure connection between your CI machine and your private repository(we will create it later in this article). By using SSH Keys, we can securely retrieve code signing certificates and provisioning profiles stored in your private repository for use in the deployment process in for code signing.


Generate Matchfile

Let’s navigate to your Flutter projects `ios` directory and run `fastlane match init`

Fastlane match will guide you through a series of prompts to configure your code signing and provisioning profiles. Here are some key points to consider during the setup:

  1. Select the type of code signing you want to use, such as appstore, ad-hoc, or development. This depends on your specific use case. Let’s use appstore since we want to upload to appstore for TestFlight
  2. Provide the Apple ID (email) associated with your Apple Developer account.
  3. Provide a secured passphrase that will be used to encrypt/decrypt these certificates. Please remember this passphrase, we will need it later.
  4. Provide the SSH(not https) link of your private repository where you want to store your certificates and provisional profiles so other team members/CI machine can access them. The SSH url should look like git@github.com:<username>/<repository>.git


It will generate a `Matchfile` in `/YOUR_APP_DIRECTORY/ios/fastlane` which will look something like this:


git_url("git@github.com:/.git")

storage_mode("git")

type("appstore") # The default type, can be: appstore, adhoc, enterprise or development

app_identifier(["your_app_indetifier"])
username("your_apple_id") # Your Apple Developer Portal username

# For all available options run `fastlane match --help`
# Remove the # in the beginning of the line to enable the other options

# The docs are available on https://docs.fastlane.tools/actions/match

You need to replace those values with real values. Now you need to run `fastlane match appstore`


Fastlane match will perform the following actions:

  • Create a new code signing certificate signing request.
  • Generate a new distribution certificate.
  • Generate an App Store provisioning profile for your app.
  • Store the generated certificates and profiles securely in the configured repository.

If you are having problems with git repository authentication via SSH then please check the next part of the article.

Generating SSH Keys

To generate SSH Keys, follow these steps:

Open your local machine’s terminal or command line interface.

Run the following command to generate a new SSH key pair:


ssh-keygen -t ed25519 -C "your_email@example.com"

It will show you:


> Enter a file in which to save the key (/Users/YOU/.ssh/id_ed25519: 
[Press enter]

You can simply press enter then it will ask for a passphrase, use a secure passphrase at this level:


> Enter passphrase (empty for no passphrase): [Type a passphrase]
> Enter same passphrase again: [Type passphrase again]

The command will generate two files: a private key (usually named id_ed25519) and a public key (usually named id_ed25519.pub). Also, remember this SSH passphrase, we will need this later.

Now to give permission to fetch the private git repository for certificates from CI machine:

  •  Go to your private repository on GitHub.
  •  Navigate to the "Settings" tab and select "Deploy keys".
  • Click on "Add deploy key" and provide a title for the key.
  • Copy the contents of the public key file (id_ed25519.pub) generated earlier and paste it into the "Key" field.
  • Make sure to enable the "Allow write access" option if necessary.
  • Save the deploy key.


Now:

  • You need to go to your flutter app’s repository’s Settings -> Secrets & variables -> Actions and click on `New Repository Secret` to add the title `SSH_PRIVATE_KEY` and paste the value of your `id_ed25519` file in `Key` section
  • Also add another repository secret with title `SSH_PASSPHRASE` and for `Key` use the passphrase you just used when generating the SSH Key

So we have completed generating SSH keys and adding environment values in github Action’s secret.

Additionally, if you are having a problem with git authentication via SSH from your local machine for fastlane match, please check this github documentation 

Note: You can always rename the keys according to your own understanding and readability in case of keeping multiple keys.

Now this step in the workflow:


- name: Setup SSH Keys and known_hosts for fastlane match
  run: |
    SSH_PATH="$HOME/.ssh"
    mkdir -p "$SSH_PATH"
    touch "$SSH_PATH/known_hosts"
    echo "$PRIVATE_KEY" > "$SSH_PATH/id_ed25519"
    chmod 700 "$SSH_PATH"
    ssh-keyscan github.com >> ~/.ssh/known_hosts
    chmod 600 "$SSH_PATH/known_hosts"
    chmod 600 "$SSH_PATH/id_ed25519"
    touch "$SSH_PATH/config"
    echo -e "Host github.com\n\tAddKeysToAgent yes\n\tUseKeychain yes\n\tIdentityFile $SSH_PATH/id_ed25519" >> "$SSH_PATH/config"
    eval $(ssh-agent -s)
    ssh-add --apple-use-keychain "$SSH_PATH/id_ed25519"
  env:
    PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
    SSH_PASSPHRASE: ${{ secrets.SSH_PASSPHRASE }}

This step sets up the SSH keys and known_hosts for Fastlane match in CI machine. The private key (generated earlier) is stored in the CI machine via env, allowing secure communication with the private repository(the repository that stores certificates and profiles for code signing). The public key has been added to the repository's deploy keys to establish a secure connection.

Note: Make sure to add the generated private key and passphrase as secrets (SSH_PRIVATE_KEY and SSH_PASSPHRASE) in your repository's Secrets.

D. Building and testing the iOS app

Before we start to build our app, we need to disable automatic code signing in Xcode and select the profile that `fastlane match` has created.

Now, let's focus on building and testing our iOS app within the GitHub Actions workflow. We'll ensure that the necessary Flutter and Java environments are set up, and configure Xcode for iOS app development.

1. Setting up Flutter and Java environments


- name: Setup up Flutter
  uses: subosito/flutter-action@v2
  with:
    channel: stable

- run: flutter doctor -v

- name: Setup JDK
  uses: actions/setup-java@v1
  with:
    java-version: "12.x"

We'll use the provided code snippets to set up the Flutter and Java environments. These steps install the Flutter SDK, check the Flutter doctor output, and configure the desired Java version for your project.

2. Configuring Xcode


- uses: maxim-lobanov/setup-xcode@v1
  with:
    xcode-version: "latest-stable"

We'll include the maxim-lobanov/setup-xcode action to configure Xcode within the workflow. This step ensures that the latest stable version of Xcode is set up for iOS app development.


3. Building and uploading the app using Fastlane


- name: Deploy to TestFlight
  working-directory: YOUR_FLUTTER_APP_DIRECTORY/ios # relative to your 
  # github repo only
  run: |
    bundle install
    bundle exec fastlane ${{ github.event.inputs.lane || 'beta' }}
  env:
    FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
    FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
    MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
    MATCH_KEYCHAIN_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }}
    MATCH_KEYCHAIN_NAME: ${{ secrets.MATCH_KEYCHAIN_NAME }}
    APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
    APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
    APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}

This will install the needed bundles and run the selected lane or the `beta` lane by default. We are assuming you have already enough knowledge of fastlane and how it works. For this article let’s use only the `beta` lane.

Now let’s understand what are all these env values.


1. FASTLANE_PASSWORD:

This variable represents the password for your Apple Developer account. It is used by Fastlane to authenticate and interact with Apple's services.

To obtain this variable, you need to set it as a secret in your GitHub repository. Secrets are encrypted environment variables that can be securely accessed within GitHub Actions workflows. Follow these steps to add a secret:

  • Go to your repository on GitHub.
  • Navigate to the "Settings" tab and select "Secrets".
  • Click on "New repository secret" and provide a name (e.g., FASTLANE_PASSWORD) and the corresponding value.

2. FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD:

This variable represents the application-specific password for your Apple ID. It is used when two-factor authentication is enabled for your Apple Developer account.

To obtain this variable, follow these steps:

  • Sign in to your Apple ID account page.
  • Navigate to the "Security" section and generate an application-specific password.
  • Store the generated password securely.
  • Add it as a secret in your GitHub repository following the same steps mentioned earlier.

3. MATCH_PASSWORD:

This variable represents the password for the Matchfile repository, which contains the code signing certificates and provisioning profiles.

To obtain this variable, set it as a secret in your GitHub repository. The password is typically used when the Matchfile repository requires authentication.

4. MATCH_KEYCHAIN_PASSWORD:

This variable represents the password for the keychain used by Match to store certificates and private keys. For CI machine you can use any password you want.

5. MATCH_KEYCHAIN_NAME:

This variable represents the name of the keychain used by Match to store certificates and private keys. Let’s use `login` as the value for this one.

6. APP_STORE_CONNECT_API_KEY_ID:

This variable represents the ID of the API key associated with your App Store Connect account. It is used for authentication and communication with the App Store Connect API.

To obtain this variable, you need to create an API key in App Store Connect.


  • Create a new App Store Connect API Key in the Users page
  • For more info, go to the App Store Connect API Docs
  • Select the "Keys" tab
  • Give your API Key an appropriate role for the task at hand. You can read more about roles in Permissions in App Store Connect But let’s use App Manager role.
  • Note the Issuer ID as you will need it for the configuration steps below
  • Download the newly created API Key file (.p8), this file name should look like APP_STORE_CONNECT_API_KEY_ID.p8
  • This file cannot be downloaded again after the page has been refreshed

7. APP_STORE_CONNECT_ISSUER_ID:

This variable represents the issuer ID associated with your App Store Connect API key.

The issuer ID is obtained when you generate the API key in App Store Connect.

8. APP_STORE_CONNECT_API_KEY_CONTENT:

This variable represents the content or value of the App Store Connect API key.

The value for this variable is typically obtained when you generate the API key in App Store Connect. It is the content of APP_STORE_CONNECT_API_KEY_ID.p8 file. You can open the file in a text editor and copy and paste it into your GitHub actions environment as a secret.

So overall the workflow file will look like this:


name: Deploy to TestFlight
on:
  # Enable manual run
  workflow_dispatch:
    inputs:
      lane:
        description: "Fastlane lane"
        required: true
        default: "beta"
        type: choice
        options:
          - beta

# Declare default permissions as read only.
permissions: read-all

jobs:
  fastlane-deploy:
    runs-on: macos-latest
    steps:
      - name: Setup up Flutter
        uses: subosito/flutter-action@v2
        with:
          channel: stable
      - run: flutter doctor -v
      - name: Setup JDK
        uses: actions/setup-java@v1
        with:
          java-version: "12.x"
      - uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: "latest-stable"
      - uses: actions/checkout@v2
      - name: Setup SSH Keys and known_hosts for fastlane match
        run: |
          SSH_PATH="$HOME/.ssh"
          mkdir -p "$SSH_PATH"
          touch "$SSH_PATH/known_hosts"
          echo "$PRIVATE_KEY" > "$SSH_PATH/id_ed25519"
          chmod 700 "$SSH_PATH"
          ssh-keyscan github.com >> ~/.ssh/known_hosts
          chmod 600 "$SSH_PATH/known_hosts"
          chmod 600 "$SSH_PATH/id_ed25519"
          touch "$SSH_PATH/config"
          echo -e "Host github.com\n\tAddKeysToAgent yes\n\tUseKeychain yes\n\tIdentityFile $SSH_PATH/id_ed25519" >> "$SSH_PATH/config"
          eval $(ssh-agent -s)
          ssh-add --apple-use-keychain "$SSH_PATH/id_ed25519"
        env:
          PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          SSH_PASSPHRASE: ${{ secrets.SSH_PASSPHRASE }}
   
      - name: Deploy to TestFlight
        working-directory: YOUR_APP_DIRECTORY/ios
        run: |
          bundle install
          bundle exec fastlane ${{ github.event.inputs.lane || 'beta' }}
        env:
          FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
          FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          MATCH_KEYCHAIN_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }}
          MATCH_KEYCHAIN_NAME: ${{ secrets.MATCH_KEYCHAIN_NAME }}
          APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
          APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
          APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}

E. Fastfile deployment setup

Now we have arrived at the end of the process. We will go through what this `bundle exec fastlane beta` do.

Let’s have a look at the Fastfile setup:


update_fastlane

default_platform(:ios)

platform :ios do
  desc "Push a new beta build to TestFlight"
  lane :beta do
    if is_ci
      create_keychain(
        name: ENV['MATCH_KEYCHAIN_NAME'],
        password: ENV["MATCH_KEYCHAIN_PASSWORD"],
        default_keychain: true,
        unlock: true,
        timeout: 3600,
        lock_when_sleeps: false
      )
    end
    match(
      type: "appstore",
      readonly: is_ci,
      keychain_name: ENV['MATCH_KEYCHAIN_NAME'],
      keychain_password: ENV["MATCH_KEYCHAIN_PASSWORD"]
    )
    sh "cd .. && cd .. && cd .. && melos bootstrap"
    sh "flutter build ios --release --no-codesign"
    build_app(
      workspace: "Runner.xcworkspace",
      scheme: "Runner",
      xcargs: "-allowProvisioningUpdates",
      output_directory: "../build/ios"
    )
    if is_ci
      api_key = app_store_connect_api_key(
        key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
        issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
        key_content: ENV["APP_STORE_CONNECT_API_KEY_CONTENT"],
        duration: 1200, # optional, in seconds, default: 1200 (20 min)
        in_house: false, # optional, if true, will use the App Store Connect API instead of App Store Connect API for downloading profiles
        verbose: true # optional, print out the request headers
      )
      upload_to_testflight(skip_waiting_for_build_processing: true, api_key: api_key, expire_previous_builds: true)
    else
      upload_to_testflight(skip_waiting_for_build_processing: true, expire_previous_builds: true)
    end
  end
end

Update Fastlane (Optional)

The update_fastlane command ensures that you have the latest version of Fastlane installed. It's optional and can be used to update Fastlane to the latest version if desired.

Default Platform

The default_platform(:ios) statement specifies that the default platform for this Fastfile is iOS.

Beta Lane

The beta lane represents the workflow for pushing a new beta build to TestFlight. Here's a breakdown of the actions performed within the beta lane:

a. Create Keychain (Conditional)

The create_keychain action creates a keychain with the specified name and password. This step is conditional and is executed only when running within a CI environment (is_ci variable). It creates and unlocks a keychain, allowing secure access to the code signing certificates and provisioning profiles.

b. Match

The match action configures code signing by using Fastlane match. It specifies the type of code signing (appstore) and sets the keychain name and password. The is_ci flag is used to determine whether to make the provisioning profiles read-only (readonly: true) in a CI environment.

c. Build App

The flutter build ios command builds the iOS app with the specified options. It generates a release build without code signing (--no-codesign) and allows provisioning updates (--allowProvisioningUpdates).

d. Upload to TestFlight

The upload_to_testflight action uploads the built app to TestFlight for beta testing. It specifies options such as skipping the build processing wait (skip_waiting_for_build_processing: true) and expiring previous builds (expire_previous_builds: true). The api_key variable is provided when running within a CI environment to authenticate with App Store Connect using the generated API key.

Voila! Now running the github action will successfully deploy our app to Testflight

Best Practices and Tips

When using GitHub Actions and Fastlane for iOS app deployment, consider the following best practices and tips to optimize your workflow:

Code Signing Management

  • Utilize Fastlane match to simplify code signing and provisioning profile management. It ensures consistency across development environments and team members.
  • Store code signing resources securely in a Git repository or use a trusted key management solution.
  • Regularly review and update your code signing configuration to align with the latest security and Apple requirements.

Secrets Management

  • Leverage GitHub Secrets to securely store sensitive information such as passwords, API keys, and certificates.
  • Avoid hardcoding secrets directly in the Fastfile or workflow files.
  • Grant appropriate access permissions to secrets and limit their usage to specific workflows or branches.

Continuous Integration (CI) Environment

  • Configure your workflows to run on dedicated macOS runners for iOS app development.
  • Ensure the runners have the necessary dependencies and tools installed, such as Flutter, Xcode, and Fastlane.
  • Regularly update your CI environment to use the latest stable versions of tools and SDKs.

Workflow Optimization

  • Utilize caching mechanisms to speed up build times by storing dependencies and intermediate build artifacts.
  • Parallelize steps and jobs when possible to maximize efficiency and reduce overall workflow execution time.
  • Monitor and optimize resource usage to avoid exceeding usage limits or incurring unnecessary costs.

Conclusion

In conclusion, automating iOS app deployment with GitHub Actions and Fastlane brings numerous benefits to your development workflow. By following the outlined steps and best practices, you can streamline the deployment process, improve productivity, and ensure consistent code signing across your team. The time and effort saved through automation allow you to focus more on developing and delivering high-quality apps to your users.

Additional Resources

To further explore iOS app deployment with GitHub Actions and Fastlane, consider the following resources:

  • Fastlane Documentation: Official documentation for Fastlane, providing in-depth guides and references.
  • GitHub Actions Documentation: Comprehensive documentation on GitHub Actions, covering various aspects of setting up and managing workflows.
  • Fastlane Plugins: Explore a wide range of community-contributed plugins that extend the functionality of Fastlane.
  • GitHub Actions Marketplace: Discover pre-built actions and workflows shared by the GitHub community to enhance your iOS app deployment process.
  • Blogs and Tutorials: Seek out tutorials and blog posts that demonstrate real-world examples and share insights from developers who have successfully automated their iOS app deployments with GitHub Actions and Fastlane.

By leveraging these resources, you can continue to enhance your understanding of the tools and techniques needed to optimize your iOS app deployment workflow.

Game Changers Unite

We are the software development agency and we love helping mission-driven startups in turning their ideas into amazing products with minimal effort and time.

LET'S TALK