Setting Up a CI/CD Pipeline for iOS Using Fastlane and GitHub Actions in a Flutter Project

Manoel Soares Neto
10 min readDec 23, 2023

--

Prefere ler em português? Clique aqui!

If you develop apps, you know that the deployment process can be repetitive and time-consuming. Doing this manually is not only tiring but also increases the risk of errors. In this article, I will show you how to automate and streamline the deployment process to the Apple Store. We will use efficient tools like Fastlane and GitHub Actions, which transform deployment from a manual task into an automated, reliable, and efficient process.

If you develop apps, you know that the deployment process can be repetitive and time-consuming. Doing this manually is not only tiring but also increases the risk of errors. In this article, I will show you how to automate and streamline the deployment process to the Apple Store. We will use efficient tools like Fastlane and GitHub Actions, which transform deployment from a manual task into an automated, reliable, and efficient process.

This guide is straightforward, ideal for those who develop in Flutter and are looking for a way to make iOS app deployment simpler and less prone to failures. With this guide, you will gain a solid foundation, allowing you to refine and enhance your deployment processes to make them even more secure and efficient.

1. Starting the Flutter Project

Let's create the Flutter project as follows: flutter create --org com.fastlaneexample.example app. The Flutter version used in this project was: 3.16.2.

Project Structure and Used Tools

In addition to Flutter, we will use Melos, a versatile tool for managing monorepos in Dart and Flutter, which helps us to simplify and automate various tasks. The version of Melos used here is 3.4.0.

Our project structure is organized as follows:

├── app
├── android
├── ios
└── pubspec.yaml
├── melos.yaml
└── pubspec.yaml

Note: This article focuses on setting up the environment and the necessary tools to automate the deployment of this project to TestFlight, therefore, we will not delve into the specifics of Flutter implementation.

2. Initializing Fastlane

To integrate Fastlane into the project, installation will be done through Homebrew, a package manager for macOS, which simplifies the installation of software on Apple's operating system. Although this guide uses Homebrew, Fastlane can be installed in various other ways. For installation alternatives, consult the official Fastlane documentation.

brew install fastlane

After installation, navigate to the ios folder of your Flutter project and initialize Fastlane:

fastlane init

During the initialization process, when asked about how you want to configure Fastlane, choose 'Manual setup'. This option allows for a more detailed configuration tailored to the specific needs of the project.

Upon completion, the initial structure of Fastlane will be created with the following files:

├── fastlane
├── Appfile
└── Fastfile
└── Gemfile

The 'Gemfile' is used by Bundler to manage Ruby dependencies, specifically Fastlane in this case. The 'Appfile' contains global settings for your app, such as the bundle identifier. And the 'Fastfile' defines the 'lanes' that automate specific tasks, such as builds or deploys.

Edit the 'Appfile' to configure the bundle of your application:

app_identifier("com.fastlaneexample.example") # The bundle identifier of your app

Update the 'Gemfile' to specify the version of Fastlane used in the project:

source "https://rubygems.org"

gem "fastlane", "~> 2.217.0"

By specifying the version of Fastlane, you ensure the consistency of the tool used by all team members and across different development environments.

3. Configuring Fastlane Match

Fastlane Match simplifies the management of certificates and provisioning profiles, making the process of developing and distributing iOS apps more efficient and secure. Let's add a new app and configure Match by following these steps:

Creating a New App ID

Begin by creating a new App ID for the project in the Apple Developer Portal. This ID will be used to uniquely identify the app in the App Store.

Next, create a new app named Flutter Fastlane Example.

Creating a Repository for Certificates and Profiles

Create a new private repository on GitHub to securely store the certificates and provisioning profiles managed by Match. For example, name this repository 'secrets'.

Initializing Fastlane Match

Navigate to the 'ios' folder of your project and execute:

fastlane match init

Choose the 'git' option and provide the URL of the newly created repository, for example: https://github.com/Flutter-Fastlane-Example/secrets.git. A 'Matchfile' will be created in the 'fastlane' folder. Edit it as follows:

git_url("https://github.com/Flutter-Fastlane-Example/secrets.git")
storage_mode("git")
type("appstore") # The default type, can be: appstore, adhoc, enterprise or development
app_identifier(["com.fastlaneexample.example"])
username("your_github_username") # Your Apple Developer Portal username

Setting Up a GitHub Personal Access Token

For Match to interact with the GitHub repository, you will need a 'Personal Access Token'. Here's how to create this token.

