Setting an iOS App version number with Git tags in Xcode Cloud
While migrating to Xcode Cloud as CI of our iOS and watchOS apps, we ran into some problems when it comes our workflow to release a new version whenever a new version tag was created, because the embedded watchOS app didn’t have a matching version to the iOS app. TLDR; you can jump to the result section for the final solution.
At SATS in December 2021 we were using Xcode Cloud to deploy the iOS app. Later we opted to migrate to CircleCI using fastlane to accomplish this. Currently, we decided to migrate back to Xcode Cloud due to issues in CircleCI with Xcode 15 and being able to use automatic signing on the CI service.
The SATS iOS app is accompanied by a watchOS app. We use a CI workflow where we upload the app to TestFlight/AppStore whenever we create a git tag with the shape of v5.8.0
to set the version of the app we are shipping and this version must match for all targets, including the watchOS app.
The way Xcode projects set the version number of an app can come from different places:
- the
Info.plist
file in the keyCFBundleShortVersionString
- the Xcode Target’s version setting for
CFBundleShortVersionString
- the Xcode Project’s version setting
When setting up Xcode Cloud and testing the archive workflows, we saw errors like:
The value of CFBundleShortVersionString in your WatchKit app’s Info.plist (4.8.0) does not match the value in your companion app’s Info.plist (5.8.0). These values are required to match.
I was extremely confused about this error, since we had a ci_scripts/ci_post_clone.sh
script that contained
echo "Version: ${versionNumber}"
echo "Setting version string in plists"
echo "iOS Version:"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $versionNumber" $CI_WORKSPACE/MainApp/Resources/Plists/Info.plist
/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString $versionNumber" $CI_WORKSPACE/MainApp/Resources/Plists/Info.plist
echo "Watch Version:"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $versionNumber" $CI_WORKSPACE/WatchApp/Info.plist
/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString $versionNumber" $CI_WORKSPACE/WatchApp/Info.plist
And in the build in question, the script executed successfully.
The problem
While debugging locally, building the iOS app. I noticed the Info.plist
of the generated product actually contained the right version.
Then doing the same with the generated watchOS app and opening its Info.plist
I noticed the version number there was indeed 4.8.0
After a lot of headbanging, I was googling around and I came across this question in the Apple Forums: Watch app version issue in Xcode Cloud which was ironically asked by me back in 2021. I also didn’t find too much info about the subject, then understanding that there were multiple sources for the version numbers, with some experimentation, I came to realize that the GENERATE_INFOPLIST_FILE
setting in the watch target was prioritizing the target’s version number over the Info.plist
The solution
With a better understanding of where the version number comes from, I came across this blog post Easily Keep Build Numbers And Marketing Versions In Sync - The Swift Dev Blog where it suggested to keep a single source of truth for the version number… the Xcode Project’s version number.
That was great, but this didn’t quite solve my problem as then I struggled to answer how do I programmatically set the Xcode project’s version number?. Again, I didn’t find too much information about it.
Later I remembered reading about Xcode Build Configuration Files or .xcconfig
files. Then looking for the documentation from apple in Adding a build configuration file to your project, I thought this could potentially help me to set the version number value.
The result
We can use a .xcconfig
to set the Xcode project’s version number. Our config file now looks like:
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
MARKETING_VERSION = 5.7.2
Finally in our ci_post_clone.sh
script we invoke a set_version.sh
script that looks like:
#!/bin/sh
echo "==== Set version"
echo "Fetching tags"
git fetch --tags
versionString=$(git tag --list --sort=-version:refname "v*" | head -n 1)
versionNumber="${versionString:1}"
echo "Set Version '${versionNumber}' in the config file"
sed -i '' "s/MARKETING_VERSION = .*/MARKETING_VERSION = $versionNumber/" ../Configuration/BaseConfig.xcconfig
Which will update the version number for the project when we create a new version tag, with the right value and across all targets.
At the end, this small detail made me spend a good time learning how this worked and coming up with a solution that fitted our workflow, which inspired me to write about this process/solution due to the lack of information on this particular topic. Let us know if there are other approaches to explore as well.