Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/reactivecircus/app-versioning

A Gradle Plugin for lazily generating Android app's versionCode & versionName from Git tags.
https://github.com/reactivecircus/app-versioning

android git gradle versioning

Last synced: 3 days ago
JSON representation

A Gradle Plugin for lazily generating Android app's versionCode & versionName from Git tags.

Awesome Lists containing this project

README

        

# App Versioning

![CI](https://github.com/ReactiveCircus/app-versioning/workflows/CI/badge.svg)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.github.reactivecircus.appversioning/app-versioning-gradle-plugin/badge.svg)](https://search.maven.org/search?q=g:io.github.reactivecircus.appversioning)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)

A Gradle Plugin for lazily generating Android app's `versionCode` & `versionName` from Git tags.

![App Versioning](docs/images/sample.png)

Android Gradle Plugin 4.0 and 4.1 introduced some [new experimental APIs](https://medium.com/androiddevelopers/new-apis-in-the-android-gradle-plugin-f5325742e614) 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](https://dev.to/ychescale9/git-based-android-app-versioning-with-agp-4-0-24ip) 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](https://search.maven.org/artifact/io.github.reactivecircus.appversioning/app-versioning-gradle-plugin). Make sure you have added `mavenCentral()` to the plugin `repositories`:

```kt
// in settings.gradle.kts
pluginManagement {
repositories {
mavenCentral()
}
}
```

or

```kt
// in root build.gradle.kts
buildscript {
repositories {
mavenCentral()
}
}
```

The plugin can now be applied to your **Android Application** module (Gradle subproject).

Kotlin

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

Groovy

```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` - generates the `versionCode` and `versionName` for the `BuildVariant`. This task is automatically triggered when assembling the APK or AAB e.g. by running `assemble` or `bundle`, and the generated `versionCode` and `versionName` will be injected into the final merged `AndroidManifest`.
- `printAppVersionInfoFor` - prints the latest `versionCode` and `versionName` 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](https://en.wikipedia.org/wiki/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:

```kt
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](https://docs.gradle.org/current/javadoc/org/gradle/api/provider/ProviderFactory.html).

`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

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

Groovy

```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](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

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

Groovy

```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

```kt
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

```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

```kt
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

```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

```kt
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

```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:

`..[-]+`

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`:

```kt
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):

```kt
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:

```kt
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:

```kt
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:

```kt
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:

```kt
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](https://github.com/actions/checkout):

```
- uses: actions/checkout@v4
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//version_code.txt
app/build/outputs/app_versioning//version_name.txt
```

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

```
VERSION_CODE=$(cat app/build/outputs/app_versioning//version_code.txt)
VERSION_NAME=$(cat app/build/outputs/app_versioning//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` 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).

## 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.
```