After creating the token, convert it to Base64 and copy the result as follows:

echo -n 'your_github_username:your_personal_access_token' | base64 | pbcopy

Generating Certificates and Provisioning Profiles

Run the Match commands to generate the certificates:

  • For development: fastlane match development
  • For distribution: fastlane match appstore

Remember to create a password for the keychain when prompted and keep it in a secure location.

Recreating Certificates (if necessary): In some circumstances, it may be necessary to recreate the certificates and provisioning profiles for the project. We can use Fastlane Match's nuke feature to handle this scenario.

For the App Store: fastlane match nuke distribution to revoke the certificates and fastlane match appstore to recreate them.

For development: fastlane match nuke development to revoke the certificates and fastlane match development to recreate them.

Check the 'secrets' repository to see if the certificates have been successfully created.

Setting Up Environment Variables

Create a .env.default file inside the 'fastlane' folder to temporarily store the necessary keys:

MATCH_PASSWORD="your_match_keychain"
MATCH_GIT_BASIC_AUTHORIZATION="your_GIT_PAT_TOKEN"

MATCH_GIT_BASIC_AUTHORIZATION is the Personal Access Token generated above and converted to Base64, MATCH_PASSWORD is the keychain created above. These keys will later be securely stored in the GitHub environment variables.

4. Configuring App Store Connect

To interact efficiently with App Store Connect, it is crucial to load the API token, which allows us to download provisioning profiles, send binaries to TestFlight, among other operations. Let's set this up through a 'lane' in the 'Fastfile'.

platform :ios do
before_all do
load_asc_api_token
end

desc "Load the App Store Connect API token"
lane :load_asc_api_token do
app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"],
key_content: ENV["ASC_KEY_P8"],
is_key_content_base64: true,
in_house: false
)
end
end

In this script, we use three environment variables: ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_P8. They will be created and temporarily stored in the environment file.

Generating the API Keys

  • Access your account on App Store Connect.
  • Navigate to 'Users and Access'.
  • Select the 'Keys' tab and click on 'Generate API Key'.
  • Name the key and assign the role of 'App Manager', which provides the necessary permissions.
  • After creation, you will receive the 'Issuer ID' (ASC_ISSUER_ID) and 'Key ID' (ASC_KEY_ID).

Download the my_api_key.p8 key. Convert this key to Base64 using the following command in the terminal:

base64 -i my_api_key.p8 | pbcopy

This will copy the key to the clipboard, and then store the converted ASC_KEY_P8 key in the temporary environment file.

ASC_KEY_ID="your_asc_key_id"
ASC_ISSUER_ID="your_asc_issuer_id"
ASC_KEY_P8="your_base64_asc_key_p8"
MATCH_PASSWORD="your_match_keychain"
MATCH_GIT_BASIC_AUTHORIZATION="your_GIT_PAT_TOKEN"

Keep these keys in a secure location and never expose them publicly. Later, these keys will be transferred to GitHub's secure environment variables for automation through GitHub Actions.

5. Configuring the Release Lane in Fastlane

To prepare the automation of our Flutter app's launch process for TestFlight, we performed some configurations in Xcode:

  1. Bumping the Minimum iOS Version: In the 'General' tab, we adjusted the minimum iOS version to 12.0, ensuring compatibility with the latest versions.
  2. Automatically manage signing: In the 'Signing & Capabilities' tab, we disabled the 'Automatically manage signing' option and selected the 'match AppStore com.fastlaneexample.example' provisioning profile.

The release lane in the Fastfile will be configured to automate the launch of new builds. To provide a clear reference point, we show the commit at which the release is initiated.

desc "Release a new build to Apple Store"
lane :release_beta do
commit = last_git_commit
puts "*** Starting iOS release for commit(#{commit[:abbreviated_commit_hash]}) ***"
end

We extract the App Store Connect API key for later use.

desc "Release a new build to Apple Store"
lane :release_beta do
...

#read api key from app_store_connect_api_key lane variable
api_key = lane_context[SharedValues::APP_STORE_CONNECT_API_KEY]
end

We use Fastlane Match to synchronize certificates and profiles.

desc "Release a new build to Apple Store"
lane :release_beta do
...

#sync certificates and profiles using match
sync_code_signing(
api_key: api_key,
type: "appstore",
readonly: true,
)
end

We automatically update the build number, based on the latest version available on TestFlight.

def bump_build_number()
latest_build_number = latest_testflight_build_number(initial_build_number: 0)
return (latest_build_number + 1)
end

