How to configure TravisCI free for multi project repos

Travis CI is a great hosted continuous integration service for Github repositories. Especially for fast tests. While I was hooking Firebase iOS quickstarts with Travis CI, I had to implement some hacks.

Because we,

  • have multiple projects inside 1 repo,
  • use CocoaPods 1.0 (default is 0.5),
  • include Google plist file into each project at test,
  • update info.plist of each project at test,
  • parallelize builds across VMs,
  • cache bundler and dependencies (pods).

The Build Lifecycle

A build on Travis CI is made up of two steps:

  • install: install any dependencies required
  • script: run the build script

You can run custom commands before the installation step (before_install), and before (before_script) or after (after_script) the script step.

In a before_install step, you can install additional dependencies required by your project such as Ubuntu packages or custom services. So here I,
  • install CocoaPods 1.0 (default is 0.5)
  • get into each example folder and install pods
  • copy in a mock Google plist file
  • update Google plist and info.plist with sed
  • add Google plist file into each target. (using ruby and xcodeproj)
- gem uninstall cocoapods -a 
— gem install cocoapods -v ‘1.1.1’
— gem install xcpretty
— cd $SAMPLE
— pod install — repo-update
— cp ../mock-GoogleService-Info.plist ./GoogleService-Info.plist
— sed -i ‘’ ‘/<key>BUNDLE_ID</{n;s/id/com.google.firebase.quickstart.’$SAMPLE’Example/;}’ GoogleService-Info.plist
— sed -i ‘’ ‘s/YOUR_REVERSED_CLIENT_ID/com.googleusercontent.apps.123456789000-hjugbg6ud799v4c49dim8ce2usclthar/’ “$SAMPLE”Example/Info.plist
— gem install xcodeproj
— ruby ../info_script.rb $SAMPLE
— cd -
Then in script, I run the xcodebuild

before_install and script are merged in execution and executed for each
entry in the build matrix. Your environment variables you set with
env: is accessible to both.

Parallelizing your builds across virtual machines

To speed up a test suite, you can break it up into several parts using
Travis CI’s build matrix feature.

Say you want to split up your unit tests and your integration tests into two
different build jobs. They’ll run in parallel and fully utilize the available
build capacity for your account.

Here’s an example on how to utilize this feature in your .travis.yml:

env:
— TEST_SUITE=units
— TEST_SUITE=integration
Here I use the “SAMPLE” variable for each sample name. That helps me to go into each folder, set their bundle ids respectively, and find out project and scheme names to build.
env: SAMPLE=AdMob

Caching directories (Bundler, dependencies)

With caches, Travis CI can persist directories between builds. This is
especially useful for dependencies that need to be downloaded and/or compiled from source.

Build phases

Travis CI attempts to upload cache after script, but before either after_success or after_failure is run. Note that the failure to upload the cache does not mark the job a failure.

Bundler

On Ruby and Objective-C projects, installing dependencies via Bundler can make up a large portion of the build duration. Caching the bundle between builds drastically reduces the time a build takes to run.

The logic for fetching and storing the cache is described below.

Enabling Bundler caching

Bundler caching is automatically enabled for Ruby projects that include a
Gemfile.lock. (Bundler caching is not yet enabled automatically) You can explicitly enable Bundler caching in your .travis.yml:

language: ruby
cache: bundler

Whenever you update your bundle, Travis CI will also update the cache.

CocoaPods

On Objective-C projects, installing dependencies via CocoaPods can take up a good portion of your build. Caching the compiled Pods between builds helps reduce this time.

If you want to enable both Bundler caching and CocoaPods caching, you can list them both:

language: objective-c
cache:
- bundler
- cocoapods

Determining the Podfile path

By default, Travis CI will assume that your Podfile is in the root of the
repository. If this is not the case, you can specify where the Podfile is
like this:

language: objective-c
podfile: path/to/Podfile
To be able to cache the pods:
- I had to specify podfile for each sample. That’s why I had to specify it inside the build-matrix.
- I tried use podfile: $SAMPLE/Podfile but it wouldn’t pick up the
 environment variable, so I had to explicitly define it in the build-matrix.
- Instead of using just the env , I created the first sample(Admob) by
 default and added all entries into build matrix using the matrix: include:.
cache:
- bundler
- cocoapods
podfile: admob/Podfile
env: SAMPLE=AdMob
matrix:
include:
- podfile: analytics/Podfile
env: SAMPLE=Analytics
- podfile: authentication/Podfile
env: SAMPLE=Authentication

Results:

  • I set up right bundle-id and run pods for each sample.
  • I cache right bundler and pods for each sample.
  • All these tests run parallel, fast, well under travis time limit.
  • With caching & build matrix, my tests finish under 10 minutes instead of 1 hour 40 minutes as before.

PS: Recently, I had an issue with my tests failing with error 65, and the reason is that simulator not being ready in time. So I wrote a script to retry them and it worked like magic!

set -o pipefail && xcodebuild \
-workspace ${SAMPLE}/${SAMPLE}Example.xcworkspace \
-scheme ${SAMPLE}Example \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 7' \
build \
test \
ONLY_ACTIVE_ARCH=YES \
CODE_SIGNING_REQUIRED=NO\
| xcpretty

RESULT=$?
if [ $RESULT == 65 ]; then
echo "xcodebuild exited with 65, retrying"
set -o pipefail && xcodebuild \
...
...
else
exit $RESULT
fi

Further read: “How to run TravisCI locally on Docker”.