Versioning builds and releases

When I was playing with Apple’s new TestFlight I ran into the issue of conflicting build versions I’m sure you’re all familiar with:

This bundle is invalid. The key CFBundleVersion in the Info.plist file must contain a higher version than that of the previously uploaded version.

Before TestFlight, iTunes Connect let you re-use the same build version after the previous one was rejected by either you or Apple. But this is no more..

Build and Release version numbers

Android and iOS both have a build and a release version number:


  • CFBundleVersion: The build version number. A string of 1 to 3 period-separated positive integers, no leading 0’s, the first greater or equal to 1, following the Semantic Versioning standard. The total string has a maximum length of 18 characters. It must be unique and incremented for each build uploaded to iTunes Connect.
  • CFBundleShortVersionString: The release version number. Same format and maximum length as the build version. This is the version you also need to enter manually in iTunes Connect and is what users see in the App Store. It must be unique and incremented for each released build, while multiple unreleased builds can share the same release version number.

Something interesting I found out is that CFBundleVersion can can also be followed by another period and 4th identifier. Given you can no longer just delete builds I haven’t fully investigated the options, but this was key to my current approach I’ll come to in a minute.


  • versionCode: The build version number; an integer between 1 and 1.414.511.624 (2^31) if we assume it to be a 32-bit signed int. It must be unique and incremented for each build uploaded to Google Play.
  • versionName: The release version number. Can be anything you want, but is advised to follow the same format as CFBundleVersion does. This is what users see in Google Play. It must be unique and incremented for each released build, while multiple unreleased builds can share the same release version number.

tiapp.xml’s version-tag

In Titanium you version your app using the version tag in tiapp.xml. This accepts the same format as CFBundleVersion plus an optional 3rd period followed by any string. For the iOS build Titanium will use the full version for CFBundleVersion and only the first 1 to 3 integers for CFBundleShortVersionString. Any values you have for CFBundleVersion and/or CFBundleShortVersionString under ios/plist/dict are ignored. For Android it will use the full version for versionName and 1 for versionCode, unless you have set these yourself as attributes of the android/manifest tag.

My approach

Knowing the above, this is my current strategy for versioning apps:

  • For each new release-cycle I set the version tag in tiapp.xml to the major/minor/patch version plus .0 as the 4th identifier.
  • For each new build I:
    • Set the 4th identifier to the current unix time.
    • Set the android:versionName attribute of the android/manifest tag in tiapp.xml to the first 3 identifiers, to match what Titanium does for CFGBundleShortVersionString.
    • Set the android:versionCode attribute to the 4th identifier.

Using this approach build release numbers are always unique and tell me exactly when the build was produced and what release version it belongs to.


Of course I don’t want to do this manually for each build, so I use the following Grunt task and Tony’s tiapp.xml module:

  grunt.registerTask('tiapp', function() {
    var tiapp = require('tiapp.xml').load();

    // var stamp = (new Date()).toISOString().replace(/[^0-9]/g, '').substr(0, 12);
    var stamp = Math.round((new Date()).getTime() / 1000);

    var versions = tiapp.version.split('.');
    versions[3] = stamp.toString();
    tiapp.version = versions.join('.');

    var androids = tiapp.doc.documentElement.getElementsByTagName('android');

    if (androids.length === 1) {
      var manifests = androids.item(0).getElementsByTagName('manifest');

      if (manifests.length === 1) {
        var manifest = manifests.item(0);

        manifest.setAttribute('android:versionName', versions.slice(0, 3).join('.'));
        manifest.setAttribute('android:versionCode', stamp);


    grunt.log.writeln(require('util').format('Bumped version to: %s', tiapp.version));

As you can see I also tried to use YYYYMMDDHHIISS because it’s easier to read. Unfortunately this doesn’t work with the maximum (length) of both CFBundleVersion and versionCode.

App imagineer: Imagining, Engineering & Speaking about Native mobile Apps with Appcelerator Titanium & Alloy • Meetup organizer • Certified Expert • Titan


  • Timan Rebel

    Very nice! Have started using it right away

  • Frank

    Great post! Thanks for sharing!
    Definitely a must-have in your build scripts when using Apple’s ‘new’ version of TestFlight!

    I performed some tests and came to find out the 4th identifier gets stripped from the CFBundleVersion key in the Ti generated Info.plist when you build with a Ti SDK < 3.3.0 This happens even when you put a custom Info.plist in your project folder. It magically gets stripped.

    So… even more reason to fix issues and bump the Ti SDK to the latest version :)

  • Fokke Zandbergen Post author

    That’s correct, that was a fix in 3.4.0

    And still in 3.4.0, a custom Info.plist indeed doesn’t help. Both versions are written by the compiler even if you have custom ones.

  • Bradleycorn

    +1 bonus point for the included grunt task!

  • jakerutter

    Great article, was definitely stumped on this one and couldn’t find an answer in any of the appcelerator documentation. Thanks so much!