Continuous Delivery of iOS applications using Fastlane

Sumit Kumar
11 min readJan 31, 2022

--

Mobile development requires a process in place to deliver code changes more frequently, reliably, and automatically.
That’s where Fastlane comes in to facilitate Continuous Delivery (CD) by automating the compile and deploy process for apps and it’s free to use.

When we come across word (CD), we hear it as Continuous Delivery or sometime as Continuous Deployment. At first we often think these as same, though they are not.

Continuous Delivery (CD) is the process of automating the build using some external tools/software, which reduces manual developer effort to create the build so you can share the build reliably in no time. Deployment to production happens manually based on the business decision when to do so.

Continuous Deployment (CD) goes one step further than continuous delivery. Here every change is deployed to production continuously and takes it directly to your clients.

The following diagram depicts the difference between Continuous Delivery and Continuous Deployment:

This article will serve as a step by step guide on Fastlane setup and usage, aligned with Continuous Delivery.
*Next part in this series will guide on CI/CD establishment and usage.

Introduction

Fastlane is a powerful tool used for automated deployment to simplify deployments for mobile platforms. It is simple and easy to use but brings amazing value to your regular ios deployment workflows.

Allows automating every aspect of the build packaging and distribution process and it's an Open-source published under MIT License.

Supports all major CI Platforms like Bitrise, Jenkins, CircleCI, Travis, GitLab CI, Azure DevOps, etc.
Fastlane is a ruby gem, hence runs on the local machine, no server needed https://rubygems.org/gems/fastlane

Setup

Installing RVM & Ruby

If you use macOS, system Ruby is not recommended. Fastlane supports Ruby versions 2.5 or newer. Use a tool to manage ruby versions, here will be using RVM to manage.

# Verify which Ruby version you’re using
$ ruby --version
# Install RVM stable with ruby
\curl -sSL https://get.rvm.io | bash -s stable --ruby
# List the versions of Ruby available to RVM
rvm list
# Use RVM managed ruby, not the system Ruby. You can select a version of ruby by using
rvm use 2.6.3
# Verify which RVM managed version ruby is in use
which ruby

Installing Fastlane

# Install the latest Xcode command-line tools:
xcode-select --install
# Using RubyGems
sudo gem install fastlane -NV
# Alternatively using Homebrew
brew install fastlane
# Verify fastlane version
fastlane -v

Initialize Fastlane tool with Project

Navigate to your iOS project directory and run the below command

# Setting up fastlane
fastlane init

This would let you choose setup options and accordingly a folder naming fastlane will be created, containing Fastfile, Appfile, Gemfile etc.

Fastfile

— Located at ./fastlane/Fastfile
This is the most interesting file, serves as a master file that will contain all the steps and actions you would perform for the app deployments.
— This file is auto-generated on setting up Fastlane for the project.

Appfile

— Located at ./fastlane/Appfile
This file serves as a credential manager, will contain the keys/secrets which can be accessed as variables in the fastfile.
— This file is auto-generated on setting up Fastlane for the project.

Gemfile

— This is to define your dependency on fastlane, plugins.
— Located at ./Gemfile

Getting Started With Fastlane

At this point, Fastlane has created all the required files and structure. Now you are ready to start configuring the steps to automate.

Fastlane operates around Fastfile, where you set the actions you want to be performed when building your app.
These actions are organized into “lanes”. For example, you would configure a lane for build and another for running test cases etc.

fastfile Life Cycle :

Lanes could be constructed for the following actions.

  1. Incrementing the build number
  2. Commit and tag the bumped version in Git
  3. Run Unit tests
  4. Run Lint check
  5. Compile and build the project
  6. Upload .ipa to distribution channel
  7. Publish build status on Microsoft Teams/Slack
  8. Push the file/version changes to remote

These are steps we do in order to deploy builds on any distribution channel(Ex: Testflight). Fastlane will handle all of this through an automated script. To use Fastlane you simply put together a fastfile that is used to drive the deployment process.

You can think of lanes as functions that group related tasks. You can even call lanes methods from another one, to further decouple and reuse your lanes.

Defining lanes is easy

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

