9 min read

How to Implement a CI/CD Workflow for iOS Using GitHub Actions

iOS Development

Development time is precious, as you all know it. So if you wonder if there’s a way to save all that precious time, I’ve got some good news for you. Say goodbye to manual deployment struggles, and welcome CI/CD!

In this article, you’ll learn how to automate the process of deploying your builds to testers, using TestFlight. But, before you start anything, you need to have your app signed - this includes registering your app ID, making a certificate, and a provisioning profile. To find out more about this topic, just follow this link.

In order to set up a CI/CD with TestFlight you’ll have to:

  • Register your app on App Store connect
  • Add testers
  • Generate an API key on App Store connect
  • Set up GitHub actions and upload secrets

Now that you're familiar with the steps, let's go through each and every one of them in more detail. Just follow along.

Registering the app on App Store Connect

Go to App Store Connect and press the apps tab. Here you should see a list of all your apps (if you have any).

Now, press the plus button next to the ‘Apps’ title.

Right about now, you should see a pop-up form. Most of the information is self-explanatory so it shouldn't be a problem for you to fill it out. Don't worry about SKU – it's a unique ID for your app in the Apple system and it won't be visible to other users.

You just registered your app to App Store Connect! Now, let’s add testers to the app.

Adding testers

Your app can be delivered to two types of testers, internal and external. For external ones, your application needs to go through Beta App Review, while for the internal ones, it doesn’t.

For more information about internal/external testers, click here

Now, it’s time to create an internal testing group – to do that, click the plus sign:

Assign a name to the group and enable automatic distribution. This way builds will be automatically delivered to testers in the group.

When created, add testers to this group by clicking on the plus sign next to ‘Testers’.

Select all users that you want to test builds from this application. If you have trouble finding all of them, go to ‘Users and Access’ and then add them. Afterward, all testers will receive an email invitation.

Congrats, you’ve just finished setting up your testers! Now, you’ll implement a CI/CD flow using GitHub actions.

GitHub Actions

Select the ‘Actions’ tab on your repository and choose the Simple workflow template. This is the easiest way to start using GitHub actions, as it will create all the essentials that you need to have in a workflow.

By clicking on the Simple workflow, GitHub will create the .github/workflows folder, and a ‘blank.yml’ file with some minimal code for a GitHub workflow (action). All your actions need to be in this folder in order for GitHub to recognize them.

Feel free to rename this file. Now, it’s time to write your own action containing the continuous integration (CI) and continuous delivery (CD) parts.

CI part

In this part, you'll archive your project into a folder containing your '.ipa' file.

I'm sure you already know how to do this manually, but let’s see how to automate the process. Find the code for this here.

Firstly, you need to set the name of your action. Secondly, decide on the event it should trigger on. Set this to be on push request and on the main branch.

In the third step, create your job and assign a runner it will run on. Write this to be ‘macos-latest’.

runs-on: macos-latest

In the example above, the action is running on a self-hosted runner. This is why you see ‘self-hosted’ as the value. If you look here, you can set up your own self-hosted runner.

In the fourth step, go to your GitHub repository and check if the code from it is available on the device your actions are running on. This action will use an already created action from the marketplace, that the GitHub team wrote.

Then, in the fifth step, you should install any dependencies you have – in the example above, I don’t use any. In case you do use dependencies, remove all comments from this step.

Finally, import all necessary secrets using the ‘env’ key. Adding secrets comes a bit later in the article. The rest of the code decodes files from a base64 string, saves the certificate, and applies the provisioning profile.

CD Part

To implement CD, you’ll need four pieces of information:

  • Path to your .ipa file
  • Issuer ID
  • App Store Connect API key ID
  • App Store Connect private key

You know the first one, but to get the other three, you need to generate an API key. You can find the steps to do this here. Note that you need to be an account holder or an admin to do this.

Afterward, you should be able to see the needed information shown as in the picture below. Also, keep in mind that you can only download the private API key once.

Now, let’s go through the code in your .yml file.

The first step would be to build your application using the provided scheme and archive it. Secondly, you should export the archive with the export options that are decoded from a base64 string.

The third step is optional. This uploads an artifact with the archived files. The files will be exactly the same as if you exported the archive manually through Xcode.

As I mentioned, this step is not necessary for the flow to work. But if you decide to add it, you’ll be able to find the files in the summary of the executed workflow.

The fourth step is removing whitespaces in the file name, in case you have them. If not, skip this step.

In the fifth step, you need to decode the App Store Connect private key from base64 and save it. You should store your key in one of these locations:

  • ./private_keys
  • ~/private_keys
  • ~/.private_keys
  • ~/.appstoreconnect/private_keys

