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.
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.
$ 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.
In the root of your iOS project, create a circle.yml file and populate it with the template below:
machine:environment:MATCH_FORCE_ENTERPRISE: "1"xcode:version: 8.2
dependencies:pre:- gem update fastlane- brew install getsentry/tools/sentry-clicache_directories:- 'vendor/bundle'- 'budbee-driver/Pods'- '~/.cocoapods'
compile:override:- fastlane build
test:override:- fastlane test
deployment:staging:branch: developcommands:- fastlane staging
release:tag: /v?[0-9]+(\.[0-9]+)*/commands:- fastlane deploy- fastlane dsym
In our circle.yml we set the environment flag MATCH_FORCE_ENTERPRISE
to 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 sentry.io)
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.
fastlane/Fastfile:
fastlane_version "2.17.1"
default_platform :ios
platform :ios do
lane :build domatchgymend
desc "Runs all the tests"lane :test doscan(workspace: 'budbee-app/myapp.xcworkspace',scheme: 'myapp',devices: ['iPhone 5s'])end
desc "Deploy new version to S3 bucket"lane :deploy doaws_s3(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: 'myapp.app.dSYM.zip',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')end
desc "Upload dSYM to Sentry"lane :dsym dosentry_upload_dsym(auth_token: ENV['SENTRY_AUTH_TOKEN'],org_slug: 'budbee-ab',project_slug: 'budbee-app',dsym_path: './myapp.app.dSYM.zip')end
desc "Deploy new staging version to S3 Bucket (staging)"lane :staging doaws_s3(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: 'myapp.app.dSYM.zip',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')endend
fastlane/Appfile:
app_identifier "com.budbee.myapp" # The bundle identifier of your appapple_id "[email protected]" # Your Apple email address
team_id "[[DEV_PORTAL_TEAM_ID]]" # Developer Portal Team ID
fastlane/Gymfile:
workspace "budbee-app/myapp.xcworkspace"scheme "myapp"export_method "enterprise"
output_directory "./"
fastlane/Matchfile:
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
fastlane/s3_ios_html_template.erb:
<html><head><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></head><body><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;}</style>
<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 %></a></span></div>
<h3 id="desktop">Please open this page on your iPhone!</h3>
<div id="finished"><p>App is being installed. You may close Safari.</p></div>
<img src="https://company.com/logo.png" id="logo" /></body>
<script type='text/javascript'>if (/iPhone|iPad|iPod/i.test(navigator.userAgent)){document.getElementById("desktop").remove()}else{document.getElementById("ios").remove()}</script></html>
fastlane/s3_ios_version_template.erb:
{"latestVersion": "<%= bundle_version %>","updateUrl": "itms-services://?action=download-manifest&url=<%= plist_url %>"}
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
UpdateManager.swift:
import Foundationimport Alamofireimport Version
@objc class UpdateManager: NSObject {
func checkForUpdate() {
let baseUrl = "[https://budbee-apps.s3-eu-west-1.amazonaws.com](https://budbee-enterprise-apps.s3-eu-west-1.amazonaws.com)"
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)!)
}
alertController.addAction(button)
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!