App Versioning
A Gradle Plugin for lazily generating Android app's versionCode
& versionName
from Git tags.
Android Gradle Plugin 4.0 and 4.1 introduced some new experimental APIs to support lazily computing and setting the versionCode
and versionName
for an APK or App Bundle. This plugin builds on top of these APIs to support the common app versioning use cases based on Git.
This blogpost should provide more context around Git-based app versioning in general and why this plugin needs to build on top of the new variant APIs introduced in AGP 4.0 / 4.1 which are currently incubating.
Android Gradle Plugin version compatibility
The minimum version of Android Gradle Plugin required is 7.0.0-beta04.
Version 0.4.0
of the plugin is the final version that's compatible with AGP 4.0 and 4.1.
Version 0.10.0
of the plugin is the final version that's compatible with AGP 4.2.
Installation
The Android App Versioning Gradle Plugin is available from both Maven Central. Make sure you have added mavenCentral()
to the plugin repositories
:
// in settings.gradle.kts
pluginManagement {
repositories {
mavenCentral()
}
}
or
// in root build.gradle.kts
buildscript {
repositories {
mavenCentral()
}
}
The plugin can now be applied to your Android Application module (Gradle subproject).
Kotlin
plugins {
id("com.android.application")
id("io.github.reactivecircus.app-versioning") version "x.y.z"
}
Groovy
plugins {
id 'com.android.application'
id 'io.github.reactivecircus.app-versioning' version "x.y.z"
}
Usage
The plugin offers 2 Gradle tasks for each build variant:
generateAppVersionInfoFor<BuildVariant>
- generates theversionCode
andversionName
for theBuildVariant
. This task is automatically triggered when assembling the APK or AAB e.g. by runningassemble<BuildVariant>
orbundle<BuildVariant>
, and the generatedversionCode
andversionName
will be injected into the final mergedAndroidManifest
.printAppVersionInfoFor<BuildVariant>
- prints the latestversionCode
andversionName
generated by the plugin to the console if available.
Default behavior
Without any configurations, by default the plugin will fetch the latest Git tag in the repository, attempt to parse it into a SemVer string, and compute the versionCode
following positional notation:
versionCode = MAJOR * 10000 + MINOR * 100 + PATCH
As an example, for a tag 1.3.1
the generated versionCode
is 1 * 10000 + 3 * 100 + 1 = 10301
.
The default versionName
generated will just be the name of the latest Git tag.
> Task :app:generateAppVersionInfoForRelease
Generated app version code: 10301.
Generated app version name: "1.3.1".
If the default behavior described above works for you, you are all set to go.
Custom rules
The plugin lets you define how you want to compute the versionCode
and versionName
by implementing lambdas which are evaluated lazily during execution:
appVersioning {
overrideVersionCode { gitTag, providers, variantInfo ->
// TODO generate an Int from the given gitTag, providers, build variant
}
overrideVersionName { gitTag, providers, variantInfo ->
// TODO generate a String from the given gitTag, providers, build variant
}
}
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.
VariantInfo
is an object that encapsulates the build variant information including buildType
, flavorName
, and variantName
.
SemVer-based version code
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, _, _ ->
val semVer = gitTag.toSemVer()
semVer.major * 1000000 + semVer.minor * 1000 + semVer.patch
}
}
Groovy
import io.github.reactivecircus.appversioning.SemVer
appVersioning {
overrideVersionCode { gitTag, providers, variantInfo ->
def semVer = SemVer.fromGitTag(gitTag)
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 (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()
}
}
Groovy
appVersioning {
overrideVersionCode { gitTag, providers, variantInfo ->
Instant.now().epochSecond.intValue()
}
}
This will generate a monotonically increasing version code every time the generateAppVersionInfoForRelease
task is run:
Generated app version code: 1599750437.
Using environment variable
We can also add a BUILD_NUMBER
environment variable provided by CI to the versionCode
or versionName
. To do this, 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
}
}
Groovy
import io.github.reactivecircus.appversioning.SemVer
appVersioning {
overrideVersionCode { gitTag, providers, variantInfo ->
def buildNumber = providers
.environmentVariable("BUILD_NUMBER")
.getOrElse("0") as Integer
def semVer = SemVer.fromGitTag(gitTag)
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})"
}
}
Groovy
appVersioning {
overrideVersionName { gitTag, providers, variantInfo ->
// a custom versionName combining the tag name, commitHash and an environment variable
def buildNumber = providers
.environmentVariable("BUILD_NUMBER")
.getOrElse("0") as Integer
"${gitTag.rawTagName} - #$buildNumber (${gitTag.commitHash})".toString()
}
}
Custom rules based on build variants
Sometimes you might want to customize versionCode
or versionName
based on the build variants (product flavor, build type). To do this, use the variantInfo
lambda parameter to query the build variant information when generating custom versionCode
or verrsionName
:
Kotlin
import io.github.reactivecircus.appversioning.toSemVer
appVersioning {
overrideVersionCode { gitTag, _, _ ->
// add 1 to the versionCode for builds with the "paid" product flavor
val offset = if (variantInfo.flavorName == "paid") 1 else 0
val semVer = gitTag.toSemVer()
semVer.major * 10000 + semVer.minor * 100 + semVer.patch + offset
}
overrideVersionName { gitTag, _, variantInfo ->
// append build variant to the versionName for debug builds
val suffix = if (variantInfo.isDebugBuild) " (${variantInfo.variantName})" else ""
gitTag.toString() + suffix
}
}
Groovy
import io.github.reactivecircus.appversioning.SemVer
appVersioning {
overrideVersionCode { gitTag, providers, variantInfo ->
// add 1 to the versionCode for builds with the "paid" product flavor
def offset
if (variantInfo.flavorName == "paid") {
offset = 1
} else {
offset = 0
}
def semVer = SemVer.fromGitTag(gitTag)
semVer.major * 10000 + semVer.minor * 100 + semVer.patch + offset
}
overrideVersionName { gitTag, providers, variantInfo ->
// append build variant to the versionName for debug builds
def suffix
if (variantInfo.debugBuild == true) {
suffix = " (" + variantInfo.variantName + ")"
} else {
suffix = ""
}
gitTag.toString() + suffix
}
}
Tag filtering
By default the plugin uses the latest tag in the current branch for versionCode
and versionName
generation.
Sometimes it's useful to be able to use the latest tag that follows a specific glob pattern.
For example a codebase might build and publish 3 different apps separately using the following tag pattern:
<maj>.<min>.<patch>[-<pre-release version>]+<app-identifier>
where app-identifier
is the build metadata component in SemVer.
Some of the possible tags are:
1.5.8+app-a
2.29.0-rc01+app-b
10.87.9-alpha04+app-c
To configure the plugin to generate version info specific to app-b
:
appVersioning {
tagFilter.set("[0-9]*.[0-9]*.[0-9]*+app-b")
}
More configurations
Disabling the plugin
To disable the plugin such that the versionCode
and versionName
defined in the defaultConfig
block are used instead (if specified):
appVersioning {
/**
* Whether to enable the plugin.
*
* Default is `true`.
*/
enabled.set(false)
}
Release build only
To generate versionCode
and versionName
only for the Release
build type:
appVersioning {
/**
* Whether to only generate version name and version code for `release` builds.
*
* Default is `false`.
*/
releaseBuildOnly.set(true)
}
With releaseBuildOnly
set to true
, for a project with the default debug
and release
build types and no product flavors, the following tasks are available (note the absense of tasks with Debug
suffix):
/gradlew tasks --group=versioning
Versioning tasks
----------------
generateAppVersionInfoForRelease - Generates app's versionCode and versionName based on git tags for the release variant.
printAppVersionInfoForRelease - Prints the versionCode and versionName generated by Android App Versioning plugin (if available) to the console for the release variant.
Fetching tags if none exists locally
Sometimes a local checkout may not contain the Git tags (e.g. when cloning was done with --no-tags
). To fetch git tags from remote when no tags can be found locally:
appVersioning {
/**
* Whether to fetch git tags from remote when no git tag can be found locally.
*
* Default is `false`.
*/
fetchTagsWhenNoneExistsLocally.set(true)
}
Custom git root directory
The plugin assumes the root Gradle project is the git root directory that contains .git
. If your root Gradle project is not your git root, you can specify it explicitly:
appVersioning {
/**
* Git root directory used for fetching git tags.
* Use this to explicitly set the git root directory when the root Gradle project is not the git root directory.
*/
gitRootDirectory.set(rootProject.file("../")) // if the .git directory is in the root Gradle project's parent directory.
}
Bare git repository
If your .git
is a symbolic link to a bare git repository, you need to explicitly specify the directory of the bare git repository:
appVersioning {
/**
* Bare Git repository directory.
* Use this to explicitly set the directory of a bare git repository (e.g. `app.git`) instead of the standard `.git`.
* Setting this will override the value of [gitRootDirectory] property.
*/
bareGitRepoDirectory.set(rootProject.file("../.repo/projects/app.git")) // if the .git directory in the Gradle project root is a symlink to app.git.
}
App versioning on CI
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 GitHub Actions:
- uses: actions/checkout@v3
with:
fetch-depth: 0
Retrieving the generated version code and version name
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, 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
andversion_name.txt
files generated by app-versioning. - later in the Publish to Play Store job, download the previously archived build outputs directory,
cat
the content ofversion_code.txt
andversion_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).
License
Copyright 2020 Yang Chen
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.