I'm an iOS dev, working for a small agency. I'm currently on a few smaller projects where I'm the only developer. We recently acquired a Jenkins server, but each project is left to fend for themselves as for how to use it.

I'd like to use it for making and distributing builds. My ideal would be:

  1. Every commit is built as a single IPA that is placed in a HTTP-accessible location. (It only needs to keep the latest one, otherwise the disk would fill up — some of our apps are 500MB or more.)
  2. Once a day it makes a build, signs it with our internal provisioning profile, tacks a build number onto the end of the version number, and sends it to our internal TestFlight team.
  3. When manually prompted, it builds the latest commit, gives it a manually specified version number, signs it with the client's provisioning profile and sends it to TestFlight.

I'm pretty new to Jenkins. The developer who set up the server is running it on one of our projects, so I know it has the right stuff installed to do Xcode builds. I believe he's only using it to run unit tests though, not to do any of the code signing, IPA creation or TestFlight stuff.

So my questions:

  • I've listed three distinct kinds of build. How does Jenkins cope with that? I see there's a "build triggers" section in the config for a Jenkins project, but it doesn't seem to mention different types of build. Should I just set up multiple Jenkins projects, called "App X (continuous)", "App X (nightly)" and "App X (client)"?
  • How do I specify the provisioning profile through Jenkins? If there isn't a way, I guess I could make different configurations in the Xcode project…
  • Has anyone else used Jenkins to actually do the release (i.e. build and push to TestFlight) of beta builds of their apps? Is it a good idea? Or should I continue just doing it manually?

The good news is that this is all possible. The bad news is that it is all manual. You will burn a lot of evenings fine-tuning things, and whether it ultimately pays off in time saved is not necessarily knowable in advance.

Here's an info-dump of things I've learned in the last few months of fiddling with Jenkins and Xcode.

There are Xcode and Testflight plugins for Jenkins, which will get you started. After you outgrow them, every chunk of logic becomes a shell/ruby/perl/python/whatever script.

For instance, I outgrew the Xcode plugin as soon as I realized I needed multi-configuration projects to create additional, closely-related builds, identical to the main one except for a single variable like an additional scheme, or a different $DEVELOPER_DIR for a beta Xcode. There was no way to pass those configuration parameters to the fields exposed by typical plugins. So, I reviewed the job logs to figure out how the Xcode plugin manipulated xcodebuild, and transplanted that into a script phase.

Signing from arbitrary identities or provisioning profiles, another thing the plugin was doing for me, became another script phase. A boolean job parameter like SIGN_WITH_DEVELOPER_ID communicates with a script phase like this:

if [ "${SIGN_WITH_DEVELOPER_ID}" = true ]; then
  echo "Re-signing build with Developer ID"
  $WORKSPACE/ci-scripts/developer-id-sign --path [...]

The script it calls boils down to something like:

/usr/bin/security unlock-keychain -p yourkeychainpassword ~/Library/Keychains/login.keychain
/usr/bin/codesign -f -vv -s 'Developer ID Application' path/to/binary 2>&1

You'll outgrow the Testflight plugin as soon as you no longer want to upload every single build output by a given job. Instead, you'll probably want to invoke Testflight's upload API programmatically, signaled by another job parameter.

(I haven't fully automated the Testflight upload process yet, because we mostly use Testflight to distribute to clients, and I've been burned a time or two sending out a client build that defaulted to the wrong profile or which got mangled in transit. So, I vet those things by hand. The process of uploading to Testflight, though, is just a matter of invoking curl on your build server. Without having done it, I'm confident that it's viable because we have Jenkins doing something almost identical to upload dsyms to HockeyApp for in-the-wild releases of Mac apps, and that's working great.)

I recommend multi-configuration jobs as the best way to achieve multiple closely-related builds without job duplication, which creates a bunch of housekeeping for yourself as you then have to keep those related jobs in sync. Multi-configuration jobs are generally incompatible with plugins, unless you want them to run on every build variant, every time. In this way, multi-configuration jobs are a tradeoff between initial complexity and down-the-road flexibility.

However, your three builds as outlined aren't really that different, and don't strictly need to be multi-configuration jobs. The continuous build, kicked off after every landed commit, is the default. Whether or not you update the CFBundleVersion, and/or upload to Testflight with any given distribution, can be runtime configuration decisions, controlled via job parameters and script phases watching for them, or by the logic which kicks off your build in the first place.

More miscellaneous thoughts:

  • Without specifying a shebang line in a script phase, /bin/bash -xe behavior is assumed, which prints each line to the log as it goes, and fails the whole build as soon as it encounters a non-zero return value. This combination is usually what you want, and makes troubleshooting less awful.

  • If you don't like bash, and you wind up writing in some other scripting language, be vigilant about exit codes and emulate the bash -xe behavior yourself. I've found it preferable to embed significant logic in scripts we attach to projects, and to conditionally invoke them from Jenkins, via script phases that are as short and declarative as possible.

  • Without getting too far into the weeds, having Jenkins write things (like updated version numbers) into source control is tricky, and considered something of a CI code smell. It's also tremendously handy for nightly builds, but I haven't found a well-sanctioned way to pull it off. I wound up grafting the ability to update the bundleversion into the script which initiates the nightly build, fetches the build artifacts, stores them on a fileserver, and uploads a copy to Dropbox. In this scenario, the updated version number is in place before Jenkins wakes up, which is preferable to trying to make Jenkins do it in the middle of a build.)

  • It's not the focus of the talk, but I got religion on a lot of Jenkins capabilities, including multi-configuration jobs, after seeing some examples tossed off in WWDC 2012 session 404.

  • I recommend running Jenkins in a virtual machine, probably Linux, with Mac build nodes to do the Xcode gruntwork. I'm a Mac guy, but Jenkins is tested most thoroughly on Linux, its weekly-ish updates are easy to apply, and you can roll back to a working snapshot in the event of a problem. A Linux VM is low-overhead, and the time investment will pay you back the first time you update to a new version that gives you trouble.

In the long run, it will give you trouble, not because Jenkins is unreliable, but because CI is a permanent addition to your company's workflow, and sooner or later, everything breaks.

| improve this answer | |
  • Thanks for an excellent writeup! Lots of food for thought there. I think it might be time to brush up on my shell scripting… :) – Amy Worrall Nov 27 '12 at 9:17
  • Excellent but depressing answer. It is indeed all manual. Setting up new projects and making changes to existing ones burns days at a time, and happens sufficiently infrequently as to render you a beginner each time you do it. It's also extremely error-prone, and it's easy to produce builds which are not quite configured as you expect. But to use CI is best practice, and what's the alternative for repeatable, configured releases? Developers producing private builds for submission to the app store?? No thanks. – funkybro May 7 '13 at 10:01

Not the answer you're looking for? Browse other questions tagged or ask your own question.