lane :my_lane do |options|
puts options #Options passed by user, |options|-> like function params
end

Make as many lanes as you like!

Fastlane Plugins
Fastlane has a plugin system that allows you and your company to provide Fastlane plugins to other Fastlane users or vice-versa.

In order to add any external plugins to your project, execute below command
fastlane add_plugin [name]
List of available plugins: https://docs.fastlane.tools/plugins/available-plugins/

For example — In order to send notification in teams you need to add teams plugin, as in teams doesn't have a defined Fastlane action.

This will create a Pluginfile inside ./fastlane/Pluginfile to maintain plugins dependencies and ensure to check in this file to source control.
Usage: fastlane add_plugin teams
https://github.com/mbogh/fastlane-plugin-teams

Stepping through Sample Fastfile & Appfile

The Appfile stores useful information that is used across all fastlane tools like your Apple ID or the application Bundle Identifier, to deploy your lanes faster and tailored to your project needs.

# Apple Configuration
app_identifier("com.test.fastlanesample") # The bundle identifier of your app
apple_id("fastlanesample@gmail.com") # Your Apple email address
ENV["FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD"] = "test-fast-lane-sample"itc_team_id("12000000") # iTunes Connect Team ID
team_id("SFA163X25Z") # Developer Portal Team ID
# Override timeout to increase to custom value
ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "180"
ENV["FASTLANE_XCODE_LIST_TIMEOUT"] = "180"
# For more information about the Appfile, see:
# https://docs.fastlane.tools/advanced/#appfile

1. Define global constants to be used across fastfile.
This would let us generalize the template required to deploy any iOS apps.

# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
default_platform(:ios)skip_docs# iOS build - Fill below configuration input as per your project need and replace dummy inputs with actual one.
# For more information on build settings , see:
# https://docs.fastlane.tools/actions/build_app/#build_app
app_name = "FastlaneSample"
scheme = "FastlaneSample-Test"
project = "FastlaneSample.xcodeproj"
workspace = "FastlaneSample.xcworkspace"
target = "FastlaneSample"
bundle_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)
# The configuration to use when building the app. Defaults to 'Release'
build_config = "Development"
clean_build = "false"
# Method used to export the archive.
# Valid values are: app-store, ad-hoc, package, enterprise, development, developer-id
export_method = "app-store"
# Code signing identities
codesign_certificate = "Apple Distribution: Fastlane Sample (SMA123X45Z)"
provisioning_profile = "fastlanesampleappstore"
# Unit test case
test_scheme = "FastlaneSample-Dev"
# Upload/Distribution
distribution = "testflight"
groups = "UAT Test Team"
# For firebase/testfairy/appcenter etc
api_key = "DISTRIBUTION_API_TOKEN"
# Push changes
remote_branch = "SK/feature/CICD_Integration"
release_notes = "FastlaneSample v1.0.0 Release\nTest build from automated CI-CD"
# Publish build
channel = "teams"
webhook_url = "https://fastlanesample.webhook.office.com/webhookb2/a7a0ghu182w77378363/63g7732/"
# Output build path
output_directory_path = ENV["PWD"] + "/build/"
# Project directory
project_directory = ENV["PWD"] + "/reports/"
# Output build name
output_build_name = ""

The required input parameters can be hardcoded here, but it is preferable to read them from environment variables due to:

  • Security — anything that is sensitive, should be retrieved from Secrets.
  • Usability — by setting some parameters dynamically we can use the same code for multiple targets

2. Lane for executing the Unit Test Cases

