App developers always strive to improve the product they are working on. So it is definite that either(mostly) you will be adding new features to existing products or (luckily) you will be setting up a whole new project. Either way, onboarding new developers or bootstrapping an entirely new project is a pretty challenging task. If not executed carefully, technical debt arises and headaches for developers, managers, sales people, and most importantly, clients.
Git-flow: A quick overview
Git flow is reasonably used in Agile as well as conventional Waterfall-based projects as well.
Before we start our thought process about implementing Continous Integration and Continous Delivery, it is crucial to decide your own git branching model. It should resemble your development workflow to make use of CI/CD tools.
Git branching model
This article is focused on mobile applications. Therefore, we will use the following git branching model for which serves the purpose as an example.
Git flow is essentially a merge base solution. It does not rebase the branch. Also, keeping tag name format vX.Y.Z-beta-N
helps to generate a changelog using automated utilities.
Neither Github-flow nor Git-flow in its purest form can help us adopt a resilient development workflow. The modern trend of Github usually seems to be having at least two branches, develop
and master
where develop
branch is always under active development and master
are holding the latest tagged releases at any given time.
Git-flow(with GitHub-flow tweaks) will help us come to a concrete solution for CI and CD. Of course, depending on your team sizes, you may have a different flow, but that flow should also be resembling the above-mentioned Git Branching Model.
Implementing Continous Integration & Delivery
By definition, an automated process or practice employed by software developers collaborating on a single software project of integrating code changes on the shared repository. This automation process may be composed of steps like testing code, packaging, and delivering and deployment to production.
During product development, developers have often been requested to give a new IPA file or APK file to the QA team or the client for checking and testing the features. Extending your workflow with CI/CD for unit-testing, distributing your binary can significantly reduce your off-development burden.
Advantages of CI/CD
CI/CD combined with git-flow gives many benefits such as the following:
- Saves developer time by sending automated builds to testing teams
- Removes probability of inconsistency in builds(mainly caused due to local caching) by the guaranteed pristine build
- Takes team engagement to the next level by encouraging open communication and free information flow
- Reduces developer dependency by promoting knowledge sharing amongst team members
- Improves developer confidence when merging code
- Helps eliminate a class of bugs that might occur due to manual handling
There could be de-merits of employing CI in a small team or individual developer adding overheads. However, for developers working on multiple projects with many people, CI is a wise investment with very high returns in terms of time and money.
Fastlane
Fastlane is recommended to implement CI/CD. Historically it became a part of Fabric in 2015, later acquired by Google in 2017. However, depending on the team skills, you may implement the solution on some scripting languages like Shell Script
, Ruby
, Python
etc. The community of Fastlane is vibrant, and you will find a lot of open-source plugins for your business use case.
We have chosen Fastlane tools for implementing our CI/CD Solutions. It does require some knowledge of Ruby however, you don't need to be an expert to work with it since it is implemented using Ruby-similar to widely used iOS dependency manager CocoaPods.
(+1 to Gradle
for its built-in dependency management function for Android)
Articles on how to get started with Fastlane can be found here. There is reasonably good documentation for both iOS and Android. However, we had difficulties finding some details related to Android Fastlane actions though we could search on forums like StackOverflow
.
Modeling Git Branching Model to Fastlane
Usually, each major branches in our Git Branching Model correspond to an environment as shown in the table below:
Environment | Alias/Tag | Branch | Description |
---|---|---|---|
Development | alpha | develop | All the features for next or distant releases are usually developed in this environment |
Staging | beta | release/ or hotfix/ | Pre-release environment |
Production | prod | master | Build made on master always uses this environment |
More complex flows may have additional layers of environment separation. For example, we can categorize our environment in the following as a lane
(a.k.a Ruby function):
# Fastfile
ALIAS_MAP = {
'alpha' => 'Develop',
'beta' => 'Staging',
'prod'=> 'Production'
}
...
desc 'Build alpha IPA'
lane :alpha do |options|
build_app # This will be replaced with custom `build_deploy` private lane later
end
desc 'Build beta IPA'
lane :beta do |options|
build_app # This will be replaced with custom `build_deploy` private lane later
end
desc 'Build production ipa'
lane :prod do |options|
build_app # This will be replaced with custom `build_deploy` private lane later
end
...
ℹ️We may use lane options
parameters to pass command line arguments from CI YAML file.
You can put some configuration of build_app
action into Gymfile
per lane
basis as shown below:
# Gymfile
for_platform :ios do
include_bitcode true
include_symbols true
for_lane :alpha do
scheme '<YOUR_DEV_SCHEME>'
export_method 'development' # or 'enterprise' for in-house testing
end
for_lane :beta do
scheme '<YOUR_STAGING_SCHEME>'
export_method 'ad-hoc'
end
for_lane :prod do
scheme '<YOUR_PRODUCTION_SCHEME>'
export_method 'app-store' # or 'enterprise' for release
end
end
For available configuration options for Gymfile
follow the link here.
After putting above code from Listing 2 in a Gymfile
, you don't need to pass relavant settings like scheme
and export_method
into build_app
action like below:
# You don't need to set parameters marked with 👈
# since it is handled in Gym file
build_app(
scheme: "Release", # 👈
export_options: { # 👈
method: "app-store" # 👈
}
)
ℹ️Note: We will use xcconfig to set bundle id
and provisioning profile
unlike Listing 3
Firebase App Distribution
To distribute the built IPA binary to Firebase, Reading this document on how to setup Firebase App Distribution is highly recommended.
In Listing 1, we used build_app
action to trigger the build. We can move that action into a private_lane
called build_deploy
and save duplicate code into these three main lanes. (Check Listing 7 at the end for further clarification)
desc 'Build and deploy ipa'
private_lane :build_deploy do |options|
#1. Check if any new commits since last release
is_releasable = analyze_commits(match: "*#{options[:environment]}*")
unless is_releasable
UI.important "No changes since last one hence skipping build"
next
end
#2. Increment build number
increment_build_number(
build_number: lane_context[SharedValues::RELEASE_NEXT_VERSION] # set a specific number
)
#3. If you can use `match`, you use `match`.
setup_provisioning_profiles
#4. Build deploy
build_app
deploy_app options
end
Step 1. Check if any new commits since the last release
analyze_commits
is a third-party action for semantic release but quite helpful if you follow conventional commits. It lets us check if there has been any change since the last release. If there is then we go further otherwise stop with a message-"No changes since the last one hence skipping build". This will help us save some build minutes on the CI machine.
Step 2. Increment build number
We can keep the Marketing Version and Internal Build Version separate. For example, if the Xcode project has AGV tooling enabled, we can use increment_build_number
, which will automatically change the build number in the target.
Step 3. Setup Provisioning Profiles
We can use the Fastlane match
command here. In case if we don't have access, we may need to install it manually using import_certificate
first and then performing FastlaneCore::ProvisioningProfile.install
.
Step 4. Build and deploy
We use build_app
action as we used in Listing 1. After the IPA file is ready we have to send it to Firebase App Distribution using deploy_app
a private lane.
Deploying
We deploy to Firebase for alpha
and beta
only. For prod
upload either we manually upload to App Store Connect or automate it using upload_to_testflight
action. We will limit our discussion to upload prod
IPA as an asset on Github Release only.
The deploy_app
lane is also self-explanatory as shown in the listing below. We can divide the process in 5 steps:
private_lane :deploy_app do |options|
environment = options[:environment]
next if environment == 'prod' # Since `prod` is uploaded to testflight and app store
#1. Generate Change Log
notes = conventional_changelog(title: "Change Log", format: "plain")
#2. Get Google App ID
gsp_path = "SupportingFiles/GoogleService/#{ALIAS_MAP[environment]}-Info.plist"
google_app_id = get_info_plist_value(path: gsp_path, key: 'GOOGLE_APP_ID')
#3. Upload to firebase
firebase_app_distribution(
app: google_app_id,
# groups: tester_group,
release_notes: notes,
firebase_cli_path: FIREBASE_CLI_PATH
)
#4. Upload dSYM files to crashlytics
upload_symbols_to_crashlytics(gsp_path: gsp_path)
clean_build_artifacts
end
Step 1. Generating Change Log:
We are using the semantic-version
plug-in of Fastlane to generate these logs. conventional_changelog
has to be used in conjunction with analyze_commits
(which we have used in build_deploy
lane to check is_releasable
). analyze_commits
takes match
argument-a regex for matching with the previous git-tag like v1.0.1-beta-5
. This helps to generate logs for only between the last tag and current v1.0.1-beta-6
.
Step 2. Get Google App ID
We need google_app_id
from the relevant GoogleService-Info.plist. This Plist is generated on Firebase. Our sample code project is a multi-configuration single target Xcode project. Three GoogleService plists are renamed and moved to GoogleService
folder:
- GoogleService/Develop-Info.plist
- GoogleService/Staging-Info.plist
- GoogleService/Production-Info.plist
We get gsp_path
first and from that we get google_app_id
.
Step 3. Upload to firebase
To use this plugin we have to install firebase CLI as well. On CircleCI within setup
command we have installed npm package for firebase-cli
. firebase_app_distribution
is nothing but a wrapper to use the CLI to upload to Firebase. We need to give firebase_cli_path
so that the appropriate binary is used.
Step 4. Upload dSYM files to crashlytics
Finally we upload dSYM
files to Firebase. This will help Firebase make crash reports de-symbolized and readable.
Note:
When bitcode is enabled in an Xcode project, App Store recompiles the code and provides us with dSYM files. These files need to be downloaded and uploaded to Firebase Crashlytics for crash report de-symbolization. Therefore, for the production version only we need to perform this step. checkout download_dsym
action for Fastlane.
Github Release
release_on_github
is a private lane and helps us automatically tag the commit, add release notes and attach IPA file for production, which can be later uploaded to App Store Connect.
Since Firebase doesn't have APIs to download the IPA file except from installing it on device only-in case where you may want to give beta
releases to the client using other distribution like deploygate
-You may want to keep the IPA as a pre-release in Github release history.
Uploading IPA as an asset
desc "Release on github"
private_lane :release_on_github do |options|
environment = options[:environment]
#1. Generate Change Log
notes = conventional_changelog(title: "Change Log")
#2. Get Version and Build Number
version_number = get_version_number
build_number = get_build_number
#3. Set Github Release
is_prerelease = environment == 'beta' ? true : false
name = "[#{ALIAS_MAP[environment]}] v#{version_number} Build: #{build_number}}"
set_github_release(
repository_name: "#{ENV['CIRCLE_PROJECT_USERNAME']}/#{ENV['CIRCLE_PROJECT_REPONAME']}",
name: name,
commitish: ENV['CIRCLE_SHA1'],
description: notes,
tag_name: "v#{version_number}-#{options[:environment]}-#{build_number}",
is_prerelease: is_prerelease,
upload_assets: [lane_context[SharedValues::IPA_OUTPUT_PATH]]
)
end
Step 1. Generate Change Log
conventional_changelog
generates by default mark-down style notes. This is handy when preparing auto-release notes.
Step 2. Get Version and Build Number
We use these to set the title of the release. AGV tooling should be enabled in the XCode project to use get_version_number
and get_build_number
.
Step 3. Set Github Release
We check if its beta then marks it as Pre-Release. Based on the environment we format the name/ title for the release notes. Set the tag name in a format like v1.0.1-beta-1234
and upload the built IPA file as an asset to the release.
Note: You will need to set your personal token or ci bot token to GITHUB_API_TOKEN
in the environment. This token should have permission to create a tag.
Revising Main Lanes in Listing 1
We will skip the alpha
releases on Github because they are quite frequent in releases and stored on the Firebase. Hence, you can save some space on Github by not tagging them and causing it hard to navigate release history on Github.
# Fastfile
ALIAS_MAP = {
'alpha' => 'Develop',
'beta' => 'Staging',
'prod'=> 'Production'
}
...
desc 'Build alpha IPA'
lane :alpha do |options|
build_deploy options
# Not releasing to Github since Firebase App Distribution
end
desc 'Build beta ipa'
lane :beta do |options|
build_deploy options
release_on_github options
end
desc 'Build production ipa'
lane :prod do |options|
build_deploy options
release_on_github options
end
...
Matching up CircleCI configuration with Fastlane
CircleCI uses YAML
files which should be simple. When a pull request is merged or code is pushed to some branch, it may trigger a CircleCI workflow given a properly set config.yml
which in turn fires a specific lane on Fastlane.
Please carefully note that we have three environment arguments called alpha
, beta
and prod
which you can refer in Table 2 in the alias column. Have a look at snippet of yaml file below:
---
jobs:
deploy:
executor:
name: default
parameters:
build_type:
type: enum
enum: ["alpha", "beta", "prod"] # Corresponds to lanes
default: alpha
steps:
- attach_workspace:
at: /Users/distiller/project
- run:
name: Build ipa
command: bundle exec fastlane ios << parameters.build_type >>
- store_artifacts:
path: output
when: on_success
Following is also snippet of the same yaml file. Here setup
installs required dependency like rubygems, npm modules, cocoapod, firebase cli, carthage etc.
---
workflows:
main:
jobs:
- setup
- test:
requires:
- setup
- deploy:
name: build_deploy_alpha
build_type: alpha
requires:
- setup
filters:
branches:
only:
- develop # RegEx
- deploy:
name: build_deploy_beta
build_type: beta
requires:
- setup
filters:
branches:
only:
- /release\/.*/ # RegEx
- /hotfix\/.*/ # RegEx
- deploy:
name: build_deploy_prod
build_type: prod
requires:
- setup
filters:
branches:
only:
- master # RegEx
Pay special attention to the filters of the each workflow as well. They are containing Regular Expressions like develop
, /release\/.*/
, /hotfix\/.*/
,master
.
These filters make sure that CI build is triggered only for those branches which match above regex.alpha
builds are triggered when code is pushed to develop
, beta
for release/*
and prod
for master
.
Push On Branch | Lane executed | Environment |
---|---|---|
develop | bundle exec fastlane ios alpha | Development |
release/* & hotfix/* | bundle exec fastlane ios beta | Staging |
master | bundle exec fastlane ios prod | Production |
The only purpose of showing the above snippet is to show you how the git branching model is applied to the CircleCI YAML file configuration and the Fastlane configuration file. You can learn more about CircleCI Yaml here.
Conclusion
Effective development workflow and CI-CD implementation can help reduce tons of developer hours. Similarly, the QA team will be able to link the bugs and issues to a particular build and can have a more productive conversation with developers. Especially, ramping up time for the new developer also reduces significantly.
Resources
Sample Code Fastlane Tools Docs Firebase App Distribution Git Branching Model by Vincent Driessen AGV tooling enabled
Article Photo by Rasa Kasparaviciene