Mastering Fastlane: The Ultimate Guide to Effortless Android and iOS Deployments with Github Pipelines

Muhammad Qazi
13 min readMar 1, 2023

--

Note: Complete code used in this guide is at

About the author:

Muhammad Qazi is a mobile app developer with a focus on React Native, as well as a Golang developer with experience in building APIs and backend systems. Passionate about using automation tools like Fastlane and Github pipelines to streamline the app deployment process and improve team collaboration. You can find more about my work on my Github or LinkedIn

What is Fastlane?

Fastlane is a powerful tool for automating the app deployment process, making it faster and more efficient. With Fastlane, you can automate tasks such as code signing, building, testing, and releasing your Android and iOS apps, all in one place. Fastlane is particularly useful when used with Github pipelines, as it allows you to easily integrate your app deployment with your code repository and collaborate with other team members. In this guide, we will show you how to set up Fastlane with Github pipelines for Android and iOS apps, and provide tips and best practices for using them effectively. Whether you’re a solo developer or part of a team, this guide will help you streamline your app deployment process and save time and effort. Let’s get started!

1. Github pipelines for iOS Testflight

We will use the following Fastlane tools for automation deployment

  • app_store_connect_api_key: A tool that allows you to authenticate with the App Store Connect API using a private key, rather than a username and password. This can help improve security and streamline the app deployment process.
  • match: A tool for managing code signing certificates and provisioning profiles across multiple machines and team members. With the match, you can automatically download and update your certificates and profiles, and ensure that they are consistent across your development and production environments.
  • gym: A tool for building and packaging your app for distribution. With a gym, you can create a distributable package for your app, including all necessary assets and dependencies, in a single command.
  • pilot: A tool for uploading and managing your app on the App Store. With the pilot, you can upload new builds, manage beta testing and release groups, and track your app's status and metrics.
  • slack: A tool for integrating Fastlane with Slack, a popular team communication platform. With slack, you can receive notifications and updates from Fastlane directly in your Slack channels, making it easy to collaborate with your team and stay up-to-date on your app deployment process.

Initialize the Fastlane

To initialize Fastlane for your project, first, open up your Xcode workspace and change the Bundle Identifier to match your project’s unique identifier.

Navigate to the “ios” directory in your project and run the command

bundle exec fastlane init or fastlane init

Enter your Apple ID developer credentials when prompted, and choose whether you want Fastlane to create the App ID and App on App Store Connect for you. If you choose to have Fastlane create these items, it will generate them for you automatically.

Finally, Fastlane will generate two files for you — an Appfile and a Fastfile — which you can modify to suit your specific deployment needs.

Initialize the match

Before initializing the match, make sure you have a new GitHub repository for storing the signing certificates, after creating GitHub repo run the following command in your terminal:

fastlane match init

Paste the ssh connection string of the git repository you just created when prompted.

Enter any string when prompted for the passphrase for Match storage, which you’ll need to remember to decrypt the generated files.

After initializing the match, execute the command fastlane match development to generate the required certificates and files for local iOS device development. During the process, you will be prompted to provide a passphrase for Match storage, which should be remembered as it will be needed to decrypt the generated files.

To create the necessary certificates and files for Testflight deployment, run fastlane match appstore. As this is the second time Match is being executed, it will automatically remember the previously provided passphrase for decryption.

After doing that step you need to make sure that,

  1. First, make sure in XCode that automatically manages signing is not checked.
  2. Set Debug to match Development <bundleIdentifier> and Release to match AppStore <bundleIdentifier> for Provisioning Profile.
  3. Check Signing & Capabilities to ensure that everything is set up correctly.
  4. Finally, run your app on an iPhone device or simulator to confirm that it’s working properly.

Update fastlane/Matchfile to the following, changing the placeholders with the appropriate information:

git_url("git@github.com:muhammadqazi/your-repo.git")
storage_mode("git")

type("appstore") # The default type, can be: appstore, adhoc, enterprise or development
app_identifier(["<your bundle identifier>"]) # The bundle identifier(s) of your app
username(ENV["APPLE_ID"]) # Your Apple Developer Portal email

If you do multiple build for each environment, you need to provide all your app identifiers in the app_identifier, this case, I only have one.

Example:

app_identifier(["<your bundle identifier 1>","<your bundle identifier 2>"])

Generate auth key in Apple Development Portal, go to https://appstoreconnect.apple.com/access/api

You can only generate those keys if you are the owner of the account.

The image below shows where you can get your key id and issuer id.

