Site Color

Text Color

Ad Color

Text Color





Sign Up to Save Your Colors


Continuous Integration and Delivery of Enterprise iOS applications using CircleCI, fastlane and AWS… by@axelmoller

Continuous Integration and Delivery of Enterprise iOS applications using CircleCI, fastlane and AWS…

Axel Möller HackerNoon profile picture

Axel Möller

Background: Budbee is a last-mile logistics service offering home deliveries for e-commerce shoppers. We’re a tech startup based in Stockholm, Sweden.

At Budbee we have several iOS applications that are used by multiple stake holders, e.g. Drivers. Since we have to move in a fast pace in our development, we distribute these applications using Apples Enterprise distribution (There’s no way we can wait 1–4 weeks for a deploy).

I personally always thought that managing code signing assets and performing deploys of iOS applications is a hassle, so we decided to automate as much as possible of the process so we can focus on more important stuff.

We use CircleCI together with fastlane to continuously build and distribute our iOS applications to an S3 bucket. This bucket has a JSON file describing the latest version so we can prompt our users to upgrade if a new version is available. We use sentry for crash analytics, and our dSYMs are automatically uploaded when we deploy a new version.

Use match to manage code signing assets

Every developer needs to have his own set of private and public keys for development, and whoever does the distribution must have the distribution certificate on their machine. When setting up a new developer or migrating to a new machine you always manage to mess something up. Bleh, we just want to get our app out there — so we use match to manage our code signing assets for us. It keeps everything synced on git.

  1. Create a new private github repo in which you will store the profiles
  2. Create a new Apple ID to use for code-signing that will be shared between your team (e.g [email protected])
  3. Run match to generate new certificates and provisioning profiles
$ MATCH_FORCE_ENTERPRISE=”1" match enterprise

Match don’t allow Enterprise distribution by default, due to it’s dangerous nature (if your keys are out and about, anyone can misuse your company name) hence the MATCH_FORCE_ENTERPRISE variable. We made sure our github repo is private, only the iOS dev team has access to it, and they have 2-factor authentication on their accounts.

4. Follow the instructions in match, use your newly created Apple ID for authentication.

Setup CircleCI

In the root of your iOS project, create a circle.yml file and populate it with the template below:

version: 8.2
- gem update fastlane
- brew install getsentry/tools/sentry-cli
- 'vendor/bundle'
- 'budbee-driver/Pods'
- '~/.cocoapods'
- fastlane build
- fastlane test
branch: develop
- fastlane staging
tag: /v?[0-9]+(\.[0-9]+)*/
- fastlane deploy
- fastlane dsym

In our circle.yml we set the environment flag MATCH_FORCE_ENTERPRISEto allow enterprise distribution, set the Xcode version to 8.2 (Use whatever you need here), update to the latest version of fastlane and install sentry-cli (for uploading dSYMs)

You will need an OS X plan on CircleCI to build iOS/OSX projects. Follow your github repo and make sure starts a build using the circle.yml settings.

