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 key CFBundleShortVersionString
  • 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.

Opening iOS product Info.plist

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

watchOS app Info.plist value

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

watchOS app target config

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.