In the previous article, we learned how to auto-deploy the latest build on the commit of the master branch.
However, auto-deployment requires auto-increment of version as AppStore requires a new version code and version name for each build.
If you haven’t read the previous article, I highly recommend reviewing it once.
Today, we will learn how we can manage auto-versioning.
We will follow semantic versioning.
Basically, the version name will be created with a combination of major, minor, and patch numbers.
For example, for app version 1.4.0 —
1 - major number
4 - minor number
0 - patch number
If we want to auto-deploy apps on every commit, we can keep major and minor versions static and make the patch number dynamic. For patch number, we can use the pipeline ID which changes with every commit, thus every build with have a unique version name and number.
To conclude —
We will need to make 3 changes in our project to support auto-versioning.
To store version numbers we will add one file in our project which will only have the build name.
Consider a scenario where we want to push the app with the 1.4.0 version. Here, we should add the file with the name VERSION
.
1.4.0
Note: Add this file to the root directory of your project so that it will be easy to access for further use.
Now we have to add a script for versioning in the deployment script of CI in the .gitlab-ci.yml
file.
As mentioned before, we will use pipeline ID as our patch number which will be incremented with every commit.
This pipeline ID is available as an environment variable CI_PIPELINE_IID
by default in GitLab CI.
Let’s first see what our script will look like after adding the auto-versioning changes to the existing deployment script.
deploy_testflight:
stage: deploy
script:
- chmod +x install_certificate.sh && ./install_certificate.sh
- chmod +x install_profile.sh && ./install_profile.sh
- pod install --repo-update
- file='ProjectName/VERSION'
- fileData=`cat $file`
- IFS='.'
- read -a versionValue <<< "$fileData"
- versionNumber=$(expr `expr ${versionValue[0]} \* 1000000` + `expr ${versionValue[1]} \* 10000` + ${CI_PIPELINE_IID})
- IFS=''
- buildName="${versionValue[0]}.${versionValue[1]}.$CI_PIPELINE_IID"
- echo "Uploading build $buildName"
- ARCHIVE_PATH="$HOME/Library/Developer/Xcode/Archives/ProjectName/${CI_COMMIT_SHA}/${CI_JOB_ID}.xcarchive"
- EXPORT_PATH="$HOME/Library/Developer/Xcode/Archives/ProjectName/${CI_COMMIT_SHA}/${CI_JOB_ID}/"
- xcodebuild app_version_code=${versionNumber} app_version_name=${buildName} -workspace ProjectName.xcworkspace -scheme "ProjectName" clean archive -sdk iphoneos -archivePath $ARCHIVE_PATH PROVISIONING_PROFILE_SPECIFIER="${DISTRIBUTION_PROVISION_UUID}" CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY="${CODE_SIGN_IDENTITY}" | xcpretty --c
- xcodebuild -exportArchive -archivePath $ARCHIVE_PATH -exportOptionsPlist ExportOptionsAppStore.plist -exportPath $EXPORT_PATH PROVISIONING_PROFILE_SPECIFIER="${DISTRIBUTION_PROVISION_UUID}" CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY="${CODE_SIGN_IDENTITY}"
- echo "Collecting artifacts.."
- cp -R "${ARCHIVE_PATH}/dSYMs" .
- IPA="${EXPORT_PATH}ProjectName.ipa"
- echo $IPA
- echo "Uploading app to iTC..."
- xcrun altool --upload-app -t ios -f $IPA -u $ITC_USER_NAME -p $ITC_USER_PASSWORD
artifacts:
paths:
- dSYMs
only:
- main
tags:
- runner-tag-name
Now, Let’s understand this whole script in detail.
We start by reading the VERSION
file we created earlier and storing the value in a fileData
variable. Here replace ProjectName/VERSION
with your actual path of the file.
- file='ProjectName/VERSION'
- fileData=`cat $file`
Now let's separate major, minor, and patch numbers from the full string.
- IFS='.'
- read -a versionValue <<< "$fileData"
Now, now we will create versionNumber
and buildName
by using the major and minor numbers from VERSION
file but replacing the patch number with CI’s pipeline number.
- versionNumber=$(expr `expr ${versionValue[0]} \* 1000000` + `expr ${versionValue[1]} \* 10000` + ${CI_PIPELINE_IID})
- IFS=''
- buildName="${versionValue[0]}.${versionValue[1]}.$CI_PIPELINE_IID"
That’s it for the version generation script!
Now, let’s pass both versionNumber
and buildName
to xcodebuild
command, so they will be replaced in the archive that gets generated as we will see in the next step.
- xcodebuild app_version_code=${versionNumber} app_version_name=${buildName} -workspace ProjectName.xcworkspace -scheme "ProjectName" clean archive -sdk iphoneos -archivePath $ARCHIVE_PATH PROVISIONING_PROFILE_SPECIFIER="${DISTRIBUTION_PROVISION_UUID}" CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY="${CODE_SIGN_IDENTITY}" | xcpretty --c
You may be wondering about app_version_code
and app_version_name
variables, they are user-defined variables.
Let’s explore them!
To pass the version code and name from the Xcode command-line tool while building the project, we will have to add app_version_code
and app_version_name
variables in Xcode at two places:
Info.plist
:CFBundleVersion
— ${app_version_code}
CFBundleShortVersionString
— ${app_version_name}
app_version_code
= 1040000
app_version_name
= 1.4.0
Here, we are asking Xcode to use user-defined variables for version name and number and then we change them dynamically during the build on CI.
Tadda… That’s it !!!
Now, if you have to push any new feature or significant update and want to bump the version with the major or minor number then you just have to update the version number in the VERSION
file and push it to GitLab. Along with merging it to master, it’ll automatically update the version in iTunes Connect.
Once done, you will never have to worry about pushing build on CI. Commit your changes and you will have a build ready on Testflight within a few minutes.
Doesn’t that sound amazing?
Obviously yes! During my initial journey as an iOS developer, I wasted so many hours a week just building, testing, and deploying iOS builds. Getting rid of all those responsibilities and automating it gives me a big relief and saves around 25% of my time!