Setting up CI for your .NET MAUI iOS app with GitHub Actions

In my previous post, I showed how to set up CI for your .NET MAUI Android app using GitHub Actions. In this post I’ll be showing you how you can do the same for iOS.

Like in my post on how to do this for iOS in Azure DevOps, there are certain prerequisites you need to setup. In his video, Gerald shows the required prerequisites like creating a signing certificate, app identifier and provisioning profile. This guide will assume that you have done all of this already and that you have your signing certificate (.p12) downloaded or backed up somewhere. Gerald also shows what you need to add to your csproj file for this to work. When all this is done, we can get started with GitHub Actions.

Create your workflow

Just as we did for the Android app, create a new workflow under the Actions tab in your GitHub repository. Select the .NET workflow, which should be suggested to you (search it up if it isn’t suggested) and hit “Configure”.

Workflow for .NET.

Out of the box, the template will look like this:

Select your VM image

The first thing we want to change is what kind of virtual image this action will run on. We’ll want to change this to macos-latest, since building iOS apps requires a Mac:

    runs-on: macos-latest

Use .NET 6

There’s already an action added to the workflow (actions/setup-dotnet@v1) which sets the job to use .NET 5. We’ll make a small edit to make this use .NET 6 instead:

    - name: Setup .NET
      uses: actions/setup-dotnet@v1
        dotnet-version: 6.0.x

Install the MAUI workload

Next we’ll remove the dotnet restore and dotnet test commands. In place of the dotnet restore command we’ll add a run where we will install the .NET MAUI workload:

    - name: Install MAUI workload
      run: dotnet workload install maui

Import certificate

“I love dealing with Apple certificates and provisioning profiles”, said no one ever. Nonetheless, we have to. Now we need to install the signing certificate onto the build agent. First you’ll have to either add your signing certificate to your repository, or get the P12 file encoded as a base64 string. In this example we’ll we using it from our repository.

Next we’ll add the Import Code-Signing Certificates action. You can search for it under the “Marketplace” tab when editing your workflow:

Search results from “code-signing” in Marketplace.

A lot of the parameters of this action are optional. The only ones we’ll be using are p12-filepath and p12-password. The p12-filepath is, as the name suggests, the filepath for your certificate. So if it’s in the root of your repository, the value would be mySigningCertificate.p12. The p12-password is the password you created for your signing certificate:

    - name: Import Code-Signing Certificates
      uses: Apple-Actions/import-codesign-certs@v1
        p12-filepath: 'DistributionCertANesheim.p12'
        p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}

Note: It is recommended to set your passwords and keys as secrets in your GitHub repository instead of directly in your workflow. To see how to set and get secrets for your GitHub Action workflows, see this article.

Download provisioning profile

Now we need to fetch the provisioning profile connected to our app. This requires some setup, as we’re going to be fetching our provisioning profile through an action that utilizes the App Store Connect API.

First we need to create an API key for the App Store Connect API. See this guide on how to do that. Back this up somewhere and open the content of this .p8 file with something like Notepad. Copy the entire content of this file (including the BEGIN/END PRIVATE KEY sections) and create a new secret for your GitHub Action. Name this APPSTORE_PRIVATE_KEY and paste the content of your .p8 file here.

Adding a new secret for APPSTORE_PRIVATE_KEY.

Next we need to find the Key ID for our newly created API key. You can find this in App Store Connect where you just created the key, under “Users and Access” -> “Keys”:

Key ID for the API key.

Copy the Key ID and create a new secret for your GitHub Action. Name this APPSTORE_KEY_ID and paste the Key ID in here.

Lastly we need our Issuer ID. This can be found on the top of the page in App Store Connect in the same section as before:

Issuer ID in App Store Connect.

Now we can finally add the action we’ll be using all this for, namely the Download Apple Provisioning Profiles action. Use the Marketplace tab again to search it up:

Search results from “provisioning profile” in Marketplace.

Copy the action into your workflow and modify it to look like this:

    - name: Download Apple Provisioning Profiles
      uses: Apple-Actions/download-provisioning-profiles@v1
        bundle-id: ''
        issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
        api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
        api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}

The bundle-id is the app identifier that you’ve registered with your app in Apple Developer Portal. I’ve omitted the profile-type argument, but you can add this if you have multiple provisioning profiles connected to your app identifier (f.ex. Development, App Store, Ad Hoc) and want to specify which type to use. The three other arguments are the ones that we’ve already added the values for as secrets in the previous steps.

Build it!

At long last, we can finally build the app. We’ll be using the dotnet publish command along with some arguments. Add or modify the existing dotnet build command in the workflow to look like this:

    - name: dotnet publish
      run: dotnet publish -c Release -f:net6.0-ios /p:ArchiveOnBuild=true /p:EnableAssemblyILStripping=false

This will build the app in the Release configuration and compile for the net6.0-ios framework. /p:ArchiveOnBuild=true means that we’re creating an IPA file on build. /p:EnableAssemblyILStripping=false seems to currently be necessary because of a bug, but this might not be needed in the future when MAUI is officially released.

And voíla! A signed IPA should now be created in the publish-folder. If you want to use your signed IPA file in f.ex. a release pipeline, you can use the Upload a Build Artifact action for this (actions/upload-artifact@v3.0.0). The following snippet will search for any IPA file in your directory and upload that:

    - name: Upload a Build Artifact
      uses: actions/upload-artifact@v3.0.0
        path: '**/*.ipa'

You can verify that the IPA file was uploaded as a build artifact by looking at the summary view of your build:

Build artifact for iOS.

Final thoughts

Retrieving the provisioning profile this way was a first for me. I’m used to updating and downloading the provisioning profiles and having to add them to source control each time. This way seems to eliminate that step by just providing the API key + credentials, which doesn’t seem to have an expiration date.

It’s been fun making these guides, as it’s allowed me to play around with GitHub Actions. I’m primarily used to Azure DevOps, so this has been a breath of fresh air. I hope you enjoyed this post, and let me know if you have any trouble!

4 thoughts on “Setting up CI for your .NET MAUI iOS app with GitHub Actions”

  1. Hello,
    Thanks for the wonderful article. While downloading Apple provisioning profile I am getting below error, can you please help
    Run Apple-Actions/download-provisioning-profiles@v1

    Error: {
    “errors”: [{
    “status”: “401”,
    “code”: “NOT_AUTHORIZED”,
    “title”: “Authentication credentials are missing or invalid.”,
    “detail”: “Provide a properly configured and signed bearer token, and make sure that it has not expired. Learn more about Generating Tokens for API Requests

    1. Did you create the APPSTORE_PRIVATE_KEY as mentioned in the post? I’m pretty sure this error code is connected to this somehow, but I don’t know exactly how to solve it other than to try to double check the private key you created and that it’s set up correctly in GitHub.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.