Auto-deploy iOS app to TestFlight using Gitlab CI and Fastlane
8 December 2020
Sharing is caring!
What is CI/CD?
CI stands for Continuous Integration – is the DevOps practice where developers in a team push code chunks to your application’s code base hosted in a Git repository. For every push, it runs a pipeline of scripts to build, test, and validate the code changes before merging them into the main branch.
Continuous Delivery – is the step after CI for delivering improvements to software code and user environments with the help of automated tools such as Fastlane. The key outcome of the continuous delivery (CD) paradigm is code that is always in a deployable state.
CI and CD together allow us to catch bugs and errors early in the development cycle, ensuring that all the code deployed to production complies with the code standards you established for your app.
End Goal
To auto build and deploy the iOS app to TestFlight with increased build number when commit(s) are pushed to a specific branch.
How does it work?
We first configure Gitlab CI to our project on our working machine which is in sync with the repo on Gitlab. It’s recommended to have one such separate machine for all of your project’s CI/CD operations. The machine can be called a CI machine, which should have high processing speed and good memory for better operability. After configuring CI for our project, we will have .gitlab-ci.yml file which defines the structure and order of the pipelines, and determines what to execute and what decisions to make when a particular condition is encountered.
Next, we will configure Fastlane for our project for automating the delivery to TestFlight. After successful setup we will be having Fastfile or Fastfile.swift where all of our build and other logic will be defined, including code signing part.
Finally, when code is pushed, the Gitlab server triggers the pipeline and runner will run the script defined in .gitlab-ci.yml file on CI machine.
Pre-requisite
Well running macOS computer for the purpose of CI machine
XCode
App Store account with App added with bundle identifier
Certificate & Provisioning Profiles need be installed on your system
xcodebuild (command line tool for Xcode)
Bundler
For installing bundler, go to root in terminal and run:
[sudo] gem install bundler:2.1.4
Step 1: Register the Gitlab Runner from CI machine
Go to Gitlab and open your project. From the left panel go to Settings > CI/CD > Runners. Expand the Runners section. There you will see two kinds of runners: Specific and Shared. Description of both is also given. We just have to setup runner manually:
Just copy the registration token and follow the commands given below for installing Gitlab Runner into your system.
Navigate to your project, and register a runner with gitlab server
gitlab-runner register
While registering, it will ask you for the gitlab-ci coordinator URL and registration token that we copied previously, along with the other stuff.
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/):
https://gitlab.com/
Please enter the gitlab-ci token for this runner:
YOUR_REGISTRATION_TOKEN
Please enter the gitlab-ci description for this runner:
Deployment
Please enter the gitlab-ci tags for this runner (comma separated):
build, testflight
Registering runner... succeeded runner=dWJsfKg2
Please enter the executor: docker, parallels, shell, ssh, virtualbox, docker+machine, custom, docker-ssh, docker-ssh+machine, kubernetes:
shell
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!
To cross check, go to Gitlab, open your project > Settings > CI/CD > Expand Runners. You will see:
Step 2: Install and Setup Fastlane on the CI Machine
Open the terminal and install Fastlane using RubyGems or HomeBrew
sudo gem install fastlane -NV
OR
brew install fastlane
Setup Fastlane for your iOS project
cd ~/path/to/your/project
fastlane init swift
After setup, there will be 4 options from which we have to choose one:
1. 📸 Automate screenshots
2. 👩✈️ Automate beta distribution to TestFlight
3. 🚀 Automate App Store distribution
4. 🛠 Manual setup - manually setup your project to automate your tasks
Select option 4 – Manual setup. It will complete the setup of Fastlane for your project.
Now, you can find your Fastfile.swift at ProjectDirectory > fastlane > Fastfile.swift. Just open that Fastfile and paste the script as follows:
import Foundation
class Fastfile: LaneFile
{
func myCustomLane()
{
desc("DEPLOY TO TESTFLIGHT")
let username = "******@gmail.com"
let appIdentifier = "com.bcs.SwiftUI-Demo"
//To see the itune connect team id for all teams, run:
//fastlane deliver -u <account_user_name>
let itcTeamId = "1184*****"
//get version and build number
let xcodeproj = "SwiftUI-Demo.xcodeproj"
let versionNumber = getVersionNumber(xcodeproj: xcodeproj)
latestTestflightBuildNumber(
appIdentifier: appIdentifier,
username: username,
version: versionNumber,
teamId: itcTeamId
)
let fastlaneContext = laneContext()
//Incrementing build number
let currentBuildNumber = fastlaneContext["LATEST_TESTFLIGHT_BUILD_NUMBER"] as! Int
let newBuildNumber: Int = currentBuildNumber + 1
incrementBuildNumber(
buildNumber: String(newBuildNumber),
xcodeproj: xcodeproj
)
//Build
let scheme = "SwiftUI-Demo"
buildIosApp(scheme: scheme)
// Upload to TestFlight
let appleId = "15211*****"
let devPortalTeamId = "K4PXVYDM96"
uploadToTestflight(
username: username,
appIdentifier: appIdentifier,
appleId: appleId,
teamId: itcTeamId,
devPortalTeamId: devPortalTeamId
)
}
}
(Here, SwiftUI-Demo is my project name/scheme. Feel free to replace all such values above with yours one.)
In order to run the script above, you can run:
fastlane myCustomLane
In case you want to update dependency defined in Gemfile, just run
[sudo] bundle update
Then every time we run Fastlane we use:
bundle exec fastlane myCustomLane
To update Fastlane, just run:
[sudo] bundle update fastlane
Important: For fastlane to deploy app, we need to obtain app specific password and set it as FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD in your shell profile:
Click here to know more about getting app specific password.
Step 3: Configure .gitlab-ci.yml file
This is the final and important step. Remember our goal, we have to trigger Fastlane script in order to build, increment build number and deploy the app to TestFlight when push is made on particular branch. Let’s say we have created one dedicated branch for this named “testflight-deploy-branch”. We have to write one more script here in .gitlab-ci.yml file to achieve this goal.
Create and open .gitlab-ci.yml file. Navigate to project directory in terminal and run:
Push all your changes to testflight-deploy-branch on gitlab. It will trigger the job in the pipeline and run fastlane script on CI machine. Pushing your code to any other branch will not trigger any job. You can see the whole result on gitlab.
It’s not necessarily be successful at first attempt, because there might be some missing stuff like app specific password, config for CocoaPods, etc. But It will trigger the Fastlane script on CI machines for every push to the specified branch in future. You can see if the job is succeeded or not on gitlab itself under Pipelines section:
Adding CocoaPods Support
Cocoapods is a widely used used dependency manager for iOS Projects. So if you’re using cocoapods like us, you need to modify some lines in your Gemfile, Fastfile.swift, and .gitlab-ci.yml files.
Add cocoapods in Gemfile
// In Gemfile
gem "cocoapods"
You may require to run “[sudo] bundle update” after adding this as mentioned earlier.
Go ahead and modify your Fastfile.swift. Just add the following lines just above //Build comment:
//for pod install
cocoapods(
cleanInstall: true,
podfile: "./Podfile"
)
As adding cocoapods require us to use .xcworkspace instead of .xcodeproj, we need to change the build section as well in Fastfile.swift to include workspace:
//Build
let scheme = "SwiftUI-Demo"
let workspace = "SwiftUI-Demo.xcworkspace"
buildIosApp(workspace: workspace, scheme: scheme)
Finally, change .gitlab-ci.yml file to let xcodebuild command build workspace instead of project:
import Foundation
class Fastfile: LaneFile
{
func myCustomLane()
{
desc("DEPLOY TO TESTFLIGHT")
let username = "******@gmail.com"
let appIdentifier = "com.bcs.SwiftUI-Demo"
//To see the itune connect team id for all teams, run:
//fastlane deliver -u <account_user_name>
let itcTeamId = "1184*****"
//get version and build number
let xcodeproj = "SwiftUI-Demo.xcodeproj"
let versionNumber = getVersionNumber(xcodeproj: xcodeproj)
latestTestflightBuildNumber(
appIdentifier: appIdentifier,
username: username,
version: versionNumber,
teamId: itcTeamId
)
let fastlaneContext = laneContext()
//Incrementing build number
let currentBuildNumber = fastlaneContext["LATEST_TESTFLIGHT_BUILD_NUMBER"] as! Int
let newBuildNumber: Int = currentBuildNumber + 1
incrementBuildNumber(
buildNumber: String(newBuildNumber),
xcodeproj: xcodeproj
)
//for pod install
cocoapods(
cleanInstall: true,
podfile: "./Podfile"
)
//Build
let scheme = "SwiftUI-Demo"
let workspace = "SwiftUI-Demo.xcworkspace"
buildIosApp(workspace: workspace, scheme: scheme)
//Upload to TestFlight
let appleId = "15211*****"
let devPortalTeamId = "K4PXVYDM96"
uploadToTestflight(
username: username,
appIdentifier: appIdentifier,
appleId: appleId,
teamId: itcTeamId,
devPortalTeamId: devPortalTeamId
)
}
}
Try modifying something little and push your code on testflight-deploy-branch. The job will trigger in pipeline to deploy your app on TestFlight:
Please feel free in case you want us to add missing info or need to know more. Thanks for reading!
[/vc_column_text][/vc_column][/vc_row]