DEV Community

George Jowitt
George Jowitt

Posted on

Deploying an iOS App with TravisCI & TestFlight

This article was originally posted here.

Travis CI is a hosted continuous integration and deployment system.

Continuous Integration (CI) is a development practice that requires developers to integrate code into a shared repository several times a day. Each check-in is then verified by an automated build, allowing teams to detect problems early.

So in lay terms, this deployment process that I have set up, starts with a git push, Travis then pulls my project from GitHub, builds it, and deploys it to TestFlight, and I've only written 1 command!

The reality of it is, is that it took me around 2 weeks and 52 git push's , so here are the notes I made on the subject, so hopefully it won't take you that long!

Before you start:

This guide assumes that you already have an iOS project ready to publish to TestFlight, that you have set up an app for it in iTunesConnect and that you have turned on TravisCI for the repo of your iOS Project in GitHub.

Initial Setup

Create a .travis.yml file

Travis CI looks for this .travis.yml file in your GitHub repo so it knows what scripts it needs to execute.

As a minimum, you need to specify the language for your project. Note that even if your project is written entirely in Swift, you will need to specify the language as Objective-C.

Create a .travis.yml file in the root of your project and add the following:

language: objective-c

The above should be enough to build your project, however, if you are building and pushing targets to different environments (e.g. development), you will want to specify what target to push to TestFlight, we will do this further down with a build.sh script.

App Signing

As we are wanting to put our Apps on TestFlight, and ultimately to the AppStore, we need to sign our app on Travis, and in order to do that we will need to create all of the necessary certificates and profiles. Later on, we will write some scripts to do this signing for us.

Certificates & Profiles

Apple Worldwide Developer Relations Certification Authority

If you already have this in your Keychain, you can just export this and save it in your project under scripts/certs/apple.cer. Alternatively, you can download this from the Apple page, by clicking this link.

iPhone Distribution Certificate + Private Key

Export this from Keychain Access and save the certificate in your project under scripts/certs/dist.cer. Then export the private key and save it into scripts/certs/dist.p12, enter a password of your choice.

Travis will need to know this password to access the profile, so after you have installed the Travis gem by running the following in the Terminal:

gem install travis

You can add the password to the Travis file by running the following (again in Terminal) in the project root.

travis encrypt "KEY_PASSWORD={password}" -add

This will add an encrypted environment variable called KEY_PASSWORD to your .travis.yml. It can then be used in any script executed by Travis CI.

Your .travis.yml file should now look something like this:

language: objective-c
osx_image: xcode9.3
env:
  global:
  - secure: {VERY LONG STRING}

Note, that I have also specified which version of Xcode to use, and also note that the order of elements in your .travis.yml file isn't important.

iOS Provisioning Profile (Distribution)

If you have not already, create a new distribution provisioning profile on developer.apple.com as follows:

Make sure that you select App Store on the first page as otherwise, it will not work when you try to use the provisioning profile later on.

Download the profile, and save it under scripts/profile e.g. scripts/profile/Distribution.mobileprovision.

Since we will need to access this profile in one of our scripts, we need to store the name as a global environment variable in .travis.yml. For example, if the file was Distribution.mobileprovision, our .travis.yml file should now look as follows:

language: objective-c
osx_image: xcode9.3
env:
  global:
  - |
      - APP_NAME="TestApp"
      - 'DEVELOPER_NAME="iPhone Distribution: Geojow Ltd (12345678)"'
      - PROFILE_NAME="Distribution"
  - secure: {VERY LONG STRING}

We have also added two more environment variables, APP_NAME, which is usually the same name as your Target. And DEVELOPER_NAME, which is what you see when checking your build settings on Xcode under Code Signing Identity > Release.

Note, if your GitHub repo is not private, you may want to encrypt your certificates and profiles. See the following link for information on how to do this:

https://www.objc.io/issues/6-build-tools/travis-ci/

Add Scripts

We will have 3 scripts once we have finished this section:

1. add-key.sh
2. build.sh
3. remove-key.sh

The names of these scripts are fairly self-explanatory, but we will explain what is happening as we go along.

Create all of these scripts in the scripts/ directory.

add-key.sh

#!/bin/sh
# Create a custom keychain
security create-keychain -p travis ios-build.keychain
# Make the custom keychain default, so xcodebuild will use it for signing
security default-keychain -s ios-build.keychain
# Unlock the keychain
security unlock-keychain -p travis ios-build.keychain
# Set keychain timeout to 1 hour for long builds
security set-keychain-settings -t 3600 -l ~/Library/Keychains/ios-build.keychain
# Add certificates to keychain and allow codesign to access them
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
# Set Key partition list
security set-key-partition-list -S apple-tool:,apple: -s -k travis ios-build.keychain
# Put the provisioning profile in place 
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
echo "My profile name is $PROFILE_NAME"
cp "./scripts/profile/$PROFILE_NAME.mobileprovision" ~/Library/MobileDevice/Provisioning\ Profiles/

Here we are creating a temporary keychain called ios-build this will contain all of the certificates that we need to code sign the app. Notice that on line 18 we are making use of our encrypted environment variable KEY_PASSWORD that we defined in .travis.yml.

In the final line, we are copying the mobile provisioning profile into the Library folder.

Once we have created this script, we need to make sure it has executable rights, so in terminal cd to the project root and run the following command.

chmod a+x scripts/add-key.sh

