Swift Talking is a blog updated every two weeks about Swift, tooling, Continuous Integration and other stuff written by Alex Salom.

Continuous delivery in iOS

Keeping an App deployable at any time is very important for a number of reasons. Deploying the app regularly somewhere (Fabric, TestFlight, etc.) means that a version will be available for anyone to test at any time and if this version is always the last version it will reduce the friction between internal consumers of the App (PO, QA, CEO, etc) and developers. I’ve worked in teams that didn’t have a continuous delivery system in place and most days one of the developers had to manually install the latest version on someone’s device. This meant a lot of hours lost into something that could be done automatically. If we are able to provide a downloadable build with the latest version at any time, developers will have more time for what the do best: implement new features and add new bugs.

The continuous delivery flow

In my team we use Gitflow, we solve every ticket in a separate branch and when we think it’s good enough we merge it back into master. Since we never commit anything directly to master, we can consider that every change in master means that we finished a task and when this happens, the continuous delivery process starts.

  1. We merge into master.
  2. GitHub notifies the CI: Travis CI in our case.
  3. Travis executes some stuff and calls one of our fastlane lanes
  4. fastlane tests the app, builds a new version and deploys it to (Fabric.
  5. Slack and JIRA are updated with the version number.

Let’s see a diagram of the flow when master gets modified.

Flow

Let’s take a look first at a simplified version of our .travis.yml configuration file:

os: osx
osx_image: xcode10.1
branches:
  only:
    - master
script:
  - bundle exec fastlane travis

Travis CI will only process a job if master is modified, or if we open a PR pointing to master. When this happens we’ll execute bundle exec fastlane travis and fastlane will be deciding what flow to follow. Let’s now see what happens inside the Fastfile.

lane :travis do
  if ENV['TRAVIS_PULL_REQUEST'] != 'false'
    pull_request
  elsif current_branch =~ /^master$/
    master # In our example, we'll execute this lane
  end
end

In this example we can perform two flows, one when we open a Pull Request and the second one, which we’ll see in more detail, when master gets modified. Here we could add as many flows as we want. We could have one flow that will be executed every time we push something into a branch named external/some_branch_name and this will build and deploy a special version for our external testers.

So, this is what happens when master gets modified:

private_lane :master do
  lint_and_test 
  deploy_debug_version
end

We lint the code, test the build and then deploy to Fabric.

I like how fastlane allows you to create such idiomatic CI flows. Look how small and declarative those lanes are. In reality, it’s thanks to Ruby, the language in which fastlane is written. Everything we write into the Fastfile is also Ruby, as lanes are simply Ruby blocks.

private_lane :lint_and_test do
  swiftlint # lint
  scan # test
end

private_lane :deploy_debug_version do
  set_build_number
  match # install certificates
  gym # build
  submit_fabric
  post_to_slack
  post_to_jira
end

When all the checks pass, we build a new version, deploy it to fabric, alert Slack and post a comment to Jira in the proper ticket with the build and version numbers so when someone goes into the ticket, they will know exactly which build to download. If your team has someone other than developers testing apps, it will increase the productivity of the whole team by avoiding making manual builds with Xcode.

The way to post into Slack and into Jira is pretty similar, let’s see how and what kind of comment we will be adding into Jira. The comment will contain the build and version numbers. First we are going to need a Jira user so fastlane can log in. Here I recommend creating a special tech user specifically for this purpose so we avoid having personal credentials managed by an automatic process.

For all of this to work we need to have the Jira ticket number somewhere inside the last commit message, from the change that triggered the build. This is how we’ll know where to post the comment. You can have a git-hook add the ticket number in each commit automatically.

private_lane :post_to_jira do |options|
  last_commit = `git log --pretty=oneline -1`
  ticket = jira_ticket(from_text: last_commit)

  next unless ticket

  jira(
    url: 'https://my-jira.atlassian.net/',
    username: 'tech.user@example.com',
    password: 'password',
    ticket_id: ticket,
    comment_text: build_information
  )
end

def jira_ticket(from_text:)
  result = from_text.match /.*?(?<story>[A-Z]+-[0-9]+)/
  result[:story].to_s unless result.nil?
end

def build_information
  "Version: #{current_version}\nBuild: #{get_build_number}"
end

def current_version
  get_version_number(xcodeproj: 'Project.xcodeproj', target: 'Project')
end

The rest of the snippet is pretty much self explanatory. We get the last commit message in the git history, we try to extract the Jira ticket number from it and if we succeed we’ll place a comment in the ticket with the build information details.

We can see this in action in this screenshot taken from crvsh’ Jira.

Flow

So what…

This flow has helped my team accomplish a complete separation of the build process between developers and testers. However we need to remember that each team is different and so are their needs. Before deciding on what flow to choose, we need to pay close attention as to which actions can be automated to ease the work of someone. Perhaps QA prefers a daily build: we should be ready to do that with a cron job. Maybe we have a team solely dedicated to sign and release applications to the store: we could create git tags and send notifications to the people in charge when we commit to a certain branch. Or maybe someone likes getting a signed .ipa file which can then be uploaded to the store: we could place an .ipa file in a Google Drive folder for that person to upload it.

The possibilities are infinite, but we need to be careful: what we automate should ease the work and not create more.

I’d love to hear which automations you created, feel free to share those in the comments.


Alex Salom

Hi! My name is Alex Salom and I’m an iOS Engineer. In this site I’ll share with you tricks & tips related to iOS and also to everything that happens around it: tooling, Continuous Integration and Continuous Delivery. You can find me as well in Twitter, GitHub and LinkedIn.