Skip to content

Instantly share code, notes, and snippets.

@FokkeZB

FokkeZB/post.md Secret

Last active August 29, 2015 14:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save FokkeZB/f7ae3ab57eb8185a338d to your computer and use it in GitHub Desktop.
Save FokkeZB/f7ae3ab57eb8185a338d to your computer and use it in GitHub Desktop.
Blog - Versioning

Versioning your apps with 4.1.0

Apps have two different type of version numbers plus there's differences between versioning for different platforms. Titanium 4.1.0 comes with better support for versioning iOS apps. Let's explore how you would use this in an app for iOS and Android.

Release version number

First of all there's the release version number. This is the version your users see. Each public release must have a unique version. Apple calls this the CFBundleShortVersionString and for Android this is android:versionName.

Both suggest and to different lengths require you to follow a subset of Semantic Versioning.

Semantic Versioning

In semantic versioning you use a string comprised of three period-separated integers. Normally the first integer would be incremented for breaking changes, but assuming your app won't crash your users' phones we typically use it to indicate new features or major changes. The second integer would be changes or less prominent features and the third one is incremented for fixes and other maintenance.

  • 1.0.0: Initial release
  • 1.0.1: Fixed a few bugs
  • 1.1.0: Changed something
  • 2.0.0: Added a new feature

Build version number

The other type of version number is the build version.

Before every public release, you probably go through several iterations of development and test builds that you distribute via alpha/beta testing on Google Play, TestFlight Beta Testing with Apple or another service like Installr.

This is where both Android and iOS (and others) do not follow semantic versioning anymore. If they did, the test builds for 1.1.0 would be versioned as 1.1.0-X where X can be many sorts of pre-release identifiers.

Instead, a separate build version is used. This version is normally not visible to the user and should be unique for each test build.

Format

Apple says you should follow the same simplified semantic versioning format for this CFBundleVersion as well, but the default for a new XCode project is 1. On Android android:versionCode must be an integer. This makes more sense, since if you would follow Apple's guidelines then how would you version test builds for an upcoming 1.1.0 release? If you would use 1.1.1, 1.1.2 etc then what would the release after 1.1.0 be and the build versions for that? We'll come back to that.

Versioning with tiapp.xml

The tiapp.xml has a <version> tag that can edit via Studio's TiApp Editor. But there's more.

iOS

Titanium will use <version> as-is for CFBundleVersion (build) and will try to format it as three period-separated semantic version for CFBundleShortVersionString (release).

Until 4.1.0 you could not manually override this. The only way to set a different build version was to set version to something like 1.1.0.123 since Titanium would then normalize it to 1.1.0 for the release and leave it as-is for the build version number. This of course is not a proper semantic version, but Apple did accept it - allowing you to distribute test multiple unique test builds via TestFlight without having to increase the release version number.

Now with 4.1.0 you can now manually override the values for both CFBundleVersion and CFBundleShortVersionString via the <ios> section in tiapp.xml:

<?xml version="1.0" encoding="UTF-8"?>
<ti:app xmlns:ti="http://ti.appcelerator.org">

  <!-- not used but still available via Ti.App.version -->
  <version>1.1.0.123</version>
  
  <ios>
    <plist>
      <dict>
        <key>CFBundleShortVersionString</key>
        <string>1.1</string>
        <key>CFBundleVersion</key>
        <string>123</string>
      </dict>
    </plist>
  </ios>
</ti:app>

Android

For Android Titanium will use <version> as-is for android:versionName (release) but unlike iOS the build version (android:versionCode) is not based of <version>. You will have to set it manually via the <android> section in tiapp.xml or it will always default to 1, which is.. well, not unique.

<?xml version="1.0" encoding="UTF-8"?>
<ti:app xmlns:ti="http://ti.appcelerator.org">

  <!-- not used but still available via Ti.App.version -->
  <version>1.1.0.123</version>
  
  <android xmlns:android="http://schemas.android.com/apk/res/android">
    <manifest android:versionName="1.0" android:versionCode="123">
    </manifest>
  </android>

Automating

All good developers are lazy, so lets automate this using a Grunt task. If you are not familiar with Grunt, read their Getting Started. We will be discussing Grunt and other task runners and how to use them for Appcelerator development in a later post.

The Grunt you will find down here allows you to bump the major, minor, patch or just the build version - which will always be incremented. It asummes <version> to be major.minor.patch and to find only android:versionCode and CFBundleVersion in the <android> and <ios> sections so that Titanium will set release version numbers.

$ npm install grunt-cli
$ grunt version:minor
Running "version:minor" (version) task
Bumped version to: 1.2.0
Bumped android:versionCode to: 124
Bumped CFBundleVersion to: 130

Gruntfile.js

var fs = require('fs');

grunt.registerTask('version', function (what) {

	// map name to index and default to patch index
	var index = ['major', 'minor', 'patch'].indexOf(what);

	var tiapp = fs.readFileSync('tiapp.xml', {
		encoding: 'utf-8'
	});

	if (index !== -1) {

		tiapp = tiapp.replace(/(<version>)([^<]+)(<\/version>)/, function (match, before, version, after) {
			version = version.split('.');

			// bump index and reset following
			for (var i = index; i <= 2; i++) {
				version[i] = (i === index) ? (parseInt(version[i], 10) + 1).toString() : '0';
			}

			version = version.join('.');

			grunt.log.writeln('Bumped version to: ' + version);

			return before + version + after;
		});

	}

	tiapp = tiapp.replace(/(android:versionCode=")([^"]+)(")/, function (match, before, versionCode, after) {
		versionCode = parseInt(versionCode, 10) + 1;

		grunt.log.writeln('Bumped android:versionCode to: ' + versionCode);

		return before + versionCode + after;
	});

	tiapp = tiapp.replace(/(<key>CFBundleVersion<\/key>\s*<string>)([^<]+)(<\/string>)/mg, function (match, before, CFBundleVersion, after) {
		CFBundleVersion = parseInt(CFBundleVersion, 10) + 1;

		grunt.log.writeln('Bumped CFBundleVersion to: ' + CFBundleVersion);

		return before + CFBundleVersion + after;
	});

	fs.writeFileSync('tiapp.xml', tiapp);
});

Happy versioning!

@sgtcoolguy
Copy link

Windows has a 4 part version string. The details I could find are here: https://msdn.microsoft.com/en-us/library/windows/apps/gg442301(v=vs.105).aspx#BKMK_Updatingversionnumbers

Essentially the 4 part version is the "full version" and is used internally, but once submitted to the store only the first two parts are tracked (and I think you set them manually and they must match the first two digits in the app's manifest file").

The first part is major version, then minor, then build #, then revision number. It looks like in practice, you can set up the MSBuild tooling to fill in the 3rd and 4th places for you (or auto-increment them over time). Not sure if that would still work if all it does it keep track of it in the appx.manifest, which we generate dynamically during our builds (and therefore means it would get wiped).

Of course, all of this doesn't matter right now as we've hard-coded the app version and need to fix that! (ugh): https://jira.appcelerator.org/browse/TIMOB-19175

@FokkeZB
Copy link
Author

FokkeZB commented Jul 10, 2015

OK, so for now I can better leave Window out? Or tell them that up to 3 parts of <version> is used with the 4th being a timestamp?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment