iOS & Android Builds using Github Actions (Without Fastlane)

The past few days I was attempting to auto deploy a react native app to App Center using Github actions. This process is the exact same for a fully native app, since we are running the normal xcode / gradle commands. I'll walk you through all the annoying things Apple put me through, so you don't have to. If you only want to build for Android, you can skip to that and be done in a few minutes... iOS setup is much more involved
Contents
- iOS
- Install GPG
- Add Certs
- Encrypt Certs
- Decrypt + Install Certs
- Selecting xcode version
- NPM / Yarn
- Pods
- Configure Build
- Exporting
- Deploy
- Android
iOS
There's not much out there when you search for help with iOS builds, and the only ones you find tell you to use fastlane for cert management. I knew there had to be a way though, even with my naive knowledge of the profiles and certificate logic that iOS app builds have, I know I only needed to download two files in order to build an app on my local machine, so it couldn't be too hard, right?
Before I start, let me quickly rant about Apple. I was able to get it mostly working, then ran into the code signing process prompting for a password and hanging indefinitely inside the build container.... The fix for this is a command that makes no sense, and someone had to reverse engineer Apple's tooling just to find it. It's been almost 4 years and Apple has yet to improve on any of these command line utilities. It's honestly a miracle we are able to make this work at all.
Getting Started
Instead of stepping through each little piece of the config, I'm going to share the final piece of the main config, then explain the parts around it. If you are not using react native, you will need to remove the ios
folder prefix from my commands. Place this inside .github/workflows/ios-build.yaml
name: Deploy iOS to App Center
on:
push:
branches:
- master
jobs:
build:
runs-on: macOS-latest
steps:
- uses: actions/checkout@v1
- name: Install gpg
run: brew install gnupg
- name: Switch XCode Version
run: sudo xcode-select -s /Applications/Xcode_11.2.app
- name: Cache NPM dependencies
uses: actions/cache@v1
with:
path: node_modules
key: ${{ runner.OS }}-npm-cache-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.OS }}-npm-cache-
- name: Install yarn dependencies
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
yarn --frozen-lockfile
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Cache Pods dependencies
uses: actions/cache@v1
with:
path: ios/Pods
key: ${{ runner.OS }}-pods-cache-${{ hashFiles('**/ios/Podfile.lock') }}
restore-keys: |
${{ runner.OS }}-pods-cache-
- name: Install pod dependencies
run: |
cd ios
pod install
shell: bash
- name: Setup provisioning profile
run: ./.github/secrets/decrypt_secrets.sh
env:
IOS_PROFILE_KEY: ${{ secrets.IOS_PROFILE_KEY }}
- name: Build app
run: |
cd ios && xcodebuild archive \
-workspace appname.xcworkspace \
-scheme appname \
-sdk iphoneos13.2 \
-configuration Release \
-archivePath $PWD/build/appname.xcarchive \
IPHONEOS_DEPLOYMENT_TARGET=9.0
PROVISIONING_PROFILE="UUID HERE" \
CODE_SIGN_IDENTITY="iPhone Distribution: Some Company (T343ff)"
- name: Export app
run: |
cd ios && xcodebuild \
-exportArchive \
-archivePath $PWD/build/appname.xcarchive \
-exportOptionsPlist $PWD/ci.plist \
-exportPath $PWD/build
- name: Deploy to App Center
run: |
npm install appcenter-cli@2.3.3
npx appcenter distribute release --token "${{secrets.APP_CENTER_TOKEN}}" --app "apporg/appname" --group "group-name" --file "ios/build/appname.ipa" --release-notes "$(git log -1 --pretty=format:%s)" --debug
This tutorial is assuming that you have a signing certificate and provisioning profile for the app on you machine already. Also, I'm doing an ad-hoc build.
Install GPG
I followed the official guide from github on how to create secrets that are too large to be stored as plain text. Unfortunately, they fail to mention that gpg isn't installed on the Mac OS images, but it is on all the others. There's an open issue about getting this added to the Mac OS image itself. This would be great because it takes about 2.5 minutes just to install gpg. We will be using it to encrypt our provisioning profile and signing certificate.
Add Cert / Profile to the repo
Here's what I did first. Open up your app in xcode, go to Build Settings under your app target, and find Signing

