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

Written by axelmoller | Published 2017/03/11
Tech Story Tags: aws | continuous-integration | ios | continuous-delivery | budbee

TLDRvia the TL;DR App

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 ios-dev@company.com)
  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:

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_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 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.

Setup fastlane

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 "ios-dev@company.com" # 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 "git@github.com:budbee/ios-certificates.git"

type "enterprise"

readonly true

app_identifier ["com.budbee.myapp"]username "ios-dev@company.com" # 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 %>"}

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

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!


Published by HackerNoon on 2017/03/11