Automating Titanium builds with Grunt

By | August 25, 2015

OMIGOSH, I submitted our app to Apple with it pointing to our development endpoints, not production! Stupid mistakes like this are the stuff of developer nightmares, and they can easily happen when you have manual steps in your release process. People screw up.

We use the awesome Installr service to distribute iOS and Android test builds. For iOS, those betas have to be ad-hoc builds. And, we send our test Android builds as production builds signed with our company’s certificate. Until Appcelerator resolves ALOY-1210 there’s no way to differentiate between ad-hoc and production builds using the built-in tooling.

Till now, we’ve manually edited the config.json file before each Installr or production build to list the correct endpoints under the env:production key. That’s a manual and error-prone process that was bound to fail, and did.

We already use Grunt to automate our Installr build process. Type grunt in the project directory and our Grunt script cleans the project, increments version numbers in the tiapp.xml, builds the app for iOS and Android, uploads to Installr, and sends out the release emails. Obviously, we should have been using Grunt to also manage configuration files.

I document the entire process of setting up grunt for a Titanium project in this Gist. Here’s the simplified version of what I did to automate swapping configuration files:

  1. I created two copies of my config.json file and put them at the root of my Titanium project: config.installr.json and config.production.json.
  2. I added the grunt-contrib-copy plugin with npm install grunt-contrib-copy --save-dev
  3. I updated my grunt script:

/* Define the pre- and post-build copy commands */
copy: {
  pre: {
    src: 'config.installr.json',
    dest: 'app/config.json',
  },
  post: {
    src: 'config.production.json',
    dest: 'app/config.json',
  },
},

/* after all the tasks are defined, load the plugin */
grunt.loadNpmTasks('grunt-contrib-copy');

/* finally, kick off all the tasks */
grunt.registerTask('default', ['copy:pre', 'tiapp', 'titanium', 'copy:post', 'shell']);

Now, when I run grunt, the first thing it does is copy my Installr-version of config.json to the app directory. It does the rest of its build tasks, then copies our day-to-day config.json file back. (That file has our testing endpoints listed in the env:development and env:test keys, and production endpoints in the env:production key. With it, we’re ready for manual dev or production builds.)

See the the Gist for full steps to set up Grunt, as well as our complete Grunt script. In reality, we’re not actually uploading via the Grunt script any more. We ran into occasional problems with the uploader failing, plus problems with parsing the change log if it contained quotes and apostrophes. We now to the clean, version bump, build tasks via Grunt then upload and send the emails out manually via the Installr Dashboard.