When saving the private key file, it should contain an APP KEY ID in its name so the altool can find it, in the format of AuthKey_{APP KEY ID}.p8.

In the last step, validate and upload your app using altool. The altool generates a JWT token to be able to communicate with App Store Connect. This is done using your private API key, its ID ad app issuer ID.

Full commands are:

xcrun altool — validate-app -f [path/to/.ipa] -t ios — apiKey
${{ secrets.APPSTORE_API_KEY_ID }} — apiIssuer ${{
xcrun altool — upload-app -f [path/to/.ipa] -t ios — apiKey
“${{ secrets.APPSTORE_API_KEY_ID }}” — apiIssuer “${{

That is it for the yml file! Now, it’s time to import the secrets you used under the ‘env’ key.

GitHub secrets

You can edit your secrets by going to repo settings -> secrets -> actions.

There, you’ll see two containers, one for the current repository and one for the organization. Secrets added to the first one are only available to the current repository, while the organization’s secrets are available in all repositories that belong to that organization.

Click on the button to add a new repository secret. Write ‘APPSTORE_API_KEY_ID’ as the secret name. For the value, put in your key id. Then, press ‘Add secret’.

Good, you just added a secret to your repository! If you look in the .yml file, you'll see you're using this secret:

For string secrets, this works just fine. But, how will you upload files?

Also as strings! You just need to encode them first. You can do that through your command-line interface (CLI) with the command:

openssl base64 -in [fileName] | pbcopy

This will encode your file to a base64 string and copy it to your clipboard.

Add a new repository secret and call it IOS_EXPORT_PRODUCTION. Open the terminal at your archived files folder and encode the ExportOptions.plist file.

openssl base64 -in ExportOptions.plist | pbcopy

Paste the base64 string as the value of your secret and add it.

Then, add all other necessary secrets as either normal strings, or encoded as base64 strings, if they’re files. To see all the secrets, check the image below:

Let’s see what each of these secrets represents: 

  • APPSTORE_API_KEY_ID is the ID of your API key in App Store Connect.
  • APPSTORE_API_PRIVATE_KEY is your private API key file ( .p8 file) encoded as a base64.
  • APPSTORE_ISSUER_ID is your Issuer ID in App Store Connect. 
  • IOS_EXPORT_PRODUCTION is your Export options.plist file encoded as a base64 string. You can find it among the archived files.
  • IOS_PROD_CERTIFICATE is your deployment certificate (.p12 file) encoded as a base64 string.To be able to encode it, go to keychain access, right-click on the certificate, and export it.
  • IOS_PROD_CERTIFICATE_PASSWORD is the password you used for exporting the certificate.
  • IOS_PROVISION_PRODUCTION is your provisioning profile encoded as a base64 string.

At this point, you successfully added CI/CD with GitHub actions. There’s only one thing left for you to do, and it’s to use them.


If you make a new push to your main branch, your build will be uploaded to TestFlight. So go ahead, go to TestFlight and find your build. If you don’t see it, check if your GitHub action failed or if you’ve received an email about any failures.

If everything works, your uploaded build will be in the ‘Processing state’.

After a couple of minutes, it’ll finish processing and show the ‘Missing Compliance’ alert:

To fix this you can click on ‘Mange’ and press ’No’ so that you don’t use any encryption. Of course, if your app does use encryption, you should go for the second option. Then press the ‘Start Internal Testing’ button.

This fixes the problem – your build is now ready and will be available for internal testers.

Even though this is a good way to do it, it can get annoying if you have to do it manually every time. So, in the true spirit of CI/CD, there’s an even better way to do it.

By changing the ‘App Uses Non-Exempt Encryption’ key in the info.plist file, you can make this automatic. TestFlight is checking this key to see if your app uses encryption. Since you don’t have this by default, it requires you to manage this manually by saying if you use encryption or not.

To make this clear, go to your info.plist file and add the ‘App Uses Non-Exempt Encryption’ key, and set its value to ‘NO’.

From now on, when you want to release a new build to your internal testers, just make a new push on the main branch.

That's right! You don’t need to think about releasing your build to testers anymore. All it takes is a single push!

The end 

Congrats, you’ve just learned how to make the signing and the delivery process of your app automatic! 

Initially, it takes a bit longer, but setting up a CI/CD workflow is worth it in the long run. Just think about how you won’t need to do any extra work every time you push a new build of your app.

If you have any questions, feel free to contact me. And if everything’s clear, go ahead and share your newly found skill! 💪🏼 

Like what you just read?

Go on, spread the news!

About the author

Ivan is an iOS Developer at COBE. He's passionate about two things – app development and basketball. Maybe writing comes next? We'll have to wait and see.


iOS Development

Write COBE
Related Stories

This was interesting to you? Check these out.