An iOS (and Android) Internal Build Distribution Solution

The Problems

If you build apps for a company that has a QA team, or any kind of internal testing process, you've probably tried TestFlight or one of several 3rd party build distribution offerings. You've probably also found this to be a bit tedious and not very friendly for the user. For those of you that have an Apple Developer Enterprise certificate, it's even more clumsy to require users to sign up for an account just to download your app, since the big point of the enterprise cert is that you can build for unlimited new and different users with less upfront (UDID) restriction.

Managing releases is also not so clear on these platforms. Creating new uploads for each build or version generates clutter quickly. At Wellframe, we have two test channels: one longer-term, low intensity build that all employees can download, and a short-term (~1 week), high-intensity build that our QA team drills before every release. In addition, we have a build that uses our staging environment, but points to the current release, so we can debug live issues. Since we manage release tags in git anyway, and the project itself contains the build version information, it seemed like busywork to be manually naming, updating, and sending out links to new builds as we developed.

The Solution

We use Jenkins to handle our builds, since it has tons of plugins for additional functionality, but it doesn't matter what you use as long as they allow you to run custom scripts (and for iOS, you will need to be running OS X, as usual). You could even write your own build server without nearly as much trouble as it sounds.

The main things you want are:

  1. An automated build generator (Jenkins plugins, custom script with xcodebuild, etc)
  2. A web server to serve your users the build download page
  3. A script to upload your build artifacts (.ipa, .apk)
  4. Github Webhooks (or similar from your SCM host) to automatically trigger your build process

I created a schema to represent builds on the web server, so our build machine can send it the relevant data. It consists of a build-index.json file that lives in the index folder of the download page, and then subdirectories for each build that contain a metadata.json file.

The build-index.json looks like this:

[
    "QA",
    "AppStore",
    "Dogfood"
]

where each entry is a unique "id" directly corresponding to the name of the folder the build lives in. This allows you to have "channels", where you simply overwrite the data in the directory with the newest build for that channel. As I wrote above, we have "QA", "AppStore", and "Dogfood" (which is our all-hands build…eat your own, you know).

And the metadata.json looks like this:

{
    "name": "QA Build",
    "version": "2.2.3",
    "jenkins-build-num": "77",
    "commit": "857e533949bf6c4506f199be245e0bb8429b6ece",
    "link": "https://your-build-page.com/QA/YourApp.plist"
}

You can customize this to include whatever information you think is relevant. The version and commit are grabbed from the project at build time. Our build server uploads the .plist and .ipa directly to the webserver via scp, so it knows the exact address for the link, but you could upload them via a web API and have the server generate the link if you want. (Side note: xcodebuild has a funky bug that requires manual manipulation of the .ipa).

I made a script that takes in arguments for the name, the "id" for the build-index.json, and other build-related things such as which Xcode scheme to build. For each "channel", I made a Jenkins job that is tied to a certain branch or tag in our repository.

Serving It Up

A coworker made a web page that reads the index and metadata files and presents links to the builds for our users. It is deployed as a single index.html, and looks like this:

Now when anyone needs to download the app, they just navigate their browser to the internal build site, and click on one of the links. No need to sign up for 3rd party accounts, download unrelated apps, or other extraneous steps.

This system was born out of necessity, to make it easy for our non-technical employees to get access to multiple internal versions of the app for testing. Right now it is a loose collection of two Ruby scripts, the code needed to build the web page, and some documentation. It could easily be bundled for reuse if you had multiple apps to build.

This kind of project really makes me appreciate the power of software, and is part of why I love programming. The quick & direct problem->resolution loop really hits the spot.

Update: I’ve sanitized my iOS build script and posted it here. It’s running from Jenkins, so it has access to Jenkins’ environment variables, such as $GIT_COMMIT.