iOS continuous deployment with Jenkins: a brief introduction

17 min read
October 18, 2016

Yes, you’ve read it right. The title you expected was probably “Continuous integration with Jenkins”. But it isn’t.

We’d like to show you another approach to using Jenkins. Not unheard of, but also not commonly used.

1 ArtyPSkucTTBSM1K8d voQ 720x675 1
It’s easy to see that Vladimir and Jenkins are pretty close-knit.

The workflow you’d expect would be: Code — commit — push — PR — trigger CI, and hopefully merge after your code is reviewed.

Continuous integration of this sort can ensure that the project with your code changes is still buildable, and that the tests pass, if you choose to run them.

We’ll expand that procedure and introduce some options that may make life easier for product owners and developers, seniors and apprentices alike.

A few notes on Jenkins installation

This is not a tutorial on installing and using Jenkins, but still, a few pointers might make your life easier.

Jenkins is a real gentleman, but also a little bit moody sometimes. Polite, but stubborn. The most common installation scenario is fetching the installer package from Jenkins and running it. Of course, you’ve remembered to install Java before, haven’t you?

To quote a nice tutorial on installing Jenkins from CIMGF:

One does not simply get Jenkins to work on the first try.

Allow me to expand that a bit:

One does not simply get Jenkins to work on the first try when the schedule is tight.

Yes, we’ve installed Jenkins from scratch a few times without a hitch. However, on the last install on El Capitan, it remained uncooperative. As time was precious, we turned to a beautiful world of home brewing— Homebrew — and Jenkins was available.

So, a simple brew install jenkins command in the terminal will pull all the dependencies, and Jenkins itself, and leave you with working Jenkins installation in no time. Also, occasional brew update && brew upgrade will make sure your Jenkins installation is up to date. The maintainers really do keep homebrew versions up to speed with the official releases.

That leaves you with a working Jenkins installation. In this case, its root is in the home folder of the user that installed it, and not Jenkins user. It resides in the folder ~/.jenkins, so take that into consideration if you can’t find it in the Finder, as it starts with a dot. This means that all the jobs you’ll be running are in~/.jenkins/jobs. So:

Jenkins install location: ~/.jenkins
Jenkins Jobs location: ~/.jenkins/jobs/Your_Job_Name
Jenkins job working location:

Adding and configuring the project

After installation, Jenkins should be available at the address localhost:8080 using the browser of your choice, just as if you’ve used the native installation.

Of course, the next step is setting it up for your needs.

Since we’re using Bitbucket as our source code repository, we’ll install the Bitbucket plugin from the Manage Jenkins menu option. While we’re at it, why not install AnsiColor plugin for a nicer output, and maybe Slack Notification Plugin too. Also, don’t forget to add the SSH keys if you haven’t already, and configure credentials on the Credentials option, just add the SSH keys underneath Manage Jenkins, so you can access your repository using SSH.

Make sure you import the correct code signing identity for the project you need to build into the Keychain or the signing will fail. Alternatively, you can have the appropriate certificates and profiles bundled with your code and pass them to the build system.

We’ll create a new Freestyle project and configure it. The first step is to configure its Git repository in the Source code management section and to choose the access credentials you’ve just created.

If you have static IP and external access to your Jenkins installation, a Bitbucket/Github trigger will do nicely. If not, and you choose not to rely on services like Dyn, the nice and safe approach is to configure the branch to be watched, and configure the build trigger.

The best approach here is to use the Poll SCM option and configure it to poll let’s say every 15 minutes using the option H/15 * * * *. This will poll the repository every 15 minutes, and check if there are new commits. If not, it will do nothing and try again later, and when a new commit is detected, it will pull and run the job.

The branch you choose is arbitrary, be it the common tc or any other name of your choice, like fabric for fabric builds… Just define it and push to it when you want to trigger Jenkins.

Now, we’re building Xcode projects so you might think we’d install the Xcode plugin? Well, we might, but we won’t. Instead, we’ll be using manual build steps to have more control. Xcode plugin works well for simple builds but has proven to be a bit limited if you want something a bit more interesting. So, if we’re forced to add further manual build steps, why not have complete control from the start?


Let’s pause Jenkins configuration for a moment and focus on some additional tools you may find useful.

The first thing you might want to install is XCPretty, a tool that lets you format xcodebuild output to a nicer format.

