Using fastlane to optimise beta distribution

Giorgos Ampavis
8 min readSep 30, 2016

--

As I am working in a busy environment and sometimes in multiple iOS applications during the same period, it was necessary for me to standardise and optimise my various workflows.

So I started looking at what I am doing repeatedly and sometimes multiple times in one day. The first one I noticed is that I was distributing AdHoc Beta builds (via Crashlytics beta) for the same application.

So I decided that I needed a tool that will allow me to configure it and with minimal input after that, it will generate a beta and distribute it to my team.

This is where fastlane comes handy. Below I will describe you how I used and how you can use it too.

The first thing we need to do is install the fastlane tool and then initialise it.

sudo gem install fastlanefastlane init

The init process takes us through few steps where we need to enter information such as apple ID, team ID etc.

A nice quick start guide for fastlane in iOS is this.

You might need to install the various fastlane tools/actions you want (here I am using gym) separately. You can find all the tools and documentation about them here.

After fastlane installs and initialises, a new folder is created with 2 files in it, the Appfile and the Fastfile. These are the required files needed to run fastlane. In my case and because I am using the gym tool I needed another one, called Gymfile. So I created an empty file with that name with no extension of course.

Let’s got through these files now…

Appfile

It is time now to edit the Appfile. This file holds information regarding the apple ID and/or the team and the application identifier (bundle ID).

app_identifier ENV[“APP_IDENTIFIER”]
apple_id “YOUR_APPLE_ID”
team_id “YOUR_TEAM_ID”

The first line is actually saying that the app_identifier will get its value from the environment variable APP_IDENTIFIER which I will setup later on.

The second line is the apple ID and the third one the team ID I belong too (for that project of course as I belong in multiple teams). The last one can be found in the iOS Member Center.

Fastfile

The second file is the Fastfile. Here we put all the lanes and actions we need.

The Fastfile runs in 3 parts, the part that runs before all the lanes the actual public and private lanes and the part that runs after all running lanes finish.

Be aware that the before-all-lanes and the after-all-lanes parts will run for ALL lanes you include in the Fastfile.

The before-all-lanes part (optional)

The first part runs just before the lane fastlane was was told to run. Here we can setup any environment variables or run common actions. In my case I just setup few variables and run the clear_derived_data action.

before_all do    ENV["VERSION_NUMBER"] = get_version_number
ENV["BUILD_NUMBER"] = get_build_number
ENV["VERSION_BUILD_DISPLAY"] = get_version_and_build_display

ENV["SLACK_URL"] = "YOUR_SLACK_HOOK_URL"
clear_derived_dataend

I will talk later on about the helper methods get_version_number, get_build_number and get_version_and_build_display.

The Public lanes & Private lanes part

Public lanes

A public lane is accessible from the terminal (as a parameter in the fastlane command) and is defined as:

lane :A_LANE_NAME doend

In my case I only have one public lane at the moment which is the beta distribution lane.

It’s name is beta and I can pass through the options parameter any other parameter I want. I am only passing the Fabric beta distribution group name which is used later on when I am uploading the archive.

The first thing I do in this lane is setting the APP_IDENTIFIER environment variable to the adhoc app ID (I am using different app IDs for every configuration and I usually suffix at the end of the main app store ID .dev or .adhoc for development and adhoc builds respectively).

ENV[“APP_IDENTIFIER”] = “YOUR_ADHOC_APP_ID”

Next, I run the private lane tag_the_badge.

tag_the_badge

Then, I run the private lane update_app_identifier

update_identifier

Then, we run the private lane gym_it

gym_it

Then, I run the private lane upload_crashlytics and I am passing the options parameter.

upload_crashlytics groups:options[:groups]

Then, I am running the private lane post_to_slack which does just that, posting a message to Slack.

post_to_slack

Finally, the last two things I need to do in this lane is set the APP_IDENTIFIER environment variable back to the debug one and then run the private lane update_identifier again. This way I set the Xcode back to the original state.

ENV[“APP_IDENTIFIER”] = “YOUR_DEV_APP_ID”update_identifier

The final beta lane looks like this:

