Automating mobile application deployments using Fastlane and CI/CD tools
Tags: fastlane, devops, reactnative, tooling
Published: 2021-05-03 14:00:21

# The Problem

---

Engineering teams these days find it troublesome to build, test and deploy their mobile application changes locally without having to maintain the tools required for it. There is a lot of maintenance involved as you need to keep track of what versions of these tools have been installed to avoid compatibility issues when you’re building the application bundles, especially hybrid apps built on React Native.

# The Cure

---

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/odbydamevv0fi94wkj2h.png)

***[fastlane](https://fastlane.tools/)***, an automation tool that aids in handling all of the tedious tasks so you don't have to. It's by far, the easiest way to build and release your mobile apps.

fastlane can easily:

- Distribute beta builds to your testers
- Publish a new release to the app store in seconds
- Reliably code sign your application - alleviates all of your headaches
- Reliably maintain your provisioning profiles and application certificates in a Git repository

# How it works

---

All actions in fastlane are written into *lanes*. Defining lanes are easy. Think of them as functions in any programming language of your choosing. You define all of your actions within that lane.

An example of a *lane* is as follows:

```ruby
lane :beta do
  increment_build_number
  build_app
  upload_to_testflight
end
```

So when it comes to executing that lane, all you do is run `fastlane beta` in your terminal.

# Installation & Setup

---

In this article, we will look at setting up a fastlane script to build, sign and deploy an iOS application to Testflight.

## Pre-requisites

As with most projects, you need to perform the initial project setup to support building your application, such as:

- Installing your project dependencies
- Install XCode and Android Studio
- Install Java SDK
- Setup git repository for Android and iOS certificates - *will be explained later in the article*

## fastlane folder structure

In ideal cases, you would have an Android application project and an iOS application project, both hosted in the same code repository as your project

```markdown
projectFolder/
	app/
	scripts/
	ios/
		fastlane/
			....
  android/
		fastlane/
			....
	package.json
	.gitignore
  ...
```

In this case, you will want to initialise the fastlane folder within the respective `android` and `ios` project sub-directories.

## Setup

1. Install fastlane 
    - via Homebrew (`brew install fastlane`)
    - via RubyGems (`sudo gem install fastlane`)
2. Navigate your terminal to your respective `android` and `ios` project directory and run `fastlane init`
3. You should have a folder structure that is similar to the one below:

```bash
fastlane/
	Fastfile
	Appfile
```

The most interesting file is `fastlane/Fastfile`, which contains all the information that is needed to distribute your app.

3. Inside your `Fastfile`, you can start writing lanes:

```ruby
lane :my_lane do
  # Whatever actions you like go in here.
end
```

4. You can start adding in actions into your lanes. fastlane actions can be found [here](https://docs.fastlane.tools/actions/). 

5. Copy the following file structure to your `Fastfile`

```ruby
default_platform(:ios)

def beta(arg1, arg2)
  # You may use Ruby functions to write custom actions for your app deployment
end

platform :ios do

	desc "Building the IPA file only"
	  lane :build_ios_app_ipa do
	    app_identifier = "com.appbundle.id"
	    beta("AppSchemeName", app_identifier)
	  end

end
```

5. `default_platform(:ios)` - Initialise your Fastfile file with a default platform

6. `platform :ios do` - Add all actions under a platform

From 5 to 6, it will inform fastlane that this particular Fastfile is purely for iOS operations. So instead of running the command `fastlane <lane_name>`, you will actually run `fastlane ios <lane_name>`. Therefore, anything parked under `platform :ios do` will be executed when lanes are invoked in your terminal.

If you are well-versed in Ruby, you may write your own Ruby functions to help you write custom actions that you require for further flexibility, especially when it comes to building several apps with different environments across your organisation. 

fastlane will identify them as an action regardless due to the fact that fastlane is written in *Ruby*.

**In this article, we will follow writing the actions using a Ruby function. This is so we can promote action re-usability across other lanes deployments.**

# Action Steps

---

Before we start writing our functionality in the lanes, we first list down our action steps:

1. **Setup API Key for App Store Connect (`app_store_connect_api_key`)** - This will allow fastlane to connect to your App Store to perform other actions that requires user authentication
2. **Setup CI (`setup_ci`)** - This will setup a temporary keychain to work on your CI pipeline of your choice
3. **Create and sync provisioning profiles and certificates (`match`)** - This will help us maintain our provisioning profiles and certificates across your teams
4. **Update code signing settings (`update_code_signing_settings`)** - This is to update the code signing identities to match your profile name and app bundle identifier
5. **Increment your app build number (`increment_build_number`)** - This will automate your application build number by retrieving the latest Testflight build number and incrementing the value
6. **Build the app (`build_app`)** - This will build the app for us and generate a binary (IPA) file
7. **Upload your binary to Testflight (`upload_to_testflight`)** - This will automate the process of uploading the binary file to Testflight and informing your testers accordingly

## Steps

### Step 1: Setup your App Store Connect API key

1. Visit the following [page](https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api). It will provide you a step-by-step process in generating an API key
2. Once you have generated a key, take note of:
    - Issuer ID
    - Key ID
3. Download the generated API key - A `.p8` file
4. Store it in a secure location in which you can easily access them. Avoid storing them in the same repository as anyone in your organisation will have access to the company's Apple account. 
    - In this article, we are storing them in another Git repository with limited read and write scopes to specific engineers within our organisation.

### Step 2: Setup the fastlane script

1. **Setup your App Store API Key to generate a hash used for JWT authorization**

```ruby
def setup_api_key()
  sh "if [ -d \"appstoreapi\" ]; then echo 'Folder exist, executing next step'; else git clone #{ENV['APPSTORE_API_GIT_URL']} appstoreapi; fi"
  app_store_connect_api_key(
    key_id: ENV['APPSTORE_KEY_ID'],
    issuer_id: ENV['APPSTORE_ISSUER_ID'],
    key_filepath: Dir.pwd + "/appstoreapi/AuthKey_xxx.p8",
  )
end
```

In the snippet above, I am cloning a Git repository which contains my App Store Connect API key, followed by utilising the `app_store_connect_api_key` action from fastlane. It takes in several parameters, however, there are 3 vital parameters:

- `key_id` - The Key ID from which you took note when you generated the API key
- `issuer_id` - The Issuer ID from which you took note when you generated the API key
- `key_filepath` - The file path to your `.p8` file

This step will generate a hash that will be used to authenticate the App Store using JWT.

I would highly recommend storing sensitive credentials or URLs and access them from an Environment Variable (`.env`) file in the same project folder - `fastlane/.env`

Once you have prepared the file, you can easily access any Environment Variable by simply passing in `ENV['ENV_NAME']` into the Fastfile.

**2. Define a Ruby function with 2 arguments**

```ruby
def beta(scheme, bundle_id)
end
```

Within this function, we specify the action steps we mentioned above

```ruby
def beta(scheme, bundle_id)
	setup_api_key() # Import the setup_api_key function
	setup_ci() # Uses fastlane action to create a temporary keychain access on a CI pipeline
end
```

**3. Utilise the `match` action from fastlane**

***[match](https://docs.fastlane.tools/actions/match/)*** is a fastlane action that allows you to easily sync your certificates and provisioning profiles. It takes a new approach to iOS and macOS code signing, where you share one code signing identity across your engineering team to simplify the setup and prevent code signing issues. The foundation of *match* was built using the implementation of [codesigning.guide concept](https://codesigning.guide/). 

You can store your code signing identities in:

- Git repository
- Google Cloud
- Amazon S3 bucket

In this article, we have chosen to store it a Git repository. However, with the case of storing in a Git repository, you would need to provide a form of basic authorization in order for match to access and clone your repository.

Whichever Git provider you choose (GitHub, Bitbucket, Gitlab or Azure DevOps), you would need to setup a Personal Access Token (PAT), which fastlane will use to clone repository and sync your code signing identities.

In your Fastfile, you will need to encode your PAT with a Base64 encryption

```ruby
authorization_token_str = ENV['GITHUB_TOKEN']
basic_authorization_token = Base64.strict_encode64(authorization_token_str)
```

The `basic_authorization_token` variable will be used in setting up the match implementation below.

```ruby
match(
  git_url: "<git_url>",
  git_basic_authorization: basic_authorization_token,
  readonly: true,
  type: "appstore",
  app_identifier: [
    bundle_id
])
```

From the snippet above, this is a very simple implementation. Take note on the `readonly` parameter. This is crucial in creating and syncing your profiles and certs with your Apple Developer account. If you set `readonly` to `false`, you will allow match to automatically sync and create new profiles and certs, should it deem your existing certs and profiles expired or corrupted. Once it has provisioned the profiles, you can set `readonly` to `true`. This is to avoid egde cases where it might accidentally create new certs and profiles.

**4. Update code signing identities**

This step is mainly used to update the Xcode settings on a CI pipeline due to its default behaviour of selecting a default cert and profile from the Mac OS agent.

```ruby
update_code_signing_settings(
  use_automatic_signing: false,
  path: "../ios/AppName.xcodeproj",
  code_sign_identity: "iPhone Distribution",
  profile_name: "match AppStore com.appbundle.id",
  bundle_identifier: "com.appbundle.id"
)
```

From the above snippet, you can retrieve your `profile_name` and `bundle_identifier` from your Apple Developer account or taking note of them from the match step once it has been executed.

**5. Increment Application build number**

We simply increment the number by accessing your latest testflight build number and incrementing the value with 1.

```ruby
increment_build_number({
  build_number: latest_testflight_build_number + 1
})

puts "BUILD_NUMBER: #{lane_context[SharedValues::BUILD_NUMBER]}"
```

The `puts` command will print out the Build number in the console, which is shared across the lane context.

You may have other app build versioning models than the one mentioned in this article. Increment the build number however you see fit to your project needs.

**6. Build the application**

This step will involve building your application to generate a binary (APK) file.

```ruby
build_app(
  workspace: "../ios/AppName.xcworkspace",
  export_xcargs: "-allowProvisioningUpdates",
  scheme: scheme,
  clean: true,
  silent: true,
  sdk: "iphoneos"
)
puts "IPA: #{lane_context[SharedValues::IPA_OUTPUT_PATH]}"
```

The `build_app` action is provided by fastlane.

The `puts` command will print out the file path location of the IPA file in which you can use to manually upload to Testflight.

**7.  Upload the binary file to Testflight**

This step will involve uploading your binary file to your Testflight account.

```ruby
app_identifier = "com.appbunde.id"
upload_to_testflight(app_identifier: app_identifier)
```

`upload_to_testflight` is an action provided by fastlane.

### **Putting them all together**

---

The final lane should look something like this:

```ruby
desc "Build and push a new build to TestFlight"
  lane :release_build do
    app_identifier = "com.appbundle.id"
    beta("AppSchemeName", app_identifier) # The custom function we wrote earlier
    upload_to_testflight(app_identifier: app_identifier)
  end
```

When you execute `fastlane ios release_build` in your terminal, it will operate based on the aforementioned steps above. 

As you can see, we utilised a Ruby function to group all common operations under the same roof so is to ensure:

- Code re-usability
- Consistency
- Clean code

# Integration with CI/CD tools

With the fastlane scripts, you can easily integrate it with any CI/CD tools of your choosing:

- GitHub Actions
- Gitlab CI
- Azure Devops
- Bitbucket Pipelines

When setting up your pre-requisite steps in your pipeline YAML file, you can simply include a bash script that executes your fastlane script

```ruby
cd ios/android folder

fastlane ios/android release_build
```

The command above will execute your fastlane script by following the aforementioned steps above.

# Conclusion

fastlane is a powerful tool that helps streamline your build process across your organisation. It can be used along side your existing CI/CD pipelines or as a standalone pipeline script to be used on your local machines or custom pipeline tools such as Buildkite. It would require additional steps such as, cloning the repository and checking out branches, however, all readily available from fastlane as actions.

Spend some time reading through the documentation as it contains [a lot of actions](https://docs.fastlane.tools/actions/) out of the box that will prove useful for your engineering teams ([match](https://docs.fastlane.tools/actions/match/), [pilot](https://docs.fastlane.tools/actions/pilot/), [cert](https://docs.fastlane.tools/actions/cert/), [sigh](https://docs.fastlane.tools/actions/sigh/), [appium](https://docs.fastlane.tools/actions/appium/), [xctool](https://docs.fastlane.tools/actions/xctool/), [supply](https://docs.fastlane.tools/actions/supply/), and [many more](https://docs.fastlane.tools/actions/)) . Furthermore, they provide an [extensive list of plugins](https://docs.fastlane.tools/plugins/available-plugins/) for third-party integrations such as Firebase, App Center, Yarn, Android Versioning, Dropbox, Slack Upload, S3, Bugsnag and many more. You can supercharge your fastlane scripts by coupling it with a CI/CD tool, such as GitHub Actions, Bitbucket Pipelines or Azure DevOps. The sky's the limit!