Blog

Auto-deploy iOS app to TestFlight using Gitlab CI and Fastlane

By 8 December 2020 January 7th, 2021 No Comments

Auto-deploy iOS app to TestFlight using Gitlab CI and Fastlane

Auto-deploy iOS app to TestFlight

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.
Run the script defined in Gitlab 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:
Runner
Just copy the registration token and follow the commands given below for installing Gitlab Runner into your system.
  • Download the binary for your system:
sudo curl --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64
  • Give it permissions to execute:
sudo chmod +x /usr/local/bin/gitlab-runner
  • Install the Runner as service and start it.
gitlab-runner install
gitlab-runner start
  • 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:
Registered Gitlab Runner

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:
export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD="APP_SPECIFIC_PASSWORD"
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:
touch .gitlab-ci.yml
open .gitlab-ci.yml
  • Enter the script below in .gitlab-ci.yml file
stages:
    - build

build_project:
 stage: build
 script:
    - xcodebuild clean -project ~/repos/SwiftUI-Sample-App/SwiftUI-Demo.xcodeproj -scheme SwiftUI-Demo | xcpretty
    - bundle exec fastlane customLane
 tags:
    - build, testflight
 only:
    - testflight-deploy-branch
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:
Pipelines

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:
xcodebuild clean -workspace ~/repos/SwiftUI-Sample-App/SwiftUI-Demo.xcworkspace -scheme SwiftUI-Demo | xcpretty
So, below is the final Fastfile.swift file:
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
        )
    }
}
And final .gitlab-ci.yml file would be:
stages:
    - build

build_project:
 stage: build
 script:
    - xcodebuild clean -workspace ~/repos/SwiftUI-Sample-App/SwiftUI-Demo.xcworkspace -scheme SwiftUI-Demo | xcpretty
    - bundle exec fastlane customLane
 tags:
    - build, testflight
 only:
    - testflight-deploy-branch
Try modifying something little and push your code on testflight-deploy-branch. The job will trigger in pipeline to deploy your app on TestFlight:
Job on gitlab.com
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]

Get started with Bloom