CI & CD for iOS Apps

tom-hartz - May 18 '20 - - Dev Community

Continuous Integration and Continuous Delivery have become pillars of modern development. Reporting breaking changes back to your dev team quickly can improve both the quality and the speed at which you deliver software. Likewise, automating the release pipeline for your deliverables cuts down on redundant labor and allows you to focus more time on meaningful work. This all applies in the realm of mobile application development, but there are some special considerations to account for with the iOS platform.

Building iOS applications requires Mac hardware. This means your development team will need MacBooks, but it also means to do CI/CD you will need a server running macOS. Recently on a project, I worked through setting up a Mac Mini machine as a Bamboo build agent to perform CI/CD tasks for both mobile platforms (Android build plans can be executed on any type of agent: Windows/Linux/Mac). In this article, I will share some of the issues I encountered as well as some of the concrete build plan steps.

Branch Detection

A standard CI setup is going to start with Branch Detection. Every time new code is pushed to a feature branch, the unit tests are executed against the changes. This part of your pipeline does not have to be restricted by platform, even if you are building a native iOS app and have a suite of XCTest cases in Swift. XCTest cases supposedly can be made to run in Linux, although I have never personally tried it. It is probably simpler and easier to set up this part on a Mac agent with Xcode anyway, and the other steps WILL require it. Branch Detection build plan steps are as follows:

  1. Source Code Checkout
  2. Run Unit Tests
  3. Check Code Coverage

Merging the feature branch into development/master should require a passing test suite and one or more code review approvals.

UI Testing

Typically after merging one or more feature branches, the next step for CI/CD is to create a build for automated UI tests and/or manual regression testing. Automated UI testing for iOS has some significant challenges! Using a device simulator is easier to manage, but you cannot install any given .ipa onto it, it has to be compiled specifically for the simulator CPU architectures.

iOS Device CPU architectures:

  • arm64 is the current 64-bit ARM CPU architecture, as used since the iPhone 5S and later (6, 6S, SE and 7), the iPad Air, Air 2 and Pro, with the A7 and later chips.
  • armv7s (a.k.a. Swift, not to be confused with the language of the same name), being used in Apple's A6 and A6X chips on iPhone 5, iPhone 5C and iPad 4.
  • armv7, an older variation of the 32-bit ARM CPU, as used in the A5 and earlier.

iOS Simulator CPU architectures:

  • i386 (i.e. 32-bit Intel) is the only option on iOS 6.1 and below.
  • x86_64 (i.e. 64-bit Intel) is optionally available starting with iOS 7.0.

This unfortunately means you cannot test an app bundle using a simulator, and then promote the same "artifact" for release. For iOS you have to do physical device testing, from which you can then promote an app bundle to production. Keep in mind that automated testing on tethered physical devices will require more overhead to maintain and keep the CI pipeline running.

Build

Producing an .ipa file from an automated build can be done in two steps:

1. Create an archive and sign

security unlock-keychain -p $PASSWORD login.keychain

xcodebuild -workspace 'MyProject.xcworkspace' -scheme 'MyProject' -configuration Debug -archivePath ./archive/'MyProject.xcarchive' clean archive -UseModernBuildSystem=NO DEVELOPMENT_TEAM=########## CODE_SIGN_IDENTITY="iPhone Developer: Tom Hartz (##########)" PROVISIONING_PROFILE='MyProject development profile' OTHER_CODE_SIGN_FLAGS='$HOME/Library/Keychains/login.keychain-db' CODE_SIGN_STYLE=Manual

2. Export the archive into an app bundle (.ipa)

xcodebuild -exportArchive -archivePath ./archive/MyProject.xcarchive' -exportPath 'archive/ipa' -exportOptionsPlist Development.plist

Development.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<dict>
    <key>compileBitcode</key>
    <false/>
    <key>method</key>
    <string>development</string>
    <key>provisioningProfiles</key>
    <dict>
        <key>com.leadingedje.myproject</key>
        <string>MyProject development profile</string>
    </dict>
</dict>
</plist>

After creating an app bundle, your build plan can save it as an artifact in Bamboo, and also upload it to TestFlight, HockeyApp/App Center, etc. for internal deployment and testing.

Artifact Promotion & Release to Prod

After regression testing has been signed off by your QA team, you will want the final step of your CD pipeline to pick up this artifact and release it to production. A release plan in Bamboo would be as follows:

1. Download Artifact
2. Modify for release
   a. Unzip .ipa bundle
   b. Replace dev provisioning profile with production
   c. Delete existing code signature
   d. Perform any custom app configuration (set Prod API url, etc.)
   e. Resign bundle using production entitlements
   f. Re-Zip bundle
3. Upload to Apple (App Store Connect)

Here is a shell script example I wrote that performs all of step 2:

echo "begin iOS release build..."

mkdir ipa

cp MyProject.ipa ipa/MyProject.zip

echo "copying provisioning profile..."

cp $HOME/Downloads/MyProject_Distribution_Provisioning_Profile.mobileprovision ./ipa

echo "unzipping ipa..."

cd ipa

unzip MyProject.zip

echo "unlocking keychain..."

security unlock-keychain -p $PASSWORD login.keychain-db

echo "replacing provisioning profile..."

cp "MyProject_Distribution_Provisioning_Profile.mobileprovision" "Payload/MyProject.app/embedded.mobileprovision"

echo "removing existing code signature..."

rm -rf Payload/MyProject.app/_CodeSignature/

echo "signing ipa..."

codesign --entitlements Entitlements.xml -f -s "iPhone Distribution: Leading EDJE LLC (##########)" Payload/MyProject.app

echo "zipping app bundle..."

zip -qr MyProject.ipa Payload/

echo "iOS build done!"

exit

Entitlements.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<dict>
    <key>application-identifier</key>
    <string>##########.com.leadingedje.myproject</string>
    <key>aps-environment</key>
    <string>production</string>
    <key>beta-reports-active</key>
    <true/>
    <key>com.apple.developer.team-identifier</key>
    <string>##########</string>
    <key>get-task-allow</key>
    <false/>
</dict>
</plist> 

DIY vs. Cloud CI

My experience and approach in this article is focused on an in-house custom solution for CI/CD in Bamboo, but there are third party services available which can provide some of these build actions for you. You may be able to subscribe to one or more of these services to avoid having to buy Mac hardware to run your builds and test suite:

DISCLAIMER: I have not tried any of these services, so your mileage with them may vary. While they may simplify things somewhat, I expect you will still need to configure them quite a bit with custom build steps and options, depending on the specifics of your iOS project.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .