Adding Continuous Integration to a Swift project

Continuous Integration (or CI for short) is one of those buzz words that have become really popular lately. It’s the idea that instead of having long-lived feature branches that you use to work on a new feature, you continuously integrate your new code into the project’s main branch (which is usually master or develop if you work with Git). Every time you do so you build the project and run its tests, to make sure everything is in order before merging the change in.

This week, let’s take a look at how we can use CI to establish a smooth and nice workflow for our team working on a Swift project.

CI is done on a server that is separate from any team member’s working machine. This adds a layer of “quality assurance” (no more “It works on my machine!” arguments 😅) and makes sure that the project builds on a “clean” setup - which has the added benefit of making on-boarding new team members easier.

The most common options for CI solutions

These are the most commonly used solutions for running a CI server that’s used for Swift projects today:

  • Travis CI, which is a cloud-based solution that gives you a VM to build your code in. You pick your environment and then you run your build commands using tools like xcodebuild, swift build, or fastlane. It’s free for open source projects, and there’s also several paid tiers for closed source ones.
  • Circle CI, very similar to Travis in that they also offer a cloud-based VM for you to build in, and give you access to a command line where you can run your build tools. However, it’s not free for open source projects (unless you’re only building on Linux) - but they do offer a 2 week free trial.
  • Buddy Build, that takes a different approach to Travis & Circle, in that they don’t require you to run any build commands manually, and instead infer how your project should be built from its structure - and does so automatically. Like Travis, it’s free for open source.
  • Jenkins, if you’re the kind of person who likes to build things yourself (no shame in that, it can make for an interesting weekend project 😀), Jenkins is usually the way to go. It’s basically a package you install on your own hardware (typically a Mac Mini in someone’s closet), which gives you a CI server you can configure to your heart’s content.
  • Xcode Server/Bots is Apple’s official way to do CI for Swift & Objective-C projects. It does provide some sweet integration with Xcode, and has a very nice UI, but I’ve yet to hear anyone deploy it successfully on any significantly complex project 😅 Please let me know if you’ve heard otherwise though! 👍

Picking the right solution

So, which solution is the best one? My kind of boring non-answer to that question is: “it depends” 🙂 Like most tech decisions, it comes down to applying a healthy mix between requirements and personal taste.

The good news is that almost all of the above mentioned solutions have a free tier, so before fully investing in any given solution - you can always try a few of them out to see which one fits your project best.

But, I do want to provide a quick guide to setting some of these solutions up - so for the purpose of this post I’m going to focus on Travis and Buddy Build. They are interesting to compare, because they take very different approaches, and are easy enough to setup that I can recommend them to anyone, irregardless of how much you like to tinker with these type of things.

So, let’s dive in! 🚀

Travis

Like mentioned above, Travis essentially gives you a command line to run your build tools on. Configuration is done through a .travis.yml file that you put in your project’s repository, and once you sign up for Travis and activate it for your project, it will automatically pick up any such .yml file and run your CI based on it.

Here’s an example of a simple .travis.yml file that uses xcodebuild (which is the command line interface to Xcode’s build system) to build & test an iOS project:

language: objective-c
osx_image: xcode8.3
script:
    - xcodebuild clean test -project MyApp.xcodeproj -scheme MyApp -destination "platform=iOS Simulator,name=iPhone 7,OS=10.3" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO -quiet

The above command is a bit of a mouthful, so let’s break it down:

  • clean test tells xcodebuild to first clean the project (you often want to start from a clean slate in CI, to avoid false positives and flakiness) and then build it and run its tests.
  • -project MyApp.xcodeproj is where we supply the path to our Xcode project (if you’re using CocoaPods, you’d use the project’s Workspace instead, and supply it using -workspace MyApp.xcworkspace).
  • -scheme MyApp is which scheme you want to build & test. Make sure you share the scheme by ticking the “Shared” box in the “Manage schemes” dialog in Xcode, to make the scheme accessible to CI.
  • The -destination argument and the string that follows it tells xcodebuild which platform we want to run on. In this case we pick an iPhone 7 simulator running iOS 10.3.
  • CODE_SIGN_IDENTITY="" and CODE_SIGNING_REQUIRED=NO allows us to override the code signing settings that we have set up in Xcode. This is very useful, since it enables our CI server to build & test without code signing, while still not modifying our Xcode project.
  • ONLY_ACTIVE_ARCH=NO makes sure that we build for all architectures, not only for the iOS simulator. This is to make sure we don’t have any architectural-specific build errors in our project.
  • Finally, -quiet reduces the otherwise super-verbose output of xcodebuild to only include warnings and errors. This is very nice for when you need to dig into the output of any build log for debugging, so that you don’t have to go through thousands of lines of output.

There are of course alternatives to using xcodebuild directly, like using xctool or fastlane (both of which come pre-installed on any Travis macOS VM). In the case of fastlane you can simply execute a given lane that you have defined in your Fastfile, making your .travis.yml look something like this:

language: objective-c
osx_image: xcode8.3
script:
    - fastlane MyTestLane

Even though Travis requires you to set up things manually, and it can be a bit unstable and slow sometimes (especially for open source projects, but hey, it’s free - so I’m not going to complain 😅), I really like it. It is widely used in the community, which means that there’s tons of help to be found in case you can’t figure something out.

BuddyBuild

Now, let’s take a look at BuddyBuild, which entered my radar after attending a couple of talks about it by CEO Dennis Pilarinos. In his talks, Dennis does a very impressive demo of the platform, where he essentially sets it all up in a matter of minutes. You can watch one of his talks from #Pragma Conference 2016 here (which will give you a more complete overview of all their features, I’ll focus on the CI side of things for this post).

Like mentioned in the overview above, BuddyBuild relies on inference instead of requiring you to set up any configuration files in your repo. Most developers (including myself) will probably be very skeptical to this approach at first, we know how hard inferring things like this correctly can be, so you might think that it’ll be extremely flaky and error prone.

But it turns out, BuddyBuild works amazingly well. From my own personal experience with the platform, and from talking to people like Arthur A. Sabintsev (who is the lead iOS developer at the Washington Post), BuddyBuild is the most stable CI platform that I’ve used so far.

To give you an example, at Hyper, we last week made over 100 builds on BuddyBuild in a fairly complex project - none of which failed due to infrastructure problems. If you’ve (like me) spent quite some time battling strange "Exit code 65" errors on CI before, this will be a breath of fresh air.

For iOS apps, there’s not much of a tutorial needed in order to get started with BuddyBuild. Their UI will walk you through setting things up and will ask you for any information required. It “just works”. But, sometimes you do need/want to dive in and customize things with your own build scripts. When I set up Marathon with BuddyBuild, I did find myself in need of doing such customization, since it uses the Swift Package Manager as its build system.

So, here’s how to easily set up a Swift Package Manager-based project using BuddyBuild:

1. Generate an Xcode project post clone:

In order to do its thing, BuddyBuild needs an Xcode project. But when using the Swift Package Manager, you are encouraged not to keep an Xcode project in your repository, and instead ad-hoc generate it when needed, so that’s what we need to do.

Add a buddybuild_postclone.sh file to your repo, with the following contents:

#!/usr/bin/env bash

swift package generate-xcodeproj

The above script will be run after your repo has been cloned, but before any build begins.

2. Run your tests post build

BuddyBuild will automatically build your project, but it won’t run its tests when the Swift Package Manager is used. So we’ll need to do that manually by invoking swift test in a custom script file.

Add a buddybuild_postbuild.sh file (a shell script that will be run after the standard build finishes) to your repo, with the following contents:

#!/usr/bin/env bash

swift test

And that’s it 🎉

Conclusion

As you can probably tell from reading the above, I’ve become quite a big fan of BuddyBuild. I especially like how easy it is to setup and manage, especially when dealing with many projects like I do - both at work and when doing open source. Eventually I’ll probably move all of my projects over to it, but for now I’m also using Travis quite a lot and am quite happy with that too.

But I do encourage you to find out for yourself. Try, experiment, and share your findings! 👩‍🔬👨‍🔬 I’m not using CI for all of my projects, but for any that at least has more people than just me working on it, I definitely think it’s worth the effort setting it up. Having that additional check to make sure that your project remains in a buildable & testable state is super valuable, especially when working in a team.

Do you have any favorite way of doing CI that I didn’t mention in this post, or any other questions, comments or feedback? I’d love to hear from you! 👍 Either post a comment here, or feel free to contact me on Twitter or email.

Thanks for reading 🚀

Using lazy properties in Swift

Picking the right way of failing in Swift