As a Software Engineer on FlightAware’s Mobile team, Zack Falgout works with Swift every day to ensure the FlightAware iOS app continues to be a gold standard in aviation apps on the Apple App Store.
Getting an app on Apple’s App Store is the end goal for many iOS developers. The amount of work that goes into reaching that goal is substantial. After first learning the language, the aspiring developer must come up with an idea that they can subsequently translate into a fully functioning application. This is already months, if not years, of work. Naturally, creating the application itself is where most developers focus at the beginning of their development journies, but there’s one final step that gets overlooked in the process: submitting the app to Apple for approval. It’s a step that can be more complicated than expected, and it’s easy to miss when starting out because there’s already a mountain of information to parse and learn.
At FlightAware, we aim to release a new version of our app each month, so frequently repeating this process could end up taking a significant amount of time. Our solution is a Continuous Integration (CI), Continuous Delivery (CD) pipeline utilizing two tools: Jenkins and Fastlane. Not only does this ease the burden of the final upload to Apple, but it also smoothes out the entire development process.
There’s some technical jargon in that last sentence, so let’s first set some definitions. Continuous Integration is the act of building and testing code automatically alongside each new commit made to a repository. Continuous Delivery simply means that your application is always in a state in which it can be deployed. There’s a manual component associated with Continuous Delivery as well: if one were to automate the Continuous Delivery portion of the pipeline, it would then be referred to as Continuous Deployment.
- Code is written to handle a particular issue or feature.
- We then begin our peer review (PR) process. Once a PR is submitted to the rest of the team, our
iOS – Tests
Jenkins job runs automatically. The integration with Fastlane will trigger thetest
lane to run. Should any of these tests fail, the Jenkins job itself fails and we can investigate the issue. This job is one of our most important because it is our early warning system that something may be wrong. - Once the code successfully passes peer review, it then gets merged into our
develop
branch. Any merges onto ourdevelop
branch will automatically begin ouriOS – Alpha
Jenkins job. This will trigger the Fastlanealpha
lane to run. The product of thealpha
lane is an.ipa
file that our QA testers can use for early testing. This procedure gets repeated for every commit made todevelop
. - At some point, we decide it is time to release our current code on
develop
into the app store. We create arelease
branch fromdevelop
. The JenkinsiOS – Beta
job is manually run. This will use ourbeta
lane in Fastlane to package and upload our files to Apple’s servers. Once the process is complete, we are free to begin our TestFlight process in App Store Connect.
Jenkins is our automation tool of choice for the Continuous Integration portion of the workflow, and we have a suite of jobs we use for different parts of the release process and special cases. We can create and destroy jobs as needed. This is beneficial because there have been many times where we had a long-running branch that could not be merged into our ‘develop’ branch due to new feature development, but the code on that branch still needed to be tested by QA.
Fastlane is a tool that allows developers to automate tasks associated with the iOS app development process. Each group of scripts is called a lane, and each lane is aimed at handling a specific phase of the CI/CD process. At FlightAware, we have lanes for many different phases of development including unit testing (step two of our CI/CD process), UI testing, alpha builds (step three of our CI/CD process), beta builds (step four of our CI/CD process), and automated screenshots. We even have a lane to handle package management for some of our in-house packages.
Here is our test lane as an example of how we are handling lane setup and execution:
desc "Runs all the tests"
desc "Generates the test coverage"
desc "Makes sure that the app can still be archived."
lane :test do
run_tests
testExportOptions = {
"provisioningProfiles" => {
"com.flightaware.iphone.live-flight-tracker" => "com.flightaware.iphone.live-flight-tracker AdHoc",
"com.flightaware.iphone.live-flight-tracker.notification-content-extension" => "com.flightaware.iphone.live-flight-tracker.notification-content-extension AdHoc"
}
}
gym(
project: ENV["XCODE_PROJECT"],
scheme: ENV["XCODE_SCHEME"],
configuration: ENV["XCODE_CONFIGURATION_ALPHA"],
export_options: testExportOptions,
skip_package_ipa: true,
use_system_scm: true
)
end
The integration between Fastlane and Jenkins is seamless and allows for us to offload time-consuming tasks to a build server, which allows any developer or non-developer to kick off a build and submit to Apple for review. In fact, there’s an ever-increasing counter of developer hours shown on Fastlane’s website that stands at 28,039,568 at the time of this writing. Because of Fastlane, we save around half an hour with each of our beta builds.
Jenkins integrates with Fastlane via a shell script. We have a Jenkins
folder that contains scripts for each Jenkins job in our project so that as much as possible, our build scripts are versioned in source control. For example, our iOS – Alpha
job has a command that looks like this:
The contents of the file at that path are:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
export XCODE_APP=Xcode.app
export LANG=en_US.UTF-8
export LANGUAGE=en_US.UTF-8
export LC_ALL=en_US.UTF-8
cp /Users/xcodeserver/.env.ios.default .env.default
make install
bundle exec fastlane alpha
The final line of that script is what will start our alpha
lane in Fastlane. This is how we keep the two tools functioning together.
Unfortunately, there are a few drawbacks to our current system. With each new tool added into the chain, we also add the overhead of understanding said tool. Both Jenkins and Fastlane have learning curves associated with them, and coming up to speed may not be an easy endeavor. Fastlane itself requires a small amount of scripting knowledge, which I admittedly did not have when I began using it. At FlightAware, we share knowledge amongst each other so that learning curves become less steep. Keeping each tool up to date and the versioning in sync across our build server and personal machines can sometimes cause headaches, but they’re typically minor and can be handled easily.
One of our greatest struggles with our current system began when Apple began requiring that all developers have two-factor authentication enabled to use their developer accounts. This meant that our build server would need to periodically re-verify its authenticity to connect to Apple’s servers. To solve the issue, we created a ruby script that will request authorization from the command line. This does require a developer to log on to the build server and execute the script, but it is a quick process to get us back up and running.
We are currently happy with our pipeline, but we also understand that we are not taking full advantage of the power offered by the tools we are currently using. For example, we have a Jenkins job and Fastlane lane to handle capturing screenshots throughout the app. The screenshots are captured on devices of different sizes and localizations. This is important as it is impossible for one developer to go through each combination looking for errors, unless that was all the developer wanted to do with their time. The issue here is that to get good screenshots, we need to delete the app from the simulators after each run. We have code in place to do that, but Apple keeps updating the prompts to remove apps from devices. Each time they add or remove a button, it breaks our code. Revisiting this job will likely be something we do soon.
Outside of that, Fastlane offers several built-in tools that we are not yet utilizing. One such tool is the automated distribution of beta builds. The implementation of this is halfway complete as we’re already using Fastlane to upload the beta build of our app. The final two components we would need to implement would be to allow Fastlane to handle incrementing the build and version number and generating a CSV file with the information of our testers so that Fastlane has access to that information. Incrementing the build and version number would be beneficial for us alone as right now we are utilizing the agvtool
to do this manually before each run of the beta
lane. Sometimes this step is missed, which means the job will fail as App Store Connect will not accept new uploads with existing build numbers.
Another pain point that Fastlane could alleviate is managing provisioning profiles. The act of onboarding new employees can be particularly painful, as they need to create new certificates, add those new certificates to the existing provisioning profiles, and then import the provisioning profiles for use with Xcode. It sounds like a simple process on the surface, but in practice it is overly complicated. Fastlane’s match
action would allow us to share a single code signing identity for the entire team. This identity would be kept in a GitHub repository, which Fastlane could access. Fastlane can even reset existing profiles and certificates when they expire.
The Continuous Integration, Continuous Delivery philosophy has benefitted us on the Mobile team at FlightAware tremendously. If we save an average of thirty minutes per beta build, then Jenkins and Fastlane have saved us months of time on that job alone. Include the time saved between all the jobs and it increases significantly. The integration between Jenkins and Fastlane has opened up delivery of any stage of the app to anyone on the team. This means we’re not reliant on a specific developer or group of developers to get an alpha build to QA or a production build out to Apple. The only privilege necessary is access to our Jenkins dashboard. All of these together have given us the freedom to focus on what is important, which is writing high-quality code and getting it to our users as quickly and efficiently as possible.