You need to populate environment variables in the CircleCI project, namely MATCH_PASSWORD (Your password to unlock the code signing certificate, which you chose when setting up match), S3_ACCESS_KEY, S3_SECRET_ACCESS_KEY (your AWS IAM keys that has access to the S3 bucket) and SENTRY_AUTH_TOKEN (an auth token generated by

We have separated our Fastfile with different lanes for compiling, testing and distributing. When we push code to any branch, the project is built and tested. When we push to the develop branch a staging version of the application is deployed to S3 and we push a new version tag (v1.2.3) a production build is deployed to S3 and the dSYM is uploaded to Sentry.

Setup fastlane


fastlane_version "2.17.1"
default_platform :ios
platform :ios do
lane :build do
desc "Runs all the tests"
lane :test do
workspace: 'budbee-app/myapp.xcworkspace',
scheme: 'myapp',
devices: ['iPhone 5s']
desc "Deploy new version to S3 bucket"
lane :deploy do
access_key: ENV['S3_ACCESS_KEY'],
secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
bucket: 'budbee-apps',
region: 'eu-west-1',
ipa: 'myapp.ipa',
dsym: '',
app_directory: 'com.budbee.myapp',
upload_metadata: true,
html_template_path: 'fastlane/s3_ios_html_template.erb',
version_template_path: 'fastlane/s3_ios_version_template.erb',
version_file_name: 'app_version.json'
desc "Upload dSYM to Sentry"
lane :dsym do
auth_token: ENV['SENTRY_AUTH_TOKEN'],
org_slug: 'budbee-ab',
project_slug: 'budbee-app',
dsym_path: './'
desc "Deploy new staging version to S3 Bucket (staging)"
lane :staging do
access_key: ENV['S3_ACCESS_KEY'],
secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
bucket: 'budbee-apps',
region: 'eu-west-1',
ipa: 'myapp.ipa',
dsym: '',
app_directory: 'com.budbee.myapp.staging',
upload_metadata: true,
html_template_path: 'fastlane/s3_ios_html_template.erb',
version_template_path: 'fastlane/s3_ios_version_template.erb',
version_file_name: 'app_version.json'


app_identifier "com.budbee.myapp" # The bundle identifier of your app
apple_id "[email protected]" # Your Apple email address
team_id "[[DEV_PORTAL_TEAM_ID]]"  # Developer Portal Team ID


workspace "budbee-app/myapp.xcworkspace"
scheme "myapp"
export_method "enterprise"
output_directory "./"


git_url "[email protected]:budbee/ios-certificates.git"
type "enterprise"
readonly true
app_identifier ["com.budbee.myapp"]
username "[email protected]" # Your Apple Developer Portal username

Install the needed fastlane plugins

$ fastlane add_plugin aws_s3
$ fastlane add_plugin sentry


<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Install <%= title %></title>
<style type="text/css">
* {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
text-align: center;
background-color: #ffffff;
.oneRow {
width: 100%;
overflow: auto;
overflow-y: hidden;
white-space: nowrap;
text-align: center;
.download {
margin: 30px;
font-size: 130%;
a {
text-decoration: none;
color: blue;
a:hover {
text-decoration: underline;
#finished { display: none; }
#tutorial {
text-align: center;
max-width: 300px;
#logo {
text-align: center;
max-width: 150px;
margin-top: 10px;
<h1 style="text-align: center;"><%= title %></h1>
<div class="oneRow">
<span class="download" id="ios">
<a href="itms-services://?action=download-manifest&url=<%= plist_url %>" id="text" class="btn btn-lg btn-default" onclick="document.getElementById('finished').id = '';">
Install <%= title %> <%= bundle_version %>
<h3 id="desktop">Please open this page on your iPhone!</h3>
<div id="finished">
<p>App is being installed. You may close Safari.</p>
<img src="" id="logo" />
<script type='text/javascript'>
if (/iPhone|iPad|iPod/i.test(navigator.userAgent))


"latestVersion": "<%= bundle_version %>",
"updateUrl": "itms-services://?action=download-manifest&url=<%= plist_url %>"

Keeping the app up-to-date

Upon start of our iOS application, it checks the S3 bucket if there is a new version available, and if so prompts the user to update.

$ pod install Alamofire
$ pod install Version


import Foundation
import Alamofire
import Version
@objc class UpdateManager: NSObject {

func checkForUpdate() {

let baseUrl = ""
let bundleIdentifier = Bundle.main.bundleIdentifier!
let url = "\(baseUrl)/\(bundleIdentifier)/app_version.json"

let currentVersion: Version = Version(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String)

Alamofire.request(url).responseJSON { response in
if let result = response.result.value {
let JSON = result as! NSDictionary

if let version = JSON.value(forKey: "latestVersion") {
let latestVersion: Version = Version(version as! String)
let updateUrl = JSON.value(forKey: "updateUrl") as! String
if (currentVersion < latestVersion) {
self.displayUpdateAlert(url: updateUrl)


func displayUpdateAlert(url: String) {
let alertController: UIAlertController = UIAlertController(title: "New Version", message: "There is a new App version available. You must update to continue", preferredStyle: .alert)

let button: UIAlertAction = UIAlertAction(title: "Update App", style: .default) { (action) in
UIApplication.shared.openURL(URL(string: url)!)


UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil)

That’s it! You should be able to visit the url to your S3 bucket on any iOS device and install the enterprise application. When you deploy a new version, you will be prompted to update right from within the application.

Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.
To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!