desc "Runs all the Unit Tests"
lane :tests do
# Publish Mac Notification
notification(subtitle: "Lane Executing Test Cases", message: "Test Cases!")
# Check if reports folder exists
# This step is optional - if you want to keep all reports like test cases, coverage, docs etc under a specific folder.
sh(" if [ ! -d #{project_directory} ]
then
mkdir #{project_directory}
fi ")
# Run tests
run_tests(clean: true,
fail_build: false,
scheme: test_scheme,
code_coverage: true,
device: "iPhone 11",
output_types: "html,junit",
output_directory: "reports/")
end

3. Lane for executing the SwiftLint Check

desc "Does a static analysis of the project. Configure the options in .swiftlint.yml"
lane :lint do
puts "Linting..."
swiftlint(
mode: :lint,
executable: "Pods/SwiftLint/swiftlint",
output_file: "reports/swiftlint.xml",
reporter: "checkstyle",
config_file: ".swiftlint.yml",
ignore_exit_status: true
)
end

4. Lane for incrementing the build number, commit the version bump and start building the app

To support automatic build numbers increment, add the below changes in your Xcode Project Workspace.
1. Go to Build Settings
2. Under Versioning, set versioning system to Apple Generic
3. Set Current Project Version to a version value you want to start
For Example — Current Project Version = 1

desc "This lane initiates the build process. Takes argument(options) as app_build_name"
lane :build do |options|
puts "build process started..."
# Options passed by user
puts options
# Ensure that the git status is clean and no megre head or un-commited code exists
ensure_git_status_clean
# Increment build number
updated_build_num = increment_build_number(xcodeproj: project)
# Commit the version bump
commit_version_bump(xcodeproj: project)
# Compile bitcode is hardcoded here, if required can be made configurable by moving to global variables list
compile_bitcode = false
puts "compile_bitcode: #{compile_bitcode}"

args = { scheme: scheme,
clean: clean_build,
export_method: export_method,
configuration: build_config,
include_bitcode: false,
export_options: ({
compileBitcode: compile_bitcode,
provisioningProfiles: {
bundle_identifier => provisioning_profile
}
}),
codesigning_identity: codesign_certificate,
output_directory: output_directory_path,
output_name: options[:app_build_name],
}
if workspace
args[:workspace] = workspace
else
args[:project] = project
end
# Fastlane build action to trigger build with required args
build_app(args)

## Uncomment below lanes if you like to execute all with one command in the sequence defined below
# Say we run "fastlane build app_build_name:FastlaneSample_Test_v1"
# Execution Sequence: RunTest->Linting->Code->Build->Upload/Publish
# tests
# lint
# build_app(args)
# upload
end

5. Lane for uploading the build.
Which distribution channel to upload is driven by the variable defined above as “distribution”.

desc "This lane will upload build to the respective distribution opted for"
lane :upload do |options|
output_build_name = options[:app_build_name]

#Release notes - If empty will show last git commits in the notes
if release_notes.empty?
# Fetch changelog from Git repo
release_notes = changelog_from_git_commits(
commits_count: "10",
pretty: "- (%ae) %s",
date_format: "short",
match_lightweight_tag: false,
merge_commit_filtering: "exclude_merges")
end
puts "APP DISTRIBUTION : #{distribution}"
# Publish to the channel
publish(build_name: output_build_name, version_num: get_version_number(target: target, configuration: build_config))
# Select upload option as per user selection
case distribution
when 'testfairy'
# Push build on to TestFairy
testfairy(api_key: api_key,
ipa: output_directory_path + output_build_name + ".ipa",
symbols_file: output_directory_path + output_build_name + ".app.dSYM.zip",
comment: release_notes,
testers_groups: groups,
notify: "on")
when 'testflight'
# Push build on to TestFlight
upload_to_testflight(
username: CredentialsManager::AppfileConfig.try_fetch_value(:apple_id),
app_identifier: bundle_identifier,
ipa: lane_context[SharedValues::IPA_OUTPUT_PATH],
skip_submission: true,
skip_waiting_for_build_processing: true,
groups: groups,
changelog: release_notes)
when 'appcenter'
desc "This lane will upload build on AppCenter"
appcenter_upload(
api_token: api_key,
owner_name: "FastlaneSampleOwner",
app_name: app_name,
file: lane_context[SharedValues::IPA_OUTPUT_PATH],
dsym: lane_context[SharedValues::DSYM_OUTPUT_PATH],
release_notes: release_notes,
notify_testers: true
)
when 'firebase'
firebase_app_distribution(
app: api_key,
ipa_path: output_directory_path + output_build_name + ".ipa",
groups: groups,
release_notes: release_notes
)
else
# No channel opted for notifing build release
puts "No distribution opted to upload app"
end

6. Lane to publish a notification to Teams/Slack

desc "This lane will publish build information to respective channels "
lane :publish do |options|
# Push the commit to remote branch
current_time = Time.new.strftime('%Y.%m.%d_%H.%M')
# Facts for the build that need to be published on Teams channel
build_details = [
{
"name"=>"Build Name",
"value"=>options[:build_name]
},
{
"name"=>"Version Number",
"value"=>options[:version_num]
},
{
"name"=>"Published Date / Time",
"value"=>current_time
}
]
puts "CHANNEL : #{channel}"
case channel
when 'teams'
puts "Notifying in Teams.."
# Publish build available notification on iOS Team channel
teams(title: "New Build Available",
message: app_name + " App successfully released!! Please download it on device to start testing.",
facts: build_details,
teams_url: webhook_url)
when 'slack'
puts "Notifying in Slack.."
slack(message: app_name + " App successfully released!! Please download it on device to start testing.",
slack_url: webhook_url,
fields: build_details)
else
# No channel opted for notifing build release
puts "No channel specified"
end
end

end

7. Lane to push the file/version changes to remote

desc "This lane will commit version bump and push changes to remote "
lane :push_to_remote do
# Push the commit to remote branch
# simple version. pushes "master" branch to "origin" remote
push_to_git_remote(remote: "origin",
local_branch: "HEAD",
remote_branch: remote_branch,
force: true,
tags: true)
end

That’s it!

Now you can run the Fastlane command and sit back relax ; )
Fastlane will do all the deployment jobs for you.

