Code Wanderlust

Continuous Delivery with Ionic, iOS and TravisCI

Introduction

Sam Brown

Sam Brown

Product Architect at a small startup and Organizer of DC Continuous Delivery. Passionate about clean code, automation and helping teams deliver great software!


Continuous Delivery with Ionic, iOS and TravisCI

Posted by Sam Brown on .
Featured

Continuous Delivery with Ionic, iOS and TravisCI

Posted by Sam Brown on .

Repeatable and Reliable

If you're going to build something once, you're probably going to have to build it again. Automation has just become the norm on any project I start so when jumping into Ionic I knew I had to find a reliable and repeatable way to get the application to testers. This proved to be challenging in a variety of ways due to platforms, bugs and workarounds so I'm hoping that this blog will get others quickly past some of the obstacles I found.

TLDR; Hopefully this will get you started in automatically deploying iOS Ionic applications.

Companion Example Repository: ionic-continuous-delivery-blog

Tools

Part 1: Setting up TravisCI

The gist of this whole thing is that you're going to use a TravisCI OSX image in order to build your iOS application using a combination of javascript and Xcode CLI commands. Hopefully this is presented in a way that is pretty easy to follow. I'm going to start with the .travis.yml file for those that are impatient and then break it down into component parts.

.travis.yml

language: objective-c  
os: osx  
osx_image: xcode7  
before_install:  
- export LANG=en_US.UTF-8
- brew update
- npm install -g npm@2.14.5
- npm install -g grunt-cli cordova ionic
before_script:  
- ./scripts/decrypt-key.sh
- ./scripts/add-key.sh
script:  
- npm install
- grunt test
- grunt pushCoverage
- ionic platform add ios
- ionic plugin add cordova-plugin-app-version
- ionic plugin add cordova-plugin-network-information
- ionic plugin add ionic-plugin-keyboard
- ionic plugin add cordova-plugin-x-toast
- ionic plugin add phonegap-plugin-push
- ionic build ios --device --release
- ionic build ios --device --release
after_success:  
- ./scripts/package-and-upload.sh
notifications:  
  hipchat: XXXXX@Your Room
env:  
  global:
  - APP_NAME="YourApp"
  - DEVELOPER_NAME="iPhone Distribution: Company LLC (IOSAPPID)"
  - PROFILE_NAME="PROFILE_NAME"
  - PROFILE_UUID="11111-11111-1111111-111111-11111"
  - DELIVER_WHAT_TO_TEST="Awesome New Features!"
  - HOCKEYAPP_UPLOAD_BRANCH="master"

Travis Image

language: objective-c  
os: osx  
osx_image: xcode7  

The above lines set the image to use Mac OSX and Xcode7. You may be able to use other versions of Xcode but I prefer to stay on the latest to keep up with Apple.

Setting up the Machine

before_install:  
- export LANG=en_US.UTF-8
- brew update
- npm install -g npm@2.14.5
- npm install -g grunt-cli cordova ionic

Before install will set up your machine with some pre-build tools. In this instance I am updating homebrew to get the latest tools, installing a specific version of npm (use whichever is best for you), and then installing grunt-cli, cordova and ionic commandline tools. All of these tools are needed later for my build. (grunt-cli since I will be using grunt).

Decrypting and Adding Apple Certs

SECURITY SECURITY SECURITY
Repeat it a couple more times if you haven't already... Many of the items you will be using in this build should be encrypted since they will be checked into a GIT repository. Luckily travis' cli tool has a lot of great encryption utilities built-in including encrypting environment variables. The first thing you'll need to do (manually) is to set up your travis secret key and the private key password for your Apple certs. You can accomplish that by typing the following from the command line:

travis encrypt "ENCRYPTION_SECRET=23409823098t56029384" --add  
travis encrypt "KEY_PASSWORD=23042fj2389uf2323r" --add  