Another thing you may need are Fastlane tools that help you with builds (proxy to the xcodebuld), and test and upload IPA to the iTunes Connect.

Both of them are available using RubyGems, so a simple

gem install xcpretty fastlane

should do the trick.

Moving on…

So, we’ve set up our initial Jenkins job and configured Git access. It’s build time.

Start by adding a new Build step in the Build section. If you’ve decided not to include Cocoapods into your repository, the first build step to add would be to do a pod install. If you have included them, it’s build time.

It is very convenient to have your Xcode project (or more probably workspace) set up with possibly multiple schemes and configurations. That way, you can have a configuration that will point to the correct API for your purpose (release, staging…), configure additional services as needed and use the correct Provisioning profile and Code signing identity. You’ll want to use your enterprise distribution or distribution profile, depending on your deployment choice.

After adding the new build step, we need to enter the build command itself:

xcodebuild -workspace YourProject.xcworkspace
    -scheme YourScheme \
    -archivePath YourProject \
    -configuration Release \
    clean archive | xcpretty --color

Don’t forget to replace the placeholder names with your own project. ?

So, you tell xcodebuild to build your project workspace, and that option also requires you to select a Scheme. You can check available schemes by running xcodebuild -workspace YourProject.xcworkspace -list to get a list of all the available schemes. If you don’t see your scheme, you should open your workspace in Xcode, click on your scheme, choose the Manage schemes option and make sure that the desired scheme is ticked as “shared”.

The archivePath option tells xcodebuild where to store the .xcarchive, (YourProfile as in the code snippet above will create the YourProfile.xcarchive in the working directory) and it will be handy if it is in the workspace directory for further use. Finally, configuration option tells which configuration to use. The pipe | xcpretty –color just formats the output in a nicer way so it doesn’t clog the console with unneeded output. Optional, but nice. Also, if you installed the Ansi plugin, you can enable the Color ANSI Console Output option in the Build environment section of the Job configuration.

When the build succeeds, you’ll have an archive directory you want to convert to an .ipa file. That’s easy, just add a new build phase and enter the command:

if [ -a YourProject.ipa ]; then
    rm YourProject.ipa
xcodebuild -exportArchive \
    -archivePath YourProject.xcarchive \
    -exportPath YourProject \
    -exportFormat ipa

The first check is needed as the operation will fail if the ipa file already exists. Furthermore, rm YourProject.ipa will also fail the build phase if it doesn’t exist, and it won’t exist the very first time so you want to wrap that into the if statement.

Finally, you’re left with an signed ipa you can distribute as you see fit.

Another way to achieve the same goal is to use fastlane tools’ command gym for building. Simply put, gym is an intelligent and powerful wrapper around the xcodebuild command that will automagically build and archive your project into an .ipa file with minimum fuss and questions. It will by default build the Release configuration and create an ipa file in the directory where it was run. However, sometimes you do need to pass it some custom parameters. You’ll probably at least need to tell it the scheme to build, so the build command for the custom build phase would be

gym --scheme YourScheme

Now, the question is why bother with xcodebuild when we can use gym? Unfortunately, sometimes gym just doesn’t work. Don’t get me wrong, Fastlane tools are great and are updated every few days, but sometimes they can be a bit unreliable, and in that case, you can always fall back to good old xcodebuild. Choose your flavor. ?

Further tweaks

Before moving on to distribution, let’s address a few more advanced options you could explore.

Manual signing
We’ve already mentioned that you can set up different schemes and configurations to target different API endpoints, signing identities, provisioning profiles, etc.

As for the project configuration, you really should use them to configure the project for stating/release endpoints. However, for provisioning and signing, there is another option — you can override what’s set in your project and tell xcodebuild or gym to use different options.

So, for xcodebuild you can use the following command for the build phase

xcodebuild -workspace YourProject.xcworkspace/ \
    -scheme “YourScheme” \
    -configuration “Release” \
    archive \\
    -archivePath YourProject.xcarchive \
PROVISIONING_PROFILE=”Your_provisioning_profile_UUID” \
    CODE_SIGNING_IDENTITY=”iPhone Distribution:
    (your_company_here)” | xcpretty --color

