Continuous Deployment for iOS Apps - noi-techpark/odh-docs GitHub Wiki

Preparation on the Apple App Store Connect

We use the NOI Community App as an example throughout this tutorial, which has an App Name called NOI Community App and a App ID it.bz.noi.community...

  • Go to https://appstoreconnect.apple.com
  • Login with your Apple account (should be connected to an Admin role)
  • Go to "My Apps" and click the + sign
    • The Bundle ID should be a reverse package name, for example: it.bz.noi.community
  • Activate the internal testing track by adding some testers
  • Fill in additional needed information for that inside "Test Information"

Prepare secrets on GitHub

Create secrets under GitHub>Settings>Secrets (choose your repository):

  • APPLEID_PASSWORD
  • APPLEID_USERNAME
  • GOOGLE_SERVICE_INFO_PLIST
  • IOS_KEY_PASSPHRASE

Some information can be found on this blog.

Create a new Certificate

When inside https://developer.apple.com/account/resources/certificates/list go to "Certificates" and click on + as seen in the Apple Developer Docs.

  1. Create a Signing Request (CSR) (this tutorial assumes you do not have a Mac):

    • Certificate Type: iOS Distribution
    • Certificate Name: NOI SPA

    Optionally, for more details on this topic, see some openSSL examples and this StackOverflow post to get an idea how to do it

  2. Create a private key and then generate a certificate request from it:

    mkdir apple-keys
    cd apple-keys
    openssl req -newkey rsa:2048 -keyout it.bz.noi.community.ios.key.pem -out 
    it.bz.noi.community.ios.req.pem

    WARNING: Upload the .req.pem file not your private key .key.pem!

  3. Download your .cer certificate by clicking on the generated certificate

  4. Go to profiles, create your App Store provisioning profile with the certificate above and download it

  5. Encrypt both with your secret IOS_KEY_PASSPHRASE and move them to .github/secrets

    gpg --symmetric --cipher-algo AES256 ios_distribution.cer
    gpg --symmetric --cipher-algo AES256 itbznoicommunity.mobileprovision
    
    mkdir -p .github/secrets
    cp itbznoicommunity.mobileprovision.gpg .github/secrets/it.bz.noi.community.mobileprovisioning.gpg
    

On your repository

Create a script called decrypt_secrets.sh

  • create a folder .github/secrets
  • create a file decrypt_secrets.sh with this content (adapt MOBILEPROV_NAME and CERTIFICATE_NAME):
#!/bin/bash

set -xeo pipefail

MOBILEPROV_NAME="it.bz.noi.community.mobileprovision"
CERTIFICATE_NAME="ios_distribution.p12"
SECRETS_PATH=".github/secrets"

# DECRYPT FILES
gpg --quiet --batch --yes --decrypt --passphrase="$IOS_KEY_PASSPHRASE" --output $SECRETS_PATH/$MOBILEPROV_NAME $SECRETS_PATH/$MOBILEPROV_NAME.gpg
gpg --quiet --batch --yes --decrypt --passphrase="$IOS_KEY_PASSPHRASE" --output $SECRETS_PATH/$CERTIFICATE_NAME $SECRETS_PATH/$CERTIFICATE_NAME.gpg

mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles/
cp $SECRETS_PATH/$MOBILEPROV_NAME ~/Library/MobileDevice/Provisioning\ Profiles/

security create-keychain -p "" build.keychain
security import $SECRETS_PATH/$CERTIFICATE_NAME -t agg -k ~/Library/Keychains/build.keychain -P "$IOS_KEY_PASSPHRASE" -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

Create a exportOptions.plist file

Take the following snippet and change to your teamID and bundle ID:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>app-store</string>
    <key>teamID</key>
    <string>5V2Q9SWB7H</string>
    <key>destination</key>
    <string>export</string>
    <key>uploadSymbols</key>
    <true/>
    <key>uploadBitcode</key>
    <true/>
	<key>provisioningProfiles</key>
    <dict>
        <key>it.bz.noi.community</key>
        <string>it.bz.noi.community</string>
    </dict>
    <key>signingCertificate</key>
    <string>Apple Distribution</string>
</dict>
</plist>

Create a GitHub Action file

  • Create a folder .github
  • add a file main.yml
  • fill it with the following content and adapt it to your folders, ID and profile:
