App Store Version Territory
Homegrown research on iOS app short and bundle version strings and how to specify them properly in respect to iTunes Connect and TestFlight.
Version Strings Intro
A typical iOS app needs 2 version strings to be defined. This is an example of Xcode 6.3.2 UI.
The first version string is referred to as just “Version”. It is also know as Short Bundle Version String and is stored under CFBundleShortVersionString
key in the app’s Info.plist.
This is what your users expect to see on the “About” screen in the app, if they care. The general rule is to use Semantic Versioning scheme in the Major.Minor.Patch
form, e.g. 1.0.0
. It is also possible to use more user-friendly lightweight option Major.Minor
, for example 1.0
, or even use just 1
, the decision is up to you. Later in this post and in the Summary section, I will give you the exact guidelines for short version string.
Second is Bundle Version. In Xcode UI it is called just “Build” and is stored under CFBundleVersion
key in Info.plist. Another common name for this version string is “Build Version” or even “Build Number”. Its purpose is to uniquely identify this particular bundle (aka build) among all the other builds for the current app version. Often bundle version can be unique across all versions of the app ever, though this is not mandatory any more.
You are free to put whatever you want as version strings, it will be compiled, linked and archived with no errors or warnings of any kind. Here’s the ultimate proof for my words.
The real fun begins when you try to upload that app bundle to TestFlight. At this time it actually matters what you put as app version strings. Let’s dissect each version string separately and figure out what the rules are. But first let’s create a test app in iTunes Connect.
Short Bundle Version String
So what happens if I try to upload an app bundle with short version string saying “poop”? Naturally it fails.
I was totally expecting arbitrary strings to be a “no no” in iTunes Connect. But guess what? Big surprise!
Well, don’t do that anyway.
Let’s use a valid version string this time, e.g. 1.0.0
and for now set build version to 1
. I will use the short-version (build-version)
format from this moment on to indicate both versions, so I’m trying to upload 1.0.0 (5)
. Why 5
? Well, I just want it to be 5
, it doesn’t really matter.
Great, so it worked. Note that the app short version string exactly matches the version string in iTunes Connect. Back in a while, before Apple integrated TestFlight into iTunes Connect, it used to be a mandatory requirement, but not any more. I’m sure you want a proof, so I’ll just upload 1.0 (1)
this time.
It worked! But that’s not convincing enough. What if there’s some advanced matching happening and 1.0
is kind of the same as 1.0.0
? OK then, I will upload 2.0 (1)
.
Worked again! Just to make sure I’m not uploading to a black hole, I’ll try 1.0.0 (0)
again. Note the 0
build version, that’s on purpose.
Aha! A very interesting error message! I personally like the train version
term. Apparently each distinct version in TestFlight has its own train, whatever that means, I like to imagine an actual Illawarra line train with carriages. Every time you upload a new prerelease version, a new train is created. The order in which you upload the builds does not matter, you can upload 1.0
after 2.0
.
However, if your app already has a live version in App Store, then your new uploads must have a greater version that the live app. Greater in this case means semantically greater, that means components of the version string are compared left to right. For example, 1.0.1
is less than 1.1
, because 1 == 1
and 0 < 1
. Here’s the proof.
Finally, to dot all the i
s, or should I say to dot all the short version string components, I will try to upload 2.1.1.1 (1)
to see what happens.
And here’s the rule for creating a short version string.
A period-separated list of at most three non-negative integers.
Very clear and simple to remember.
One more thing, before I move on to bundle version. You can upload new prerelease builds to iTunes Connect even if you don’t have a new version created there yet. That’s right, just upload the builds and they will show up in Prerelease tab anyway. That also explains why the version string of uploaded bundle doesn’t have to match the version with Prepare for Submission status. As usual, I can prove it.
OK, enough of short version string. Don’t worry if facts look a bit scattered around the text, I’ll summarize them all in the end. Now it’s time for…
Bundle Version
The rules for bundle version are not the same as for short version string. For convenience and to reduce tautology in the text, I will call it build version by default.
There are many ways to compose a build version. The most popular options are
- Integer value (Build Number)
- Period-separated non negative integers
- Date string in
YYYYMMDD
or similar format - Git commit hash
- Some weird stuff
Let’s look at each option in detail.
Build Number
This is the most basic and simple way to manage a bundle/build version. An incremental non-negative integer. Any CI server gives you this number in some way. The incremental part really matters. As I demonstrated before by uploading 1.0.0 (0)
after 1.0.0 (5)
, the build version of each new upload must be greater than the previous build version on the same train. This time there is no special meaning for greater, just good old integer comparison, so 11
is less than 101
.
Another important note is that build versions need to increment in the scope of the same train only. In the past, build version had to be incremental across all versions. So if you had 1.0 (10)
live and then tried to upload 2.0 (1)
it would fail because 1 < 10
. Thankfully that’s not the case any more.
So to sum it up, build number used as bundle version
- Must be incremental
- Independent for each short version string train
Period-separated
This is a little less popular way to manage build version, but still can be found “in the wild” so to say. For example, a build version is composed by joining short version string with the build number, like major.minor.patch.build
: 1.0.0.1
, 2.0.42
and so on.
So what happens with this build version string when it gets to iTunes Connect? I will first tell you what happens, then will prove it with concrete examples.
- The build version is separated into components using
.
s as separators - Each component is stripped of leading zeroes
- Processed components are joined back together to form one big integer
The resulting integer is non negative, to be more specific it’s unsigned long
integer.
Some examples.
1.0.0.0
->1
,0
,0
,0
->1000
2.001.030
->2
,001
,003
->2
,1
,30
->2130
1.000.02.03.04
->1
,000
,02
,03
,04
->1
,0
,2
,3
,4
->10234
As you can see in the last example, the number of period-separated components is not limited this time. At least I tried all the way up to 4, can’t tell what happens when you have 5 or more of them, but I am about 99% sure the sky is the limit, I mean there’s some limit on the maximum length of the string.
Once the build version string is converted into an integer, the same rules apply as for the simple incremental build number. The newly uploaded build version for the given train must be greater than any previous build version.
Some examples again.
2 < 1.1
[2 < 11
]1.0 < 1.0.0
[10 < 100
]1.0 == 1.01
[10 == 10
]1.0.1.020 == 1.001.1.20
[10120 == 10120
]
When you try to upload the same version more than once, you get a “Redundant Binary Upload” error message.
I think I can stop now, the rules are pretty clear.
Date String
Date string is another way to compose a build version. For example, when using YYYYMMDD
format your build version may look like 20150604
. Actually, this is a very good option. The resulting integer will be always incremental, thanks to the way the world around us works.
If you have an advanced CI setup, you should think about adding more components to the date format. Such as hours and minutes YYYYMMDDHHmm
. This way if you have more than one build happening within the same hour you will be able to tell them apart.
If you choose to go with this option, don’t use any kind of separators, such as periods, colons or alike. With periods you will have unwanted side effects when leading zeroes are removed from components. Stuff like colons falls into Weird Stuff category, which I will describe later on.
Git Commit Hash
Another quite popular approach is to use git commit hash as build version string, or as a part of build version string. One useful property of commit hashes is uniqueness. But that’s not enough for TestFlight.
I tried quite a few things, such as hex numbers like a
, f
, e
, e4
, 5e
and even real life git commit hashes. So I lay the evidence before your eyes.
Obviously, going outside the a - f
range will take you nowhere, the poop
example in the beginning of the article covers that well, and I tried things like xyz
too.
Weird Stuff
Finally, the weird stuff. I’ll just list some of the things I tried.
1+0-2/5
1+++0---2***5
1...0...2...20
Some error messages from iTunes Connect.
Surprisingly though, despite the fact that upload fails, validation is successful. Whatever validation does it doesn’t check the validity of version strings.
Legacy
I subjectively call things like HockeyApp a legacy in this article. You may have different opinion. What I really want to say is that all these iTunes Connect and TestFlight rules do not apply to things like HockeyApp or in-house Over The Air distribution. Despite that fact, it would be reasonable to start making changes towards TestFlight guidelines.
Summary
In conclusion I will make an attempt to summarize the version string rules.
Short Bundle Version String
- A period-separated list of at most three non-negative integers
- Must be semantically incremental
- Must be greater than current live version
Bundle Version
- Converted to unsigned long integer
- Periods are removed
- Leading zeroes removed from each period-separated component
- Resulting integer must be incremental
- But only among other bundle versions for the same short version string (version train)
- Alphabet characters and git hashes in particular will not work
- Various punctuation marks will not work
Of course, I’m not claiming that this is the complete guide. I don’t know what is the limit for maximum version string length. Could be that some punctuation marks are legal, I didn’t try them all. Obviously I am giving no reference to any official documentation, could not find it or didn’t look hard enough. In any case, this information should be enough for you to come up with a robust and reliable versioning strategy for your apps.