fastlane build app_build_name:FastlaneSample_Test_v1

You can refer to the sample project with integrated workflow.

Challenges

— Certificate/Provisioning Issue

In order to build an app for the app store ensure that its building using the right provisioning profile. The machine you are building with must have the right distribution certificate and provisioning profiles installed in its keychain.
- A Distribution Certificate (.cer) and private key (.p12)
- A Provisioning Profile for Appstore distribution tied to app bundle ID and cert

— Two-step verification for Testflight/AppStore

As per 2-step verification on appstore, submitting testflight build or AppStore release you will be prompted to manually confirm and enter 6-bit code in the workflow.
To fix this you can use the below solutions provided by Fastlane.

  • Creating and storing a temporary Apple login session in the FASTLANE SESSION variable. Fastlane’s spaceauth functionality can be used to accomplish this.
    It is, however, limited not just in terms of duration (anything between 1 day and 1 month), but also in terms of geography (cannot be used in a different region from where it is generated).
  • On the Apple ID portal, you can create passwords for specific Apple applications. The environment variable makes these available to Fastlane. FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD.
    However, since they are only good for uploading binaries to TestFlight, Fastlane still needs to log in and store the session in order to provide the metadata required by the App Store for a release. e.g: updating any metadata like setting release notes or distributing to testers, etc.

The following environment variables will need to be present in our workflow, so we should store their values as repo Secrets:

  • FASTLANE_USER : Apple ID used for submission
  • FASTLANE_PASSWORD : Password for this ID
  • FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: Application-specific password
  • FASTLANE_ITC_TEAM_ID : iTunes Connect Team ID to submit under

More details: https://docs.fastlane.tools/getting-started/ios/authentication/

Conclusion

I hope this guide helps you save some of your deployments efforts and remember to follow the updates from Fastlane.

Fastlane is a powerful tool used for automated deployment that is simple and easy to use. But, it brings amazing value to your regular ios deployment workflows. More details: Fastlane official website

Where to go from here?

Next up, will learn to how to blend CI system to accomplish CI & CD for iOS. Follow this article.

https://medium.com/@sumit16.kumar/ci-cd-using-fastlane-jenkins-ios-69c86ae0cdcc

Thanks for reading the article, hope this helps!!!

— — —— Happy Deploying!!— — — —

Any further queries/suggestions please leave a comment below or reach out to me on sumit16.kumar@gmail.com

--

--

Sumit Kumar

Mobile Digital Solution Expert | Devops | ML/AI Enthusiast