iOS Continous Integration for Enterprise
Updated Oct 19, 2015: Added updates for OS X El Capitan. Updates highlighted in yellow.
Updated Jan 23, 2015: this post is part of a series on iOS Continuous Integration.
- Part One: This post
- Part Two: Continuous Delivery for Enterprise on iOS
At Metal Toad, we wanted to come up with a CI (Continuous Integration) solution that would fill all the traditional roles of continuous integration, but also allow for continuous deployment of enterprise builds. Xcode doesn’t make this easy. In this post, we’ll go over how to create an Xcode Bot, how to (optionally) fetch any pods associated with your project, how to inject custom certificates and provisioning profiles into Xcode Server, and finally, how to manually pull app builds down from the server.
Note that the injecting certificates into Xcode Server falls well outside Apple’s intended purpose, and (as we have learned from several failed attempts to do so) can easily leave your OS X server in a non-bootable state. If you want this functionality properly supported in Xcode Server, submit a radar to let Apple know.
This post is assuming that you’re using OS X Yosemite, OS X Server 4, and Xcode 6. If your project has CocoaPods dependencies, you’ll need to install that on your CI server as well.
Creating an Xcode Bot
The basics of creating Xcode bots are covered well by Apple, and outside the scope of this article. If you haven’t created your bot already, we’d recommend following Apple‘s Guide. If you’re using CocoaPods, read the next section before finalizing the “Configure bot triggers” screen.
As mentioned in the prerequisites section, you’ll need CocoaPods installed on your server if your project uses pods. That’s as simple as
sudo gem install cocoapods . You’ll only need to do that once, but you’ll want to make sure your pods are up to date every time you run an integration. Apple provides a way to do this via Triggers. Triggers let you run arbitrary code before and after your integration.
On the “Configure bot triggers screen”, click “Add Trigger” then “Run Script” under “Before Integration”.
Apple provides a host of environmental variables (which we’ll go over in a future post), but for the purpose of this script, we’ll simply need
$XCS_SOURCE_DIR, which tells us where our repository is being pulled to, for our purposes here. Here’s the script we use to install applicable pods.
Pre El Capitan
#!/bin/bash cd "$XCS_SOURCE_DIR/<repo_name>" #change <repo_name> to your repository's name if [ -e "Pods" ] #See if the Pods directory already exists then pod update #update pods if it does else pod install #install if it doesn't fi
#!/bin/bash cd "$XCS_SOURCE_DIR/<repo_name>" #change <repo_name> to your repository's name if [ -e "Pods" ] #See if the Pods directory already exists then /usr/local/pod update #update pods if it does else /usr/local/pod install #install if it doesn't fi
Unfortunately Xcode doesn’t provide a way to link files directly with Triggers, so you’ll need to paste in code from an external editor, or type it in manually.
Injecting Certificates & Provisioning Profiles
Once your bot is implemented, you'll need to do some work on the server side of things. This is where things get hairy. We’re going to be messing with the keychain in ways that Apple doesn’t really intend, so move with caution. It’s very easy to leave your server badly broken, or even completely bricked if anything goes wrong here. These steps have been thoroughly tested on our internal configuration, but your mileage may vary.
That said, having automated and distributed daily builds is highly useful, so for us, this was definitely worth the hassle.
Step One: Importing Provisioning Profiles
To start, you'll want to selected a provisioning profile for the project in Xcode. Our shared scheme specifies that the Archive action will use the Release configuration, but you can set this up however works best for your particular project (maybe even a dedicated CI configuration).
If we try to run an integration at this point, it should fail on the archiving step. Sometimes Xcode will give you a useful error here, but usually it will simply say that
codesign has failed. Checking the integration’s log tab generally has much more useful information. In this case, the bottom of our build log shows something that will look very familiar to most iOS developers:
Code Sign error: No matching provisioning profile found: Your build settings specify a provisioning profile with the UUID “7d0591b6-2cfa-4e7a-b750-5540ded544bf”, however, no such provisioning profile was found. Since our XcodeServer instance is headless, we can't add the provisioning profile to it by dragging/dropping like we normally would. Instead, we'll grab the file from our local machine, and move it to the server manually.
- On your local machine, grab the
.mobileprovisionfile matching the UUID mentioned by Xcode. It should be stored in
- Copy the file to the OS X Server, and move it to
- Integrate again.
Again, looking at our build log, we have a new error:
Code Sign error: No codesigning identities found: No codesigning identities (i.e. certificate and private key pairs) that match the provisioning profile specified in your build settings (“CI Test Distribution”) were found. Progress! Now XcodeServer has found our provisioning profile, but is failing to complete the archive step because it can't find the certificate that corresponds to it.
Step Two: Injecting Certificates
One of the big things that hung us up while trying to get this all to work, was that XcodeServer uses its own keychain. Once you find that keychain, everything starts to fall into place. It’s located at
/Library/Developer/XcodeServer/Keychains/Portal.keychain and its passphrase is stored at
/Library/Developer/XcodeServer/SharedSecrets/PortalKeychainSharedSecret. Using these two pieces of information, we can unlock our keychain, add our certificates, and re-lock it as follows (Huge thanks to Oliver Eikemeier on Stack Overflow for this part):
- Make a temporary copy of the keychain on our desktop:
sudo cp /Library/Developer/XcodeServer/Keychains/Portal.keychain ~/Desktop/
- Change the permissions so we'll be able to open it:
sudo chown [username]:staff ~/Desktop/Portal.keychain(replacing [username] with your own)
- Use the shared secret to change the password to something temporary (this will prompt you for a new password):
security set-keychain-password -o "`sudo cat /Library/Developer/XcodeServer/SharedSecrets/PortalKeychainSharedSecret`" ~/Desktop/Portal.keychain
Portal.keychainin Keychain Access
- Unlock it
- Add your certificates (and their private keys) to Portal
- Re-lock it
- Restore the original password:
security set-keychain-password -p "`sudo cat /Library/Developer/XcodeServer/SharedSecrets/PortalKeychainSharedSecret`" ~/Desktop/Portal.keychain
- Reset permissions:
sudo chown _xcsbuildd:_xcs ~/Desktop/Portal.keychain
- Replace the original keychain:
sudo cp ~/Desktop/Portal.keychain /Library/Developer/XcodeServer/Keychains/
- Reboot server, then integrate again.
That’s it! Our integration should work successfully at this point, and we can move on to pulling down our executable for distribution.
Grabbing the Build
Now for the easy part. We’ve managed to get a properly signed and provisioned integration. All we have to do now is grab the build. In Xcode, visit the Report Navigator (
⌘+7). Find your bot on the list, then click the most recent integration. From there, you should see a link to the
.ipa and the
.xarchive under “Build Results”