Generate auth key in Apple Development Portal

Once you generate the key, make sure to securely download and store the .p8 file as you will only have one chance to do so. Losing the file would require generating a new key.

Appfile

Make sure your fastlane/Appfile should look like this.

app_identifier("<app identifer>") # The bundle identifier of your app
apple_id(ENV["APPLE_ID"]) # Your Apple email address

itc_team_id(ENV["ITUNES_TEAM_ID"]) # App Store Connect Team ID
team_id(ENV["APPSTORE_TEAM_ID"]) # Developer Portal Team ID

Fastlane file

Copy the code below and paste it into the fastlane/Fastfile

######## IOS CONFIGURATIONS
# If you want to make the build automatically available to external groups,
# add the name of the group to the array below, after "App Store Connect Users"
groups = ["External Tests group name"]
workspace = "yourapp.xcworkspace"
project = "yourapp.xcodeproj"
slack_weebhook_url = ENV["SLACK_WEBHOOK_URL"]

# If you build for multiple environments, you might wanna set this specifically on build_app
configuration = "Release"
scheme = "yourapp"
export_method = "app-store" # specify the export method
key_id = ENV["APPSTORE_KEY_ID"] # The key id of the p8 file
issuer_id = ENV["APPSTORE_ISSUER_ID"] # issuer id on appstore connect
key_filepath = "fastlane/secretKey.p8" # The path to p8 file generated on appstore connect
######## END IOS CONFIGURATIONS



default_platform(:ios)

platform :ios do
desc "Push a new build to TestFlight"

lane :release do
setup_ci
app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)
api_key = app_store_connect_api_key(
key_id: key_id,
issuer_id: issuer_id,
key_filepath: key_filepath,
in_house: true
)

latest_build_number = latest_testflight_build_number(
api_key: api_key,
app_identifier: app_identifier
)
increment_build_number(
build_number: (latest_build_number + 1),
)


clear_derived_data
match(api_key: api_key)

gym(
scheme: scheme,
configuration: configuration,
export_options: {
method: export_method,
compile_bitcode: true
}
)

pilot(
changelog: "Build #{latest_build_number + 1}",
api_key: api_key,
app_identifier: app_identifier,
distribute_external: true,
groups: groups,
)

slack(
slack_url: slack_weebhook_url,
message: "Build #{latest_build_number + 1} is available on TestFlight",
channel: "#your-channel",
default_payloads: [],
success: true,
payload: {
'Build Date' => Time.now,
'Build Number' => latest_build_number + 1
}
)

clean_build_artifacts
end
end

Github pipelines

To set up an SSH key for your match repository ( the repository you use to store certificates ), navigate to the “Deploy keys” section in the repository’s settings. Generate an SSH key pair by running the command ssh-keygen -t rsa -b 4096 -C <your apple email here> and save it wherever convenient. Leave the passphrase empty. If you already used that ssh somewhere you can also use ssh-keygen -t ed25519 -C "YOUR_EMAIL" to generate ssh key.

Next, add the newly generated public key by running cat path/to/the/key.pub and copying the contents of the file. Paste this value into the appropriate field in the setting -> “Deploy keys” section and save the changes. The key can be named as desired.

Setting up github secrets for main project

Now it’s time set up github secrets for your main project repository. Open your main project repo and navigate to settings and in the left menu you will see secrets and variables -> actions. Add the following keys to your secrets.

  1. Create a secret named “MATCH_PASSWORD” and input the Match passphrase you previously entered as the value.
  2. Generate the private key by running “cat path/to/the/key” and create a new secret named “MATCH_REPO_KEY”. Copy and paste the contents of the private key file into the value field.
  3. Create a secret called “P8_AUTH_KEY” and copy the contents of the “secretKey.p8” file into the value field.
  4. Create a secret named “APPSTORE_KEY_ID” and input your key ID as the value.
  5. Create a secret named “APPSTORE_ISSUER_ID” and input your issuer ID as the value.
  6. Create a secret named “APPSTORE_TEAM_ID” and input your team ID as the value. You can find your team ID on the Apple Developer website or in the Fastlane Appfile.
  7. Create a secret named “ITUNES_TEAM_ID” and input your iTunes Connect team ID as the value. You can find your team ID on the iTunes Connect website, under “associatedAccounts”.
  8. Create a secret named “APPLE_ID” and input your Apple ID used to generate the P8 file as the value.
  9. Create a secret named “MATCH_REPO_SSH” and input your Git URL as the value. The URL must be the SSH URL to your Match repository.
  10. Go to slack incoming weebhook webiste, log in, and after that you will be able too see screen like this