Those are obviously made-up values so please create your own and then make sure you put them somewhere safe for future use. Also note that the KEY_PASSWORD should be used as the password for your apple certs so that you can add them to the keychain. You will re-use your ENCRYPTION_SECRET to encrypt any files that you need travis to decrypt later.

.travis.yml before_script snippet

before_script:  
- ./scripts/decrypt-key.sh
- ./scripts/add-key.sh

In order to build your application you will need your Apple distribution certificates on the local keychain of the OSX image. In order to check these files (certs, provisioning profiles) in to be used in the build it is HIGHLY RECOMMENDED that you encrypt them. I used the travis cli tool to encrypt them with a secret key and then they are decrypted using the decrypt-key.sh script on the OSX image.

Once decrypted, the keys are then added to the the OSX keychain using the add-key.sh script. Additionally, I also move the decrypted provisioning profile to the location that Xcode tools expect it.

decrypt-key.sh

#!/bin/sh

if [[ -z "$ENCRYPTION_SECRET" ]]; then  
    echo "Error: Missing encryption secret."
    exit 1
fi

if [[ -z "$PROFILE_NAME" ]]; then  
    echo "Error: Missing provision profile name"
    exit 1
fi

if [[ ! -e "./scripts/profile/$PROFILE_NAME.mobileprovision.enc" ]]; then  
    echo "Error: Missing encrypted provision profile"
    exit 1
fi

if [[ ! -e "./scripts/certs/dist.cer.enc" ]]; then  
    echo "Error: Missing encrypted distribution cert."
    exit 1
fi

if [[ ! -e "./scripts/certs/dist.p12.enc" ]]; then  
    echo "Error: Missing encrypted private key."
    exit 1
fi

openssl aes-256-cbc \  
-k "$ENCRYPTION_SECRET" \
-in "./scripts/profile/$PROFILE_NAME.mobileprovision.enc" -d -a \
-out "./scripts/profile/$PROFILE_NAME.mobileprovision"

openssl aes-256-cbc \  
-k "$ENCRYPTION_SECRET" \
-in "./scripts/certs/dist.cer.enc" -d -a \
-out "./scripts/certs/dist.cer"

openssl aes-256-cbc \  
-k "$ENCRYPTION_SECRET" \
-in "./scripts/certs/dist.p12.enc" -d -a \
-out "./scripts/certs/dist.p12"

add-key.sh

#!/bin/sh
if [[ -z "$KEY_PASSWORD" ]]; then  
    echo "Error: Missing password for adding private key"
    exit 1
fi

security create-keychain -p travis ios-build.keychain

security import ./scripts/certs/apple.cer \  
-k ~/Library/Keychains/ios-build.keychain \
-T /usr/bin/codesign

security import ./scripts/certs/dist.cer \  
-k ~/Library/Keychains/ios-build.keychain \
-T /usr/bin/codesign

security import ./scripts/certs/dist.p12 \  
-k ~/Library/Keychains/ios-build.keychain \
-P $KEY_PASSWORD \
-T /usr/bin/codesign

security set-keychain-settings -t 3600 \  
-l ~/Library/Keychains/ios-build.keychain

security default-keychain -s ios-build.keychain

mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles

cp "./scripts/profile/$PROFILE_NAME.mobileprovision" ~/Library/MobileDevice/Provisioning\ Profiles/  

Build Script

script:  
- npm install
- grunt test
- grunt pushCoverage
- ionic platform add ios
- ionic plugin add cordova-plugin-app-version
- ionic plugin add cordova-plugin-network-information
- ionic plugin add ionic-plugin-keyboard
- ionic plugin add cordova-plugin-x-toast
- ionic plugin add phonegap-plugin-push
- ionic build ios --device --release
- ionic build ios --device --release

The above is the meat of the build and will execute the steps to put together your Ionic application for release. The gist is:

  • NPM install all npm libraries
  • Run any tests you have for the application (I HIGHLY RECOMMEND having tests)
  • Add the iOS platform for Ionic
  • Install each plugin individually (1)
  • Build for iOS TWICE (2)