Note that you need to have the correct .mobileprovision file installed on the system. If you don’t know the UUID, just locate the file on your disk (or download from the Developer portal), open it in a text editor and find the value for the key ‘UUID’.

The command is similar for gym:

gym — scheme YourScheme \
 — provisioning_profile_path
 “./path_to_provisioning.mobileprovision” \
 — codesigning_identity “iPhone Distribution:

This will result in an .ipa with the selected provisioning profile and code signing identity.

Changing the bundle identifier and team ID

If you need even more customization — let’s say you’re developing and testing using your company’s Developer account and bundle identifier — and want to distribute tests using your client’s one, or you just want to change the bundle identifier for test builds, there’s a way to do that too.

So, for the xcodebuild path, you just expand a little bit on the previous command and use

xcodebuild -workspace YourProject.xcworkspace/ \
    -scheme “YourScheme” \
    -configuration YourConfiguration \
    CODE_SIGNING_IDENTITY=”iPhone Distribution: (company here)” \
PROVISIONING_PROFILE=”your_provisioning_profile_UUID” \ \
    archive \
    -archivePath YourProject | xcpretty --color

So, almost the same as before, just note a difference — there’s another option PRODUCT_BUNDLE_IDENTIFIER providing a new bundle identifier.

To get the .ipa, you need to run

if [ -a YourProject.ipa ]; then
    rm YourProject.ipa
xcrun xcodebuild \
    -exportArchive \
    -archivePath YourProject.xcarchive \
    -exportPath YourProject.ipa \
PROVISIONING_PROFILE=”Your_provisioning_profile_UUID” \
    CODE_SIGNING_IDENTITY=”iPhone Distribution: (your company)” \ \
    -exportProvisioningProfile ‘Your Provisioning Profile’

Note that we’re repeating some options from the previous xcodebuild command. Also, you need to have a plist file in your jenkins workspace directory called exportoptions.plist with contents:

<?xml version=”1.0" encoding=”UTF-8"?>
<!DOCTYPE plist PUBLIC “-//Apple//DTD PLIST 1.0//EN”
<plist version=”1.0">

in order for the archives to be correctly signed with the correct TeamID, Provisioning and Code signing.

You can check the intermediate .xcarchive by cding into it and checking if the contained Info.plist file contains the correct bundle identifier and correct signing identity name. Furthermore, you can go to the archive folder to Fazaa.xcarchive/Products/Applications/ and from there to less embedded.mobileprovision to check if the correct TeamID, bundle identifier and provisioning profiles were used. Provisioning profile is referenced by its UUID, but you can open the correct .mobileprovision file and check if it’s UUID matches.

You can also check the same information in the .ipa file, which is just a zip file. So, if you unzip it and go to directory Payload/, you’ll find Info.plist and embedded.mobileprovisioning there as well.


Finally, the good stuff. We use three different means of test distribution — hosting in a secure place on a web site, Fabric’sBeta distribution and Apple’s Testflight.

FTP upload
For this means of distribution, you need to have an HTTPS server you can upload your ipa. You also need to install a Jenkins FTP plugin and configure it (server name, credentials and server path) to upload the files to the desired location. Then in your Job configuration add new Post-build Action, choose Send build artifacts or similar option, choose your FTP configuration and files you want to upload. If everything is correct, the resulting files should be available for your clients and testers to download. Also, make sure that the appropriate enterprise distribution plist is available on that location.

Fabric Beta distribution
If you use Crashlytics/Fabric (and you should!), you can upload the .ipa to Fabric for test distribution. After successful .ipa build, just add a new build phase. In it, paste the following command

./Pods/Crashlytics/submit API_KEY BUILD_SECRET \
 -ipaPath YourProject.ipa \
-groupAliases testers_group,another_testers_group \
-notifications YES

where API_KEY and BULD_SECRET can be determined from the Run script build phase of your .xcproject/.xcworkspace. -groupAliases just tells Fabric to send test build invites/new build notice to testers defined in those particular groups. You can define them at the Fabric website. You can also provide the individual emails if you want, using -emails followed by the comma separated list of emails. Finally, -notifications option turns on or off Fabric sending email informing the testers there is a new build available.

Also, please notice that this command is appropriate if you’ve installed Fabric as CocoaPod. If you have installed by drag&drop into the project, syntax is the same, only the path to the submit executable will be different.

App Store / TestFlight

Finally, if your choice of distribution is Apple-acquired TestFlight, you’ll need the Fastlane tools, more precisely it’s deliver tool for uploading binaries to the iTunes Connect. As before, all the steps that lead to the correctly built and signed .ipa are the same. In the place where your .ipa is, and from where you’ll be executing deliver, you need to create the file called Deliverfile (or you can feed it the info as parameters, see deliver — help for more info). In it, you need to provide information about the bundle identifier and the iTunes connect account you want to use. The contents are:

app_identifier “” # The bundle
identifier of your app
username “” # your Apple ID user

Mind you, the password for that account is determined from the keychain so do log in to iTC using a browser beforehand, so thatthe credentials are available.

With that set up, you just need to add another Build step with command

deliver --skip_screenshots --kip_metadata --force

This will use Fastlane tools’ Spaceship to communicate with iTC and deliver to upload the binary to the correct account. –skip_screenshots and –skip_metadata options are useful as we’re just submitting the binary for testing, so the eye-candy and the metadata are not yet needed, and the –force option is needed as fastlane would otherwise prompt you for some confirmations stopping the delivery.

Alternatively, you can use the Fastlane’s Pilot that will automatically upload, and also distribute to testers.


So, there it is. Hopefully, we’ve given you some alternative options for distributing your betas to your testers. Whether you use xcodebuild or gym is completely up to you, the result is the same.

Also, I’ve described three separate means of beta distribution. That doesn’t mean you should pick one only. Feel free to mix as you see fit.

Direct download may be the easiest option, but gives you no overview of what’s happening. You also have to notify your testers to download new builds manually.

iTC distribution is a very useful option, as your testers will be notified of new builds, that will be available from the Testflight app. You can also see if the testers installed the beta, and which version.

Concerning the latter, Fabric probably gives you the most information in that regard, so you can really have full overview of what’s happening with your testing. Testers can also choose which version to download, and of course, you have all the Crashlytics power as well.

OK, you may say — that’s nothing new. I do all those things manually! Sure, no problem. That’s a viable option. Nothing wrong with that. However, this approach gives you a few advantages.

Do you have you project set up for sending test builds or do you need to change provisionings&signing manually every time? If you can’t or simply don’t want to bother with sending tests, do you teammates know how to build the test builds, do they all even have the correct signing credentials?

For this ‘continuous distribution’ process, all you need to do is to bump the build number and commit to the correct branch when your code is ready for testing. You can do that.

Any member of your team can easily do that. You can even do that remotely, if the need arises. So, you commit to the correct branch, Jenkins will poll the SCM and determine there’s a new commit, pull, build and distribute. So it’s commit & forget. Easy.

Final notes

I’ve described, hopefully in enough detail, and yet not too much to bore you to death, how to setup individual build steps.

However, while configuring I strongly suggest to move step by step. First, set up git access and check if the Jenkins pulls your repo correctly. From there, I’d go to the terminal and execute first xcodebuild to produce the archive, and tweak everything until it succeeds. Then, copy that command into Jenkins.

Then, manually setup the .ipa creation phase, and finally test various binary distribution methods and add them to Jenkins. Don’t get me wrong, editing options in Jenkins will also work, but then you need to wait for all the build steps to finish. As we’re dealing with Release configurations, build times can really take their time so we don’t want to rebuild every time from scratch if we’re testing the distribution part, do we?

Finally, to have an overview of what’s (not) happening, it’s useful to install the Slack plugin for Jenkins and set it up to send Slack message to a channel of choice informing you of different build states. As we have builds triggered by new commits only, we use the following notifications:

This way we know when the build has started and how it’s ended so we can act appropriately.

Hopefully, that means that we’ll get a Build success so we can be happy. So our testers can be happy. So our clients can be happy.

And that makes us even happier. ?

Written by

Vladimir Kolbas

IOS Team Lead

When something unusual happens, Vlado is the man with an explanation. An experienced iOS Team Lead with a PhD in Astrophysics, he has a staggering knowledge of IT. Vlado has a lot of responsibilities, but still has time to help everybody on the team, no matter how big or small the need. His passions include coffee brewing, lengthy sci-fi novels and all things Apple. On nice days, you might find Vlado on a trail run. On rainier days, you’ll probably find him making unique furniture in the garage.

Related articles