desc "Release a new build to Apple Store"
lane :release_beta do
...

build_number = bump_build_number()
end

We define the version name, using logic to handle the absence of previous versions.

def get_version_name()
version_name = lane_context[SharedValues::LATEST_TESTFLIGHT_VERSION]

if version_name.empty?
puts "*** Version name is empty, add version 1.0.0 ***"
version_name = "1.0.0"
end

return version_name
end

desc "Release a new build to Apple Store"
lane :release_beta do
...

version_name = get_version_name()
end

We finalize the lane configuration with the compilation of the Flutter project and the upload of the build to TestFlight.

desc "Release a new build to Apple Store"
lane :release_beta do
...

Dir.chdir "../.." do
puts "*** Build flutter iOS release for version #{version_name}+#{build_number} ***"
sh("flutter", "build", "ipa", "--release", "--build-number=#{build_number}", "--build-name=#{version_name}")
end

puts "*** Build and sign iOS app release ***"
build_app(
skip_build_archive: true,
archive_path: "../build/ios/archive/Runner.xcarchive",
)

puts "*** Upload app to testflight ***"
upload_to_testflight(api_key: api_key)
end

Note that the target name in the Xcode project, 'Runner', may vary; replace it with the correct name of your project as needed.

Run fastlane release_beta inside the app/ios folder to generate a new version for TestFlight.

Keychain access

When running locally, you will be prompted to add the password to release access to the keychain. Enter your computer login password and click 'Always Allow'.

6. Storing Keys on GitHub

Securely storing the keys and setting up a correct workflow are essential to automate the build and release process with GitHub Actions. Access your GitHub, click on 'Settings', and add all the keys that were created following the images shown.

7. Configuring a Workflow in GitHub Actions

At the root of your project, create the .github/workflows folder and add a new file for the workflow, such as apple-release.yaml.

├── .github
└── workflows
└── apple-release.yaml
├── app
├── android
├── ios
├── fastlane
├── Appfile
├── Fastfile
└── Matchfile
└── Gemfile
└── pubspec.yaml
├── melos.yaml
└── pubspec.yaml

In the workflow file, define the steps that GitHub Actions should follow to build and distribute your app.

name: Apple Release

on:
push:
branches:
- apple-release

jobs:
build-and-deploy-ios:
runs-on: macos-latest
defaults:
run:
working-directory: app
steps:
- name: Set up git and fetch all history for all branches and tags
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "2.7"
- name: Bundle install for iOS Gemfile
timeout-minutes: 5
run: cd ./ios && bundle install
- name: Set up Flutter SDK
uses: subosito/flutter-action@v2
with:
flutter-version: "3.16.2"
channel: "stable"
architecture: x64
cache: true
- name: Set up melos
run: flutter pub global activate melos
- name: Melos Bootstrap
run: melos bs
- name: Build and Deploy to TestFlight
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.GIT_PAT_TOKEN }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
run: |
cd ./ios
bundle exec fastlane release_beta

When pushing the apple-release branch, the previously defined workflow in GitHub Actions will be automatically triggered. However, it is important to highlight a crucial aspect related to keychain access in the CI/CD environment.

Automating Keychain Access

Just as when we run the release_beta lane locally and are asked to enter the computer password to release keychain access, the workflow in GitHub Actions faces a similar challenge. In a virtual machine environment, like the one provided by GitHub Actions, the request for a password to access the keychain can interrupt the workflow, leaving it 'stuck'.

To solve this issue, it is necessary to use Fastlane's setup_ci action. The setup_ci is designed to configure the CI environment, creating a temporary keychain specifically for the build process, eliminating the need for a password. This temporary keychain is automatically removed at the end of the workflow, ensuring security and efficiency.

Include the setup_ci in your Fastfile configuration as shown below.

platform :ios do
before_all do
setup_ci
load_asc_api_token
end

...
end

Testing the Workflow

With these settings applied, you are ready to test the workflow. Push the apple-release branch and watch GitHub Actions execute the build and release process of your app.

Acknowledgement

I would like to express my gratitude to everyone who has followed this guide to this point. A special thanks to William Cho, whose contribution was fundamental in the preparation of this content. I hope the information shared here can help you simplify and automate the iOS app release process, making development more efficient and enjoyable. Feel free to leave any questions or suggestions in the comments. Thank you for dedicating your time and effort, and I wish you success in your projects!

--

--