name: CI
on: [ pull_request, push ]
jobs:

  ## Test on latest MacOS
  test:
    runs-on: macos-11
    steps:

      - name: Checkout the code
        uses: actions/checkout@v2

      #   # List possible Xcode versions
      # - name: Force Xcode versions
      #   run: sudo ls /Applications/*xcode*

      # See https://xcodereleases.com/ for details (we choose the latest Release)
      - name: Force XCode 13.0.0
        run: sudo xcode-select -switch /Applications/Xcode_13.0.app

      - name: Inject GoogleService-Info.plist
        env:
          GOOGLE_SERVICE_INFO_PLIST: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST }}
        run: echo "$GOOGLE_SERVICE_INFO_PLIST" > NOICommunity/GoogleService-Info.plist        

      - name: Resolve package dependencies
        run: xcodebuild -resolvePackageDependencies

      - name: List schemes (optional step to gather information)
        run: xcodebuild -list -project NOICommunity.xcodeproj

      - name: Test the app
        run: |
          set -eo pipefail
          xcodebuild clean test \
            -scheme NOICommunity \
            -destination 'platform=iOS Simulator,OS=15.0,name=iPhone 13' \
            IPHONEOS_DEPLOYMENT_TARGET='15.0' \
          | xcpretty

  ## Deploy to TestFlight
  deploy_testflight:
    name: Deploy to Testflight
    runs-on: macos-11
    if: github.ref == 'refs/heads/development'
    needs: [ test ]
    steps:

      - name: Checkout the code
        uses: actions/checkout@v2

      # See https://xcodereleases.com/ for details (we choose the latest Release, available on macos-latest)
      - name: Force XCode 13.0.0
        run: sudo xcode-select -switch /Applications/Xcode_13.0.app

      - name: Resolve package dependencies
        run: xcodebuild -resolvePackageDependencies

      - name: Inject GoogleService-Info.plist
        env:
          GOOGLE_SERVICE_INFO_PLIST: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST }}
        run: echo "$GOOGLE_SERVICE_INFO_PLIST" > NOICommunity/GoogleService-Info.plist

      - name: Install gpg (if absent)
        run: gpg --version &>/dev/null || brew install gnupg

      - name: Setup provisioning profile
        env:
          IOS_KEY_PASSPHRASE: ${{ secrets.IOS_KEY_PASSPHRASE }}
        run: ./.github/secrets/decrypt_secrets.sh

      - name: Increment build number
        run: |
          set -eo pipefail
          BUILD_NUMBER=$(date "+%Y%m%d%H%M%S")
          /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" NOICommunity/Info.plist

      - name: Archive the project
        run: |
          set -eo pipefail
          xcodebuild clean archive \
            -configuration Release \
            -scheme NOICommunity \
            -sdk iphoneos \
            -archivePath "$PWD/build/NOICommunity.xcarchive" \
            -destination "generic/platform=iOS,name=Any iOS Device" \
            OTHER_CODE_SIGN_FLAGS="--keychain ~/Library/Keychains/build.keychain" \
            CODE_SIGN_STYLE=Manual \
            PROVISIONING_PROFILE='a5e85966-b44d-4f43-b01d-babf8ce192c3' \
            CODE_SIGN_IDENTITY="Apple Distribution"

      - name: Export .ipa
        run: |
          set -eo pipefail
          xcodebuild -archivePath "$PWD/build/NOICommunity.xcarchive" \
            -exportOptionsPlist exportOptions.plist \
            -exportPath $PWD/build \
            -allowProvisioningUpdates \
            -exportArchive 

      - name: Publish the App on TestFlight
        if: success()
        env:
          APPLEID_USERNAME: ${{ secrets.APPLEID_USERNAME }}
          APPLEID_PASSWORD: ${{ secrets.APPLEID_PASSWORD }}
        run: |
          xcrun altool \
            --upload-app \
            -t ios \
            -f $PWD/build/NOICommunity.ipa \
            -u "$APPLEID_USERNAME" \
            -p "$APPLEID_PASSWORD" \
            --verbose

Hint: To get the UUID of your provisioning profile you can do:

grep UUID -A1 -a it.bz.noi.community.mobileprovision | grep -io "[-A-F0-9]\{36\}"
⚠️ **GitHub.com Fallback** ⚠️