CircleCI: How to Create Signed iOS Builds

Brightec Eye Image
We set up nightly QA builds for our iOS projects on CircleCI so that we didn’t have to keep creating them manually.

As with all development agencies, we’re always looking for ways to improve our processes, particularly with manual jobs that can be automated.

Our requirements were as follows:

  • Automated. It should just happen at the end of each working day.
  • Only when necessary. We regularly move between projects and there’s no point distributing builds every day for weeks on end when no one’s working on the project.
  • Ad Hoc build uploaded to We have an internal policy which says that we only upload releasable builds to TestFlight. This means any build that’s pointing at a dev API can’t be uploaded to TestFlight so we use fabric beta these builds.
  • Minimal impact on Xcode manual builds. Xcode likes you to use its Automatic settings and it can be a pain if you try to fight it. While some developers are able to configure the right settings, others find it confusing so it’s easier to just leave Xcode alone.

With this in mind, we started looking at the tools we’d need. The first requirement is straightforward in CircleCI because workflows can be triggered on a schedule. Our workflow for the nightly QA build is as follows:



 - schedule:

 cron: "0 18 * * 1-5"




 - master


 - dist_qa

This gives us an automated build that runs at 6 pm Monday to Friday (we don’t work at the weekends so there’s no point running builds on these days). If you need help creating the cron string, I suggest you look at 'crontab guru'.

Next steps

Our next requirement needs us to keep track of when the last build was distributed, and be able to check whether anything has changed since then. After a bit of head scratching and searching, I came across - a free API for storing key/value pairs. With this, we’re able to store the SHA-1 hash of the commit that the last build is based on.

If master is still on this commit, nothing has changed and no build is necessary. The API documentation is the best place to start but basically, you send an HTTP POST request with your chosen key and this will return a URL which contains your key along with a random token to authenticate the requests. You then use this URL to retrieve the current value or update it with a new value. Generating this URL isn’t covered in the CircleCI steps below so you’ll need to do it manually.

There are a few steps in the config file to achieve our requirement. First, we get the current value from the API. This value is then stored in an environment variable so that we can access it later. We achieve this by adding an export command to $BASH_ENV (this is a special CircleCI variable that references a file that gets loaded in to bash before each step. More information about this can be found in this document, 'Introduction to environment variables').

- run:

 name: Get last build SHA1

 command: |


 echo "export LAST_BUILD=$LAST_BUILD" >> $BASH_ENV

Then we update the API with the new value, which comes from an environment variable that CircleCI provides - the commit SHA-1 that the job is running against. We do this straight away even if the value hasn’t changed as the API requires the key to be updated at least once a week otherwise it will be deleted.

- run:

 name: Save current SHA1

 command: curl -X POST -IL{token}/{key}/$CIRCLE_SHA1

We can now check if the value has changed and stop the job if no deployment is necessary. Using ‘circleci step halt’ rather than a non-zero exit code means that the job and workflow are marked as successful rather than it looking like there’s a problem with a failed job.

- run:

 name: Stop job if not required

 command: |

 if [ "$LAST_BUILD" != '' ] && [ $LAST_BUILD == $CIRCLE_SHA1 ]; then

 echo "No deployment necessary"

 circleci step halt


The next two requirements

With all this in place, we’ve achieved our first 2 requirements. The next 2 requirements can be achieved using fastlane. While we could use match to manage our signing, I wasn’t comfortable with all required changes in Xcode and it would break our 4th requirement so I went with a manual approach. This involves storing the necessary certificates and profiles in a git repository and manually specifying some export options in fastlane. This is probably the main part of this process that needs some improvement but it’s a good first step.

You’ll need to create a private repository and put in your developer and distribution certificates, along with developer and ad hoc distribution provisioning profiles. Note: I don’t know why but we’ve found the process only works when you use the “iOS Team Provisioning Profile” profiles from “~/Library/MobileDevice/Provisioning\ Profiles”. Custom development profiles don’t work. You’ll then need to give CircleCI access to this repository, either by adding a deploy key to the repository or setting up a machine user. Once this is done, add the SSH key to the project on CircleCI in the “SSH Permissions” section of the project settings. This will allow CircleCI to check out your repository that contains all the certificates and profiles.

We now have a few steps in our CircleCI config where enable the SSH key, clone the repository (which we assume is called ios-certificates), copy the profiles to the relevant directory then execute the fastlane command.

- add_ssh_keys:


 - "<your fingerprint>"

- run: git clone

- run:

 name: Install Profiles

 command: |

 mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles

 cp ios-certificates/*.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles

- run:

 name: Fastlane

 command: bundle exec fastlane adhoc

For completeness, I’ll include our fastlane config here with a few comments. However, there’s plenty of documentation about how to setup fastlane to carry out your required actions. We’re defining 2 lanes, a private one that loads the certificates into the keychain and another that calls the first lane, triggers the build, uploads it to fabric beta then sends a slack notification.

desc "Ad-hoc build"

lane :adhoc do |options|



 scheme: “SchemeName”,

 export_method: "ad-hoc",

 export_options: {

 provisioningProfiles: {

 "" => "App Ad Hoc Distribution"





 notifications: "false",

 groups: "your-group",

 api_token: "{token}",

 build_secret: "{secret}"



 message: "App QA build distributed",

 slack_url: "{url}"



private_lane :importCerts do


 certificate_path: "./ios-certificates/Certificates_Developer.p12",

 keychain_name: "fastlane_tmp_keychain"



 certificate_path: "./ios-certificates/Certificates_Distribution.p12",

 keychain_name: "fastlane_tmp_keychain"



This completed all our original requirements. However, another requirement you may have is that you want fastlane to increment the build number each time. This has a knock-on effect that the git SHA-1 check we setup earlier no longer works as each build generates a new commit and therefore the check fails and a new build is distributed every time. To solve this, we added another step at the end of the job that updates the key value API after the version bump commit has happened.

- run:

 name: Save current SHA1

 command: |

 CURRENT_SHA=$(git rev-parse HEAD)

 curl -X POST -IL{token}/{key}/$CURRENT_SHA

Hopefully, this helps you to get your automated deployment working. The full CircleCI config can be viewed here.

Looking for something else?

Search over 400 blog posts from our team

Want to hear more?

Subscribe to our monthly digest of blogs to stay in the loop and come with us on our journey to make things better!