Automated Godot iOS upload with Github Actions

29. December 2022

Github Actions allow you to automate repetitive tasks, like exporting your game for iOS and uploading it to the App Store. It also brings the advantage that you don't need a Mac at all. So it saves you time and money, if you're not a Mac user.
I'm using the action already in my Godot games like Pocket Broomball or Ball2Box.

Note: The action steps and versions change over time. Find the latest version of the action Github.

Github Actions pricing

Github Actions are always free only for Open Source repositories. On private you get 2000 to 3000 run minutes per month, depending on the type of your account. Additionally, MacOS actions consume 10x times the minutes of a Linux machine, so you actually get 200 to 300 minutes per month.

Here the detailed documentation about pricing

How does the action work?

The action uses only macOS built-in command line tools like xcodebuild and xcrun and has no third party dependencies. So you can be sure that it always works with the tools Apple itself provides.

First, a check is made to see if the action actually runs on a macOS runner, because it wouldn't work on a Linux or Windows runner.

- name: Check is running on mac-os
    if: runner.os != 'macos'
    shell: bash
    run: exit 1

Then a cache is created using the actions/cache@v3 action. This cache saves the Godot Engine executable, configurations and export templates in a persistent memory, in order to save time and Github's bandwidth. A new cache is created if the godot-version of the inputs changes.

- name: Cache Godot files
    id: cache-godot
    uses: actions/cache@v3
    path: |
    key: ${{ runner.os }}-godot-${{ inputs.godot-version }}

The action uses a headless macOS build that you can find on Github. This build allows the action to run Godot without UI and exporting the game for iOS. So, here the headless build together with the export templates is downloaded, but if there is a cache hit, the files from the cache you created before are used.

- name: Download and config Godot Engine headless linux server and templates
    if: steps.cache-godot.outputs.cache-hit != 'true'
    shell: bash
    run: |
    wget -q${{ inputs.godot-version }}-stable/Godot_v${{ inputs.godot-version }}
    wget -q${{ inputs.godot-version }}/Godot_v${{ inputs.godot-version }}-stable_export_templates.tpz
    unzip Godot_v${{ inputs.godot-version }}-stable_export_templates.tpz
    unzip Godot_v${{ inputs.godot-version }}
    mkdir -p ~/.config/godot
    mkdir -p ~/.local/share/godot/templates/${{ inputs.godot-version }}.stable
    mv templates/* ~/.local/share/godot/templates/${{ inputs.godot-version }}.stable
    mv bin/godot .
    ./godot -e -q
    rm -f Godot_v${{ inputs.godot-version }} Godot_v${{ inputs.godot-version }}-stable_export_templates.tpz

Now the game gets exported for iOS with this simple one liner. The exported files will be located in $PWD/build/.

- name: Godot iOS export
    shell: bash
    run: ./godot --path ${{ inputs.working-directory }} --export iOS

For the signing of the iOS export we need the UUID of the provisioning profile. With this command, the UUID gets extracted and saved as PP_UUID in the Github Actions environment.

- name: Extract Provisioning profile UUID and create PP_UUID env variable
    shell: bash
    run: echo "PP_UUID=$(grep -a -A 1 'UUID' ${{ inputs.provision-profile-path }} | grep string | sed -e "s|<string>||" -e "s|</string>||" | tr -d '\t')" >> $GITHUB_ENV

To make sure the runner uses the correct XCode version, you force it to use a version that works well with the Godot Engine.

- name: Force XCode 13.4
    shell: bash
    run: sudo xcode-select -switch /Applications/

Now you use the xcodebuild to make the iOS export ready for the upload. If external dependencies are used with the following command, the action makes sure that they are configured correctly.

- name: Resolve package dependencies
    shell: bash
    run: xcodebuild -resolvePackageDependencies

An archive of the export is needed before we can create the .ipa file that can be uploaded. In this step, also the signing with the Developer Certificate and Provisioning Profile takes place.

- name: Build the xarchive
    shell: bash
    run: |
    set -eo pipefail
    xcodebuild  clean archive \
        -scheme ${{ inputs.project-name }} \
        -configuration "Release" \
        -sdk iphoneos \
        -archivePath "$PWD/build/${{ inputs.project-name }}.xcarchive" \
        -destination "generic/platform=iOS,name=Any iOS Device" \
        OTHER_CODE_SIGN_FLAGS="--keychain $RUNNER_TEMP/app-signing.keychain-db" \
        CODE_SIGN_STYLE=Manual \
        CODE_SIGN_IDENTITY="Apple Distribution"

Then you can export the archive of the latest step to a single .ipa file that is ready for the upload.

- name: Export .ipa
    shell: bash
    run: |
    set -eo pipefail
    xcodebuild -archivePath "$PWD/build/${{ inputs.project-name }}.xcarchive" \
        -exportOptionsPlist exportOptions.plist \
        -exportPath $PWD/build \
        -allowProvisioningUpdates \

The final step uploads the .ipa file to the App Store using the xcrun tool. An Apple user account with upload permission is needed and I recommend using a separate service account instead of the admin account.

- name: Publish the App on TestFlight
    shell: bash
    if: success()
    run: |
    xcrun altool \
        --upload-app \
        -t ios \
        -f $PWD/build/*.ipa \
        -u "${{ }}" \
        -p "${{ }}" \

You can find the complete action on Github.
If you have problems or need help with the action, simply open an issue in the repository.

Certificate Request without a Mac

To upload a game to the App Store, you need to create a Developer Certificate. The official Apple guide shows you how to do it easily with a Mac device in its official documentation.

But it can be done also without a Mac, using a Linux System, following this guide of a Github user.

Here you can find the needed steps, but since this might change over time, please always check the comments of the guide above.

  1. Generate a private key and certificate signing request: Change "" and "Simon Dalvai" with your values.
openssl genrsa -out distribution.key 2048
openssl req -new -key distribution.key -out distribution.csr -subj '/, CN=Simon Dalvai, C=IT'
  1. Upload CSR to apple at: Choose Production -> App Store and Ad Hoc

  2. Download the resulting distribution.cer, and convert it to .pem format:

openssl x509 -inform der -in distribution.cer -out distribution.pem
  1. Download Apple's Worldwide developer cert from portal and convert it to pem: - Worldwide Developer Relations - G4 (Expiring 12/10/2030 00:00:00 UTC
openssl x509 -in AppleWWDRCAG4.cer -inform DER -out AppleWWDRCAG4.pem -outform PEM
  1. Convert your cert plus Apple's cert to p12 format (choose a password for the .p12): Note: use -legacy if using opensssl v3.x
openssl pkcs12 -export -legacy -out distribution.p12 -inkey distribution.key -in distribution.pem -certfile AppleWWDRCAG4.pem 

Finally, update any provisioning profiles with the new cert, and download from dev portal.

  1. Create base64 of distribution.p12 for github actions Now you can prepare it for the Github Action
base64 distribution.p12 -w 0 > distribution.base64
  1. Add distribution.base64 and the previous created p12 password to the Github Action secrets in your repository settings

Every feedback is welcome

Feel free to write me an email at and comment on Mastodon or HackerNews.

github buttoncodeberg buttonlinkedin buttonmastodon buttonrss buttonemail button