Notes on the above:
(1) For some reason having plugins installed via ionic platform add ios and the package.json had unpredictable results. Some plugin javascript would not be in the .appfile or sometimes the Objective-C code wouldn't be compiled in. Installing fresh each build has resulted in a predictable build. I also removed the plugins from my package.json.
(2) I found this on another blog but for some reason to get the build to create a full .app file I had to run the ionic build ios command twice. It didn't work just running it once so I went with it. YMMV.

Package and Upload

after_success:  
- ./scripts/package-and-upload.sh

The above call will invoke a script to package your application as an .ipa file via the xcrun tool. It will then upload your .ipa file to the service of your choosing. I am going to use hockeyapp in my example but you can also upload to TestFlight. I may get to that in a future blog.

package-and-upload.sh

#!/bin/sh
if [[ "$TRAVIS_BRANCH" != "$APPLE_TESTFLIGHT_UPLOAD_BRANCH" ]]; then  
  echo "This is not a deployment branch, skipping IPA build and upload."
  exit 0
fi

#####################
# Make the ipa file #
#####################
OUTPUTDIR="$PWD/platforms/ios/build/device"

xcrun -log -sdk iphoneos \  
PackageApplication -v "$OUTPUTDIR/$APP_NAME.app" \  
-o "$OUTPUTDIR/$APP_NAME.ipa"

/usr/bin/zip --verbose --recurse-paths "$OUTPUTDIR/$APP_NAME.dsym.zip" "$OUTPUTDIR/$APP_NAME.app.dsym"

#######################
# Upload to HockeyApp #
#######################
if [[ "$TRAVIS_BRANCH" == "$HOCKEYAPP_UPLOAD_BRANCH" ]]; then  
  if [[ -z "$HOCKEY_APP_ID" ]]; then
    echo "Error: Missing HockeyApp ID"
    exit 1
  fi

  if [[ -z "$HOCKEY_APP_TOKEN" ]]; then
    echo "Error: Missing HockeyApp Token"
    exit 1
  fi

  echo "At $HOCKEYAPP_UPLOAD_BRANCH branch, upload to hockeyapp."
  curl https://rink.hockeyapp.net/api/2/apps/$HOCKEY_APP_ID/app_versions/upload \
    -F status="2" \
    -F notify="0" \
    -F notes="$DELIVER_WHAT_TO_TEST" \
    -F ipa="@$OUTPUTDIR/$APP_NAME.ipa" \
    -F dsym="@$OUTPUTDIR/$APP_NAME.dsym.zip" \
    -F commit_sha="$TRAVIS_COMMIT" \
    -H "X-HockeyAppToken: $HOCKEY_APP_TOKEN"

  if [[ $? -ne 0 ]]; then
    echo "Error: Fail uploading to HockeyApp"
    exit 1
  fi
fi  

Part 2: Ionic Project Preparation

In this section I'm going to walk through some of the set up needed to make sure you get a correct production build out of the project WITHOUT having to use Xcode which is part of the goal of continuous delivery...no manual steps

Specifying Provisioning Profiles via build-release.xcconfig

In order to build your iOS application you need specific settings in your platforms/ios/cordova/build-release.xcconfig file so that the build will include the correct provisioning profile as well as CODE_SIGN_IDENTITY.

To accomplish this I created an after_platform_add hook file that updates the values with values from my environment variables. Here is the script:

#!/usr/bin/env node

/**
 * Update build-release.xcconfig with correct profile and developer values for app signing
 */
var path = require('path');  
var fs = require('fs');  
var PROFILE_UUID_TEMPLATE_VAL = '%PROFILE_UUID%';  
var PROFILE_UUID_ENV_VAR = process.env.PROFILE_UUID;  
var DEVELOPER_NAME_REGEX_GLOBAL = /%DEVELOPER_NAME%/g;  
var DEVELOPER_NAME_ENV_VAR = process.env.DEVELOPER_NAME;

