At last year’s WWDC, Apple introduced Xcode Server for doing continuous integration. Lots of people have tried to use it with mixed results. Recently after dealing with our build server for work (we use Jenkins), I decided to give Xcode server a try for one of my projects.
I was hesitant at first to use OS X Server (which includes Xcode Server) as I’ve had poor results with it in the past. It intrigued me often that I decided to install server and see where it went. OS X server install was a breeze and so was setting up Xcode Server.
I created a “Bot” from Xcode on my development machine and saw that it failed when the server tried to build it. No problem, I searched the Internet and all the results said to add the repository from the server instead of when creating the bot. Perfect, I did that and then the build succeeded. Yeah! I also added my Team to Xcode server so it could pull down provisioning profiles and create a certificate to build.
I checked the log and found 2 problems. First, I have a run script build phase that puts the version number in the Settings.bundle that failed and second, it was being signed using the wrong mobile provisioning profile.
I decided to tackle the second problem first. Through my searches, I found that I needed to put the mobile provisioning profile (an AdHoc one) in /Library/Server/Xcode/Data/ProvisioningProfiles. No problem. Next I also found that I needed to put the certificate and private key for the profile in the System keychain as the Xcode Server runs under _teamserver which doesn’t have its own keychain. I did that as well and found the problem didn’t go away. I started thinking about it and found that sometimes Xcode ignores the Automatic Distribution Code Signing Setting. So, I removed my Team and deleted all the other mobile provisioning profiles. Bingo, now my app was building and being signed correctly.
Next step was to figure out my build phase. After dumping the environment variables, I found that in order to put files in the .app after the build, I had to set my script phase to be:
shortversionnumber=`/usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" ${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}`
bundleversionnumber=`/usr/libexec/PlistBuddy -c "print CFBundleVersion" ${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}`
shortversionnumber=`echo $shortversionnumber '('$bundleversionnumber')'`
echo ${shortversionnumber}
src_file="${SRCROOT}/Piccee/Resources/Settings.bundle/Root.plist"
dest_file="${BUILT_PRODUCTS_DIR}""/""${UNLOCALIZED_RESOURCES_FOLDER_PATH}""/Settings.bundle/Root.plist"
echo $dest_file
/usr/libexec/PlistBuddy -c "delete :PreferenceSpecifiers" "$dest_file"
/usr/libexec/PlistBuddy -c "add :PreferenceSpecifiers array" "$dest_file"
/usr/libexec/PlistBuddy -c "add :PreferenceSpecifiers:0:Title string About" "$dest_file"
/usr/libexec/PlistBuddy -c "add :PreferenceSpecifiers:0:Type string PSGroupSpecifier" "$dest_file"
/usr/libexec/PlistBuddy -c "add :PreferenceSpecifiers:1:Key string VersionKey" "$dest_file"
/usr/libexec/PlistBuddy -c "add :PreferenceSpecifiers:1:DefaultValue string ${shortversionnumber}" "$dest_file"
/usr/libexec/PlistBuddy -c "add :PreferenceSpecifiers:1:Title string Version" "$dest_file"
/usr/libexec/PlistBuddy -c "add :PreferenceSpecifiers:1:Type string PSTitleValueSpecifier" "$dest_file"
/usr/libexec/PlistBuddy -c "add :PreferenceSpecifiers:2:File string Acknowledgements" "$dest_file"
/usr/libexec/PlistBuddy -c "add :PreferenceSpecifiers:2:Title string Acknowledgements" "$dest_file"
/usr/libexec/PlistBuddy -c "add :PreferenceSpecifiers:2:Type string PSChildPaneSpecifier" "$dest_file"
The key was the ${BUILT_PRODUCTS} variable.
So I was almost done with the build server. I setup 1 bot for the master branch that builds when code changes and another on the build branch that also builds when code changes; however, I only push to build when I’m ready for a build. The build branch also had to upload to TestFlight, so I researched that and came up with very convoluted solutions.
I created a second scheme for the TestFlight upload and set my build branch bot to use that scheme. Then I added a Post Action to the Archive phase that looks like this (make sure you select Provide Build Settings From “Your Target”):
API_TOKEN="API_TOKEN_HERE"
TEAM_TOKEN="TEAM_TOKEN_HERE"
DISTRIBUTION_LISTS="People"
IPA=/tmp/MyApp.ipa
rm -rf ${IPA}
/usr/bin/xcrun -sdk iphoneos PackageApplication -v ${ARCHIVE_PRODUCTS_PATH}${INSTALL_PATH}/${WRAPPER_NAME} -o ${IPA}
RELEASE_NOTES=`cat ${SRCROOT}/CurrentRelease.txt`
echo "*** Uploading ${IPA} to TestFlight ***"
/usr/bin/curl "http://testflightapp.com/api/builds.json" \
-F file=@"${IPA}" \
-F api_token="${API_TOKEN}" \
-F team_token="${TEAM_TOKEN}" \
-F distribution_lists="${DISTRIBUTION_LISTS}" \
-F notify=True \
-F notes="${RELEASE_NOTES}"
echo "TestFlight upload finished!"
I thought I was all done; builds worked and were uploaded to TestFlight. The moment of truth came when it was time to upload to the AppStore. I have push notifications enabled in this app and it is properly provisioning. After the upload, I got email saying that the ape-environment key wasn’t in the entitlements. Very strange, I checked and rechecked the build I got out of Xcode Server and it was correct. I tried a number of things and in the end sort of gave up. I took the archive that the build server created, put it on my main machine, imported into Xcode, exported for AdHoc distribution, resigned it using the same profile it was already signed with and submitted it. For some strange reason, this worked. I can live with that as I don’t submit to the app store all the time.
Things to note
- You do NOT have to create an AppStore distribution profile. You can submit the AdHoc build to the AppStore without problems. This is key as it lets you test the exact version going to the AppStore without rebuilding or resigning.
- You have to manually move your mobile provisioning profiles for AdHoc builds to your build server.
- You have to manually add your private key and certificate to the System Keychain on your build machine.
- Be careful about your build scripts as the paths are different on Xcode Server.
- You have to add your repositories from Xcode Server and not from the bot you create.
- Read the logs carefully when there is a problem.
- Apple appears to have created Xcode Server for testing and not necessarily for release builds; I hope some of this changes in the future. I’m not a huge fan of unit tests, so I may just be abusing the server by doing what I’m doing.
Overall, I’m pretty plea with having my own continuous integration server. It will catch issues where I have something different from Debug builds to Release builds. In addition, it will upload to TestFlight along with the release notes saving me time. I am a little disappointed about having to resign for the app store, but it isn’t a huge deal. I could probably modify my script to do that during the archive, but I really don’t like having to hard code in the ID of a mobile provisioning profile. I could probably check the profile into source control and reference that, but I’m OK with this solution. As I already want to move the archives to my main machine, the extra signing process isn’t a big deal.
I know that there is a lot here, but hopefully it helps someone else in the future (that person may be me when I forget what I’ve done!).