Now that certificates and profiles are imported we can sign our application.

build.sh

#!/bin/sh
if [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then
  echo "This is a pull request. No deployment will be done."
  exit 0
fi
security list-keychains -s ios-build.keychain
rm ~/Library/MobileDevice/Provisioning\ Profiles/$PROFILE_NAME.mobileprovision 
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles/
cp ./scripts/profile/$PROFILE_NAME.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
echo "*********************"
echo "*     Archiving     *"
echo "*********************"
xcrun xcodebuild -workspace TestApp.xcworkspace -scheme TestApp\ Stg -archivePath $ARCHIVE_NAME.xcarchive archive
echo "**********************"
echo "*     Exporting      *"
echo "**********************"
xcrun xcodebuild -exportArchive -archivePath $ARCHIVE_NAME.xcarchive -exportPath . -exportOptionsPlist ExportOptions.plist

Here we start by checking if this is a pull request if it is we want to stop the deployment!

Next, we need to place the correct provisioning profile in a location where it will be picked up on lines 10–12.

Then we are into archiving the build. Note that in this file I use $PROFILE_NAME and $ARCHIVE_NAME, these are global environment variables that I have added to my .travis.yml file to make it easier to manage to build to different environments. This is out of the scope of this How-To guide.

This is done on line 18. We output the archive inside the current repo so that we can easily pick it up in the next step to export it.

Before we export the archive and generate the .ipa file that will be pushed to iTunesConnect, we need to define a plist file with the ExportOptions.

<?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">
<plist version="1.0">
<dict>
    <key>method</key>
        <string>app-store</string>
    <key>teamID</key>
        <string>123456789</string>
    <key>uploadBitcode</key>
    <false/>
    <key>compileBitcode</key>
        <false/>
    <key>uploadSymbols</key>
        <true/>
    <key>signingStyle</key>
        <string>manual</string>
    <key>signingCertificate</key>
        <string>iPhone Distribution: Geojow Ltd</string>
    <key>provisioningProfiles</key>
        <dict>
            <key>co.geojow.TestApp</key>
            <string>Distribution</string>
        </dict>

</dict>
</plist>

This is fairly standard, we just need to make sure the method is 'app-store' and that we have all of our relevant provisioning profiles in here.

Then the script will export the app with the above options and we are ready to push it to iTunes Connect!

Before we do that, however, we should remove the temporary keychain with the following script.

remove-key.sh

#!/bin/sh
security delete-keychain ios-build.keychain
rm -f "~/Library/MobileDevice/Provisioning Profiles/$PROFILE_NAME.mobileprovision"

This script simply removes the temporary keychain and deletes the mobile provisioning profile. It is not strictly necessary but it helps when testing locally.

Now we can push to TestFlight!

Publishing to iTunesConnect (TestFlight)

Now we add the following to the bottom of build.sh. This navigates into the folder where altool is located and executes the command.

Note that it requires a username and password, but they need to be encrypted, to add these to the Travis file, execute the following commands in the project root directory.

travis encrypt "USERNAME={your-apple-account-email}" -add
travis encrypt "PASSWORD={a-app-specific-password}" --add

Note you can generate an app-specific password at appleid.apple.com.

#!/bin/sh
if [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then
  echo "This is a pull request. No deployment will be done."
  exit 0
fi
security list-keychains -s ios-build.keychain
rm ~/Library/MobileDevice/Provisioning\ Profiles/$PROFILE_NAME.mobileprovision 
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles/
cp ./scripts/profile/$PROFILE_NAME.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
echo "*********************"
echo "*     Archiving     *"
echo "*********************"
xcrun xcodebuild -workspace TestApp.xcworkspace -scheme TestApp\ Stg -archivePath $ARCHIVE_NAME.xcarchive archive
echo "**********************"
echo "*     Exporting      *"
echo "**********************"
xcrun xcodebuild -exportArchive -archivePath $ARCHIVE_NAME.xcarchive -exportPath . -exportOptionsPlist ExportOptions.plist
echo "************************************"
echo "*     Upload to iTunesConnect      *"
echo "************************************"
ln -s "/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool" /usr/local/bin/altool
ln -s "/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/itms" /usr/local/bin/itms #itms is needed, otherwise altool will not work correctly
altool --upload-app -f "$IPA_NAME.ipa" -u $USERNAME -p $PASSWORD

Now you need to update your Travis file to run the right script at the right time!

Update .travis.yml file

Add the following to your .travis.yml file. Note it won't look exactly like this, but it's a start!

after_script:
- "./scripts/remove-key.sh"
before_script:
- "./scripts/add-key.sh"
branches:
  only:
  - master
  - development
env:
  global:
  - |
      - APP_NAME="TestApp"
      - 'DEVELOPER_NAME="iPhone Distribution: Geojow Ltd (12345678)"'
      - PROFILE_NAME="Dev_Distribution"
      - IPA_NAME="TestApp"
      - ARCHIVE_NAME="TestApp_Dev"
  - secure: {VERY LONG STRING}
  - secure: {VERY LONG STRING}
  - secure: {VERY LONG STRING}
language: objective-c
osx_image: xcode9.3
script:
- "./scripts/build.sh"

Now when you push to GitHub, Travis should archive, export and push your app to iTunesConnect, it is then available to pass on to Test Users on TestFlight.

Related articles

For more info, see https://www.objc.io/issues/6-build-tools/travis-ci/.

Top comments (0)