Steps to automate deployment process for iOS applications to Test flight on any machine (p1)

Introduction

Are you tired of the manual and time-consuming process of deploying your iOS applications? Look no further. With the power of GitHub Actions, deploying iOS apps has never been easier. By automating the building, testing, and deployment tasks, you can save time and eliminate human error.

In this blog post, we will explore how to deploy iOS applications using GitHub Actions. I'll walk through the essential elements you need to prepare, such as setting up a build machine, managing certificates and provisioning profiles. Additionally, we'll dive into the concept of environment variables and how they can simplify the configuration process.

Preparation

A build machine with Installed Xcode

To build and package an iOS application, you need a dedicated build machine. Set up your correct version of Xcode. I would recommend to use Xcodes tool to manage multiples XCode versions on your build machine.
https://github.com/XcodesOrg/xcodes

Code signing certificate

I assume you know how to create/store/export a deployment certificate. Our certificate .p12 will be encoded to base64 string and stored as environment variables (please find detail in next segment). Then we create a bash script install_certificate.sh to

  • Create new keychain.
  • Install the certificate to the keychain.
  • Unlock the keychain to allow xcode access it to use the cert when exporting ipa.

Here is the content of the sript

#!/usr/bin/env sh

CERTIFICATE_P12=certificate.p12

# Decoding the base 64 cert and write it to a file
echo $CERT_BASE64 | base64 --decode > $CERTIFICATE_P12

# Creating new keychain with a password
security create-keychain -p $BUILD_KEY_CHAIN_PASSWORD $BUILD_KEY_CHAIN_NAME
# Setting the keychain as default
security list-keychain -s $BUILD_KEY_CHAIN_NAME
security default-keychain -s $BUILD_KEY_CHAIN_NAME
# Unlock the keychain
security unlock-keychain -p $BUILD_KEY_CHAIN_PASSWORD $BUILD_KEY_CHAIN_NAME
# start manipulate it
security set-keychain-settings $BUILD_KEY_CHAIN_NAME
# import the cert content, $2 is the certificate password
security import $CERTIFICATE_P12 -k $BUILD_KEY_CHAIN_NAME -P $CERT_PASSWORD -T /usr/bin/codesign;

# we allow codesign to use the keychain above
# codesign is the tool that xcode uses to sign our app
# https://stackoverflow.com/questions/39868578/security-codesign-in-sierra-keychain-ignores-access-control-settings-and-ui-p
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $BUILD_KEY_CHAIN_PASSWORD $BUILD_KEY_CHAIN_NAME

# Then just remove the file
rm -fr *.p12

This bash script will be called like

chmod +x install_profile.sh && ./install_certificate.sh

Provisioning profile

It's simpler with provisioning profile. We just need to write it to the file with correct name in a correct folder. Let make another bash script install_profile.sh

PROFILE_FILE=$1.mobileprovision

echo $PROVISIONING_BASE64 | base64 --decode > $PROFILE_FILE

cp ${PROFILE_FILE} "$HOME/Library/MobileDevice/Provisioning Profiles/$1.mobileprovision"

rm -fr *.mobileprovision

Nice, really easy to understand.

So your machine is ready to build because it has the cert & provisioning. From next steps, I will share what i achieve to automate the whole process.

Fastlane script

I will give you an example of the lane I made, please adjust depends on your team need.

desc "PROD Build and upload to AppStore if need"
lane :prod_appstore do |options|
  $version = options[:version] || get_version_number(xcodeproj: ENV["PROJECT_IDENTIFIER"], target: ENV["BUILD_TARGET"])
  $build = options[:build] || get_build_number(xcodeproj: ENV["PROJECT_IDENTIFIER"])

  prepare(
    plist: ENV["PLIST_FILE_NAME"], 
    version: $version, 
    build: $build, 
    team_id: ENV["APP_STORE_TEAM_ID"], 
    code_sign: ENV["CODE_SIGN_IDENTITY_NAME"], 
    provisioning: ENV["PROVISIONING_NAME"], 
    bundle_id: ENV["APP_BUNDLE_ID"],
    xcode_version: options[:xcode_version]
  )
  gym(
    scheme: scheme,
    export_method: "app-store",
    export_team_id: ENV["APP_STORE_TEAM_ID"],
    export_options: {
      provisioningProfiles: {
        ENV["APP_BUNDLE_ID"] => ENV["PROVISIONING_NAME"]
      },
      manageAppVersionAndBuildNumber: false
    },
    codesigning_identity: ENV["CODE_SIGN_IDENTITY_NAME"],
    clean: true,
    output_directory: "./builds",
    skip_build_archive: false,
    output_name: "just-a-name.ipa"
  )
  verify_build(
    provisioning_type: "distribution",
    bundle_identifier: ENV["APP_BUNDLE_ID"],
    team_identifier: ENV["APP_STORE_TEAM_ID"]
  )
end

def prepare(plist:, version:, build:, team_id:, code_sign:, provisioning:, bundle_id:, xcode_version:)
  puts "Configure Xcode and Project"
  if xcode_version
    xcversion(version: xcode_version)
  end

  ensure_git_status_clean
  increment_version_number(version_number: version)
  increment_build_number(build_number: build)

  update_code_signing_settings(
    use_automatic_signing: false,
    targets: [ENV["BUILD_TARGET"]],
    path: ENV["PROJECT_IDENTIFIER"],
    team_id: team_id,
    code_sign_identity: code_sign, 
    profile_name: provisioning,
  )

  update_app_identifier(
    plist_path: plist,
    app_identifier: bundle_id
  )

  update_project_team(
    path: ENV["PROJECT_IDENTIFIER"],
    targets: [ENV["BUILD_TARGET"]],
    teamid: team_id
  )

  cocoapods(clean_install: true)
end

Fastlane upload to App Store

This is sample steps in fastlane.

api_key = app_store_connect_api_key(
  key_id: ENV["APP_STORE_KEY_ID"],
  issuer_id: ENV["APP_STORE_ISSUER_ID"],
  key_content: ENV["APP_STORE_KEY"],
  is_key_content_base64: true,
  duration: 1200, # optional (maximum 1200)
  in_house: false
)

upload_to_testflight(
  ipa: "./builds/just-a-name.ipa", 
  skip_submission: true,
  skip_waiting_for_build_processing: true,
  api_key: api_key
)
end

I wrote details about some ways to upload in this blog
https://www.hapq.me/two-ways-to-upload-ipa-to-testflight-with-fastlane/

Environment Variables

This is the thing I missed for long time in my whole career. I found out these variables are live inside a terminal session. We can store it by running this command

export MY_FUTURE_USE_VARIABLE=Hello

then when need to use, add a dollar sign before the name

echo $MY_FUTURE_USE_VARIABLE

Really simple but i don't know why I missed this stuff.

In any CI/CD tools such as Gitlab, Jenkins, Github action... there're always predefined environment variables that tell you about the job, the pull request, the machine etc.... You can access it anywhere anytime you want.
Gitlab CI shared details of them: https://docs.gitlab.com/ee/ci/variables/predefined_variables.html

Github action & Gitlab CI

please wait for part 2 of this serie.