Streamline releases with Android
When it comes to releasing a build, it usually goes without saying that there are a few housekeeping tasks that need accomplished before an actual release goes out. Things like tagging and pushing your current branch to your remote repository could be automated so that you can focus on what’s important.
Having to do this repeatedly for every beta build of our app became a chore and was something I didn’t want to really think about. I should be able to simply enter a single command and boom, tag and push the current branch. I eventually came up with the following:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'org.ajoberstar:gradle-git:1.2.0'
}
}
apply plugin: 'org.ajoberstar.grgit'
android {
...
}
task releaseBeta << {
// Ex: v1.2.3.45
def tagName = "v${android.defaultConfig.versionName}.${android.defaultConfig.versionCode}"
grgit.tag.add(name: tagName, message: "Beta release ${tagName}")
grgit.push(refsOrSpecs: [tagName])
}
Running ./gradlew releaseBeta
will now tag my current branch and push it up for me automatically. As an added bonus, If I forgot to update the versionName
or versionCode
, the build will simply fail.
Simple and handy, but can we do more with this? I sought out to automate the entire versioning system, within reason. When it comes to versioning, you still would want the option to have a hand in some decisions such as when to bump the major/minor version number.
In the following examples, I will be using a very simple versioning scheme of
<major>.<minor>.<revision>.<build_number>
versionName = <major>.<minor>.<revision>
versionCode = <build_number>
Where we increment the build_number
each time we release a beta build and increment both build_number
and revision
when we release a production build. We leave major
and minor
alone since we want to decide when those numbers are updated.
The first step is to decouple versioning from the Android Gradle plugin. Typically in an Android project we would specify the versionCode
and versionName
with hardcoded values, but since we want to be able to modify those values from a build task, we need to be able to provide those values from another source (i.e. a file).
With some elbow grease and refactoring, the following…
android {
defaultConfig {
applicationId "com.myapp"
versionCode 1
versionName "1.2.3"
}
}
Now becomes…
android {
defaultConfig {
applicationId "com.myapp"
versionCode project.ext.versionCode
versionName project.ext.versionName
}
}
Where both project.ext.versionCode
and project.ext.versionCode
references the versionCode
and versionName
. We dynamically generate versionCode
and versionName
from a JSON file that looks like this:
{
"major": 1,
"minor": 2,
"revision": 3,
"buildNumber": 1
}
Now we can work on building a Gradle task that will increment the version for our beta and production releases. First thing that needs to be done is to read in the above JSON file into an object…
def versionFile = file("/path/to/version/file")
def versionJSON = getJSON(versionFile)
def versionCode = versionJSON.buildNumber
def versionName = "${versionJSON.major}.${versionJSON.minor}.${versionJSON.revision}"
// Expose as extra properties at project level
ext.versionCode = versionCode
ext.versionName = versionName
def getJSON(file) {
return new JsonSlurper().parseText(file.text)
}
Then we can build out our task…
task prepareBetaRelease << {
// Ensure our working copy is clean first
if (!grgit.status().isClean()) {
throw new GradleException("You must NOT have any changes in your working copy!")
}
// Update version code
versionJSON.buildNumber += 1
versionCode = versionJSON.buildNumber
versionFile.write(new JsonBuilder(versionJSON).toPrettyString())
// Apply version code to all variants. This is necessary
// so when we build the APK, it gets the updated values
android.applicationVariants.all { variant ->
variant.mergedFlavor.versionCode = versionCode
}
// Add changes
def changes = grgit.status().unstaged.getAllChanges()
grgit.add(update: true, patterns: changes)
// Commit
grgit.commit(message: 'Prepare for beta release')
// Push
grgit.push()
// Tag
def tagName = "v${versionName}.${versionCode}"
grgit.tag.add(name: tagName, message: "Beta release ${tagName}")
// Push
grgit.push(refsOrSpecs: [tagName])
}
We can then take the above task, and hook it into something like HockeyApp with their Gradle plugin for distributing APKs…
// NOTE: The order in which the tasks appear in the array does not mean
// that is the order they will run in.
task releaseBeta(dependsOn: ['prepareBetaRelease', 'uploadBetaToHockeyApp'])
// Here we explicitly specify the order in which the two tasks should run in.
// We use 'whenTaskAdded' because 'uploadBetaToHockeyApp' isn't created until
// a build starts.
tasks.whenTaskAdded { task ->
if (task.name.equals("uploadBetaToHockeyApp")) {
task.mustRunAfter prepareBetaRelease
}
}
Now when we run ./gradlew releaseBeta
, this will do the following:
- Increment our build number
- Commit changed build number
- Tag and push our current branch
- Push the committed changes
- Release to HockeyApp
To accomplish this for production builds, we follow the same exact procedure, except we increment the build_number
and revision
.