Choose your channel, click Add incoming WebHooks integration. Next step is to copy the Webhook URL and add it to github secrets “SLACK_WEBHOOK_URL”.

Make sure all the ENV variables used in matchfile and fastfile are in github secrets.

Setup github actions workflow

Create the file ./github/workflows/ios-deployment.yaml

name: ios-deployment

on:
push:
branches:
- 'master'

jobs:
build:
runs-on: macos-latest
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
P8_AUTH_KEY: ${{ secrets.P8_AUTH_KEY }}
APPSTORE_KEY_ID: ${{ secrets.APPSTORE_KEY_ID }}
APPSTORE_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
ITUNES_TEAM_ID: ${{ secrets.ITUNES_TEAM_ID }}
APPSTORE_TEAM_ID: ${{ secrets.APPSTORE_TEAM_ID }}
MATCH_REPO_SSH: ${{ secrets.MATCH_REPO_SSH }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
BASE_URL : ${{ vars.BASE_URL }}

steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '13.1.0'

- uses: actions/checkout@master

- uses: webfactory/ssh-agent@v0.5.0
with:
ssh-private-key: ${{ secrets.MATCH_REPO_KEY }}

- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7.4

- name: Install Dependencies
run: |
cd ios
bundle install

- uses: actions/cache@v2
with:
path: Pods
key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-

- name: Setup dependencies
run: |
npm install
cd ios
pod install

- name: Build and deploy
run: |
cd ios
echo "${{ secrets.P8_AUTH_KEY }}" > secretKey.p8
bundle exec fastlane ios release

This is a GitHub Actions workflow file named “ios-deployment”. It defines a job named “build” that runs on the latest version of macOS when a push to the master branch occurs.

The environment variables are set to access various secrets needed for the deployment process, such as the passphrase for Match storage, P8 authentication key, App Store key ID, issuer ID, iTunes team ID, App Store team ID, SSH URL to Match repo, and Slack webhook URL. If you have environment variables make sure you add those in the GitHub secrets -> variables section, you can call them in the workflow file by {{ vars.YOUR_ENV_VAR }}

The steps in the job are as follows:

  • Sets up Xcode version 13.1.0
  • Checks out the repository
  • Sets up SSH agent with the Match repo private key
  • Sets up Ruby version 2.7.4
  • Installs dependencies using bundle install
  • Caches the Pods directory and restores it for future builds
  • Installs dependencies using npm and pod install
  • Builds and deploys the iOS app using Fastlane and the “ios release” lane.

If the deployment is successful, a notification will be sent to the Slack channel specified in the “SLACK_WEBHOOK_URL” secret.

2. Github pipelines for Android

Setup Google Cloud Platform (GCP) Project

  1. Got to https://console.cloud.google.com/home/dashboard and follow the steps in the screenshots below.

If you got Google Api Error: forbidden: The caller does not have permission — The caller does not have permission try this https://stackoverflow.com/questions/66638362/google-api-error-forbidden-the-caller-does-not-have-permission-the-caller-do

Initialize Fastlane

Create a Gemfile in the android directory and add the following code to it

source "https://rubygems.org"

gem "fastlane"

plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

In the same “android” directory of your project run the command

bundle exec fastlane init or fastlane init

Run bundle config --local set path "./vendor/bundle", and add vendor/ to your .gitignore.

You will be asked to enter applicationId and path for the file google service account key which you just download, you can find your app applicationId in android/app/build.gradle

Chose nfor downloading existing metadata and setup metadata management

Verify connection to Google Play Console

First, take the Google secret key that you previously generated and downloaded and copy it into the root directory of your project. Rename the file to google-service-account-key.json, and make sure to add it to your .gitignore file.

Then, navigate to the /android directory and run bundle exec fastlane run validate_play_store_json_key json_key:google-service-account-key.json. This command will validate your Google Play Store JSON key.

Fastfile

######## Android CONFIGURATIONS

package_name = "<appid>"
json_key_file = "google-service-account-key.json"
gradle_file_path = "./app/build.gradle"
slack_weebhook_url = ENV["SLACK_WEBHOOK_URL"]
######## END Android CONFIGURATIONS

default_platform(:android)

platform :android do

desc "Build and deploy to internal track"
lane :internal do

previous_build_number = google_play_track_version_codes(
package_name: package_name,
json_key: json_key_file,
track: "beta",
)[0]

current_build_number = previous_build_number + 1

increment_version_code(
gradle_file_path: gradle_file_path,
version_code: current_build_number
)

gradle(
task: "clean bundleRelease",
gradle_path: "./gradlew"
)

upload_to_play_store(
track: "beta",
aab: "app/build/outputs/bundle/release/app-release.aab",
)

slack(
slack_url: slack_weebhook_url,
message: "Android : Build #{previous_build_number + 1} is available for Open Testing",
channel: "#channel",
default_payloads: [],
success: true,
payload: {
'Build Date' => Time.now,
'Build Number' => previous_build_number + 1
}
)
end
end

This is a Fastlane configuration file for Android, which includes specific settings for the Android package name, Google service account key file, Gradle file path, and Slack webhook URL.

The default platform is set to Android, and there is one lane defined called “internal”. This lane builds and deploys to the beta testing on Google Play Store.

The lane first retrieves the previous build number from the beta track on Google Play Store and increments it by one to get the current build number. It then updates the version code in the Gradle file using the current build number.

The gradle the command is then executed to clean and build the release bundle. The upload_to_play_store command is used to upload the generated app bundle to the beta track on the Google Play Store.

Finally, the slack the command is used to send a Slack notification with details about the newly uploaded build. It includes the build date and number and posts the message to the "#channel" channel on Slack.

You can now run bundle exec fastlane android internalto test it locally on your machine if it’s working.

Github Pipelines

You will need to follow https://reactnative.dev/docs/signed-apk-android to generate a key that you will use to sign your builds.

Then you need to upload the signing key that you generated, but GitHub secrets do not support files, so we will use gpg to transform it into a string that represents a file, and we will also secure it with a passphrase.

Run the following command to generate .asc file

gpg -c --armor app/my-upload-key.keystore

This will produce my-upload-key.keystore.asc which contains a string that we can copy-paste into GitHub secrets.

Github secrete

  1. To create a secret for the Android signing key, name it “ANDROID_SIGNING_KEY” and copy the contents of the “my-upload-key.keystore.asc” file.
  2. Create another secret called “ANDROID_SIGNING_KEY_PASSPHRASE” and input the passphrase you used in GPG when you created the key.
  3. To create a secret for the Google service account key, name it “GOOGLE_SERVICE_ACCOUNT_KEY” and copy the contents of the “google-service-account-key.json” file.

Now create .github/workflows/android-deployment.yaml and add the following content:

name: android-deployment

on:
push:
branches:
- 'master'

jobs:
build:
runs-on: ubuntu-latest
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
BASE_URL : ${{ vars.BASE_URL }}
continue-on-error: false

steps:
- uses: actions/checkout@master

- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7.4

- name: Install Dependencies
run: |
cd android
bundle install

- name: Setup dependencies
run: |
echo '${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}' > android/google-service-account-key.json
echo "${{ secrets.ANDROID_SIGNING_KEY }}" > my-upload-key.keystore.asc
gpg -d --passphrase "${{ secrets.ANDROID_SIGNING_KEY_PASSPHRASE }}" --batch my-upload-key.keystore.asc > android/app/my-upload-key.keystore

- name: Build and deploy
run: |
npm install
export NODE_OPTIONS=--openssl-legacy-provider
cd android
bundle exec fastlane android internal

This is a GitHub Actions workflow file for deploying an Android app to the internal track on the Google Play Store.

The workflow is triggered on push events to the master branch. The job runs on the latest version of Ubuntu and has an environment variable SLACK_WEBHOOK_URL set to the value of a secret called SLACK_WEBHOOK_URL.

The job includes the following steps:

  1. The first step checks out the code from the repository using the actions/checkout action.
  2. The second step sets up the Ruby environment using the ruby/setup-ruby action, specifying Ruby version 2.7.4.
  3. The third step installs dependencies by changing the directory to android and running bundle install.
  4. The fourth step sets up dependencies by creating the google-service-account-key.json and my-upload-key.keystore files with the secrets values, and decrypting the my-upload-key.keystore.asc file with the GPG passphrase.
  5. The final step builds and deploys the app by changing the directory to android, installing Node.js dependencies, setting an environment variable to use the legacy OpenSSL provider, and executing the fastlane command to deploy the app to the internal track on Google Play Store.

If any step fails, the workflow stops and reports a failure.

--

--

Muhammad Qazi

Software engineer experienced in Golang, Node.js, React/Next.js, React Native, and CI/CD DevOps pipelines.