Continuous Deployment — iOS Apps

Rakesh Chander
3 min readOct 25, 2020

CD is becoming part of mandate for iOS Apps Release process. Here I am going to explain — How to setup Jenkins Job for uploading iOS Apps on App Store.

Pre-Requiste

Add one plist file at root of your project folder — ExportOptions.plist — This file is required for AppStore Connect Details of project.
Generate App Specific Password for your App Store Account — for Application Loader
Distribution Certificates & Provisioning profiles are already installed on Jenkins Runner Machine.

Jenkins Job SetUp —

Aborting Job in case error occurs at any stage of job — add below line as first line of your shell script -

set -e

Clean up workspace from previous builds -

rm -rf $WORKSPACE;mkdir $WORKSPACE || true
cd $WORKSPACE

Specify App Store Account credentials — app sepcific credentials for Application Loader —

APPSTORE_USERNAME=”<Account_Email_ID>”
APPSPECIFIC_PASSWORD=”<Password_Generated_On_Portal>”

Specify GIT_URL , GIT_USERNAME, GIT_PASSWORD —

GIT_URL=”<URL>”
GIT_USERNAME=”<Username>”
GIT_PASSWORD=”<Password>”
GIT_TAG=”<Tag Value>”

Specify Project Properties —

SCHEME=”<Project_Scheme>”
EXPORT_OPTIONS_PLIST=”<File_Name_ExportOptions>”

Add below lines to checkout code from tag only (if any of above value is incorrect (tag also), job will fail and abort) —

url=${GIT_URL#*//}
git clone http://${GIT_USERNAME}:${GIT_PASSWORD}@${url} .
git checkout tags/${GIT_TAG}

If your project is using cocoapods, add below lines —

rm -rf Pods
rm -rf Podfile.lock
pod install

Set up archive & ipa path —

workspace=$(ls | grep xcworkspace | awk -F.xcworkspace ‘{print $1}’)
ARCHIVE_PATH=$PWD/archive/${workspace}.xcarchive
EXPORT_PATH=$PWD/build
IPA=”$PWD/build/${SCHEME}.ipa”

Archive your project —

xcodebuild -quiet clean archive -workspace ${workspace}.xcworkspace -scheme “${SCHEME}” -sdk iphoneos -archivePath $ARCHIVE_PATH -allowProvisioningUpdates

Exporting ARCHIVE to generate IPA —

xcodebuild -quiet -exportArchive -archivePath $ARCHIVE_PATH -exportOptionsPlist ${EXPORT_OPTIONS_PLIST}.plist -exportPath $EXPORT_PATH

Upload IPA on App Store —

xcrun altool — upload-app -f “${IPA}” -u ${APPSTORE_USERNAME} -p ${APPSPECIFIC_PASSWORD}

Thats It!!

Now just run job and it will take care of everything — You can enjoy coffee!!

Add On

  • Single Jenkins Job for different Variants of project — DEV / UAT / PVG / PROD

If your project is having different schemes for different EndPoints and you want to generate IPA accordingly then you can make SCHEME variable as choice parameter (Make Sure — Choice options are having exact names as that of your scheme names in Project)
- Parameter Name is SCHEME
- Remove SCHEME declaration from section “Specify Project Properties”

  • Setting Up Build Version / Build No from Jenkins Job

In your project target settings — set Build Version & Build No as variable VERSION_NUMBER & BUILD_NUMBER respectively. (By default, this is the setting)

Then in Jenkins Job, take two more inputs and name that as above. In Jenkins Script, just before archiving , put below lines -

xcrun agvtool new-marketing-version ${VERSION_NUMBER}
xcrun agvtool new-version -all ${BUILD_NUMBER}

Continuous Deployment —

We can execute this Jenkins Job as soon as project is getting tagged on Master Branch to automate the process. This is achieved via YML file in Continuous Integration. Refer my story Continuous Integration for setup and basics details.

At your CI runner machine, create a shell script file — deploy_app.sh & edit below script code —

#!/bin/bashVERSION_TAG=`git describe --tags`curl -X POST --data-urlencode json='{"parameter": [{"PROJECT": "<PROJECT_NAME>", "value": "VERSION_TAG", "scheme":"PROD"}]}' http://<JENKINS_SERVER_URL>/job/<TESTFLIGHT_JOB_NAME>/build

As per your Jenkins Job needs, you can pass required parameters via “json” from depoly_app.sh

Now, in your YML file, add a new stage named “deploy” and corresponding job with below details —

stages:
- test
- build
- sonar
- deploy
build_core_all:
stage: build
script:
- rm -rf Pods
- rm -rf Podfile.lock
- pod install
- bash $HOME/Documents/Pre-Build/pre_build.sh
- xcodebuild -workspace <WORKSPACE_NAME>.xcworkspace -scheme <SCHEME_NAME> clean build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED=NO -destination generic/platform=iOS
except:
- tags
test_all:
stage: test
script:
- rm -rf Pods
- rm -rf Podfile.lock
- pod install
- xcodebuild -workspace <WORKSPACE_NAME>.xcworkspace -scheme <SCHEME_NAME> clean test -destination 'platform=iOS Simulator,name=iPhone 11,OS=14.0' -enableCodeCoverage YES
except:
- tags
- master
test_sonar:
stage: sonar
script:
- rm -rf Pods
- rm -rf Podfile.lock
- pod install
- xcodebuild -workspace <WORKSPACE_NAME>.xcworkspace -scheme <SCHEME_NAME> clean test -destination 'platform=iOS Simulator,name=iPhone 11,OS=14.0' -enableCodeCoverage YES -derivedDataPath Build/
- bash xcode-coverage-mapper.sh Build/Logs/Test/*.xcresult/ > sonarqube-generic-coverage.xml
- bash $HOME/Documents/Sonar/sonar*/bin/sonar-scanner
- bash sonar_validation.sh
only:
- develop
deploy:
stage: deploy
script:
- echo "Release to Production"
- bash $HOME/Documents/Deploy/deploy_app.sh
only:
- tags
except:
- branches

So, You are all done now. As soon as you will be merging your code to master and creating a tag onto that your App will automatically be deployed to TestFlight.

--

--

Rakesh Chander

I believe in modular development practices for better reusability, coding practices, robustness & scaling — inline with automation.