A guide to writing your own iOS CI/CD integration script

If you are working on a relatively big mobile project, most definitely you are going to need a CI/CD pipeline for building your application and distributing it to your testers. There are many tools which can be easily used for setting up this pipeline, the most popular one being Fastlane (https://fastlane.tools). While those tools are easier to use, I think is important to understand what happens under the hood and how can we create a CI/CD pipeline using the tools provided by Apple.

The idea of a CI/CD pipeline is to have a device that is able to build, test and distribute your app. Complex pipelines can be set up based on the branching model ensuring that the development branch compiles and all the tests run, however this is meant for another article as today we are only focusing on creating a CI/CD bash script that will: build, test, gather coverage data and distribute our application via Fabric.

In order to build the application we firstly need to fetch it from our repositories, usually this is done with a Git or SVN command or commit hooks, this really depends on the versioning system you are using for your project.

After the project is fetched on the build machine, we would need to make sure that all the third party dependencies are also fetched (if there are any). One of the most common tools used for managing the external dependencies of an iOS project are CocoaPods, so if you are using it, the first thing you should do in your script is what you would normally do on your machine when adding new libraries; fetching and installing those.

After all the external libraries have been installed, we would need to build the app and run the embedded tests. In order to do so we would use the Xcode command line tools, which is also used by Xcode in order to build the iOS application. The tool can be accessed from the terminal under the name “xcodebuild”.

In order to test that everything is installed correctly and see all the targets, schemas and build configurations of the current project the following command can be used

xcodebuild -list -project <your_project_name>.xcodeproj

So, lets get down to business, how can you run and test the project?

xcodebuild has many possible configurations and flags, all of them can be checked by reading Apple’s documentation or by looking in the manual:

Since we have mentioned CocoaPods in the begging of the article the application we are trying to compile should be a Xcode workspace, however the same steps can be applied in the case of an Xcode project as well.

Ok, so the above command looks scary, it has many parameters, but we’ll go trough each one of them:

  • workspace — specifies the name of our workspace
  • schema — the schema to be used for building (can be obtained from the “xcodebuild -list -project <your_project_name>.xcodeproj” command). A project can have multiple schemas, for instance an Apple Watch one, by using this param, we can specify which one we want to build against.
  • configuration — the configuration of the project (can be obtained from the “xcodebuild -list -project <your_project_name>.xcodeproj” command)
  • destination — this is used to specify for what kind of device you want to build your application for, it can be used for specifying a real device, if you have one attached to your build machine, or for using a certain simulator (with a desired OS version and device type)
  • derivedDataPath — used for specifying another path for derived data folder, this is useful if you want to use any files created while your app was built
  • BUILD_DIR — is used for specifying the executables and the build products should be put once the build is done, this as well as the derivedDataPath can be omitted if your requirements don’t need them
  • UseModernBuildSystem — Xcode 10 has a new build system and in order to force the build process to use the new or legacy one this flag can be used.
  • clean — is used for cleaning previous build products
  • build — builds the specified target
  • enableCodeCoverage — Used for specifying weather or not the coding coverage should be collected when running the tests
  • test — runs the tests for the specified target

After this command has ran, we should have all the build products in the specified folders. The coverage report is an encoded file which is accessible in the derived data folders under the name “Coverage.profdata”, however this file does not have an readable file format, so if you want to use it for feeding those details into an external service for monitoring the codebase health, or you just want to read it yourself, you need to convert it.

In order to convert the Coverage.profdata file to a readable format another tool should be used, specifically xcrun.

xcrun is another tool from the command line toolset, its purpose is to find tools and to execute them. The tool we are interested in is llvm-cov it’s purpose is to show “code coverage information for programs that are instrumented to emit profile data”. In the converted file we want to be able to see the coding coverage for every line of the project, in order to do so we will use the show command of the llvm-cov tool — “The llvm-cov show command shows line by line coverage of the binaries BIN”.

The show command takes 2 parameters, the path to the Coverage.profdata file generated by the build command as well as binary obtained as a result of the build command (this is usually located in the <your_app_name>.app package under the name <your_app_name>).

So to recap, we build the application, we have ran the tests and we have converted the coding coverage data into a readable format. Next, we need to distribute the application via Fabric, but before we are going to do this, we need to create an archive and to sign it with the correct provisioning profile.

If you are reading this, you might probably be familiar with the way iOS applications are distributed in the AppStore, otherwise, you should read this.

In order to sign an application via the command line tool you need a development/distribution certificate, the name of the provisioning profile you want to use for signing and a .plist which specifies the export options for the archive.

The development/distribution certificate can be obtained from Apple’s developer’s console and it should be “installed” on the build machine. By installed I mean added into a keychain, aaand here comes the fun part 🙃 If you are plan to use your CI/CD shell script on a remote Mac machine which will be accessible via an automation software such as Jenkins the default keychain (login) might not be accessible by the automation software, so in order to solve this issue a new keychain must be created. (If you just want to test it on your machine, ignore the keychain part and jump directly to the creating of the archive)

The idea is the following:

  • create a new keychain which stores the development/distribution certificate
  • before creating the archive, set the default keychain of the system to the newly created one
  • unlock the keychain — so that the certificate can be used by the signing process
  • creating the archive and sign it
  • reset the keychain to the default one

So let’s get down to business, in order to create a new keychain the keychain access app from the mac can be used File -> New keychain… and follow the steps. After the keychain has been created, the development/distribution certificate should be added in this keychain (drag and drop the certificate in the newly created keychain in the Keychain Access app).

Now back to our build script, we have a new keychain which contains the certificate needed for the signing of the application, now we need to make this keychain the default keychain of the system. All the keychain manipulation will be done using the “security” tool which is already installed on every MacOS.

After we have set the new keychain as the default one, we need to unlock it so that the certificate is available for signing, this can be easily achieved with the following command:

After this step, we can resume our archive creation process. We will use the same xcodebuild command with which we are already familiar

The new parameters used are:

  • archivePath — specifies the location in which the generated archive will reside
  • PROVISIONING_PROFILE — the name of the provisioning profile used for signing
  • archive — creates an archive from the specified target

The output of this command will be the archive of the project, however in order for this archive to be usable for submitting it to the AppStore or internal testing, this needs to be exported and signed.

For the export process a .plist file with the export options should be used. This file can be generated by creating and exporting an archive from Xcode:

  1. Open your project in your local Xcode
  2. Archive the project
  3. Once the archive is done, export the generated .xcarchive file into an IPA file. Xcode will copy the used Export Options plist file next to the generated IPA file
  4. Copy the export file to the build machine

The contents of the plist file are the ones presented above, the export file specifies weather or not bitcode should be enabled, the distribution method, the provisioning profiles used (the name must match the <the_name_of_the_provisioning_profile> used when generating the archive), and other options such as strip symbols and weather or not to use app thinning.

After this step, we are ready to generate the .ipa file. As you probably already know, we are going to use the xcodebuild command for this as well:

  • exportOptionsPlist — specifies the path of the export options file (the file above generated)
  • exportPath — the path where the output (the .ipa file) of the export will reside

🎉 after this step we should have an .ipa file which can be used for distibution, however we still have some cleaning up to do.

We need to reset the keychain to the old login keychain.

security list-keychains -d user -s <path_to_the_old_login_keychain> <path_to_the_new_keychain>
security list-keychains -s <path_to_the_old_login_keychain>
security default-keychain -s <path_to_the_old_login_keychain>

The only thing left to do is to distribute the application. For the purpose of this walk trough, we are going to distribute it via Fabric. In order to do so, we can use their own tool which was dowloaded by the “pod install” as an external library and placed in the Pods folder.

In order to submit the archive for distribution the Crashlytics submit tool should be used.

This tool will upload the generated .ipa file to Fabric and will email all the testers from the specified group that a new build is available.

Hopefully this short walktrough has made you aware of the complexity of the distribution process and has shed some light on how you can implement your own CI/CD shell script.

The whole script can looks as this, this can be used into an automation tool such as Jenkins for building your app 🙂

# Install external dependencies
pod install
# Build and run tests
xcodebuild -workspace <your_project_name>.xcworkspace \
-scheme <your_desired_schema> -configuration <your_desired_configuration> \
-destination 'platform=iOS Simulator,name=iPad Pro (12.9-inch),OS=10.2' \
-derivedDataPath <derived_data_path> BUILD_DIR=<build_directory_path> \
-UseModernBuildSystem=YES clean build -enableCodeCoverage=YES test
# Convert the coverage report to a redable form
xcrun llvm-cov show -instr-profile <path_of_the_Coverage.profdata_file> \
<path_of_the_executable> > <name_of_the_report_file>
# Set the default keychain to the new one
security list-keychains -s <path_to_the_new_keychain>
security default-keychain -s <path_to_the_new_keychain>
# Unlock the keychain
security unlock-keychain -p <my-super-secret-password> <path_to_the_new_keychain>
# Archive the project
xcodebuild -workspace <your_project_name>.xcworkspace \
-scheme <your_desired_schema> -archivePath <path_of_the_archive>\
-configuration <your_desired_configuration> \
PROVISIONING_PROFILE=<the_name_of_the_provisioning_profile> \
archive -UseModernBuildSystem=YES
# Export the archive
xcodebuild -exportArchive -archivePath <path_of_the_archive> \
-exportOptionsPlist <path_of_the_export_plist_file> \
-exportPath <path_of_the_exported_archive_location> -UseModernBuildSystem=YES
# Reset the keychain
security list-keychains -d user -s <path_to_the_old_login_keychain> <path_to_the_new_keychain>
security list-keychains -s <path_to_the_old_login_keychain>
security default-keychain -s <path_to_the_old_login_keychain>
# Distribute the app via Crashlytics
Pods/Crashlytics/submit <id-of-the-fabric-organisation> \
-ipaPath <path_of_the_exported_archive_location>/<your_project_name>.ipa \
-groupAliases <the_alias_of_the_testing_group>
view raw build-script.sh hosted with ❤ by GitHub

See you next time, happy coding!