Git-based Android App Versioning with AGP 4.0
Tags: android, git, gradle
Published: 2020-09-11 11:27:37

An Android app has 2 pieces of [version information](https://developer.android.com/studio/publish/versioning#appversioning):
1. `versionCode` - a positive integer used as an internal version number of the app that needs to monotonically increase with each subsequent release.
2. `versionName` - a string as a version number shown to users.

To automate the process of generating the `versionCode` and `versionName` for an app release, a common approach is to compute these values from **Git tags**:

```groovy
android {
  defaultConfig {
    versionCode buildVersionCode()
    versionName buildVersionName()
  }
}
```

The `buildVersionCode()` and `buildVersionName()` need to execute a command-line process to fetch the git information (e.g. latest tag), parse it and generate the `versionCode` and `versionName` required:

```groovy
def buildVersionCode = {
  try {
    def stdout = new ByteArrayOutputStream()
      exec {
        commandLine 'git', 'describe', '--tags'
        standardOutput = code
      }
      // computeVersionCodeFromTag(tag) might use a formula such as MAJOR * 10000 + MINOR * 100 + PATCH for SemVer tags.
      return computeVersionCodeFromTag(stdout.toString().trim())
  } catch (ignored) {
    return -1
  }
}

def buildVersionName = {
  try {
      def stdout = new ByteArrayOutputStream()
      exec {
          commandLine 'git', 'describe', '--tags'
          standardOutput = stdout
      }
      return stdout.toString().trim()
  } catch (ignored) {
    return null
  }
}
```

The main issue here is that we are doing **computation during configuration** of the build which is extremely inefficient as `buildVersionCode` and `buildVersionName` are executed every time a project is configured (e.g. every Gradle sync from the IDE, or just running `./gradlew help`), whereas in practice the `versionCode` and `versionName` are only required when assembling an APK or AAB.

While Gradle has been pushing for [Lazy Task Configuration](https://docs.gradle.org/current/userguide/lazy_configuration.html) and [Task Configuration Avoidance](https://docs.gradle.org/current/userguide/task_configuration_avoidance.html) for a couple of years now, the **Android Gradle Plugin (AGP)** had not migrated some of the most important APIs to support Gradle properties and providers until very recently. Thus there wasn't an easy way to avoid computing `versionCode` and `versionName` during configuration (here's [a workaround](https://www.youtube.com/watch?v=sQC9-Rj2yLI&list=PLWz5rJ2EKKc9FfSQIRXEWyWpHD6TtwxMM&index=40) provided by the AGP team back at IO 19).

This issue was finally addressed in **AGP 4.0** with the introduction of 2 new APIs for configuring the build variants early in the configuration phase: [onVariants](https://developer.android.com/reference/tools/gradle-api/com/android/build/api/dsl/CommonExtension#onvariants_1) and [onVariantsProperties](https://developer.android.com/reference/tools/gradle-api/com/android/build/api/dsl/CommonExtension#onvariantproperties_1). Here's the [skeleton](https://github.com/android/gradle-recipes/blob/master/BuildSrc/setVersionsFromTask/buildSrc/src/main/kotlin/CustomPlugin.kt) for generating `versionCode` and `versionName` in a Gradle task from the official [Android Plugin for Gradle cookbook](https://github.com/android/gradle-recipes).

## Android App Versioning Gradle Plugin

A few months ago while working on automating the release process at work, I decided to take a closer look at the new `onVariantProperties` API and move the version info computation logic to a proper Gradle task. The initial prototype was developed in [streamlined](https://github.com/ReactiveCircus/streamlined/tree/187fa01d0b3245dcd4af30467780f3b19b3eb05d/buildSrc/src/main/kotlin/io/github/reactivecircus/streamlined/versioning) and had received some [interests from the community](https://twitter.com/ychescale9/status/1249557110444023809).  

After adding support for customizing **versionCode** and **versionName** with a lambda and simplifying the plugin configurations, today I’m happy to share [App Versioning](https://github.com/ReactiveCircus/app-versioning) - a Gradle Plugin for lazily generating Android app's `versionCode` & `versionName` from Git tags.

### How it works

Let's start by applying the plugin to the `app` module:

```kotlin
plugins {
    id("com.android.application")
    id("io.github.reactivecircus.app-versioning") version "x.y.z"
}
```

*Note that the samples in the post are written with [Gradle Kotlin DSL](https://docs.gradle.org/current/userguide/kotlin_dsl.html) but traditional Groovy DSL is also fully supported.*

At this point if your repository has a Git tag that follows [Semantic Versioning](https://semver.org/), **app-versioning** will start generating `versionCode` and `versionName` for your APK / AAB!

Let's imagine the latest tag in your current branch is `1.3.1`. Assembling the APK with `./gradlew assembleRelease` will trigger a `generateAppVersionInfoForRelease` task which generates both the `versionCode` and `versionName` for the assembled APK:

```
> Task :app:generateAppVersionInfoForRelease
Generated app version code: 10301.
Generated app version name: "1.3.1".
```

The generated `versionCode` and `versionName` are injected into the merged manifest at `app/build/intermediates/merged_manifests/release/AndroidManifest.xml`:

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="io.github.reactivecircus.streamlined"
    android:versionCode="10301"
    android:versionName="1.3.1" >
...
</manifest>
```

### Default rules

Without providing any configurations, **app-versioning** will fetch the latest Git tag in the repository, attempt to parse it into a **SemVer** string, and compute the `versionCode` following [positional notation](https://en.wikipedia.org/wiki/Positional_notation):

```
versionCode = MAJOR * 10000 + MINOR * 100 + PATCH
```

So for the tag `1.3.1`, the generated `versionCode` is `1 * 10000 + 3 * 100 + 1 = 10301`.

The `versionName` generated will just be the name of the latest Git tag (`1.3.1` in the example above).

If the default rules described above work for you, you are now ready to enjoy automated version info generation without further configurations! But chances are you might want to tweak the formula used for generating `versionCode`, or inject additional metadata into the `versionCode` and/or `versionName` to fit your existing process and workflow:

- Instead of reserving **2** digits for each of the **MAJOR**, **MINOR**, and **PATCH** components, you might want to use a smaller or bigger range.
- You might want to add an additional `BUILD_NUMBER` from CI to the `versionCode` or `versionName`.
- You might need to include the **commit hash** in the `versionName`.

With that in mind, let's now take a look at how these custom rules can be specified with a couple of plugin configurations.

### Custom rules

To maximize the supported use cases without introducing too many configurations, **app-versioning** lets you define how you want to compute the `versionCode` and `versionName` by implementing lambdas which are evaluated lazily during execution:

```kotlin
appVersioning {

  overrideVersionCode { gitTag, providers ->
    // TODO generate an Int from the given gitTag and/or providers
  }

  overrideVersionName { gitTag, providers ->
    // TODO generate a String from the given gitTag and/or providers
  }
}
```

`GitTag` is a type-safe representation of a tag encapsulating the `rawTagName`, `commitsSinceLatestTag` and `commitHash`, provided by the plugin.

`providers` is a `ProviderFactory` instance which is a Gradle API that can be useful for [reading environment variables and system properties lazily](https://docs.gradle.org/current/javadoc/org/gradle/api/provider/ProviderFactory.html).

### SemVer-based version code

As mentioned earlier the plugin by default reserves 2 digits for each of the **MAJOR**, **MINOR** and **PATCH** components in a SemVer tag.

To allocate 3 digits per component instead (i.e. each version component can go up to 999):

```kotlin
import io.github.reactivecircus.appversioning.toSemVer
appVersioning {
  overrideVersionCode { gitTag, providers ->
    val semVer = gitTag.toSemVer()
    semVer.major * 1000000 + semVer.minor * 1000 + semVer.patch
  }
}
```

`toSemVer()` is an extension function (or `SemVer.fromGitTag(gitTag)` if you use Groovy) provided by the plugin to help create a type-safe `SemVer` object from the `GitTag` by parsing its `rawTagName` field.

If a Git tag is not fully [SemVer compliant](https://semver.org/#semantic-versioning-specification-semver) (e.g. `1.2`), calling `gitTag.toSemVer()` will throw an exception. In that case we'll need to find another way to compute the `versionCode`.

### Using timestamp for version code

Since the key characteristic for `versionCode` is that it must monotonically increase with each app release, a common approach is to use the Epoch / Unix timestamp for `versionCode`:

```kotlin
import java.time.Instant
appVersioning {
  overrideVersionCode { _, _ ->
    Instant.now().epochSecond.toInt()
  }
}
```

This will generate a monotonically increasing version code every time the `generateAppVersionInfoFor<BuildVariant>` task is run:

```
Generated app version code: 1599750437.
```

### Using environment variable

Another common practice when computing version code is to add a `BUILD_NUMBER` environment variable provided by CI to the formula. To do this, we can use the `providers` lambda parameter to create a provider that's only queried during execution:

```kotlin
import io.github.reactivecircus.appversioning.toSemVer
appVersioning {
  overrideVersionCode { gitTag, providers ->
    val buildNumber = providers
        .environmentVariable("BUILD_NUMBER")
        .getOrElse("0").toInt()
    val semVer = gitTag.toSemVer()
    semVer.major * 10000 + semVer.minor * 100 + semVer.patch + buildNumber
  }
}
```

`versionName` can be customized with the same approach:

```kotlin
import io.github.reactivecircus.appversioning.toSemVer
appVersioning {
  overrideVersionName { gitTag, providers ->
    // a custom versionName combining the tag name, commitHash and an environment variable
    val buildNumber = providers
        .environmentVariable("BUILD_NUMBER")
        .getOrElse("0").toInt()
    "${gitTag.rawTagName} - #$buildNumber (${gitTag.commitHash})"
  }
}
```

Custom version name generated:

```
Generated app version name: "1.3.1 - #345 (f0b6056)".
```

## App versioning on CI

Release APKs are usually built on CI, where the build configurations and host environment are likely to be different from a local dev environment. There are a couple of things worth noting when using the **app-versioning** plugin on CI.

### Enable the plugin only when necessary

For performance reason many CI providers only fetch a single commit by default when checking out the repository. For **app-versioning** to work we need to make sure Git tags are also fetched. Here's an example for doing this with [GutHub Actions](https://github.com/actions/checkout):

```
- uses: actions/checkout@v2
  with:
    fetch-depth: 0
```

CI jobs such as unit tests and Lint usually do not require a `versionCode` to be present in the merged `AndroidManifest` to complete. If these CI jobs are sharded such that they run in separate VMs in parallel, we can disable the **app-versioning** plugin and not worry about telling the CI to fetch those additional Git tags when checking out.

To do this we can define an environment variable `ENABLE_APP_VERSIONING` and set it to `false` to indicate that no `versionCode` or `versionName` need to be generated by the **app-versioning** plugin for this job:

```
unit-tests:
  name: Unit tests
  runs-on: ubuntu-latest
  env:
    ENABLE_APP_VERSIONING: false
```

We can then enable / disable the plugin based on the value of the environment variable:

```kotlin
val enableAppVersioning = providers
  .environmentVariable("ENABLE_APP_VERSIONING")
  .forUseAtConfigurationTime()
  .getOrElse("true").toString().toBoolean()

appVersioning {
  enabled.set(enableAppVersioning)
  ...
}
```

*Note that we need to use the experimental [forUseAtConfigurationTime()](https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Provider.html#forUseAtConfigurationTime--) API to create a configuration time provider.*

### Retrieving the generated version code and version name

There are cases where you may need to retrieve the `versionCode` and `versionName` of a generated APK / AAB:

- including the version code and version name in a Slack message sent from CI after a successful build
- communicating with a 3rd party service where version code and version name need to be included in the request (e.g. uploading R8 mapping file to a crashing reporting service)

Both the `versionCode` and `versionName` generated by **app-versioning** are in the build output directory:

```
app/build/outputs/app_versioning/<buildVariant>/version_code.txt
app/build/outputs/app_versioning/<buildVariant>/version_name.txt
```

We can `cat` the output of these files into variables: 

```
VERSION_CODE=$(cat app/build/outputs/app_versioning/<buildVariant>/version_code.txt)
VERSION_NAME=$(cat app/build/outputs/app_versioning/<buildVariant>/version_name.txt)
```

Note that if you need to query these files in a different VM than where the APK (and its version info) was originally generated, you need to make sure these files are "carried over" from the original VM. Otherwise you'll need to run the `generateAppVersionInfoFor<BuildVariant>` task again to generate these files, but the generated version info might not be the same as what's actually used for the APK (e.g. if you use the Epoch timestamp for `versionCode`).

Here's an example with GitHub Actions that does the following:

- in the [Assemble job](https://github.com/ReactiveCircus/streamlined/blob/f0b605627ffaa2a51b37cdec7c2dd846ad3a7dbf/.github/workflows/ci.yml#L33-L72), build the App Bundle and archive / upload the build outputs directory which include the AAB and its R8 mapping file, along with the `version_code.txt` and `version_name.txt` files generated by **app-versioning**.
- later in the [Publish to Play Store job](https://github.com/ReactiveCircus/streamlined/blob/f0b605627ffaa2a51b37cdec7c2dd846ad3a7dbf/.github/workflows/ci.yml#L202-L247), download the previously archived build outputs directory, `cat` the content of `version_code.txt` and `version_name.txt` into variables, upload the R8 mapping file to Bugsnag API with curl and passing the retrieved `$VERSION_CODE` and `$VERSION_NAME` as parameters, and finally upload the AAB to Play Store (without building the AAB or generating the app version info again).

## Lazy, incremental, cacheable

By moving version info generation to a Gradle task, we're finally able to avoid doing computation during Gradle's task configuration phase.

We can take this further by making the `generateAppInfoFor<BuildVariant>` task **incremental** and **cacheable**.

To do this the plugin tracks the `.git/refs` directory as a task input, so the task only becomes "dirty" and re-runs when there are changes to the Git references in the repository e.g. adding new commits or tags.

Task is executed with the first run:
```
./gradlew generateAppVersionInfoForProdRelease
...
BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed
```

Task is `up-to-date` with the subsequent run:

```
./gradlew generateAppVersionInfoForProdRelease
...
BUILD SUCCESSFUL in 1s
1 actionable task: 1 up-to-date
```

Task output is loaded from build cache ([Gradle build cache](https://docs.gradle.org/current/userguide/build_cache.html) needs to be enabled) after cleaning the project level build directory and running the task again:

```
./gradlew clean
./gradlew generateAppVersionInfoForProdRelease
...
BUILD SUCCESSFUL in 1s
1 actionable task: 1 from cache
```

## What's next

Customizing version code and version name has become easier and faster thanks to the [new APIs introduced in Android Gradle Plugin 4.x](https://medium.com/androiddevelopers/new-apis-in-the-android-gradle-plugin-f5325742e614). The [Android App Versioning Gradle Plugin](https://github.com/ReactiveCircus/app-versioning) builds on top of these APIs to specifically improve the experience for app versioning use cases based on Git.

Instructions for installing, configuring and using the plugin are available on [GitHub](https://github.com/ReactiveCircus/app-versioning).

Please give it a try and [share your feedback](https://github.com/ReactiveCircus/app-versioning/issues) as we work towards the stable release!

------

### References:

- [Build Bigger, Better: Gradle for Large Projects (Google I/O'19)](https://youtu.be/sQC9-Rj2yLI)
- [Android Plugin for Gradle cookbook](https://github.com/android/gradle-recipes/)
- [New APIs in the Android Gradle Plugin](https://medium.com/androiddevelopers/new-apis-in-the-android-gradle-plugin-f5325742e614)