var xcconfigFinal = path.resolve(__dirname, '../../platforms/ios/cordova/build-release.xcconfig');  
var xcconfigTemplate = path.resolve(__dirname, '../../scripts/xcconfig/build-release.xcconfig.template');

fs.readFile(xcconfigTemplate, 'utf8', function (err,data) {  
  if (err) {
    return console.log(err);
  }

  var result = data.replace(PROFILE_UUID_TEMPLATE_VAL, PROFILE_UUID_ENV_VAR)
    .replace(DEVELOPER_NAME_REGEX_GLOBAL, DEVELOPER_NAME_ENV_VAR);

  fs.writeFile(xcconfigFinal, result, 'utf8', function (err) {
    if (err) return console.log('No directory Found for cordova iOS! Skipping xcconfig creation. ',err);
    console.log('Cordova iOS build-release.xcconfig updated with profile and developer values.')
  });
});

The script uses a template file to replace tokens with the values from environment variables. You can find the template for this file in the github companion repo for this blog post. Also feel free to add any other values you want included in your release.

Remove ResourceRules.plist from build.xcconfig

NOTE This is only if you are building in XCode 7

In XCode 7 you no longer need to specify the ResourceRules.plist location in your build.xcconfig file and if you do, you will probably get an error. Much like in the last section I created an after_platform_add hook that will replace the default file with a template. Here is the script

#!/usr/bin/env node

/**
 * Replace build.xcconfig with template that doesn't include ResourceRules.plist to
 * fix upload to iTunes error starting with XCode 7
 */
var path = require('path');  
var fs = require('fs');

var xcconfigFinal = path.resolve(__dirname, '../../platforms/ios/cordova/build.xcconfig');  
var xcconfigTemplate = path.resolve(__dirname, '../../scripts/xcconfig/build.xcconfig.template');

fs.readFile(xcconfigTemplate, 'utf8', function (err,templateFile) {  
  if (err) {
    return console.log(err);
  }

  fs.writeFile(xcconfigFinal, templateFile, 'utf8', function (err) {
    if (err) return console.log('No directory Found for cordova iOS! Skipping xcconfig creation. ',err);
    console.log('Cordova iOS build.xcconfig replaced successfully.')
  });
});

The template file can be found in the companion github repo for this blog post.

App Transport Security

The new iOS 9 and Xcode 7 builds require you to include NSAppTransportSecurity dictionary values in your application-Info.plist file. At this time I am allowing arbitrary URL loads so I have a bash script that adds these values to the plist file. You can modify this script to add whichever dict values you need. It makes use of the PListBuddy tool to set the values correctly.

Once again I have this script in the after_platform_add hooks folder

#!/bin/bash

PLIST=platforms/ios/*/*-Info.plist

cat << EOF |  
Add :NSAppTransportSecurity dict  
Add :NSAppTransportSecurity:NSAllowsArbitraryLoads bool YES  
Add :UIRequiresFullScreen bool YES  
EOF  
while read line  
do  
    /usr/libexec/PlistBuddy -c "$line" $PLIST
done

true  

I also believe there is now a cordova plugin for this but I have not tried it myself.

Wrap Up

Once you have the above in place you should only have to push your code to github and have a travisci webhook to kick things off!

I hope that those who come across this post find it useful. I took quite awhile to work through a few of the issues and make this a consistent process but I am very happy with the results and the ability to distribute the app any time I check in code.

References

I am still working through my bookmarks but I hope to post references to all of the helpful blogs that made this possible. If you see any of your work here, please let me know in the comments so that I can attribute you properly!

Sam Brown

Sam Brown

Product Architect at a small startup and Organizer of DC Continuous Delivery. Passionate about clean code, automation and helping teams deliver great software!

View Comments...