Hover over the release profile, and to the right of it you should see Profile Name (uuid)
Copy this uuid. We need it to get the profile and it's also needed in a later step.
Open up a terminal window inside of your app's directory. Make a folder called .github
inside it create workflows
and secrets
Next, run this:
cat ~/Library/MobileDevice/Provisioning\ Profiles/UUID.mobileprovision >> profile.mobileprovision
I know, so weird that I didn't use cp
. I was thinking the same thing. Anyways, be sure to paste the UUID from xcode so that you get the right one.
Next, we need to encrypt it, this way other people can't access the profile. But first, we need to grab the signing certificate as well.
Inside xcode, go to preferences -> accounts -> click the account you're using -> manage certificates -> right click on the cert you are using, and click export.
Don't give it a password. Put it inside the app folder as well, it should be named Certificates.p12
Encrypt Certificate and Profile
Encrypting them is easy, make sure you have gpg
installed. On Mac, you can run brew install gpg
, linux machines should have it already installed. From a terminal window inside your app:
gpg --symmetric --cipher-algo AES256 Certificates.p12
gpg --symmetric --cipher-algo AES256 profile.mobileprovision
For both commands, I used a randomly generated password. I recommend using the same password for both files.
Immediately after you finish running the command for both files, open up the settings page for your repo on github, go to secrets, and create a new one called IOS_PROFILE_KEY
and the value is the password you just used for gpg. Next, delete Certificates.p12
and profile.mobileprovision
. We don't want to accidentally commit these since they are unencrypted.
Lastly, move Certificates.p12.gpg
and profile.mobileprovision.gpg
to the .github/secrets
folder.
Decrypt and Install Certs on the server
We're almost there with this gpg stuff, I promise. Create .github/secrets/decrypt_secrets.sh
and put the following inside it:
#!/bin/sh
gpg --quiet --batch --yes --decrypt --passphrase="$IOS_PROFILE_KEY" --output ./.github/secrets/profile.mobileprovision ./.github/secrets/profile.mobileprovision.gpg
gpg --quiet --batch --yes --decrypt --passphrase="$IOS_PROFILE_KEY" --output ./.github/secrets/Certificates.p12 ./.github/secrets/Certificates.p12.gpg
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp ./.github/secrets/profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/86ce4d81-fd7e-46b2-ae6a-3092b4af6cd7.mobileprovision
security create-keychain -p "" build.keychain
security import ./.github/secrets/Certificates.p12 -t agg -k ~/Library/Keychains/build.keychain -P "" -A
security list-keychains -s ~/Library/Keychains/build.keychain
security default-keychain -s ~/Library/Keychains/build.keychain
security unlock-keychain -p "" ~/Library/Keychains/build.keychain
security set-key-partition-list -S apple-tool:,apple: -s -k "" ~/Library/Keychains/build.keychain
First off, we decrypt both files using the secret key we just added to github. Next, we copy the mobile provision into the same location that it existed on our local machine. Funny enough.... I never saw this anywhere because I could only find people using fastlane. It was just a lucky guess that this is the correct procedure.
After that is the weird stuff... We create a new keychain on the build server. This might not be necessary, but through my trials and errors I ended up doing it. The problem I initially ran into was xcode building correctly, but hanging forever because the codesign tool would prompt asking for a password, clearly we can't go into the UI and hit the enter key (the password is blank).
The final line, set-key-partition-list, is the most important part that fixes the issue I described above. Even though nobody seems to really understand the meaning of this command. You can read more about it on this SO post I found. The commenter seemed to give more info than Apple does about this whole process, so I followed them fully, by including the new keychain part. Do as you wish, but the config I have works :)
Make sure you run chmod +x .github/secrets/decrypt_secrets.sh
before you commit any of this.
Back to the actions
With most of the setup out of the way, let me continue explaining the actions file I posted above. Please have it open while reading along below.
Selecting xcode version
After installing gpg, we can select an xcode version. This may be really outdated by the time you're reading it. Github has a nifty docs page that is about a mile long. Here's the section on xcode. This way you can adjust the config if you need to use a different version of xcode or the iOS sdk.
npm / yarn
Next step is caching the npm dependencies then running yarn install. You can remove it if you're doing a fully native app. If you are using React Native, this will cache the node_modules
folder until the yarn.lock
changes. If you use npm, simply change yarn.lock
to package.json
You'll notice my yarn install step is setting an NPM_TOKEN value. My app uses private npm packages so this piece of code is needed for it to download them.
Pods
Very similar to npm / yarn step, this will cache all the pods until the lock file changes. Very self explanatory.
Build app
Okay, our last couple things to configure...
cd ios && xcodebuild archive \
-workspace appname.xcworkspace \
-scheme appname \
-sdk iphoneos13.2 \
-configuration Release \
-archivePath $PWD/build/appname.xcarchive \
IPHONEOS_DEPLOYMENT_TARGET=9.0
PROVISIONING_PROFILE="UUID" \
CODE_SIGN_IDENTITY="iPhone Distribution: Company Name (RT483843)"
Replace appname
with the actual app name you see in xcode. Replace UUID
with the mobile provision uuid that we copied over in the gpg encryption step. There may be an easier way, but here's how I got the CODE_SIGN_IDENTITY
value... Inside xcode, with the project open, go to General
and click the little i
icon next to the Signing (Release)
section. It should be spelled out under the Certificates included
section. I hope this helps. If you still can't find it, the next step will have you do a build inside xcode, and the outputted plist file should have it inside.
Exporting
cd ios && xcodebuild \
-exportArchive \
-archivePath $PWD/build/appname.xcarchive \
-exportOptionsPlist $PWD/ci.plist \
-exportPath $PWD/build
Replace appname with the app's name one more time, this command actually spits out the .ipa
file. To get the exportOptionsPlist
, inside xcode, do a product -> archive of your app. Choose distribute, choose ad-hoc. And then export it to your desktop. Now copy the file inside it, ExportOptions.plist
to appname/ios/ci.plist
. It's almost all set for you, but we have to add the following BEFORE ApplicationProperties
<key>method</key>
<string>ad-hoc</string>
<key>compileBitcode</key>
<false/>
<key>provisioningProfiles</key>
<dict>
<key>com.cnn.marvel</key>
<string>UUID of provisiong profile</string>
</dict>
Now it should be good to go and successfully build...
Deploy it!
npm install appcenter-cli@2.3.3
npx appcenter distribute release --token "${{secrets.APP_CENTER_TOKEN}}" --app "org/appname" --group "groupname" --file "ios/build/cnnrnv2.ipa" --release-notes "$(git log -1 --pretty=format:%s)" --debug
The ipa is here: ios/build/cnnrnv2.ipa
Which means it's up to you what to do with it. The Mac OS environment doesn't support docker actions on github, most actions won't work. If there's not an existing action for the platform you're using, you will have to make a node module and deploy it to npm like appcenter does.
You'll see that I am deploying to app center. They offer a node cli, so I install it, and then run the distribute command with the path to the ipa file. Take note of this part: --release-notes "$(git log -1 --pretty=format:%s)"
this attaches the commit to the release notes. Might be useful to pass this git log command for any service you may be using.
Android
This is much quicker than iOS. Here's the config. Place it in .github/workflows/android-deploy.yaml
name: Deploy Android to App Center
on:
push:
branches:
- master
jobs:
build-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- uses: actions/setup-node@v1
with:
node-version: 12
registry-url: https://registry.npmjs.org/
- name: Cache NPM dependencies
uses: actions/cache@v1
with:
path: node_modules
key: ${{ runner.OS }}-npm-cache-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.OS }}-npm-cache-
- name: Install yarn dependencies
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
yarn --frozen-lockfile
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Build for Android
run: cd android && ./gradlew assembleRelease
- name: Deploy to App Center
uses: zackify/AppCenter-Github-Action@1.0.0
with:
appName: org/appname
token: ${{secrets.APP_CENTER_TOKEN}}
group: group name
file: android/app/build/outputs/apk/release/app-release.apk
Thats.... seriously it. So much easier than iOS. Not much setup is required here, but I'll go over the file.
Setup
The first three actions are setup. Checking out the code, adding in node and java. If you are not using react native you may remove the node action and yarn install step.
Caching npm dependencies
As explained in the iOS tutorial, this will hash the yarn.lock file (change to package-lock.json if using npm) and restore the node_modules
each time it is ran, if the lock hasn't changed.
Install yarn dependencies
This step adds my NPM_TOKEN secret to let yarn access private npm packages, otherwise its a basic yarn install command.
Build for Android
Builds and spits out the apk for us! Standard gradle.
Deploy
Please reference the deploy section for iOS. The only difference is the nicely built action that we are able to use, because linux supports all docker based actions.
Conclusion
I hope I didn't miss anything. I tried my best to reference issues and articles I found along the way. If you get stuck during any of this, or have other solutions, please reach out on twitter @zachcodes so I can update the article. I really wish Apple would invest in making their command line tooling better. I don't mind having to encrypt the cert and profile, but let me pass them in to the build command as arguments... Also, why do we need to create this extra plist file just to export? Give me an ad-hoc build command and remove the duplication of setting the profile uuid in every command... Oh well...
At least we have a way to make it work! Overall I'm happy with the outcome, just shocked at how Apple doesn't bother making any of this an easy process. Github has definitely taken away much of the pain.