lane :beta do |options|    log(“STARTING LANE BETA…”)    #Set environment variables top adhoc
ENV[“APP_IDENTIFIER”] = “YOUR_ADHOC_APP_ID”
log(“GENERATING ADHOC BUILD WITH BUNDLE ID: # {ENV[“APP_IDENTIFIER”]}”)
#Add tag with version and build number in app icon
tag_the_badge
#Update app identifier to adhoc
update_identifier
#Gym
gym_it
#Crashlytics
upload_crashlytics groups:options[:groups]
#Post to slack
post_to_slack
#Set environment variables to DEV
ENV[“APP_IDENTIFIER”] = “YOUR_DEV_APP_ID”
log(“SETTING APP IDENTIFIER TO DEV: #{ENV[“APP_IDENTIFIER”]}”)
#Update app identifier to DEV
update_identifier
end

In various places you will see that I am using log, a helper method which just prints a message in Terminal and it helps me debug whenever needed in the terminal output.

Private lanes

A private lane is run only from inside the Fastfile e.g. from another lane and is defined as:

private_lane :A_LANE_NAME doend

As mentioned above, I have 5 private lanes and I am running them from inside the public one, beta.

The first one is the tag_the_badge lane.

private_lane :tag_the_badge do
log(“CREATING NEW APP ICONS”)

badgeDisplay = get_badge_text
badge(
glob: “/**/*-beta.appiconset/*.{png,PNG}”,
shield: badgeDisplay,
no_badge: false,
shield_no_resize: true,
shield_gravity: “north”
)
end

As i mentioned earlier I am using different app IDs for every configuration (I will discuss this in another post) and by that I am able to have 3 different versions of the app installed at the same time, one for every configuration, Release, AdHoc and Debug, With the badge action I have an easy way to distinguish between them. All I need to do is use a different app icon for every configuration which I have suffixed with -beta and it is defined in the build settings. This way the main app icon for the app store is not changed.

Here, I am calling the badge action and I am passing the actual text of the badge (generated in a helper method get_badge_text and I will talk about it at the end) and few more needed properties. You can read more about badge here.

The second one is the update_identifier lane.

private_lane :update_identifier do    log(“UPDATING APP IDENTIFIER”)    update_app_identifier(
xcodeproj: “YOUR_APP_NAME.xcodeproj”,
plist_path: “YOUR_PATH_TO/Info.plist”,
app_identifier: ENV[“APP_IDENTIFIER”]
)
end

In this lane, I am just setting the Xcode project’s app identifier by using the update_app_identifier action. I only need to pass the Xcode project name, the relative path to the Info.plist file and the application identifier I need to change to for the adhoc builds (explained above). You can read more about update_app_identifier here

The third one is the gym_it lane.

private_lane :gym_it do

log(“ARCHIVING FOR ADHOC DISTRIBUTION: #{ENV[“VERSION_BUILD_DISPLAY”]}”)

gym(
workspace: “APP_NAME.xcworkspace”,
scheme: “ADHOC_SCHEME_NAME”,
output_name: “OUTPUT_NAME”,
configuration: “AdHoc”
)
end

This lane runs the gym tool which will create the adhoc archive. I am passing the workspace name (I am using Cocoapods for all my projects so this is needed), the scheme name, the output name and the configuration I want to archive with. You can read more about gym here.

The fourth one is the upload_crashlytics lane.

private_lane :upload_crashlytics do |options| 

groups = options[:groups]
if (groups == nil)
groups = “developers”
end
notes = “Weengs #{ENV[“VERSION_BUILD_DISPLAY”]} AdHoc Build Distribution”
log(“UPLOADING TO CRASHLYTICS & NOTIFYING GROUPS: #{groups}”)
crashlytics(
api_token: “CRASHLYTICS_TOKEN”,
build_secret: “CRASHLYTICS_SECRET”,
groups: groups,
notes: notes
)
end

Here I am just calling the crashlytics actions that will upload the archive in Crashlytics and notify the group defined in the command line parameter groups.

The fifth one is the post_to_slack lane.

private_lane :post_to_slack do

log("POSTING TO SLACK")

version = ENV["VERSION_NUMBER"]
build = ENV["BUILD_NUMBER"]
slack(
message: "<!here|here>: New :iOS: *#{version}* (#{build})"
)
end

Here I am just creating and sending a text message (that has build and version number in it) to Slack informing my team about the new adhoc build.

The after-all-lanes part (optional)

The third and last part runs just after all lanes running finish. It has 2 parts, the one where everything run successfully and the one that an error occurred when running a lane.

The success part

Here I am just sending a system notification telling me that the lane finished successfully.

after_all do |lane|

notification(title: “Fastlane”, subtitle: “’#{lane}’ result”, message: “Finished lane ‘#{lane}’ successfully”)
end

The error part

Here I am just sending a system notification notifying me about the error.

error do |lane, exception|    notification(title: “Fastlane”, subtitle: “’#{lane}’ result”, message: “Finished lane ‘#{lane}’ with error”)end

The Helper methods

These helper methods are used all over the Fastfile and they do various jobs like printing messages in the console output, generating (concatenating) text and getting various application information.

def log(message)
UI.header message
end
def get_badge_text
ENV[“VERSION_NUMBER”] + “-” + ENV[“BUILD_NUMBER”] + “-” + “red”
end

def get_version_and_build_display
“#{ENV[“VERSION_NUMBER”]}_#{ENV[“BUILD_NUMBER”]}”
end
def get_build_number
`/usr/libexec/PlistBuddy -c ‘Print CFBundleVersion’ ../PATH_TO/Info.plist`.strip
end
def get_version_number
`/usr/libexec/PlistBuddy -c ‘Print CFBundleShortVersionString’ ../PATH_TO/Info.plist`.strip
end

Gymfile

The second file is the Gymfile which holds configuration properties that are used in the gym action.

clean true
output_directory “./build” #where to store the adhoc ipa
export_method “ad-hoc”

You can read more about the Gymfile here.

Conclusion

So now it’s super-easy for me to send a new build to my team. You can be very creative and add all sort of actions here, for generating .pem files, generating provisioning profiles etc.

This is a very basic Fastfile that covers my needs in a specific app. I will probably add few more lanes in the future for deployment, screenshots etc.

One more thing…

Sometimes, the more we automate a workflow the more lazy we become. So I started looking into ways of not having to open terminal to execute fastlane.

So I came up with 2 solutions:

  • Terminal shell script

I can super-easily create shell script and make executable by double clicking them. All I need to do is add the necessary shell commands in it e.g. fastlane beta.

#!/bin/shBASEDIR=$(dirname $0)
cd ${BASEDIR}
fastlane beta

Then, I am saving with a .command extension and by running

chmod 777 filename.command

I can make it executable.

  • custom OSX app

As I wanted to explore mac app development too, I created a small tool which runs as a toolbar app and I can easily execute fastlane and distribute a beta from my OSX toolbar. The app is not finished yet so I will be posting an update and a link to Github as soon as it is ready.

You can find all the files discussed above here.

--

--