Blog 53

Here at Degree 53, we use HockeyApp to distribute apps to our testers (and clients for UAT). This blog documents how to upload Android apps to HockeyApp from our CI server (Thoughtworks Go) using cURL.

If you're trying to upload to HockeyApp from Gradle your first step should be to try the Gradle HockeyApp Plugin by x2on or some other fully developed solution. Unfortunately I couldn't get the plugin to recognise our Android build flavors and generate a task to upload them (a few other people have had similar issues when the project has a top-level build.gradle file - which is most Android projects), HockeyApp have an extensive API so it should be easy enough to get something working quickly; upload using cURL run from a bash script.

Assuming you're already using HockeyApp, as a test try and list the versions of an existing app from a shell/terminal, it's a simple GET request in the format /api/2/apps/APP_ID/app_versions with a HockeyApp token added as a header:

curl \
    -H "X-HockeyAppToken: 1234567890abcdef1234567890abcdef" \

That will list the version json in the shell but would be better saving to a file:

curl \ 
    -H "X-HockeyAppToken: 1234567890abcdef1234567890abcdef" \ \ >> versions.json

Better, but still an unreadable unformatted mess, this pretty-prints the json before saving to file (requires Python 2.6+):

curl \
    -H "X-HockeyAppToken: 1234567890abcdef1234567890abcdef" \ \
    | python -m json.tool \
    >> versions.json

A slight edit to this command saves download, install, and crash statistics to file:

curl \
    -H "X-HockeyAppToken: 1234567890abcdef1234567890abcdef" \ \
    | python -m json.tool \
    >> statistics.json

The upload API is a little more complicated, a POST request with several parameters, the actual .apk is confusingly added with a parameter called ipa - looks like HockeyApp started out with support for just iOS and they never changed the name.

Before uploading from the build server let's do a manual upload using HockeyApp's example, the meaning of the various parameters can be found here:

curl \
    -F "status=2" \
    -F "notify=1" \
    -F "notes=Testing manual upload using cURL" \
    -F "notes_type=0" \
    -F "ipa=@GoHockey.apk" \
    -H "X-HockeyAppToken: 1234567890abcdef1234567890abcdef" \ \
    | python -m json.tool  

Success, the upload works, the app is visible in the HockeyApp dashboard, the command returned with:

    "app_id": 164429,
    "appsize": 904713,
    "build_url": "",
    "config_url": "",
    "created_at": "2015-03-27T12:01:30Z",
    "device_family": null,
    "external": false, "id": 1,
    "mandatory": false,
    "minimum_os_version": "4.1",
    "notes": "<p>Testing manual upload using cURL</p>",
    "public_url": "",
    "restricted_to_tags": false,
    "shortversion": "1.0",
    "status": 2,
    "tags": [],
    "timestamp": 1427457691,
    "title": "GoHockey",
    "updated_at": "2015-03-27T12:01:30Z",
    "version": "1" 

The last step is to get this working with our build server, we use Thoughtworks Go, but the majority of the Android build process is carried out in a custom build.gradle script so this will work with any CI server. Once the compile has completed we need to pass the path to the .apk and a couple of other arguments to a bash script with the cURL command in.

Go sets an environment variable for the Git commit hash that triggered a build, we can use this to pass release details automatically to HockeyApp, if we just use the last commit set by the GO_REVISION variable and the commit is the result of a merge the message will be unhelpful: Merge branch ‘master’ of, to make the message more useful we can use GO_FROM_REVISION and return the range of commit messages to HockeyApp using: git log --oneline aCommit..anotherCommit

In addition to using the Git commit environment variables set by Go we're also manually adding two secure environment variables to the Go project so that we're not storing hard-coded HockeyApp API token and app ID values in files that are checked into source control (you can also do this with a local properties file that's added to .gitignore):

Hockey App Environment Variables

These variables are then added as arguments to be passed to the script (added inside the android{} block in build.gradle):

applicationVariants.all { variant ->
    def hockeyUpload = project.tasks.create("hockey${}Upload", Exec){
        //Need to initialise this way to prevent compile error in Android Studio
        def revision = ""
        revision = System.getenv('GO_REVISION')
        def fromRevision = ""
        fromRevision = System.getenv('GO_FROM_REVISION')
        def appID = ""
        appID = System.getenv('HOCKEY_APP_ID')
        def apiToken = ""
        apiToken = System.getenv('HOCKEY_API_TOKEN')
        if(revision == null || appID == null || apiToken == null){
          println('GO_REVISION, HOCKEY_APP_ID, or HOCKEY_API_TOKEN not available')
          commandLine 'bash', '-c', new StringBuilder().append("bash")
                .append(" " + project.projectDir + "/") 
                .append(" " + variant.outputs[0].outputFile) 
                .append(" \"" + revision + "\"") 
                .append(" \"" + fromRevision + "\"") 
                .append(" " + appID) 
                .append(" " + apiToken) 
    hockeyUpload.description = "Upload .apk to HockeyApp (add token/key to environment variables on the build server)"
    hockeyUpload.dependsOn variant.assemble

If you run gradlew tasks you should now see a hockeyVariantNameUpload task for each variant. is a bash script in your project root:

echo "Getting Git commit message"
if [ "$FROM_REVISION" == "$REVISION" ]; then
    COMMIT_MESSAGE=`git log -5 --oneline $REVISION`
curl \
    -F "status=2" \
    -F "notify=1" \
    -F "notes=$COMMIT_MESSAGE" \
    -F "notes_type=0" \
    -F "ipa=@$APK_PATH" \
    -H "X-HockeyAppToken: $HOCKEYTOKEN" \$APPID/app_versions/upload
echo $?

The last thing to do is to create an exception in build.gradle if the HockeyApp environment variables haven't been added to the Go pipeline, this needs to be done in such a way that the exception is only thrown when the build process is carried out on the build server - not locally by the developer using Android Studio, to do this we can use another environment variable that's guaranteed to be set when the script is run by Go: GO_SERVER_URL:

if(revision == null || appID == null || apiToken == null){
    def serverURL = ""
    serverURL = System.getenv('GO_SERVER_URL')
    if(serverURL != null){
        throw new GradleException('GO_REVISION, HOCKEY_APP_ID, or HOCKEY_API_TOKEN not available')
        println('GO_REVISION, HOCKEY_APP_ID, or HOCKEY_API_TOKEN not available')
    //Exec bash HockeyApp upload script

View the complete build.gradle in the sample GoHockey app on Github which also uses GO_PIPELINE_COUNTER to increment the app versionCode, screenshots below of what the testers see on HockeyApp:

Hockey App Download Page Hockey App Version Page

Loading Icon Loading