diff --git a/CHANGELOG.md b/CHANGELOG.md
index f950e3e9..d429a9e4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,15 @@
## Unreleased
+This is the first preview release of **App Versioning 2.0**, featuring redesigned `VersionCodeSpec` and `VersionNameSpec` APIs along with improved `GitTag` and `SemVer` APIs.
+
+Please see the [2.0 Migration Guide](docs/migration-guide.md) for full details on breaking changes and how to upgrade from 1.x.
+
+Note that the APIs are subject to change before the stable `2.0.0` release. Feedback and bug reports are welcome!
+
### Changed
-- Minimum Android Gradle Plugin version is now **9.0.0** when using Gradle **9.6.0** and above.
+- New `AppVersioningExtension`, `GitTag`, and `SemVer` APIs.
+- Minimum Android Gradle Plugin version is now **9.0.0**.
- Compile with AGP 9.4.1.
- Compile with Kotlin 2.4.21.
diff --git a/README.md b/README.md
index 8287e6f4..60840fad 100644
--- a/README.md
+++ b/README.md
@@ -8,25 +8,26 @@ A Gradle Plugin for lazily generating Android app's `versionCode` & `versionName

-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.
+Modern versions of Android Gradle Plugin provide new [Variant APIs](https://medium.com/androiddevelopers/new-apis-in-the-android-gradle-plugin-f5325742e614) that support **lazily** computing the `versionCode` and `versionName` of an APK or App Bundle.
-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**.
+This plugin builds on top of these APIs to support common Git-based app versioning use cases with full Configuration Cache and Isolated Projects support.
## Android Gradle Plugin version compatibility
The minimum version of Android Gradle Plugin required is **9.0.0**.
-Version `1.6.0` of the plugin is the final version that's compatible with AGP **8.x**.
-
-Version `1.4.0` of the plugin is the final version that's compatible with AGP **7.x** and below.
-
-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**.
+| App Versioning Plugin | Android Gradle Plugin |
+| --------------------- | --------------------- |
+| 2.0.0+ | 9.0.0+ |
+| 1.6.0 | 8.2.2+ |
+| 1.5.0 | 8.0.0+ |
+| 1.0.0 - 1.4.0 | 7.0.0+ |
+| 0.5.0 - 0.10.0 | 4.2 |
+| 0.1.0 - 0.4.0 | 4.0 - 4.1 |
## 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`:
+The **Android App Versioning Gradle Plugin** is available from [Maven Central](https://central.sonatype.com/artifact/io.github.reactivecircus.appversioning/app-versioning-gradle-plugin). Make sure you have added `mavenCentral()` to the plugin `repositories`:
```kt
// in settings.gradle.kts
@@ -37,17 +38,6 @@ pluginManagement {
}
```
-or
-
-```kt
-// in root build.gradle.kts
-buildscript {
- repositories {
- mavenCentral()
- }
-}
-```
-
The plugin can now be applied to your **Android Application** module (Gradle subproject).
Kotlin
@@ -72,24 +62,26 @@ plugins {
+> Upgrading from 1.x? See the [2.0 Migration Guide](docs/migration-guide.md).
+
## 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`.
+- `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` are injected into the final merged `AndroidManifest.xml`.
- `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):
+Without any configuration, the plugin fetches the latest Git tag in the repository, parses it as a **SemVer** string, and computes `versionCode` using [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`.
+For 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.
+The default `versionName` generated is the name of the latest Git tag.
```
> Task :app:generateAppVersionInfoForRelease
@@ -97,44 +89,38 @@ 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 specs
-### 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:
+You can customize how `versionCode` and `versionName` are computed by configuring `versionCode` (`Property`) and `versionName` (`Property`):
```kt
appVersioning {
- overrideVersionCode { gitTag, providers, variantInfo ->
- // TODO generate an Int from the given gitTag, providers, build variant
+ versionCode.set {
+ // generate an Int
}
- overrideVersionName { gitTag, providers, variantInfo ->
- // TODO generate a String from the given gitTag, providers, build variant
+ versionName.set {
+ // generate a String
}
}
```
-`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`.
+Inside the lambda receiver (`VersionContext`):
+- `tag` (`GitTag`) encapsulates `name`, `commitsSinceLatestTag`, and `commitHash`.
+- `variant` (`VariantInfo`) encapsulates `buildType`, `flavorName`, `variantName`, `isDebugBuild`, and `isReleaseBuild`.
#### SemVer-based version code
-The plugin by default reserves 2 digits for each of the **MAJOR**, **MINOR** and **PATCH** components in a SemVer tag.
+The default `VersionCodeSpec.SemVer` reserves 2 digits for each of the **MAJOR**, **MINOR**, and **PATCH** components in a SemVer tag (accepting an optional `"v"` prefix).
-To allocate 3 digits per component instead (i.e. each version component can go up to 999):
+To allocate 3 digits per component instead (up to 999 per component) or strip a custom tag prefix:
Kotlin
```kt
-import io.github.reactivecircus.appversioning.toSemVer
appVersioning {
- overrideVersionCode { gitTag, _, _ ->
- val semVer = gitTag.toSemVer()
- semVer.major * 1000000 + semVer.minor * 1000 + semVer.patch
+ versionCode.set {
+ tag.toSemVer(prefix = "android-").toVersionCode(digitsPerComponent = 3)
}
}
```
@@ -144,74 +130,36 @@ appVersioning {
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
+ versionCode {
+ tag.toSemVer("android-").toVersionCode(3)
}
}
```
+`tag.toSemVer(prefix = "")` parses `GitTag#name` (after stripping `prefix` if specified) into a `SemVer` object, and `SemVer#toVersionCode(digitsPerComponent = 2)` converts it to an integer using positional notation. You can also parse arbitrary strings directly via `SemVer.parse(text)` or `SemVer.parseOrNull(text)`.
-`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 environment variables or Gradle properties
-#### 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`:
+When computing `versionCode` or `versionName`, you can use an environment variable or Gradle property by mapping its `Provider` to the `VersionCodeSpec` or `VersionNameSpec` property:
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
+import io.github.reactivecircus.appversioning.VersionCodeSpec
-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
+val buildNumber = providers.environmentVariable("BUILD_NUMBER").map(String::toInt).orElse(0)
-```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
- }
+ versionCode.set(
+ buildNumber.map { buildNum ->
+ VersionCodeSpec {
+ tag.toSemVer().toVersionCode() + buildNum
+ }
+ }
+ )
}
```
@@ -220,74 +168,40 @@ appVersioning {
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})"
- }
-}
-```
+import io.github.reactivecircus.appversioning.VersionCodeSpec
-
-
-Groovy
+def buildNumber = providers.environmentVariable("BUILD_NUMBER").map { it.toInteger() }.orElse(0)
-```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()
- }
+ versionCode.set(
+ buildNumber.map { buildNum ->
+ {
+ tag.toSemVer().toVersionCode() + buildNum
+ } as VersionCodeSpec
+ }
+ )
}
```
-#### Custom rules based on build variants
+#### Custom specs 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`:
+Sometimes you might want to customize `versionCode` or `versionName` based on the build variants (product flavor, build type). To do this, use `variant` (`VariantInfo`) from the `VersionContext` receiver when generating custom `versionCode` or `versionName`:
Kotlin
```kt
-import io.github.reactivecircus.appversioning.toSemVer
appVersioning {
- overrideVersionCode { gitTag, _, _ ->
+ versionCode.set {
// 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
+ val offset = if (variant.flavorName == "paid") 1 else 0
+ tag.toSemVer().toVersionCode() + offset
}
- overrideVersionName { gitTag, _, variantInfo ->
+ versionName.set {
// append build variant to the versionName for debug builds
- val suffix = if (variantInfo.isDebugBuild) " (${variantInfo.variantName})" else ""
- gitTag.toString() + suffix
+ val suffix = if (variant.isDebugBuild) " (${variant.variantName})" else ""
+ "${tag.name} (${tag.commitHash})$suffix"
}
}
```
@@ -297,28 +211,16 @@ appVersioning {
Groovy
```groovy
-import io.github.reactivecircus.appversioning.SemVer
appVersioning {
- overrideVersionCode { gitTag, providers, variantInfo ->
+ versionCode {
// 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
+ def offset = variant.flavorName == "paid" ? 1 : 0
+ tag.toSemVer().toVersionCode() + offset
}
- overrideVersionName { gitTag, providers, variantInfo ->
+ versionName {
// append build variant to the versionName for debug builds
- def suffix
- if (variantInfo.debugBuild == true) {
- suffix = " (" + variantInfo.variantName + ")"
- } else {
- suffix = ""
- }
- gitTag.toString() + suffix
+ def suffix = variant.debugBuild ? " (${variant.variantName})" : ""
+ "${tag.name} (${tag.commitHash})${suffix}"
}
}
```
@@ -357,38 +259,28 @@ appVersioning {
### Disabling the plugin
-To disable the plugin such that the `versionCode` and `versionName` defined in the `defaultConfig` block are used instead (if specified):
+To disable the plugin so the `versionCode` and `versionName` defined in the `defaultConfig` block or manifest are used instead:
```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:
+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):
+With `releaseBuildOnly` set to `true`, tasks are only registered for release variants:
```
-/gradlew tasks --group=versioning
+./gradlew tasks --group=versioning
Versioning tasks
----------------
@@ -398,53 +290,39 @@ printAppVersionInfoForRelease - Prints the versionCode and versionName generated
### 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:
+When a local checkout does not contain Git tags (e.g. cloned with `--no-tags`), you can configure the plugin to fetch tags from remote when none exist 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:
+The plugin assumes the root Gradle project is the git root directory containing `.git`. If your root Gradle project is not the git root, 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.
+ gitRootDirectory.set(rootDir.resolve("../"))
}
```
### 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:
+If your `.git` is a symbolic link to a **bare** git repository, specify the directory of the bare git repository explicitly (overrides `gitRootDirectory`):
```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.
+ bareGitRepoDirectory.set(rootDir.resolve("../.repo/projects/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):
+Many CI providers only fetch a single commit by default when checking out the repository. Make sure Git tags and history are fetched (e.g. with [GitHub Actions](https://github.com/actions/checkout)):
-```
+```yaml
- uses: actions/checkout@v5
with:
fetch-depth: 0
@@ -452,27 +330,20 @@ For performance reason many CI providers only fetch a single commit by default w
### Retrieving the generated version code and version name
-Both the `versionCode` and `versionName` generated by **app-versioning** are in the build output directory:
+Both the `versionCode` and `versionName` generated by **app-versioning** are written to 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:
+You can read the contents of these files into shell variables:
-```
+```sh
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
```
diff --git a/api/app-versioning.api b/api/app-versioning.api
index 5fdaccc1..0b9e4b34 100644
--- a/api/app-versioning.api
+++ b/api/app-versioning.api
@@ -1,82 +1,61 @@
public class io/github/reactivecircus/appversioning/AppVersioningExtension {
- public static final field Companion Lio/github/reactivecircus/appversioning/AppVersioningExtension$Companion;
public final fun getBareGitRepoDirectory ()Lorg/gradle/api/file/DirectoryProperty;
public final fun getEnabled ()Lorg/gradle/api/provider/Property;
public final fun getFetchTagsWhenNoneExistsLocally ()Lorg/gradle/api/provider/Property;
public final fun getGitRootDirectory ()Lorg/gradle/api/file/DirectoryProperty;
public final fun getReleaseBuildOnly ()Lorg/gradle/api/provider/Property;
public final fun getTagFilter ()Lorg/gradle/api/provider/Property;
- public final fun overrideVersionCode (Lgroovy/lang/Closure;)V
- public final fun overrideVersionCode (Lkotlin/jvm/functions/Function3;)V
- public final fun overrideVersionName (Lgroovy/lang/Closure;)V
- public final fun overrideVersionName (Lkotlin/jvm/functions/Function3;)V
-}
-
-public final class io/github/reactivecircus/appversioning/AppVersioningExtension$Companion {
+ public final fun getVersionCode ()Lorg/gradle/api/provider/Property;
+ public final fun getVersionName ()Lorg/gradle/api/provider/Property;
+ public final fun versionCode (Lgroovy/lang/Closure;)V
+ public final fun versionName (Lgroovy/lang/Closure;)V
}
public final class io/github/reactivecircus/appversioning/AppVersioningPlugin : org/gradle/api/Plugin {
- public static final field Companion Lio/github/reactivecircus/appversioning/AppVersioningPlugin$Companion;
public fun ()V
public synthetic fun apply (Ljava/lang/Object;)V
public fun apply (Lorg/gradle/api/Project;)V
}
-public final class io/github/reactivecircus/appversioning/AppVersioningPlugin$Companion {
-}
-
public final class io/github/reactivecircus/appversioning/GitTag {
- public fun (Ljava/lang/String;ILjava/lang/String;)V
- public final fun component1 ()Ljava/lang/String;
- public final fun component2 ()I
- public final fun component3 ()Ljava/lang/String;
- public final fun copy (Ljava/lang/String;ILjava/lang/String;)Lio/github/reactivecircus/appversioning/GitTag;
- public static synthetic fun copy$default (Lio/github/reactivecircus/appversioning/GitTag;Ljava/lang/String;ILjava/lang/String;ILjava/lang/Object;)Lio/github/reactivecircus/appversioning/GitTag;
public fun equals (Ljava/lang/Object;)Z
public final fun getCommitHash ()Ljava/lang/String;
public final fun getCommitsSinceLatestTag ()I
- public final fun getRawTagName ()Ljava/lang/String;
+ public final fun getName ()Ljava/lang/String;
public fun hashCode ()I
+ public final fun toSemVer ()Lio/github/reactivecircus/appversioning/SemVer;
+ public final fun toSemVer (Ljava/lang/String;)Lio/github/reactivecircus/appversioning/SemVer;
+ public static synthetic fun toSemVer$default (Lio/github/reactivecircus/appversioning/GitTag;Ljava/lang/String;ILjava/lang/Object;)Lio/github/reactivecircus/appversioning/SemVer;
public fun toString ()Ljava/lang/String;
}
-public final class io/github/reactivecircus/appversioning/SemVer {
+public final class io/github/reactivecircus/appversioning/SemVer : java/lang/Comparable {
public static final field Companion Lio/github/reactivecircus/appversioning/SemVer$Companion;
public fun (IIILjava/lang/String;Ljava/lang/String;)V
public synthetic fun (IIILjava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public final fun component1 ()I
- public final fun component2 ()I
- public final fun component3 ()I
- public final fun component4 ()Ljava/lang/String;
- public final fun component5 ()Ljava/lang/String;
- public final fun copy (IIILjava/lang/String;Ljava/lang/String;)Lio/github/reactivecircus/appversioning/SemVer;
- public static synthetic fun copy$default (Lio/github/reactivecircus/appversioning/SemVer;IIILjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/github/reactivecircus/appversioning/SemVer;
+ public fun compareTo (Lio/github/reactivecircus/appversioning/SemVer;)I
+ public synthetic fun compareTo (Ljava/lang/Object;)I
public fun equals (Ljava/lang/Object;)Z
- public static final fun fromGitTag (Lio/github/reactivecircus/appversioning/GitTag;)Lio/github/reactivecircus/appversioning/SemVer;
- public static final fun fromGitTag (Lio/github/reactivecircus/appversioning/GitTag;Z)Lio/github/reactivecircus/appversioning/SemVer;
public final fun getBuildMetadata ()Ljava/lang/String;
public final fun getMajor ()I
public final fun getMinor ()I
public final fun getPatch ()I
public final fun getPreRelease ()Ljava/lang/String;
public fun hashCode ()I
+ public static final fun parse (Ljava/lang/String;)Lio/github/reactivecircus/appversioning/SemVer;
+ public static final fun parseOrNull (Ljava/lang/String;)Lio/github/reactivecircus/appversioning/SemVer;
public fun toString ()Ljava/lang/String;
+ public final fun toVersionCode ()I
+ public final fun toVersionCode (I)I
+ public static synthetic fun toVersionCode$default (Lio/github/reactivecircus/appversioning/SemVer;IILjava/lang/Object;)I
}
public final class io/github/reactivecircus/appversioning/SemVer$Companion {
- public final fun fromGitTag (Lio/github/reactivecircus/appversioning/GitTag;)Lio/github/reactivecircus/appversioning/SemVer;
- public final fun fromGitTag (Lio/github/reactivecircus/appversioning/GitTag;Z)Lio/github/reactivecircus/appversioning/SemVer;
- public static synthetic fun fromGitTag$default (Lio/github/reactivecircus/appversioning/SemVer$Companion;Lio/github/reactivecircus/appversioning/GitTag;ZILjava/lang/Object;)Lio/github/reactivecircus/appversioning/SemVer;
-}
-
-public final class io/github/reactivecircus/appversioning/SemVerKt {
- public static final fun toSemVer (Lio/github/reactivecircus/appversioning/GitTag;Z)Lio/github/reactivecircus/appversioning/SemVer;
- public static synthetic fun toSemVer$default (Lio/github/reactivecircus/appversioning/GitTag;ZILjava/lang/Object;)Lio/github/reactivecircus/appversioning/SemVer;
+ public final fun parse (Ljava/lang/String;)Lio/github/reactivecircus/appversioning/SemVer;
+ public final fun parseOrNull (Ljava/lang/String;)Lio/github/reactivecircus/appversioning/SemVer;
}
public final class io/github/reactivecircus/appversioning/VariantInfo : java/io/Serializable {
- public static final field Companion Lio/github/reactivecircus/appversioning/VariantInfo$Companion;
- public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public final fun getBuildType ()Ljava/lang/String;
public final fun getFlavorName ()Ljava/lang/String;
public final fun getVariantName ()Ljava/lang/String;
@@ -84,79 +63,49 @@ public final class io/github/reactivecircus/appversioning/VariantInfo : java/io/
public final fun isReleaseBuild ()Z
}
-public final class io/github/reactivecircus/appversioning/VariantInfo$Companion {
+public abstract interface class io/github/reactivecircus/appversioning/VersionCodeSpec : java/io/Serializable {
+ public static final field Companion Lio/github/reactivecircus/appversioning/VersionCodeSpec$Companion;
+ public abstract fun versionCode (Lio/github/reactivecircus/appversioning/VersionContext;)I
}
-public final class io/github/reactivecircus/appversioning/internal/CommitId {
- public static final synthetic fun box-impl (Ljava/lang/String;)Lio/github/reactivecircus/appversioning/internal/CommitId;
- public static fun constructor-impl (Ljava/lang/String;)Ljava/lang/String;
- public fun equals (Ljava/lang/Object;)Z
- public static fun equals-impl (Ljava/lang/String;Ljava/lang/Object;)Z
- public static final fun equals-impl0 (Ljava/lang/String;Ljava/lang/String;)Z
- public final fun getValue ()Ljava/lang/String;
- public fun hashCode ()I
- public static fun hashCode-impl (Ljava/lang/String;)I
- public fun toString ()Ljava/lang/String;
- public static fun toString-impl (Ljava/lang/String;)Ljava/lang/String;
- public final synthetic fun unbox-impl ()Ljava/lang/String;
+public final class io/github/reactivecircus/appversioning/VersionCodeSpec$Companion {
+ public final fun getSemVer ()Lio/github/reactivecircus/appversioning/VersionCodeSpec;
+}
+
+public abstract interface class io/github/reactivecircus/appversioning/VersionContext {
+ public abstract fun getTag ()Lio/github/reactivecircus/appversioning/GitTag;
+ public abstract fun getVariant ()Lio/github/reactivecircus/appversioning/VariantInfo;
}
-public final class io/github/reactivecircus/appversioning/internal/GitClient {
- public static final field Companion Lio/github/reactivecircus/appversioning/internal/GitClient$Companion;
- public final fun checkoutTag (Ljava/lang/String;)V
- public final fun commit-HhBVPuw (Ljava/lang/String;Z)Ljava/lang/String;
- public static synthetic fun commit-HhBVPuw$default (Lio/github/reactivecircus/appversioning/internal/GitClient;Ljava/lang/String;ZILjava/lang/Object;)Ljava/lang/String;
- public final fun describeLatestTag (Ljava/lang/String;)Ljava/lang/String;
- public static synthetic fun describeLatestTag$default (Lio/github/reactivecircus/appversioning/internal/GitClient;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String;
- public final fun fetchRemoteTags ()V
- public final fun listLocalTags ()Ljava/util/List;
- public final fun tag-9kPyyPo (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
- public static synthetic fun tag-9kPyyPo$default (Lio/github/reactivecircus/appversioning/internal/GitClient;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
+public abstract interface class io/github/reactivecircus/appversioning/VersionNameSpec : java/io/Serializable {
+ public static final field Companion Lio/github/reactivecircus/appversioning/VersionNameSpec$Companion;
+ public abstract fun versionName (Lio/github/reactivecircus/appversioning/VersionContext;)Ljava/lang/String;
}
-public final class io/github/reactivecircus/appversioning/internal/GitClient$Companion {
- public final fun initialize (Ljava/io/File;)Lio/github/reactivecircus/appversioning/internal/GitClient;
- public final fun open (Ljava/io/File;)Lio/github/reactivecircus/appversioning/internal/GitClient;
+public final class io/github/reactivecircus/appversioning/VersionNameSpec$Companion {
+ public final fun getTagName ()Lio/github/reactivecircus/appversioning/VersionNameSpec;
}
public abstract class io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfo : org/gradle/api/DefaultTask {
- public static final field Companion Lio/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfo$Companion;
- public static final field TASK_DESCRIPTION_PREFIX Ljava/lang/String;
- public static final field TASK_NAME_PREFIX Ljava/lang/String;
- public static final field VERSION_CODE_FALLBACK I
- public static final field VERSION_NAME_FALLBACK Ljava/lang/String;
- public fun (Lorg/gradle/workers/WorkerExecutor;)V
- public final fun generate ()V
+ public fun ()V
public abstract fun getFetchTagsWhenNoneExistsLocally ()Lorg/gradle/api/provider/Property;
public abstract fun getGitHead ()Lorg/gradle/api/file/RegularFileProperty;
public abstract fun getGitRefsDirectory ()Lorg/gradle/api/file/DirectoryProperty;
- public abstract fun getGroovyVersionCodeCustomizer ()Lorg/gradle/api/provider/Property;
- public abstract fun getGroovyVersionNameCustomizer ()Lorg/gradle/api/provider/Property;
- public abstract fun getKotlinVersionCodeCustomizer ()Lorg/gradle/api/provider/Property;
- public abstract fun getKotlinVersionNameCustomizer ()Lorg/gradle/api/provider/Property;
public abstract fun getRootProjectDirectory ()Lorg/gradle/api/file/DirectoryProperty;
public abstract fun getRootProjectDisplayName ()Lorg/gradle/api/provider/Property;
public abstract fun getTagFilter ()Lorg/gradle/api/provider/Property;
public abstract fun getVariantInfo ()Lorg/gradle/api/provider/Property;
public abstract fun getVersionCodeFile ()Lorg/gradle/api/file/RegularFileProperty;
+ public abstract fun getVersionCodeSpec ()Lorg/gradle/api/provider/Property;
public abstract fun getVersionNameFile ()Lorg/gradle/api/file/RegularFileProperty;
-}
-
-public final class io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfo$Companion {
+ public abstract fun getVersionNameSpec ()Lorg/gradle/api/provider/Property;
}
public abstract class io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfo : org/gradle/api/DefaultTask {
- public static final field Companion Lio/github/reactivecircus/appversioning/tasks/PrintAppVersionInfo$Companion;
- public static final field TASK_DESCRIPTION_PREFIX Ljava/lang/String;
- public static final field TASK_NAME_PREFIX Ljava/lang/String;
public fun ()V
public abstract fun getBuildVariantName ()Lorg/gradle/api/provider/Property;
public abstract fun getProjectName ()Lorg/gradle/api/provider/Property;
public abstract fun getVersionCodeFile ()Lorg/gradle/api/file/RegularFileProperty;
public abstract fun getVersionNameFile ()Lorg/gradle/api/file/RegularFileProperty;
- public final fun print ()V
-}
-
-public final class io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfo$Companion {
}
diff --git a/build.gradle.kts b/build.gradle.kts
index 682edd7d..b2ac342a 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -11,6 +11,7 @@ plugins {
alias(libs.plugins.detekt)
alias(libs.plugins.mavenPublish)
alias(libs.plugins.burst)
+ alias(libs.plugins.testkit)
}
group = property("GROUP") as String
@@ -21,6 +22,10 @@ mavenPublishing {
signAllPublications()
}
+gradleTestKitSupport {
+ disablePublication()
+}
+
samWithReceiver {
annotation(HasImplicitReceiver::class.qualifiedName!!)
}
@@ -56,34 +61,19 @@ tasks.pluginUnderTestMetadata {
pluginClasspath.from(fixtureClasspath)
}
-val functionalTestSourceSet: SourceSet = sourceSets.create("functionalTest") {
- compileClasspath += sourceSets["main"].output + configurations["testRuntimeClasspath"]
- runtimeClasspath += output + compileClasspath
-}
-
-val functionalTestImplementation: Configuration = configurations.getByName("functionalTestImplementation")
- .extendsFrom(configurations.getByName("testImplementation"))
-
-gradlePlugin.testSourceSets(functionalTestSourceSet)
-
-val functionalTest = tasks.register("functionalTest", Test::class) {
- failFast = true
+tasks.withType().configureEach {
testLogging {
events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
}
- testClassesDirs = functionalTestSourceSet.output.classesDirs
- classpath = functionalTestSourceSet.runtimeClasspath
}
-val check = tasks.getByName("check", Task::class) {
- dependsOn(functionalTest)
+tasks.functionalTest {
+ failFast = true
+ systemProperty("fixtureAgpVersion", fixtureAgpVersion.get())
}
-val test = tasks.getByName("test", Test::class) {
+tasks.test {
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
- testLogging {
- events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
- }
}
val fixtureAgpVersion: Provider = providers
@@ -93,12 +83,16 @@ val fixtureAgpVersion: Provider = providers
dependencies {
compileOnly(libs.agp.build.api)
- testImplementation(kotlin("test"))
+ testImplementation(kotlin("test-junit"))
+ functionalTestImplementation(kotlin("test-junit"))
+ functionalTestImplementation(libs.burst)
+ functionalTestImplementation(libs.agp.build.api)
fixtureClasspath(libs.agp.build.gradle.flatMap { dependency ->
fixtureAgpVersion.map { version ->
"${dependency.group}:${dependency.name}:$version"
}
})
+ detektPlugins(libs.plugin.detektKtlintWrapper)
}
detekt {
@@ -116,6 +110,3 @@ tasks.withType().configureEach {
html.outputLocation.set(file("build/reports/detekt/${project.name}.html"))
}
}
-
-
-dependencies.add("detektPlugins", libs.plugin.detektKtlintWrapper.get())
diff --git a/docs/images/sample.png b/docs/images/sample.png
index cc4a010a..19da8a53 100644
Binary files a/docs/images/sample.png and b/docs/images/sample.png differ
diff --git a/docs/migration-guide.md b/docs/migration-guide.md
new file mode 100644
index 00000000..2c23694d
--- /dev/null
+++ b/docs/migration-guide.md
@@ -0,0 +1,239 @@
+# Migrating to App Versioning 2.0
+
+This guide covers the breaking changes and new APIs in **App Versioning 2.0** when upgrading from **1.x**.
+
+## Minimum requirements
+
+- **Android Gradle Plugin**: **9.0.0** or higher.
+- **Java**: Targets **JVM 17** bytecode.
+
+## Spec APIs
+
+The `overrideVersionCode` and `overrideVersionName` lambdas have been replaced by `versionCode` and `versionName` spec properties:
+
+- `versionCode: Property` (defaults to `VersionCodeSpec.SemVer`)
+- `versionName: Property` (defaults to `VersionNameSpec.TagName`)
+
+Both `VersionCodeSpec` and `VersionNameSpec` are SAM (`fun interface`) types with `VersionContext` (`tag: GitTag`, `variant: VariantInfo`) as the implicit receiver.
+
+### Kotlin DSL
+
+**1.x**
+
+```kt
+import io.github.reactivecircus.appversioning.toSemVer
+
+appVersioning {
+ overrideVersionCode { gitTag, _, variantInfo ->
+ val semVer = gitTag.toSemVer()
+ val offset = if (variantInfo.flavorName == "paid") 1 else 0
+ semVer.major * 10000 + semVer.minor * 100 + semVer.patch + offset
+ }
+ overrideVersionName { gitTag, _, variantInfo ->
+ val suffix = if (variantInfo.isDebugBuild) " (${variantInfo.variantName})" else ""
+ "${gitTag.rawTagName} (${gitTag.commitHash})$suffix"
+ }
+}
+```
+
+**2.0**
+
+```kt
+appVersioning {
+ versionCode.set {
+ val offset = if (variant.flavorName == "paid") 1 else 0
+ tag.toSemVer().toVersionCode() + offset
+ }
+ versionName.set {
+ val suffix = if (variant.isDebugBuild) " (${variant.variantName})" else ""
+ "${tag.name} (${tag.commitHash})$suffix"
+ }
+}
+```
+
+### Groovy DSL
+
+In Groovy, `AppVersioningExtension` provides `versionCode { ... }` and `versionName { ... }` convenience methods that configure a `Closure` with `VersionContext` as its delegate:
+
+**1.x**
+
+```groovy
+import io.github.reactivecircus.appversioning.SemVer
+
+appVersioning {
+ overrideVersionCode { gitTag, providers, variantInfo ->
+ def semVer = SemVer.fromGitTag(gitTag)
+ def offset = variantInfo.flavorName == "paid" ? 1 : 0
+ semVer.major * 10000 + semVer.minor * 100 + semVer.patch + offset
+ }
+ overrideVersionName { gitTag, providers, variantInfo ->
+ def suffix = variantInfo.debugBuild ? " (${variantInfo.variantName})" : ""
+ "${gitTag.rawTagName} (${gitTag.commitHash})${suffix}"
+ }
+}
+```
+
+**2.0**
+
+```groovy
+appVersioning {
+ versionCode {
+ def offset = variant.flavorName == "paid" ? 1 : 0
+ tag.toSemVer().toVersionCode() + offset
+ }
+ versionName {
+ def suffix = variant.debugBuild ? " (${variant.variantName})" : ""
+ "${tag.name} (${tag.commitHash})${suffix}"
+ }
+}
+```
+
+## Using `ProviderFactory`
+
+In 1.0 `ProviderFactory` was passed into the version code / name computation lambda as a parameter, so external values (e.g., `BUILD_NUMBER` from environment variables) could be read inside the lambda. This was problematic because:
+
+- Gradle computes a task's up-to-date fingerprint and build-cache key **before** task execution, using only declared task input properties.
+- If a lambda calls `providers.environmentVariable("BUILD_NUMBER").get()` during execution, Gradle never sees that read when fingerprinting inputs.
+- If `BUILD_NUMBER` changes on the next build while `.git` does not, Gradle will wrongly mark the task `UP-TO-DATE` (or restore `FROM-CACHE`) with the stale value, preventing the `GenerateAppVersionInfo` task from executing to produce the new version code / name.
+
+In 2.0 `ProviderFactory` is no longer available in the lambda (via the `VersionContext` receiver type).
+Instead, use Gradle's [Provider chaining APIs](https://docs.gradle.org/current/userguide/properties_providers.html#chaining_providers) to lazily map the external values provider to the `VersionCodeSpec` or `VersionNameSpec` property.
+
+### Kotlin DSL
+
+**1.x**
+
+```kt
+appVersioning {
+ overrideVersionName { gitTag, providers, _ ->
+ val buildNumber = providers
+ .environmentVariable("BUILD_NUMBER")
+ .getOrElse("0")
+ "$gitTag (#$buildNumber - ${gitTag.commitHash})"
+ }
+}
+```
+
+**2.0**
+
+```kt
+import io.github.reactivecircus.appversioning.VersionNameSpec
+
+val buildNumber = providers.environmentVariable("BUILD_NUMBER").orElse("0")
+
+appVersioning {
+ versionName.set(
+ buildNumber.map { buildNum ->
+ VersionNameSpec {
+ "$tag (#$buildNum - ${tag.commitHash})"
+ }
+ }
+ )
+}
+```
+
+### Groovy DSL
+
+**1.x**
+
+```groovy
+appVersioning {
+ overrideVersionName { gitTag, providers, variantInfo ->
+ def buildNumber = providers
+ .environmentVariable("BUILD_NUMBER")
+ .getOrElse("0")
+ "$gitTag (#$buildNumber - ${gitTag.commitHash})".toString()
+ }
+}
+```
+
+**2.0**
+
+```groovy
+import io.github.reactivecircus.appversioning.VersionNameSpec
+
+def buildNumber = providers.environmentVariable("BUILD_NUMBER").orElse("0")
+
+appVersioning {
+ versionName.set(
+ buildNumber.map { buildNum ->
+ { "$tag (#$buildNum - ${tag.commitHash})" } as VersionNameSpec
+ }
+ )
+}
+```
+
+## New `GitTag` and `SemVer` APIs
+
+There are a few API changes to `GitTag` and `SemVer` to make SemVer-based version code customization easier.
+
+### Creating `SemVer` from `GitTag`
+
+Both the `fun GitTag.toSemVer(allowPrefixV: Boolean = true): SemVer` extension function and the `fun fromGitTag(gitTag: GitTag, allowPrefixV: Boolean = true): SemVer` factory function have been removed.
+
+Instead, use the new member function on `GitTag`:
+
+```kt
+fun toSemVer(prefix: String = ""): SemVer
+```
+
+Note that SemVer parsing is now strict, meaning `"v"` prefix is no longer automatically stripped.
+
+Use `prefix` to strip any characters from the start of the tag name before SemVer parsing:
+
+```kt
+// For tags like "v1.2.3"
+tag.toSemVer(prefix = "v")
+
+// For tags like "android-1.2.3"
+tag.toSemVer(prefix = "android-")
+```
+
+### Creating `SemVer` from raw strings
+
+If your `GitTag` cannot be directly parsed to `SemVer` using `toSemVer(prefix)`, we now offer APIs for parsing arbitrary strings to `SemVer`.
+
+For example, if your tag name is `"1.2.3-android"`, you can manually strip off the `"-android"` suffix before parsing:
+
+```kt
+val semVer = SemVer.parse(tag.name.removeSuffix("-android"))
+val semVerOrNull = SemVer.parseOrNull(tag.name.removeSuffix("-android"))
+```
+
+### Converting `SemVer` to version code
+
+Previously to customize how SemVer tags were converted to version codes, you had to reimplement the positional notation formula from the SemVer components, even when you just wanted to add a fixed offset to the default SemVer-based version code.
+
+Now you can use the new `SemVer.toVersionCode(digitsPerComponent: Int = 2): Int` API:
+
+```kt
+// 1.x
+val semVer = gitTag.toSemVer()
+semVer.major * 1000000 + semVer.minor * 1000 + semVer.patch + offset
+
+// 2.0
+tag.toSemVer().toVersionCode(digitsPerComponent = 3) + offset
+```
+
+### Other notable API changes
+
+- `rawTagName` has been renamed to `name`.
+- `GitTag` and `SemVer` are no longer data classes.
+- `SemVer` now implements `Comparable`.
+
+## Migrating away from timestamp-based `versionCode`
+
+Using `Instant.now().epochSecond.toInt()` for `versionCode` is no longer recommended:
+
+1. Epoch-second timestamps will hit Google Play's maximum allowed `versionCode` of **2,100,000,000** in 2036, and Play Console warns when `versionCode` jumps by large increments.
+2. Calling `Instant.now()` inside a spec produces a different value whenever the spec is evaluated.
+
+If your app previously published epoch-second version codes (e.g. `~1,700,000,000`), you can migrate to SemVer-based version codes by adding a fixed offset above your last published `versionCode`:
+
+```kt
+appVersioning {
+ versionCode.set {
+ tag.toSemVer().toVersionCode() + 1_800_000_000
+ }
+}
+```
diff --git a/gradle.properties b/gradle.properties
index 91a5ac94..f320094d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
GROUP=io.github.reactivecircus.appversioning
-VERSION_NAME=1.7.0-SNAPSHOT
+VERSION_NAME=2.0.0-SNAPSHOT
POM_ARTIFACT_ID=app-versioning-gradle-plugin
POM_NAME=Android App Versioning Gradle Plugin
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f64b48ef..b9751d7c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -4,6 +4,7 @@ agp = "9.4.1"
detekt = "2.0.0-alpha.6"
mavenPublish = "0.37.0"
burst = "2.14.0"
+testkit = "0.21"
toolchainsResolver = "1.0.0"
[libraries]
@@ -11,6 +12,7 @@ plugin-detektKtlintWrapper = { module = "dev.detekt:detekt-rules-ktlint-wrapper"
agp-build-gradle = { module = "com.android.tools.build:gradle", version.ref = "agp" }
agp-build-api = { module = "com.android.tools.build:gradle-api", version.ref = "agp" }
+burst = { module = "app.cash.burst:burst", version.ref = "burst" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
@@ -18,3 +20,4 @@ kotlin-gradle-samWithReceiver = { id = "org.jetbrains.kotlin.plugin.sam.with.rec
detekt = { id = "dev.detekt", version.ref = "detekt" }
mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" }
burst = { id = "app.cash.burst", version.ref = "burst" }
+testkit = { id = "com.autonomousapps.testkit", version.ref = "testkit" }
diff --git a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt
index fd741731..1c9895aa 100644
--- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt
+++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt
@@ -5,11 +5,11 @@ package io.github.reactivecircus.appversioning
import app.cash.burst.Burst
import io.github.reactivecircus.appversioning.fixtures.AppProjectTemplate
import io.github.reactivecircus.appversioning.fixtures.BuildScriptLanguage
+import io.github.reactivecircus.appversioning.fixtures.ConfigurationCacheEntryLog
import io.github.reactivecircus.appversioning.fixtures.LibraryProjectTemplate
+import io.github.reactivecircus.appversioning.fixtures.assertHasConfigurationCacheEntryLog
+import io.github.reactivecircus.appversioning.fixtures.initGitRepo
import io.github.reactivecircus.appversioning.fixtures.withFixtureRunner
-import io.github.reactivecircus.appversioning.internal.GitClient
-import io.github.reactivecircus.appversioning.tasks.GenerateAppVersionInfo
-import io.github.reactivecircus.appversioning.tasks.PrintAppVersionInfo
import org.gradle.testkit.runner.TaskOutcome
import org.junit.Rule
import org.junit.Test
@@ -29,11 +29,11 @@ class AppVersioningPluginIntegrationTest(
@Test
fun `plugin cannot be applied to project without Android App plugin`() {
- GitClient.initialize(fixtureDir.root)
+ initGitRepo(fixtureDir.root)
withFixtureRunner(
fixtureDir = fixtureDir,
- subprojects = listOf(LibraryProjectTemplate())
+ subprojects = listOf(LibraryProjectTemplate(buildScriptLanguage = buildScriptLanguage))
).runAndExpectFailure(
"help"
) {
@@ -48,11 +48,11 @@ class AppVersioningPluginIntegrationTest(
@Test
fun `plugin tasks are registered for Android App project without product flavors`() {
- GitClient.initialize(fixtureDir.root)
+ initGitRepo(fixtureDir.root)
withFixtureRunner(
fixtureDir = fixtureDir,
- subprojects = listOf(AppProjectTemplate())
+ subprojects = listOf(AppProjectTemplate(buildScriptLanguage = buildScriptLanguage))
).runAndCheckResult(
"tasks",
"--group=versioning"
@@ -60,22 +60,22 @@ class AppVersioningPluginIntegrationTest(
assertTrue(output.contains("Versioning tasks"))
assertTrue(
output.contains(
- "generateAppVersionInfoForDebug - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the debug variant."
+ "generateAppVersionInfoForDebug - Generates app's versionCode and versionName based on git tags for the debug variant."
)
)
assertTrue(
output.contains(
- "generateAppVersionInfoForRelease - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the release variant."
+ "generateAppVersionInfoForRelease - Generates app's versionCode and versionName based on git tags for the release variant."
)
)
assertTrue(
output.contains(
- "printAppVersionInfoForDebug - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the debug variant."
+ "printAppVersionInfoForDebug - Prints the versionCode and versionName generated by Android App Versioning plugin (if available) to the console for the debug variant."
)
)
assertTrue(
output.contains(
- "printAppVersionInfoForRelease - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} 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."
)
)
}
@@ -83,7 +83,7 @@ class AppVersioningPluginIntegrationTest(
@Test
fun `plugin tasks are registered for Android App project with product flavors`() {
- GitClient.initialize(fixtureDir.root)
+ initGitRepo(fixtureDir.root)
val flavors = listOf("mock", "prod")
withFixtureRunner(
@@ -96,42 +96,42 @@ class AppVersioningPluginIntegrationTest(
assertTrue(output.contains("Versioning tasks"))
assertTrue(
output.contains(
- "generateAppVersionInfoForMockDebug - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the mockDebug variant."
+ "generateAppVersionInfoForMockDebug - Generates app's versionCode and versionName based on git tags for the mockDebug variant."
)
)
assertTrue(
output.contains(
- "generateAppVersionInfoForProdDebug - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the prodDebug variant."
+ "generateAppVersionInfoForProdDebug - Generates app's versionCode and versionName based on git tags for the prodDebug variant."
)
)
assertTrue(
output.contains(
- "generateAppVersionInfoForMockRelease - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the mockRelease variant."
+ "generateAppVersionInfoForMockRelease - Generates app's versionCode and versionName based on git tags for the mockRelease variant."
)
)
assertTrue(
output.contains(
- "generateAppVersionInfoForProdRelease - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the prodRelease variant."
+ "generateAppVersionInfoForProdRelease - Generates app's versionCode and versionName based on git tags for the prodRelease variant."
)
)
assertTrue(
output.contains(
- "printAppVersionInfoForMockDebug - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the mockDebug variant."
+ "printAppVersionInfoForMockDebug - Prints the versionCode and versionName generated by Android App Versioning plugin (if available) to the console for the mockDebug variant."
)
)
assertTrue(
output.contains(
- "printAppVersionInfoForProdDebug - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the prodDebug variant."
+ "printAppVersionInfoForProdDebug - Prints the versionCode and versionName generated by Android App Versioning plugin (if available) to the console for the prodDebug variant."
)
)
assertTrue(
output.contains(
- "printAppVersionInfoForMockRelease - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the mockRelease variant."
+ "printAppVersionInfoForMockRelease - Prints the versionCode and versionName generated by Android App Versioning plugin (if available) to the console for the mockRelease variant."
)
)
assertTrue(
output.contains(
- "printAppVersionInfoForProdRelease - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the prodRelease variant."
+ "printAppVersionInfoForProdRelease - Prints the versionCode and versionName generated by Android App Versioning plugin (if available) to the console for the prodRelease variant."
)
)
}
@@ -139,7 +139,7 @@ class AppVersioningPluginIntegrationTest(
@Test
fun `plugin tasks are not registered for debug builds when releaseBuildOnly is enabled`() {
- GitClient.initialize(fixtureDir.root)
+ initGitRepo(fixtureDir.root)
val extension = """
appVersioning {
@@ -148,70 +148,26 @@ class AppVersioningPluginIntegrationTest(
""".trimIndent()
withFixtureRunner(
fixtureDir = fixtureDir,
- subprojects = listOf(AppProjectTemplate(pluginExtension = extension))
- ).runAndCheckResult(
- "tasks",
- "--group=versioning"
- ) {
- assertFalse(output.contains("generateAppVersionInfoForDebug"))
- assertFalse(output.contains("printAppVersionInfoForDebug"))
- }
- }
-
- @Test
- fun `plugin tasks are registered for debug builds when releaseBuildOnly is disabled`() {
- GitClient.initialize(fixtureDir.root)
-
- val extension = """
- appVersioning {
- releaseBuildOnly.set(false)
- }
- """.trimIndent()
- withFixtureRunner(
- fixtureDir = fixtureDir,
- subprojects = listOf(AppProjectTemplate(pluginExtension = extension))
- ).runAndCheckResult(
- "tasks",
- "--group=versioning"
- ) {
- assertTrue(
- output.contains(
- "generateAppVersionInfoForDebug - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the debug variant."
- )
- )
- assertTrue(
- output.contains(
- "printAppVersionInfoForDebug - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the debug variant."
+ subprojects = listOf(
+ AppProjectTemplate(
+ buildScriptLanguage = buildScriptLanguage,
+ pluginExtension = extension,
)
)
- }
- }
-
- @Test
- fun `plugin tasks are registered when plugin is enabled`() {
- GitClient.initialize(fixtureDir.root)
-
- val extension = """
- appVersioning {
- enabled.set(true)
- }
- """.trimIndent()
- withFixtureRunner(
- fixtureDir = fixtureDir,
- subprojects = listOf(AppProjectTemplate(pluginExtension = extension))
).runAndCheckResult(
"tasks",
"--group=versioning"
) {
- assertTrue(output.contains("Versioning tasks"))
+ assertFalse(output.contains("generateAppVersionInfoForDebug"))
+ assertFalse(output.contains("printAppVersionInfoForDebug"))
assertTrue(
output.contains(
- "generateAppVersionInfoForRelease - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the release variant."
+ "generateAppVersionInfoForRelease - Generates app's versionCode and versionName based on git tags for the release variant."
)
)
assertTrue(
output.contains(
- "printAppVersionInfoForRelease - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} 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."
)
)
}
@@ -219,7 +175,7 @@ class AppVersioningPluginIntegrationTest(
@Test
fun `plugin tasks are not registered when plugin is disabled`() {
- GitClient.initialize(fixtureDir.root)
+ initGitRepo(fixtureDir.root)
val extension = """
appVersioning {
@@ -228,7 +184,12 @@ class AppVersioningPluginIntegrationTest(
""".trimIndent()
withFixtureRunner(
fixtureDir = fixtureDir,
- subprojects = listOf(AppProjectTemplate(pluginExtension = extension))
+ subprojects = listOf(
+ AppProjectTemplate(
+ buildScriptLanguage = buildScriptLanguage,
+ pluginExtension = extension,
+ )
+ )
).runAndCheckResult(
"tasks",
"--group=versioning"
@@ -241,7 +202,7 @@ class AppVersioningPluginIntegrationTest(
@Test
fun `plugin generates versionCode and versionName for the assembled APK when assemble task is run`() {
- GitClient.initialize(fixtureDir.root).apply {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
@@ -260,38 +221,9 @@ class AppVersioningPluginIntegrationTest(
fixtureDir.root,
"app/build/outputs/app_versioning/release/version_name.txt"
).readText()
-
- assertEquals(TaskOutcome.SUCCESS, task(":app:generateAppVersionInfoForRelease")?.outcome)
- assertEquals(TaskOutcome.SUCCESS, task(":app:assembleRelease")?.outcome)
-
- assertTrue(output.contains("Generated app version code: 10203."))
- assertTrue(output.contains("Generated app version name: \"1.2.3\"."))
-
- assertEquals("10203", versionCodeFileContent)
- assertEquals("1.2.3", versionNameFileContent)
- }
- }
-
- @Test
- fun `plugin generates versionCode and versionName for the assembled APKs when splits-APKs is enabled`() {
- GitClient.initialize(fixtureDir.root).apply {
- val commitId = commit(message = "1st commit.")
- tag(name = "1.2.3", message = "1st tag", commitId = commitId)
- }
-
- withFixtureRunner(
- fixtureDir = fixtureDir,
- subprojects = listOf(AppProjectTemplate(buildScriptLanguage = buildScriptLanguage, splitsApks = true))
- ).runAndCheckResult(
- "assembleRelease"
- ) {
- val versionCodeFileContent = File(
+ val apkOutputMetadata = File(
fixtureDir.root,
- "app/build/outputs/app_versioning/release/version_code.txt"
- ).readText()
- val versionNameFileContent = File(
- fixtureDir.root,
- "app/build/outputs/app_versioning/release/version_name.txt"
+ "app/build/outputs/apk/release/output-metadata.json"
).readText()
assertEquals(TaskOutcome.SUCCESS, task(":app:generateAppVersionInfoForRelease")?.outcome)
@@ -302,12 +234,14 @@ class AppVersioningPluginIntegrationTest(
assertEquals("10203", versionCodeFileContent)
assertEquals("1.2.3", versionNameFileContent)
+ assertTrue(apkOutputMetadata.contains("\"versionCode\": 10203"))
+ assertTrue(apkOutputMetadata.contains("\"versionName\": \"1.2.3\""))
}
}
@Test
- fun `plugin generates versionCode and versionName for the assembled APKs when splits-APKs is enabled and universal mode is on`() {
- GitClient.initialize(fixtureDir.root).apply {
+ fun `plugin generates versionCode and versionName for all assembled APKs when splits-APKs and universal APK are enabled`() {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
@@ -332,6 +266,10 @@ class AppVersioningPluginIntegrationTest(
fixtureDir.root,
"app/build/outputs/app_versioning/release/version_name.txt"
).readText()
+ val apkOutputMetadata = File(
+ fixtureDir.root,
+ "app/build/outputs/apk/release/output-metadata.json"
+ ).readText()
assertEquals(TaskOutcome.SUCCESS, task(":app:generateAppVersionInfoForRelease")?.outcome)
assertEquals(TaskOutcome.SUCCESS, task(":app:assembleRelease")?.outcome)
@@ -341,12 +279,18 @@ class AppVersioningPluginIntegrationTest(
assertEquals("10203", versionCodeFileContent)
assertEquals("1.2.3", versionNameFileContent)
+ val expectedOutputsCount = 5
+ assertEquals(expectedOutputsCount, "\"versionCode\": 10203".toRegex().findAll(apkOutputMetadata).count())
+ assertEquals(
+ expectedOutputsCount,
+ "\"versionName\": \"1.2.3\"".toRegex().findAll(apkOutputMetadata).count()
+ )
}
}
@Test
fun `plugin (when disabled) does not generate versionCode and versionName from git tag for the assembled APK when assemble task is run`() {
- GitClient.initialize(fixtureDir.root).apply {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.0.0", message = "1st tag", commitId = commitId)
}
@@ -375,6 +319,10 @@ class AppVersioningPluginIntegrationTest(
fixtureDir.root,
"app/build/outputs/app_versioning/release/version_name.txt"
)
+ val apkOutputMetadata = File(
+ fixtureDir.root,
+ "app/build/outputs/apk/release/output-metadata.json"
+ ).readText()
assertEquals(TaskOutcome.SUCCESS, task(":app:assembleRelease")?.outcome)
@@ -383,6 +331,102 @@ class AppVersioningPluginIntegrationTest(
assertFalse(versionCodeFile.exists())
assertFalse(versionNameFile.exists())
+ assertFalse(apkOutputMetadata.contains("\"versionCode\": 10000"))
+ assertFalse(apkOutputMetadata.contains("\"versionName\": \"1.0.0\""))
+ }
+ }
+
+ @Test
+ fun `plugin is compatible with configuration cache and isolated projects in multi-project builds`() {
+ initGitRepo(fixtureDir.root) {
+ val commitId1 = commit(message = "appA release.")
+ tag(name = "1.0.0+appA", message = "1st tag", commitId = commitId1)
+ val commitId2 = commit(message = "appB release.")
+ tag(name = "2.0.0+appB", message = "2nd tag", commitId = commitId2)
+ }
+
+ val extensionAppA = if (buildScriptLanguage == BuildScriptLanguage.Kts) {
+ $$"""
+ appVersioning {
+ tagFilter.set("[0-9]*.[0-9]*.[0-9]*+appA")
+ versionCode.set {
+ tag.toSemVer().toVersionCode() + tag.commitsSinceLatestTag
+ }
+ versionName.set {
+ "appA-${tag.name}"
+ }
+ }
+ """.trimIndent()
+ } else {
+ $$"""
+ appVersioning {
+ tagFilter.set("[0-9]*.[0-9]*.[0-9]*+appA")
+ versionCode {
+ tag.toSemVer().toVersionCode() + tag.commitsSinceLatestTag
+ }
+ versionName {
+ "appA-${tag.name}"
+ }
+ }
+ """.trimIndent()
+ }
+
+ val extensionAppB = """
+ appVersioning {
+ tagFilter.set("[0-9]*.[0-9]*.[0-9]*+appB")
+ }
+ """.trimIndent()
+
+ val runner = withFixtureRunner(
+ fixtureDir = fixtureDir,
+ subprojects = listOf(
+ AppProjectTemplate(
+ projectName = "app-a",
+ buildScriptLanguage = buildScriptLanguage,
+ pluginExtension = extensionAppA,
+ ),
+ AppProjectTemplate(
+ projectName = "app-b",
+ buildScriptLanguage = buildScriptLanguage,
+ pluginExtension = extensionAppB,
+ ),
+ )
+ )
+
+ runner.runAndCheckResult(
+ "generateAppVersionInfoForRelease",
+ ) {
+ assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Stored)
+ assertEquals(TaskOutcome.SUCCESS, task(":app-a:generateAppVersionInfoForRelease")?.outcome)
+ assertEquals(TaskOutcome.SUCCESS, task(":app-b:generateAppVersionInfoForRelease")?.outcome)
+ }
+
+ runner.runAndCheckResult(
+ "generateAppVersionInfoForRelease",
+ ) {
+ assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Reused)
+ assertEquals(TaskOutcome.UP_TO_DATE, task(":app-a:generateAppVersionInfoForRelease")?.outcome)
+ assertEquals(TaskOutcome.UP_TO_DATE, task(":app-b:generateAppVersionInfoForRelease")?.outcome)
+ }
+
+ runner.runAndCheckResult(
+ "printAppVersionInfoForRelease",
+ ) {
+ assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Stored)
+ assertEquals(TaskOutcome.SUCCESS, task(":app-a:printAppVersionInfoForRelease")?.outcome)
+ assertEquals(TaskOutcome.SUCCESS, task(":app-b:printAppVersionInfoForRelease")?.outcome)
+ assertTrue(output.contains("versionCode: 10001"))
+ assertTrue(output.contains("versionName: \"appA-1.0.0+appA\""))
+ assertTrue(output.contains("versionCode: 20000"))
+ assertTrue(output.contains("versionName: \"2.0.0+appB\""))
+ }
+
+ runner.runAndCheckResult(
+ "printAppVersionInfoForRelease",
+ ) {
+ assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Reused)
+ assertEquals(TaskOutcome.SUCCESS, task(":app-a:printAppVersionInfoForRelease")?.outcome)
+ assertEquals(TaskOutcome.SUCCESS, task(":app-b:printAppVersionInfoForRelease")?.outcome)
}
}
}
diff --git a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/ConfigurationCacheCompatibility.kt b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/ConfigurationCacheCompatibility.kt
new file mode 100644
index 00000000..56d22a75
--- /dev/null
+++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/ConfigurationCacheCompatibility.kt
@@ -0,0 +1,49 @@
+package io.github.reactivecircus.appversioning.fixtures
+
+import com.android.build.api.AndroidPluginVersion
+import org.gradle.testkit.runner.BuildResult
+import kotlin.test.assertTrue
+
+fun currentAgpSupportsConfigurationCache(): Boolean {
+ return fixtureAgpVersion >= MIN_CC_COMPLIANT_AGP_VERSION
+}
+
+private val fixtureAgpVersion: AndroidPluginVersion by lazy {
+ parseAgpVersion(System.getProperty("fixtureAgpVersion"))
+}
+
+private fun parseAgpVersion(versionString: String): AndroidPluginVersion {
+ val (major, minor, micro) = versionString
+ .substringBefore('-')
+ .split('.')
+ .map { it.toInt() }
+ val baseVersion = AndroidPluginVersion(major, minor, micro)
+ val preRelease = versionString.substringAfter('-', missingDelimiterValue = "")
+ return when {
+ preRelease.isEmpty() -> baseVersion
+ preRelease.startsWith("alpha") -> baseVersion.alpha(preRelease.removePrefix("alpha").toInt())
+ preRelease.startsWith("beta") -> baseVersion.beta(preRelease.removePrefix("beta").toInt())
+ preRelease.startsWith("rc") -> baseVersion.rc(preRelease.removePrefix("rc").toInt())
+ preRelease == "dev" -> baseVersion.dev()
+ else -> error("Unsupported AGP pre-release version: $versionString")
+ }
+}
+
+private val MIN_CC_COMPLIANT_AGP_VERSION = AndroidPluginVersion(9, 2, 0)
+
+fun BuildResult.assertHasConfigurationCacheEntryLog(
+ expectedEntryLog: ConfigurationCacheEntryLog
+) {
+ if (!currentAgpSupportsConfigurationCache()) return
+ assertTrue {
+ when (expectedEntryLog) {
+ ConfigurationCacheEntryLog.Stored -> output.contains("Configuration cache entry stored.")
+ ConfigurationCacheEntryLog.Reused -> output.contains("Reusing configuration cache.")
+ }
+ }
+}
+
+enum class ConfigurationCacheEntryLog {
+ Stored,
+ Reused,
+}
diff --git a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/FixtureRunner.kt b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/FixtureRunner.kt
index e9844219..137094ef 100644
--- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/FixtureRunner.kt
+++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/FixtureRunner.kt
@@ -10,19 +10,19 @@ fun withFixtureRunner(
fixtureDir: TemporaryFolder,
subprojects: List,
parentDirectoryName: String? = null,
- dryRun: Boolean = false
+ dryRun: Boolean = false,
) = FixtureRunner(
fixtureDir = fixtureDir,
subprojects = subprojects,
parentDirectoryName = parentDirectoryName,
- dryRun = dryRun
+ dryRun = dryRun,
)
class FixtureRunner(
fixtureDir: TemporaryFolder,
subprojects: List,
parentDirectoryName: String?,
- private val dryRun: Boolean
+ private val dryRun: Boolean,
) {
private val gradleRoot: File = if (parentDirectoryName.isNullOrBlank()) {
fixtureDir.root
@@ -37,7 +37,6 @@ class FixtureRunner(
fun runAndCheckResult(vararg commands: String, action: BuildResult.() -> Unit) {
val buildResult = runner.withProjectDir(gradleRoot)
.withArguments(buildArguments(commands.toList()))
- .withPluginClasspath()
.withGradleVersion(GradleVersion.current().version)
.build()
action(buildResult)
@@ -46,7 +45,6 @@ class FixtureRunner(
fun runAndExpectFailure(vararg commands: String, action: BuildResult.() -> Unit) {
val buildResult = runner.withProjectDir(gradleRoot)
.withArguments(buildArguments(commands.toList()))
- .withPluginClasspath()
.withGradleVersion(GradleVersion.current().version)
.buildAndFail()
action(buildResult)
@@ -63,7 +61,6 @@ class FixtureRunner(
}
private val runner = GradleRunner.create()
- .withPluginClasspath()
.withDebug(true)
private fun TemporaryFolder.buildFixture(gradleRoot: File, subprojects: List) {
@@ -71,14 +68,14 @@ private fun TemporaryFolder.buildFixture(gradleRoot: File, subprojects: List
diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/internal/GitClient.kt b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/GitRepo.kt
similarity index 52%
rename from src/main/kotlin/io/github/reactivecircus/appversioning/internal/GitClient.kt
rename to src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/GitRepo.kt
index a624f112..29e1bc27 100644
--- a/src/main/kotlin/io/github/reactivecircus/appversioning/internal/GitClient.kt
+++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/GitRepo.kt
@@ -1,29 +1,15 @@
-package io.github.reactivecircus.appversioning.internal
+package io.github.reactivecircus.appversioning.fixtures
import java.io.File
import java.util.concurrent.TimeUnit
-class GitClient private constructor(private val projectDir: File) {
- fun listLocalTags(): List {
- return listOf("git", "tag", "--list").execute(projectDir).lines()
- }
-
- fun fetchRemoteTags() {
- listOf("git", "fetch", "--tags").execute(projectDir)
- }
+fun initGitRepo(rootDir: File, action: GitRepo.() -> Unit = {}): GitRepo {
+ return GitRepo(rootDir).apply(action)
+}
- fun describeLatestTag(pattern: String? = null): String? {
- val command = buildList {
- add("git")
- add("describe")
- if (pattern != null) {
- add("--match")
- add(pattern)
- }
- add("--tags")
- add("--long")
- }
- return runCatching { command.execute(projectDir) }.getOrNull()
+class GitRepo(private val rootDir: File) {
+ init {
+ listOf("git", "init").execute(rootDir)
}
fun commit(message: String, allowEmpty: Boolean = true): CommitId {
@@ -36,10 +22,10 @@ class GitClient private constructor(private val projectDir: File) {
add("-m")
add(message)
}
- commitCommand.execute(projectDir)
+ commitCommand.execute(rootDir)
val getCommitIdCommand = listOf("git", "rev-parse", "--short", "HEAD")
- return CommitId(getCommitIdCommand.execute(projectDir))
+ return CommitId(getCommitIdCommand.execute(rootDir))
}
fun tag(name: String, message: String, commitId: CommitId? = null) {
@@ -54,7 +40,7 @@ class GitClient private constructor(private val projectDir: File) {
add("-m")
add(message)
}
- command.execute(projectDir)
+ command.execute(rootDir)
}
fun checkoutTag(tag: String) {
@@ -63,32 +49,13 @@ class GitClient private constructor(private val projectDir: File) {
add("checkout")
add(tag)
}
- commitCommand.execute(projectDir)
- }
-
- companion object {
- fun initialize(projectDir: File): GitClient {
- return GitClient(projectDir).apply {
- listOf("git", "init").execute(projectDir)
- }
- }
-
- fun open(projectDir: File): GitClient {
- require(projectDir.isInValidGitRepo)
- return GitClient(projectDir)
- }
+ commitCommand.execute(rootDir)
}
}
@JvmInline
value class CommitId(val value: String)
-private val File.isInValidGitRepo: Boolean
- get() = runCatching {
- listOf("git", "rev-parse", "--is-inside-work-tree").execute(this).toBoolean()
- }.getOrDefault(false)
-
-@Suppress("UseCheckOrError")
private fun List.execute(workingDir: File, timeoutInSeconds: Long = DEFAULT_COMMAND_TIMEOUT_SECONDS): String {
val process = ProcessBuilder(this)
.directory(workingDir)
diff --git a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/ProjectTemplates.kt b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/ProjectTemplates.kt
index 643822c4..3a10b773 100644
--- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/ProjectTemplates.kt
+++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/ProjectTemplates.kt
@@ -1,17 +1,30 @@
package io.github.reactivecircus.appversioning.fixtures
+import org.gradle.testkit.runner.internal.PluginUnderTestMetadataReading
+
+private val pluginClasspath: String by lazy {
+ PluginUnderTestMetadataReading.readImplementationClasspath()
+ .joinToString(",\n") { "\"${it.invariantSeparatorsPath}\"" }
+}
+
fun settingsFileContent(
localBuildCacheUri: String,
subprojects: List
) = """
- dependencyResolutionManagement {
+ pluginManagement {
repositories {
mavenCentral()
google()
}
}
- pluginManagement {
+ buildscript {
+ dependencies {
+ classpath(files($pluginClasspath))
+ }
+ }
+
+ dependencyResolutionManagement {
repositories {
mavenCentral()
google()
@@ -30,15 +43,12 @@ fun settingsFileContent(
""".trimIndent()
fun gradlePropertiesFileContent(enableConfigurationCache: Boolean): String {
- val configurationCacheProperties = if (enableConfigurationCache) {
- """
- org.gradle.isolated-projects=true
- """.trimIndent()
- } else {
- ""
- }
return """
- $configurationCacheProperties
+ org.gradle.parallel=true
+ org.gradle.configureondemand=true
+ org.gradle.caching=true
+ org.gradle.configuration-cache=$enableConfigurationCache
+ org.gradle.isolated-projects=$enableConfigurationCache
""".trimIndent()
}
diff --git a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt
index fc3f0320..f94045a1 100644
--- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt
+++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt
@@ -4,8 +4,10 @@ package io.github.reactivecircus.appversioning.tasks
import io.github.reactivecircus.appversioning.fixtures.AppProjectTemplate
import io.github.reactivecircus.appversioning.fixtures.BuildScriptLanguage
+import io.github.reactivecircus.appversioning.fixtures.ConfigurationCacheEntryLog
+import io.github.reactivecircus.appversioning.fixtures.assertHasConfigurationCacheEntryLog
+import io.github.reactivecircus.appversioning.fixtures.initGitRepo
import io.github.reactivecircus.appversioning.fixtures.withFixtureRunner
-import io.github.reactivecircus.appversioning.internal.GitClient
import org.gradle.testkit.runner.TaskOutcome
import org.junit.Rule
import org.junit.Test
@@ -59,9 +61,33 @@ class GenerateAppVersionInfoTest {
}
}
+ @Test
+ fun `GenerateAppVersionInfo fails when bareGitRepoDirectory provided is not a bare git repository`() {
+ val extensions = """
+ appVersioning {
+ bareGitRepoDirectory.set(rootDir.resolve("../"))
+ }
+ """.trimIndent()
+
+ withFixtureRunner(
+ fixtureDir = fixtureDir,
+ subprojects = listOf(AppProjectTemplate(pluginExtension = extensions)),
+ parentDirectoryName = "android"
+ ).runAndExpectFailure(
+ "generateAppVersionInfoForRelease"
+ ) {
+ assertEquals(TaskOutcome.FAILED, task(":app:generateAppVersionInfoForRelease")?.outcome)
+ assertTrue(
+ output.contains(
+ "Android App Versioning Gradle Plugin works with git tags but root project 'app-versioning-fixture' is not a git root directory, and a valid gitRootDirectory is not provided."
+ )
+ )
+ }
+ }
+
@Test
fun `GenerateAppVersionInfo generates versionCode and versionName when root Gradle project is not a git root directory but gitRootDirectory provided is a git root directory`() {
- GitClient.initialize(fixtureDir.root).apply {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
@@ -88,63 +114,88 @@ class GenerateAppVersionInfoTest {
}
@Test
- fun `GenerateAppVersionInfo generates versionCode by converting latest SemVer-compliant git tag to a single integer using positional notation when no custom versionCode generation rule is provided`() {
- val gitClient = GitClient.initialize(fixtureDir.root).apply {
+ fun `GenerateAppVersionInfo generates versionCode and versionName when bareGitRepoDirectory is provided`() {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
- tag(name = "0.0.1", message = "1st tag", commitId = commitId)
+ tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
val versionCodeFile = File(fixtureDir.root, "app/build/outputs/app_versioning/release/version_code.txt")
+ val versionNameFile = File(fixtureDir.root, "app/build/outputs/app_versioning/release/version_name.txt")
- val runner = withFixtureRunner(
- fixtureDir = fixtureDir,
- subprojects = listOf(AppProjectTemplate())
- )
+ val extensions = """
+ appVersioning {
+ gitRootDirectory.set(rootDir.resolve("non-existent"))
+ bareGitRepoDirectory.set(rootDir.resolve(".git"))
+ }
+ """.trimIndent()
- runner.runAndCheckResult(
+ withFixtureRunner(
+ fixtureDir = fixtureDir,
+ subprojects = listOf(AppProjectTemplate(pluginExtension = extensions))
+ ).runAndCheckResult(
"generateAppVersionInfoForRelease"
) {
- assertEquals("1", versionCodeFile.readText())
+ assertEquals(TaskOutcome.SUCCESS, task(":app:generateAppVersionInfoForRelease")?.outcome)
+ assertEquals("10203", versionCodeFile.readText())
+ assertEquals("1.2.3", versionNameFile.readText())
}
+ }
- val commitId2 = gitClient.commit(message = "2nd commit.")
- gitClient.tag(name = "0.1.0", message = "2nd tag", commitId = commitId2)
+ @Test
+ fun `GenerateAppVersionInfo fetches remote tags when fetchTagsWhenNoneExistsLocally is enabled and no local tags exist`() {
+ val remoteFolder = TemporaryFolder().apply { create() }
+ try {
+ val remoteGitRepo = initGitRepo(remoteFolder.root)
+ val commitId = remoteGitRepo.commit(message = "1st commit.")
- runner.runAndCheckResult(
- "generateAppVersionInfoForRelease"
- ) {
- assertEquals("100", versionCodeFile.readText())
- }
+ ProcessBuilder("git", "clone", "--no-tags", remoteFolder.root.absolutePath, fixtureDir.root.absolutePath)
+ .start()
+ .waitFor()
- val commitId3 = gitClient.commit(message = "3rd commit.")
- gitClient.tag(name = "1.0.0", message = "3rd tag", commitId = commitId3)
+ remoteGitRepo.tag(name = "1.2.3", message = "remote tag", commitId = commitId)
- runner.runAndCheckResult(
- "generateAppVersionInfoForRelease"
- ) {
- assertEquals("10000", versionCodeFile.readText())
+ val extension = """
+ appVersioning {
+ fetchTagsWhenNoneExistsLocally.set(true)
+ }
+ """.trimIndent()
+
+ withFixtureRunner(
+ fixtureDir = fixtureDir,
+ subprojects = listOf(AppProjectTemplate(pluginExtension = extension))
+ ).runAndCheckResult(
+ "generateAppVersionInfoForRelease"
+ ) {
+ assertEquals(TaskOutcome.SUCCESS, task(":app:generateAppVersionInfoForRelease")?.outcome)
+ assertTrue(output.contains("No git tags found. Fetching tags from remote."))
+ assertEquals(
+ "10203",
+ File(fixtureDir.root, "app/build/outputs/app_versioning/release/version_code.txt").readText()
+ )
+ assertEquals(
+ "1.2.3",
+ File(fixtureDir.root, "app/build/outputs/app_versioning/release/version_name.txt").readText()
+ )
+ }
+ } finally {
+ remoteFolder.delete()
}
+ }
- val commitId4 = gitClient.commit(message = "4th commit.")
- gitClient.tag(name = "1.0.99", message = "4th tag", commitId = commitId4)
-
- runner.runAndCheckResult(
- "generateAppVersionInfoForRelease"
- ) {
- assertEquals("10099", versionCodeFile.readText())
+ @Test
+ fun `GenerateAppVersionInfo generates versionCode by converting latest SemVer-compliant git tag to a single integer using positional notation when no custom versionCode spec is provided`() {
+ val gitRepo = initGitRepo(fixtureDir.root) {
+ val commitId = commit(message = "1st commit.")
+ tag(name = "1.2.3-alpha01+build.567", message = "1st tag", commitId = commitId)
}
- val commitId5 = gitClient.commit(message = "5th commit.")
- gitClient.tag(name = "1.99.99", message = "5th tag", commitId = commitId5)
-
- runner.runAndCheckResult(
- "generateAppVersionInfoForRelease"
- ) {
- assertEquals("19999", versionCodeFile.readText())
- }
+ val versionCodeFile = File(fixtureDir.root, "app/build/outputs/app_versioning/release/version_code.txt")
- val commitId6 = gitClient.commit(message = "6th commit.")
- gitClient.tag(name = "1.2.3-alpha01+build.567", message = "6th tag", commitId = commitId6)
+ val runner = withFixtureRunner(
+ fixtureDir = fixtureDir,
+ subprojects = listOf(AppProjectTemplate())
+ )
runner.runAndCheckResult(
"generateAppVersionInfoForRelease"
@@ -152,8 +203,8 @@ class GenerateAppVersionInfoTest {
assertEquals("10203", versionCodeFile.readText())
}
- val commitId7 = gitClient.commit(message = "7th commit.")
- gitClient.tag(name = "v3.0.0-SNAPSHOT", message = "7th tag", commitId = commitId7)
+ val commitId2 = gitRepo.commit(message = "2nd commit.")
+ gitRepo.tag(name = "v3.0.0-SNAPSHOT", message = "2nd tag", commitId = commitId2)
runner.runAndCheckResult(
"generateAppVersionInfoForRelease"
@@ -163,8 +214,8 @@ class GenerateAppVersionInfoTest {
}
@Test
- fun `GenerateAppVersionInfo fails when latest git tag is not SemVer-compliant and no custom versionCode generation rule is provided`() {
- GitClient.initialize(fixtureDir.root).apply {
+ fun `GenerateAppVersionInfo fails when latest git tag is not SemVer-compliant and no custom versionCode spec is provided`() {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2", message = "1st tag", commitId = commitId)
}
@@ -180,7 +231,7 @@ class GenerateAppVersionInfoTest {
output.contains(
"""
Could not generate versionCode as "1.2" does not follow semantic versioning.
- Please either ensure latest git tag follows semantic versioning, or provide a custom rule for generating versionCode using the `overrideVersionCode` lambda.
+ Please either ensure latest git tag follows semantic versioning, or provide a custom spec for generating versionCode using the `versionCode` property.
""".trimIndent()
)
)
@@ -188,8 +239,8 @@ class GenerateAppVersionInfoTest {
}
@Test
- fun `GenerateAppVersionInfo fails when SemVer-compliant git tag has a component that exceeds the maximum digits allocated (2) and no custom versionCode generation rule is provided`() {
- GitClient.initialize(fixtureDir.root).apply {
+ fun `GenerateAppVersionInfo fails when SemVer-compliant git tag has a component that exceeds the maximum digits allocated (2) and no custom versionCode spec is provided`() {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.100", message = "1st tag", commitId = commitId)
}
@@ -207,7 +258,7 @@ class GenerateAppVersionInfoTest {
Could not generate versionCode from "1.2.100" as the SemVer cannot be represented as an Integer.
This is usually because MAJOR or MINOR version is greater than 99, as by default maximum of 2 digits is allowed for MINOR and PATCH components of a SemVer tag.
Another reason might be that the overall positional notation of the SemVer (MAJOR * 10000 + MINOR * 100 + PATCH) is greater than the maximum value of an integer (2147483647).
- As a workaround you can provide a custom rule for generating versionCode using the `overrideVersionCode` lambda.
+ As a workaround you can provide a custom spec for generating versionCode using the `versionCode` property.
""".trimIndent()
)
)
@@ -216,7 +267,7 @@ class GenerateAppVersionInfoTest {
@Test
fun `GenerateAppVersionInfo generates fallback versionCode and versionName when no git tags exist`() {
- GitClient.initialize(fixtureDir.root).apply {
+ initGitRepo(fixtureDir.root) {
commit(message = "1st commit.")
}
@@ -233,7 +284,7 @@ class GenerateAppVersionInfoTest {
assertTrue(
output.contains(
"""
- No git tags found. Falling back to version code ${GenerateAppVersionInfo.VERSION_CODE_FALLBACK} and version name "${GenerateAppVersionInfo.VERSION_NAME_FALLBACK}".
+ No git tags found. Falling back to version code 0 and version name "".
If you want to fallback to the versionCode and versionName set via the DSL or manifest, or stop generating versionCode and versionName from Git tags:
appVersioning {
enabled.set(false)
@@ -241,14 +292,14 @@ class GenerateAppVersionInfoTest {
""".trimIndent()
)
)
- assertEquals(GenerateAppVersionInfo.VERSION_CODE_FALLBACK.toString(), versionCodeFile.readText())
- assertEquals(GenerateAppVersionInfo.VERSION_NAME_FALLBACK, versionNameFile.readText())
+ assertEquals("0", versionCodeFile.readText())
+ assertEquals("", versionNameFile.readText())
}
}
@Test
- fun `GenerateAppVersionInfo generates custom versionCode when custom versionCode generation rule is provided`() {
- GitClient.initialize(fixtureDir.root).apply {
+ fun `GenerateAppVersionInfo generates custom versionCode when custom versionCode spec is provided`() {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
@@ -256,15 +307,16 @@ class GenerateAppVersionInfoTest {
val versionCodeFile = File(fixtureDir.root, "app/build/outputs/app_versioning/release/version_code.txt")
val extensions = """
- import io.github.reactivecircus.appversioning.toSemVer
+ import io.github.reactivecircus.appversioning.VersionCodeSpec
+ val buildNumber = providers.gradleProperty("buildNumber").map(String::toInt).orElse(0)
appVersioning {
- overrideVersionCode { gitTag, providers, _ ->
- val buildNumber = providers
- .gradleProperty("buildNumber")
- .orNull?.toInt()?: 0
- val semVer = gitTag.toSemVer()
- semVer.major * 1000000 + semVer.minor * 10000 + semVer.patch * 100 + buildNumber
- }
+ versionCode.set(
+ buildNumber.map { buildNum ->
+ VersionCodeSpec {
+ tag.toSemVer().toVersionCode() * 100 + buildNum
+ }
+ }
+ )
}
""".trimIndent()
@@ -280,18 +332,16 @@ class GenerateAppVersionInfoTest {
}
@Test
- fun `GenerateAppVersionInfo fails when converting a non-SemVer compliant git tag to SemVer in a custom versionCode generation rule`() {
- GitClient.initialize(fixtureDir.root).apply {
+ fun `GenerateAppVersionInfo fails when converting a non-SemVer compliant git tag to SemVer in a custom versionCode spec`() {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2", message = "1st tag", commitId = commitId)
}
val extensions = """
- import io.github.reactivecircus.appversioning.toSemVer
appVersioning {
- overrideVersionCode { gitTag, providers, _ ->
- val semVer = gitTag.toSemVer()
- semVer.major * 1000000 + semVer.minor * 10000 + semVer.patch * 100
+ versionCode.set {
+ tag.toSemVer().toVersionCode() * 100
}
}
""".trimIndent()
@@ -303,13 +353,13 @@ class GenerateAppVersionInfoTest {
"generateAppVersionInfoForRelease"
) {
assertEquals(TaskOutcome.FAILED, task(":app:generateAppVersionInfoForRelease")?.outcome)
- assertTrue(output.contains("\"1.2\" is not a valid SemVer."))
+ assertTrue(output.contains("\"1.2\" is not a valid SemVer (https://semver.org/#semantic-versioning-200)."))
}
}
@Test
- fun `GenerateAppVersionInfo generates versionName directly from the latest git tag when no custom versionName generation rule is provided`() {
- val gitClient = GitClient.initialize(fixtureDir.root).apply {
+ fun `GenerateAppVersionInfo generates versionName directly from the latest git tag when no custom versionName spec is provided`() {
+ val gitRepo = initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "0.0.1", message = "1st tag", commitId = commitId)
}
@@ -318,7 +368,7 @@ class GenerateAppVersionInfoTest {
val extensions = """
appVersioning {
- overrideVersionCode { _, _, _ -> 0 }
+ versionCode.set { 0 }
}
""".trimIndent()
@@ -333,8 +383,8 @@ class GenerateAppVersionInfoTest {
assertEquals("0.0.1", versionNameFile.readText())
}
- val commitId2 = gitClient.commit(message = "2nd commit.")
- gitClient.tag(name = "v1.1.0-alpha01", message = "2nd tag", commitId = commitId2)
+ val commitId2 = gitRepo.commit(message = "2nd commit.")
+ gitRepo.tag(name = "v1.1.0-alpha01", message = "2nd tag", commitId = commitId2)
runner.runAndCheckResult(
"generateAppVersionInfoForRelease"
@@ -342,8 +392,8 @@ class GenerateAppVersionInfoTest {
assertEquals("v1.1.0-alpha01", versionNameFile.readText())
}
- val commitId3 = gitClient.commit(message = "3rd commit.")
- gitClient.tag(name = "alpha", message = "3rd tag", commitId = commitId3)
+ val commitId3 = gitRepo.commit(message = "3rd commit.")
+ gitRepo.tag(name = "alpha", message = "3rd tag", commitId = commitId3)
runner.runAndCheckResult(
"generateAppVersionInfoForRelease"
@@ -353,18 +403,18 @@ class GenerateAppVersionInfoTest {
}
@Test
- fun `GenerateAppVersionInfo generates custom versionName when custom versionName generation rule is provided`() {
- GitClient.initialize(fixtureDir.root).apply {
+ fun `GenerateAppVersionInfo generates custom versionName when custom versionName spec is provided`() {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
val versionNameFile = File(fixtureDir.root, "app/build/outputs/app_versioning/release/version_name.txt")
- val extensions = """
+ val extensions = $$"""
appVersioning {
- overrideVersionName { gitTag, _, _ ->
- "Version " + gitTag.toString()
+ versionName.set {
+ "Version $tag"
}
}
""".trimIndent()
@@ -381,7 +431,7 @@ class GenerateAppVersionInfoTest {
@Test
fun `GenerateAppVersionInfo can generate custom versionCode based on build variant`() {
- GitClient.initialize(fixtureDir.root).apply {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
@@ -404,12 +454,10 @@ class GenerateAppVersionInfoTest {
)
val extensions = """
- import io.github.reactivecircus.appversioning.toSemVer
appVersioning {
- overrideVersionCode { gitTag, _, variantInfo ->
- val offset = if (variantInfo.flavorName == "paid") 1 else 0
- val semVer = gitTag.toSemVer()
- semVer.major * 1000000 + semVer.minor * 10000 + semVer.patch * 100 + offset
+ versionCode.set {
+ val offset = if (variant.flavorName == "paid") 1 else 0
+ tag.toSemVer().toVersionCode() * 100 + offset
}
}
""".trimIndent()
@@ -447,7 +495,7 @@ class GenerateAppVersionInfoTest {
@Test
fun `GenerateAppVersionInfo can generate custom versionName based on build variant`() {
- GitClient.initialize(fixtureDir.root).apply {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
@@ -469,44 +517,40 @@ class GenerateAppVersionInfoTest {
"app/build/outputs/app_versioning/paidRelease/version_name.txt"
)
- val extensions2 = """
+ val extensions = $$"""
appVersioning {
- overrideVersionName { gitTag, _, variantInfo ->
- val suffix = if (!variantInfo.isReleaseBuild) {
- " (" + variantInfo.variantName + ")"
- } else {
- ""
- }
- "Version " + gitTag.toString() + suffix
+ versionName.set {
+ val suffix = if (!variant.isReleaseBuild) " (${variant.variantName})" else ""
+ "Version $tag$suffix"
}
}
""".trimIndent()
- val flavors2 = listOf("free", "paid")
- val runner2 = withFixtureRunner(
+ val flavors = listOf("free", "paid")
+ val runner = withFixtureRunner(
fixtureDir = fixtureDir,
- subprojects = listOf(AppProjectTemplate(pluginExtension = extensions2, flavors = flavors2))
+ subprojects = listOf(AppProjectTemplate(pluginExtension = extensions, flavors = flavors))
)
- runner2.runAndCheckResult(
+ runner.runAndCheckResult(
"generateAppVersionInfoForFreeDebug"
) {
assertEquals("Version 1.2.3 (freeDebug)", versionNameFileForFreeDebug.readText())
}
- runner2.runAndCheckResult(
+ runner.runAndCheckResult(
"generateAppVersionInfoForFreeRelease"
) {
assertEquals("Version 1.2.3", versionNameFileForFreeRelease.readText())
}
- runner2.runAndCheckResult(
+ runner.runAndCheckResult(
"generateAppVersionInfoForPaidDebug"
) {
assertEquals("Version 1.2.3 (paidDebug)", versionNameFileForPaidDebug.readText())
}
- runner2.runAndCheckResult(
+ runner.runAndCheckResult(
"generateAppVersionInfoForPaidRelease"
) {
assertEquals("Version 1.2.3", versionNameFileForPaidRelease.readText())
@@ -515,7 +559,7 @@ class GenerateAppVersionInfoTest {
@Test
fun `GenerateAppVersionInfo generates versionCode and versionName from the latest git tag matching the provided tagFilter pattern when a tagFilter pattern is provided`() {
- val gitClient = GitClient.initialize(fixtureDir.root).apply {
+ val gitRepo = initGitRepo(fixtureDir.root) {
commit(message = "Initial commit.")
}
@@ -567,8 +611,8 @@ class GenerateAppVersionInfoTest {
)
// 1st appA release
- val commitId = gitClient.commit(message = "appA release.")
- gitClient.tag(name = "0.1.0+appA", message = "1st tag", commitId = commitId)
+ val commitId = gitRepo.commit(message = "appA release.")
+ gitRepo.tag(name = "0.1.0+appA", message = "1st tag", commitId = commitId)
runner.runAndCheckResult(
"generateAppVersionInfoForRelease"
@@ -578,20 +622,20 @@ class GenerateAppVersionInfoTest {
assertEquals("0.1.0+appA", versionNameFileAppA.readText())
assertEquals(TaskOutcome.SUCCESS, task(":app-b:generateAppVersionInfoForRelease")?.outcome)
- assertEquals(GenerateAppVersionInfo.VERSION_CODE_FALLBACK.toString(), versionCodeFileAppB.readText())
- assertEquals(GenerateAppVersionInfo.VERSION_NAME_FALLBACK, versionNameFileAppB.readText())
+ assertEquals("0", versionCodeFileAppB.readText())
+ assertEquals("", versionNameFileAppB.readText())
assertEquals(TaskOutcome.SUCCESS, task(":app-c:generateAppVersionInfoForRelease")?.outcome)
- assertEquals(GenerateAppVersionInfo.VERSION_CODE_FALLBACK.toString(), versionCodeFileAppC.readText())
- assertEquals(GenerateAppVersionInfo.VERSION_NAME_FALLBACK, versionNameFileAppC.readText())
+ assertEquals("0", versionCodeFileAppC.readText())
+ assertEquals("", versionNameFileAppC.readText())
}
- gitClient.commit(message = "Commit a.")
- gitClient.commit(message = "Commit b.")
+ gitRepo.commit(message = "Commit a.")
+ gitRepo.commit(message = "Commit b.")
// 1st appB release
- val commitId2 = gitClient.commit(message = "appB release.")
- gitClient.tag(name = "1.2.3-rc01+appB", message = "2nd tag", commitId = commitId2)
+ val commitId2 = gitRepo.commit(message = "appB release.")
+ gitRepo.tag(name = "1.2.3-rc01+appB", message = "2nd tag", commitId = commitId2)
runner.runAndCheckResult(
"generateAppVersionInfoForRelease"
@@ -605,15 +649,15 @@ class GenerateAppVersionInfoTest {
assertEquals("1.2.3-rc01+appB", versionNameFileAppB.readText())
assertEquals(TaskOutcome.SUCCESS, task(":app-c:generateAppVersionInfoForRelease")?.outcome)
- assertEquals(GenerateAppVersionInfo.VERSION_CODE_FALLBACK.toString(), versionCodeFileAppC.readText())
- assertEquals(GenerateAppVersionInfo.VERSION_NAME_FALLBACK, versionNameFileAppC.readText())
+ assertEquals("0", versionCodeFileAppC.readText())
+ assertEquals("", versionNameFileAppC.readText())
}
- gitClient.commit(message = "Commit c.")
+ gitRepo.commit(message = "Commit c.")
// 2nd appB release
- val commitId3 = gitClient.commit(message = "appB release.")
- gitClient.tag(name = "1.2.3-rc02+appB", message = "3rd tag", commitId = commitId3)
+ val commitId3 = gitRepo.commit(message = "appB release.")
+ gitRepo.tag(name = "1.2.3-rc02+appB", message = "3rd tag", commitId = commitId3)
runner.runAndCheckResult(
"generateAppVersionInfoForRelease"
@@ -627,13 +671,13 @@ class GenerateAppVersionInfoTest {
assertEquals("1.2.3-rc02+appB", versionNameFileAppB.readText())
assertEquals(TaskOutcome.SUCCESS, task(":app-c:generateAppVersionInfoForRelease")?.outcome)
- assertEquals(GenerateAppVersionInfo.VERSION_CODE_FALLBACK.toString(), versionCodeFileAppC.readText())
- assertEquals(GenerateAppVersionInfo.VERSION_NAME_FALLBACK, versionNameFileAppC.readText())
+ assertEquals("0", versionCodeFileAppC.readText())
+ assertEquals("", versionNameFileAppC.readText())
}
// 1st appC release
- val commitId4 = gitClient.commit(message = "appC release.")
- gitClient.tag(name = "10.3.5-alpha03+appC", message = "4th tag", commitId = commitId4)
+ val commitId4 = gitRepo.commit(message = "appC release.")
+ gitRepo.tag(name = "10.3.5-alpha03+appC", message = "4th tag", commitId = commitId4)
runner.runAndCheckResult(
"generateAppVersionInfoForRelease"
@@ -651,11 +695,11 @@ class GenerateAppVersionInfoTest {
assertEquals("10.3.5-alpha03+appC", versionNameFileAppC.readText())
}
- gitClient.commit(message = "Commit d.")
+ gitRepo.commit(message = "Commit d.")
// 2nd appA release
- val commitId5 = gitClient.commit(message = "appA release.")
- gitClient.tag(name = "0.2.1+appA", message = "4th tag", commitId = commitId5)
+ val commitId5 = gitRepo.commit(message = "appA release.")
+ gitRepo.tag(name = "0.2.1+appA", message = "4th tag", commitId = commitId5)
runner.runAndCheckResult(
"generateAppVersionInfoForRelease"
@@ -675,8 +719,8 @@ class GenerateAppVersionInfoTest {
}
@Test
- fun `overrideVersionCode and overrideVersionName configurations work in groovy`() {
- GitClient.initialize(fixtureDir.root).apply {
+ fun `versionCode and versionName configurations work in groovy`() {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
commit(message = "2nd commit.")
@@ -693,21 +737,14 @@ class GenerateAppVersionInfoTest {
"app/build/outputs/app_versioning/release/version_name.txt"
)
- val extensions = """
- import io.github.reactivecircus.appversioning.SemVer
+ val extensions = $$"""
appVersioning {
- overrideVersionCode { gitTag, providers, variantInfo ->
- def semVer = SemVer.fromGitTag(gitTag)
- semVer.major * 10000 + semVer.minor * 100 + semVer.patch + gitTag.commitsSinceLatestTag
+ versionCode {
+ tag.toSemVer().toVersionCode() + tag.commitsSinceLatestTag
}
- overrideVersionName { gitTag, providers, variantInfo ->
- def suffix
- if (variantInfo.debugBuild == true) {
- suffix = " (" + variantInfo.variantName + ")"
- } else {
- suffix = ""
- }
- "Version " + gitTag.toString() + suffix
+ versionName {
+ def suffix = variant.debugBuild ? " (${variant.variantName})" : ""
+ "Version ${tag.name}${suffix}"
}
}
""".trimIndent()
@@ -740,8 +777,62 @@ class GenerateAppVersionInfoTest {
}
@Test
- fun `GenerateAppVersionInfo is incremental without custom versionCode and versionName generation rules`() {
- GitClient.initialize(fixtureDir.root).apply {
+ fun `versionCode and versionName configurations with external Provider work in groovy`() {
+ initGitRepo(fixtureDir.root) {
+ val commitId = commit(message = "1st commit.")
+ tag(name = "1.2.3", message = "1st tag", commitId = commitId)
+ commit(message = "2nd commit.")
+ }
+
+ val versionCodeFileForRelease = File(
+ fixtureDir.root,
+ "app/build/outputs/app_versioning/release/version_code.txt"
+ )
+ val versionNameFileForRelease = File(
+ fixtureDir.root,
+ "app/build/outputs/app_versioning/release/version_name.txt"
+ )
+
+ val extensions = $$"""
+ import io.github.reactivecircus.appversioning.VersionCodeSpec
+
+ def buildNumber = providers.gradleProperty("buildNumber").map { it.toInteger() }.orElse(0)
+
+ appVersioning {
+ versionCode.set(
+ buildNumber.map { buildNum ->
+ { tag.toSemVer().toVersionCode() * 100 + buildNum } as VersionCodeSpec
+ }
+ )
+ versionName {
+ "Version ${tag.name}"
+ }
+ }
+ """.trimIndent()
+
+ val runner = withFixtureRunner(
+ fixtureDir = fixtureDir,
+ subprojects = listOf(
+ AppProjectTemplate(
+ pluginExtension = extensions,
+ buildScriptLanguage = BuildScriptLanguage.Groovy,
+ )
+ )
+ )
+
+ runner.runAndCheckResult(
+ "generateAppVersionInfoForRelease",
+ "-PbuildNumber=42"
+ ) {
+ assertEquals(TaskOutcome.SUCCESS, task(":app:generateAppVersionInfoForRelease")?.outcome)
+ assertEquals("1020342", versionCodeFileForRelease.readText())
+ assertEquals("Version 1.2.3", versionNameFileForRelease.readText())
+ }
+ }
+
+ @Test
+ fun `GenerateAppVersionInfo is incremental without custom versionCode and versionName specs`() {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
@@ -754,33 +845,33 @@ class GenerateAppVersionInfoTest {
runner.runAndCheckResult(
"generateAppVersionInfoForRelease"
) {
+ assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Stored)
assertEquals(TaskOutcome.SUCCESS, task(":app:generateAppVersionInfoForRelease")?.outcome)
}
runner.runAndCheckResult(
"generateAppVersionInfoForRelease"
) {
+ assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Reused)
assertEquals(TaskOutcome.UP_TO_DATE, task(":app:generateAppVersionInfoForRelease")?.outcome)
}
}
@Test
- fun `GenerateAppVersionInfo is incremental with custom versionCode and versionName generation rules`() {
- GitClient.initialize(fixtureDir.root).apply {
+ fun `GenerateAppVersionInfo is incremental with custom versionCode and versionName specs`() {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
commit(message = "2nd commit.")
}
- val extensions = """
- import io.github.reactivecircus.appversioning.toSemVer
+ val extensions = $$"""
appVersioning {
- overrideVersionCode { gitTag, _, _ ->
- val semVer = gitTag.toSemVer()
- semVer.major * 10000 + semVer.minor * 100 + semVer.patch + gitTag.commitsSinceLatestTag
+ versionCode.set {
+ tag.toSemVer().toVersionCode() + tag.commitsSinceLatestTag
}
- overrideVersionName { gitTag, _, _ ->
- "Version " + gitTag.toString()
+ versionName.set {
+ "Version $tag"
}
}
""".trimIndent()
@@ -793,19 +884,21 @@ class GenerateAppVersionInfoTest {
runner.runAndCheckResult(
"generateAppVersionInfoForRelease"
) {
+ assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Stored)
assertEquals(TaskOutcome.SUCCESS, task(":app:generateAppVersionInfoForRelease")?.outcome)
}
runner.runAndCheckResult(
"generateAppVersionInfoForRelease"
) {
+ assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Reused)
assertEquals(TaskOutcome.UP_TO_DATE, task(":app:generateAppVersionInfoForRelease")?.outcome)
}
}
@Test
- fun `GenerateAppVersionInfo is cacheable without custom versionCode and versionName generation rules`() {
- GitClient.initialize(fixtureDir.root).apply {
+ fun `GenerateAppVersionInfo is cacheable without custom versionCode and versionName specs`() {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
@@ -833,22 +926,20 @@ class GenerateAppVersionInfoTest {
}
@Test
- fun `GenerateAppVersionInfo is cacheable with custom versionCode and versionName generation rules`() {
- GitClient.initialize(fixtureDir.root).apply {
+ fun `GenerateAppVersionInfo is cacheable with custom versionCode and versionName specs`() {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
commit(message = "2nd commit.")
}
- val extensions = """
- import io.github.reactivecircus.appversioning.toSemVer
+ val extensions = $$"""
appVersioning {
- overrideVersionCode { gitTag, _, _ ->
- val semVer = gitTag.toSemVer()
- semVer.major * 10000 + semVer.minor * 100 + semVer.patch + gitTag.commitsSinceLatestTag
+ versionCode.set {
+ tag.toSemVer().toVersionCode() + tag.commitsSinceLatestTag
}
- overrideVersionName { gitTag, _, _ ->
- "Version " + gitTag.toString()
+ versionName.set {
+ "Version $tag"
}
}
""".trimIndent()
@@ -877,7 +968,7 @@ class GenerateAppVersionInfoTest {
@Test
fun `GenerateAppVersionInfo is incremental for specific build variants`() {
- GitClient.initialize(fixtureDir.root).apply {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
@@ -903,7 +994,7 @@ class GenerateAppVersionInfoTest {
@Test
fun `GenerateAppVersionInfo is cacheable for specific build variants`() {
- GitClient.initialize(fixtureDir.root).apply {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
@@ -933,7 +1024,7 @@ class GenerateAppVersionInfoTest {
@Test
fun `GenerateAppVersionInfo (up-to-date) is re-executed after changing git refs`() {
- val gitClient = GitClient.initialize(fixtureDir.root).apply {
+ val gitRepo = initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
@@ -955,7 +1046,7 @@ class GenerateAppVersionInfoTest {
assertEquals(TaskOutcome.UP_TO_DATE, task(":app:generateAppVersionInfoForRelease")?.outcome)
}
- gitClient.commit(message = "2nd commit.")
+ gitRepo.commit(message = "2nd commit.")
runner.runAndCheckResult(
"generateAppVersionInfoForRelease"
@@ -966,11 +1057,11 @@ class GenerateAppVersionInfoTest {
@Test
fun `GenerateAppVersionInfo (from cache) is re-executed after changing git refs`() {
- val gitClient = GitClient.initialize(fixtureDir.root).apply {
+ val gitRepo = initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
- val commitId2 = gitClient.commit(message = "2nd commit.")
+ val commitId2 = gitRepo.commit(message = "2nd commit.")
val runner = withFixtureRunner(
fixtureDir = fixtureDir,
@@ -993,7 +1084,7 @@ class GenerateAppVersionInfoTest {
assertEquals(TaskOutcome.FROM_CACHE, task(":app:generateAppVersionInfoForRelease")?.outcome)
}
- gitClient.tag(name = "1.3.0", message = "2nd tag", commitId = commitId2)
+ gitRepo.tag(name = "1.3.0", message = "2nd tag", commitId = commitId2)
runner.runAndCheckResult(
"clean",
@@ -1006,7 +1097,7 @@ class GenerateAppVersionInfoTest {
@Test
fun `GenerateAppVersionInfo (up-to-date) is re-executed after changing git HEAD`() {
- val gitClient = GitClient.initialize(fixtureDir.root).apply {
+ val gitRepo = initGitRepo(fixtureDir.root) {
val commitId1 = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId1)
val commitId2 = commit(message = "2nd commit.")
@@ -1030,7 +1121,7 @@ class GenerateAppVersionInfoTest {
assertEquals(TaskOutcome.UP_TO_DATE, task(":app:generateAppVersionInfoForRelease")?.outcome)
}
- gitClient.checkoutTag(tag = "1.2.3")
+ gitRepo.checkoutTag(tag = "1.2.3")
runner.runAndCheckResult(
"generateAppVersionInfoForRelease"
@@ -1041,7 +1132,7 @@ class GenerateAppVersionInfoTest {
@Test
fun `GenerateAppVersionInfo (from cache) is re-executed after changing git HEAD`() {
- val gitClient = GitClient.initialize(fixtureDir.root).apply {
+ val gitRepo = initGitRepo(fixtureDir.root) {
val commitId1 = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId1)
val commitId2 = commit(message = "2nd commit.")
@@ -1069,7 +1160,7 @@ class GenerateAppVersionInfoTest {
assertEquals(TaskOutcome.FROM_CACHE, task(":app:generateAppVersionInfoForRelease")?.outcome)
}
- gitClient.checkoutTag(tag = "1.2.3")
+ gitRepo.checkoutTag(tag = "1.2.3")
runner.runAndCheckResult(
"clean",
diff --git a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfoTest.kt b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfoTest.kt
index 90fe6506..4745bcf7 100644
--- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfoTest.kt
+++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfoTest.kt
@@ -3,8 +3,10 @@
package io.github.reactivecircus.appversioning.tasks
import io.github.reactivecircus.appversioning.fixtures.AppProjectTemplate
+import io.github.reactivecircus.appversioning.fixtures.ConfigurationCacheEntryLog
+import io.github.reactivecircus.appversioning.fixtures.assertHasConfigurationCacheEntryLog
+import io.github.reactivecircus.appversioning.fixtures.initGitRepo
import io.github.reactivecircus.appversioning.fixtures.withFixtureRunner
-import io.github.reactivecircus.appversioning.internal.GitClient
import org.gradle.testkit.runner.TaskOutcome
import org.junit.Rule
import org.junit.Test
@@ -18,7 +20,7 @@ class PrintAppVersionInfoTest {
@Test
fun `PrintAppVersionInfo prints latest generated versionCode and versionName to the console when they are available`() {
- GitClient.initialize(fixtureDir.root).apply {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
@@ -55,7 +57,7 @@ class PrintAppVersionInfoTest {
@Test
fun `PrintAppVersionInfo prints warning message to the console when they are not available`() {
- GitClient.initialize(fixtureDir.root).apply {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
@@ -76,56 +78,43 @@ class PrintAppVersionInfoTest {
}
@Test
- fun `PrintAppVersionInfo prints latest generate versionCode and versionName (or warning message if not available) to the console for each project with AppVersioningPlugin applied`() {
- GitClient.initialize(fixtureDir.root).apply {
+ fun `PrintAppVersionInfo prints latest generated versionCode and versionName (or warning message if not available) to the console for each project with AppVersioningPlugin applied`() {
+ initGitRepo(fixtureDir.root) {
commit(message = "Initial commit.")
- // appA release
val commitId1 = commit(message = "appA release.")
tag(name = "0.1.0+appA", message = "1st tag", commitId = commitId1)
- // appB release
val commitId2 = commit(message = "appB release.")
tag(name = "1.2.3+appB", message = "2nd tag", commitId = commitId2)
- // appC release
- val commitId4 = commit(message = "appC release.")
- tag(name = "10.3.5+appC", message = "4th tag", commitId = commitId4)
+ val commitId3 = commit(message = "appC release.")
+ tag(name = "10.3.5+appC", message = "3rd tag", commitId = commitId3)
}
- val tagFilterAppA = "[0-9]*.[0-9]*.[0-9]*+appA"
- val tagFilterAppB = "[0-9]*.[0-9]*.[0-9]*+appB"
- val tagFilterAppC = "[0-9]*.[0-9]*.[0-9]*+appC"
-
- val extensionsAppA = """
- appVersioning {
- tagFilter.set("$tagFilterAppA")
- }
- """.trimIndent()
-
- val extensionsAppB = """
- appVersioning {
- tagFilter.set("$tagFilterAppB")
- }
- """.trimIndent()
-
- val extensionsAppC = """
- appVersioning {
- tagFilter.set("$tagFilterAppC")
- }
- """.trimIndent()
-
val runner = withFixtureRunner(
fixtureDir = fixtureDir,
subprojects = listOf(
AppProjectTemplate(
projectName = "app-a",
- pluginExtension = extensionsAppA
+ pluginExtension = """
+ appVersioning {
+ tagFilter.set("[0-9]*.[0-9]*.[0-9]*+appA")
+ }
+ """.trimIndent()
),
AppProjectTemplate(
projectName = "app-b",
- pluginExtension = extensionsAppB
+ pluginExtension = """
+ appVersioning {
+ tagFilter.set("[0-9]*.[0-9]*.[0-9]*+appB")
+ }
+ """.trimIndent()
),
AppProjectTemplate(
projectName = "app-c",
- pluginExtension = extensionsAppC
+ pluginExtension = """
+ appVersioning {
+ tagFilter.set("[0-9]*.[0-9]*.[0-9]*+appC")
+ }
+ """.trimIndent()
)
)
)
@@ -167,7 +156,7 @@ class PrintAppVersionInfoTest {
)
)
- assertEquals(TaskOutcome.SUCCESS, task(":app-a:printAppVersionInfoForRelease")?.outcome)
+ assertEquals(TaskOutcome.SUCCESS, task(":app-c:printAppVersionInfoForRelease")?.outcome)
assertTrue(
output.contains(
"No app version info (versionCode and versionName) generated by the Android App Versioning plugin for the \"release\" variant of project \":app-c\" is available."
@@ -178,7 +167,7 @@ class PrintAppVersionInfoTest {
@Test
fun `PrintAppVersionInfo is not incremental`() {
- GitClient.initialize(fixtureDir.root).apply {
+ initGitRepo(fixtureDir.root) {
val commitId = commit(message = "1st commit.")
tag(name = "1.2.3", message = "1st tag", commitId = commitId)
}
@@ -197,6 +186,7 @@ class PrintAppVersionInfoTest {
runner.runAndCheckResult(
"printAppVersionInfoForRelease"
) {
+ assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Stored)
assertEquals(TaskOutcome.SUCCESS, task(":app:printAppVersionInfoForRelease")?.outcome)
assertTrue(
output.contains(
@@ -214,6 +204,7 @@ class PrintAppVersionInfoTest {
runner.runAndCheckResult(
"printAppVersionInfoForRelease"
) {
+ assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Reused)
assertEquals(TaskOutcome.SUCCESS, task(":app:printAppVersionInfoForRelease")?.outcome)
assertTrue(
output.contains(
diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningExtension.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningExtension.kt
index 81c01cba..4a094d89 100644
--- a/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningExtension.kt
+++ b/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningExtension.kt
@@ -1,14 +1,13 @@
package io.github.reactivecircus.appversioning
import groovy.lang.Closure
+import groovy.lang.DelegatesTo
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
-import org.gradle.api.provider.ProviderFactory
/**
* Extension for [AppVersioningPlugin].
*/
-@Suppress("unused")
open class AppVersioningExtension internal constructor(objects: ObjectFactory) {
/**
* Whether to enable the plugin.
@@ -52,79 +51,45 @@ open class AppVersioningExtension internal constructor(objects: ObjectFactory) {
val tagFilter = objects.property().convention(null)
/**
- * Provides a custom rule for generating versionCode by implementing a [GitTag], [ProviderFactory], [VariantInfo] -> Int lambda.
- * [GitTag] is generated from latest git tag lazily by the plugin during task execution.
- * [ProviderFactory] can be used for fetching environment variables, Gradle and system properties.
- * [VariantInfo] can be used to customize version code based on the build variants (product flavors and build types).
+ * Specification for generating `versionCode`.
*
- * By default the plugin attempts to generate the versionCode by converting a SemVer compliant tag to an integer
- * using positional notation: versionCode = MAJOR * 10000 + MINOR * 100 + PATCH
- *
- * If your tags don't follow semantic versioning, you don't like the default formula used to convert a SemVer tag to versionCode,
- * or if you want to fully customize how the versionCode is generated, you can implement this lambda to provide your own versionCode generation rule.
- *
- * Note that you can use the `gitTag.toSemVer()` extension (or `SemVer.fromGitTag(gitTag)` if you use groovy) to get a type-safe `SemVer` model
- * if your custom rule is still based on semantic versioning.
- */
- fun overrideVersionCode(customizer: VersionCodeCustomizer) {
- kotlinVersionCodeCustomizer.set(customizer)
- }
-
- /**
- * Same as `overrideVersionCode(customizer: VersionCodeCustomizer)` above but for groovy support.
+ * Default is [VersionCodeSpec.SemVer].
*/
- fun overrideVersionCode(customizer: Closure) {
- groovyVersionCodeCustomizer.set(customizer.dehydrate())
- }
+ val versionCode: Property = objects.property().convention(VersionCodeSpec.SemVer)
/**
- * Provides a custom rule for generating versionName by implementing a [GitTag], [ProviderFactory], [VariantInfo] -> String lambda.
- * [GitTag] is generated from latest git tag lazily by the plugin during task execution.
- * [ProviderFactory] can be used for fetching environment variables, Gradle and system properties.
- * [VariantInfo] can be used to customize version name based on the build variants (product flavors and build types).
+ * Specification for generating `versionName`.
*
- * This is useful if you want to fully customize how the versionName is generated.
- * If not specified, versionName will be the name of the latest git tag.
+ * Default is [VersionNameSpec.TagName].
*/
- fun overrideVersionName(customizer: VersionNameCustomizer) {
- kotlinVersionNameCustomizer.set(customizer)
- }
+ val versionName: Property =
+ objects.property().convention(VersionNameSpec.TagName)
/**
- * Same as `overrideVersionName(customizer: VersionNameCustomizer)` above but for groovy support.
+ * Configures [versionCode] using a Groovy [Closure] with [VersionContext] as its delegate.
*/
- fun overrideVersionName(customizer: Closure) {
- groovyVersionNameCustomizer.set(customizer.dehydrate())
+ fun versionCode(
+ @DelegatesTo(VersionContext::class, strategy = Closure.DELEGATE_FIRST)
+ closure: Closure,
+ ) {
+ versionCode.set(closure.toVersionCodeSpec())
}
/**
- * A lambda (Kotlin function type) for specifying a custom rule for generating versionCode.
+ * Configures [versionName] using a Groovy [Closure] with [VersionContext] as its delegate.
*/
- internal val kotlinVersionCodeCustomizer = objects.property()
-
- /**
- * A lambda (Groovy closure) for specifying a custom rule for generating versionCode.
- */
- internal val groovyVersionCodeCustomizer = objects.property>()
-
- /**
- * A lambda (Kotlin function type) for specifying a custom rule for generating versionName.
- */
- internal val kotlinVersionNameCustomizer = objects.property()
-
- /**
- * A lambda (Groovy closure) for specifying a custom rule for generating versionName.
- */
- internal val groovyVersionNameCustomizer = objects.property>()
+ fun versionName(
+ @DelegatesTo(VersionContext::class, strategy = Closure.DELEGATE_FIRST)
+ closure: Closure,
+ ) {
+ versionName.set(closure.toVersionNameSpec())
+ }
- companion object {
- internal const val DEFAULT_ENABLED = true
- internal const val DEFAULT_RELEASE_BUILD_ONLY = false
- internal const val DEFAULT_FETCH_TAGS_WHEN_NONE_EXISTS_LOCALLY = false
+ internal companion object {
+ const val DEFAULT_ENABLED = true
+ const val DEFAULT_RELEASE_BUILD_ONLY = false
+ const val DEFAULT_FETCH_TAGS_WHEN_NONE_EXISTS_LOCALLY = false
}
}
private inline fun ObjectFactory.property(): Property = property(T::class.java)
-
-internal typealias VersionCodeCustomizer = (GitTag, ProviderFactory, VariantInfo) -> Int
-internal typealias VersionNameCustomizer = (GitTag, ProviderFactory, VariantInfo) -> String
diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningPlugin.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningPlugin.kt
index bf4107dc..4ec5477a 100644
--- a/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningPlugin.kt
+++ b/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningPlugin.kt
@@ -1,5 +1,6 @@
package io.github.reactivecircus.appversioning
+import com.android.build.api.AndroidPluginVersion
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.ApplicationVariant
import com.android.build.gradle.AppPlugin
@@ -18,6 +19,9 @@ import java.util.concurrent.atomic.AtomicBoolean
*/
class AppVersioningPlugin : Plugin {
override fun apply(project: Project) {
+ check(AndroidPluginVersion.getCurrent() >= MIN_AGP_VERSION) {
+ "Android App Versioning plugin requires Android Gradle Plugin ${MIN_AGP_VERSION.version} or later. Detected Android Gradle Plugin version: ${AndroidPluginVersion.getCurrent().version}."
+ }
val androidAppPluginApplied = AtomicBoolean(false)
val pluginDisabled = AtomicBoolean(false)
val appVersioningExtension = project.extensions.create("appVersioning", AppVersioningExtension::class.java)
@@ -76,10 +80,8 @@ class AppVersioningPlugin : Plugin {
rootProjectDisplayName.set(rootProject.displayName)
fetchTagsWhenNoneExistsLocally.set(extension.fetchTagsWhenNoneExistsLocally)
tagFilter.set(extension.tagFilter)
- kotlinVersionCodeCustomizer.set(extension.kotlinVersionCodeCustomizer)
- kotlinVersionNameCustomizer.set(extension.kotlinVersionNameCustomizer)
- groovyVersionCodeCustomizer.set(extension.groovyVersionCodeCustomizer)
- groovyVersionNameCustomizer.set(extension.groovyVersionNameCustomizer)
+ versionCodeSpec.set(extension.versionCode.map { it.asSerializable() })
+ versionNameSpec.set(extension.versionName.map { it.asSerializable() })
versionCodeFile.set(
layout.buildDirectory.file("$APP_VERSIONING_TASK_OUTPUT_DIR/${variant.name}/$VERSION_CODE_RESULT_FILE")
)
@@ -148,7 +150,9 @@ class AppVersioningPlugin : Plugin {
return replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
}
- companion object
+ private companion object {
+ private val MIN_AGP_VERSION = AndroidPluginVersion(9, 0, 0)
+ }
}
private const val APP_VERSIONING_TASK_GROUP = "versioning"
diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/GitTag.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/GitTag.kt
index caae8667..e3961e82 100644
--- a/src/main/kotlin/io/github/reactivecircus/appversioning/GitTag.kt
+++ b/src/main/kotlin/io/github/reactivecircus/appversioning/GitTag.kt
@@ -1,14 +1,40 @@
package io.github.reactivecircus.appversioning
+import java.util.Objects
+
/**
* Type-safe representation of a git tag.
*/
-data class GitTag(
- val rawTagName: String,
+class GitTag internal constructor(
+ val name: String,
val commitsSinceLatestTag: Int,
- val commitHash: String
+ val commitHash: String,
) {
- override fun toString() = rawTagName
+ /**
+ * Parses this tag's [name] into a [SemVer], stripping [prefix] first if non-empty.
+ *
+ * @param prefix expected prefix before the semantic version (default is `""`).
+ * @throws [IllegalArgumentException] when [name] does not start with [prefix] or the remaining string is not a valid [SemVer].
+ */
+ @JvmOverloads
+ fun toSemVer(prefix: String = ""): SemVer {
+ require(name.startsWith(prefix)) {
+ "Tag \"$name\" does not start with expected prefix \"$prefix\"."
+ }
+ return SemVer.parse(name.removePrefix(prefix))
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is GitTag) return false
+ return name == other.name &&
+ commitsSinceLatestTag == other.commitsSinceLatestTag &&
+ commitHash == other.commitHash
+ }
+
+ override fun hashCode(): Int = Objects.hash(name, commitsSinceLatestTag, commitHash)
+
+ override fun toString(): String = name
}
/**
@@ -18,6 +44,6 @@ internal fun String.toGitTag(): GitTag {
val result = requireNotNull("(.*)-(\\d+)-g([0-9,a-f]{7,40})\$".toRegex().matchEntire(this)) {
"$this is not a valid git tag."
}
- val (rawTagName, commitsSinceLatestTag, commitHash) = result.destructured
- return GitTag(rawTagName, commitsSinceLatestTag.toInt(), commitHash)
+ val (name, commitsSinceLatestTag, commitHash) = result.destructured
+ return GitTag(name, commitsSinceLatestTag.toInt(), commitHash)
}
diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/SemVer.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/SemVer.kt
index 0649ddb8..7eddb25f 100644
--- a/src/main/kotlin/io/github/reactivecircus/appversioning/SemVer.kt
+++ b/src/main/kotlin/io/github/reactivecircus/appversioning/SemVer.kt
@@ -1,18 +1,58 @@
package io.github.reactivecircus.appversioning
+import java.util.Objects
import kotlin.math.pow
/**
* Type-safe representation of a version number that follows [Semantic Versioning 2.0.0](https://semver.org/#semantic-versioning-200).
*/
-data class SemVer(
+class SemVer(
val major: Int,
val minor: Int,
val patch: Int,
val preRelease: String? = null,
- val buildMetadata: String? = null
-) {
- override fun toString() = buildString {
+ val buildMetadata: String? = null,
+) : Comparable {
+ /**
+ * Generates an integer version code from this [SemVer] using positional notation.
+ *
+ * @param digitsPerComponent number of digits allocated to the [minor] and [patch] components (default is 2).
+ * @throws [IllegalArgumentException] when [digitsPerComponent] is not positive, [minor] or [patch] version is out of range allowed by [digitsPerComponent], or when the result is above [Int.MAX_VALUE].
+ */
+ @JvmOverloads
+ fun toVersionCode(digitsPerComponent: Int = DEFAULT_DIGITS_PER_COMPONENT): Int {
+ require(digitsPerComponent > 0)
+ val maxComponentValue = 10.0.pow(digitsPerComponent)
+ require(patch in 0.. major.compareTo(other.major)
+ minor != other.minor -> minor.compareTo(other.minor)
+ patch != other.patch -> patch.compareTo(other.patch)
+ preRelease == null && other.preRelease == null -> 0
+ preRelease == null -> 1
+ other.preRelease == null -> -1
+ else -> comparePreRelease(preRelease, other.preRelease)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SemVer) return false
+ return major == other.major &&
+ minor == other.minor &&
+ patch == other.patch &&
+ preRelease == other.preRelease &&
+ buildMetadata == other.buildMetadata
+ }
+
+ override fun hashCode(): Int = Objects.hash(major, minor, patch, preRelease, buildMetadata)
+
+ override fun toString(): String = buildString {
append("$major.$minor.$patch")
if (preRelease != null) {
append("-$preRelease")
@@ -23,53 +63,68 @@ data class SemVer(
}
companion object {
+ private const val DEFAULT_DIGITS_PER_COMPONENT = 2
+
+ /**
+ * Parses [text] into a strict [SemVer] (no prefix allowed).
+ *
+ * @throws [IllegalArgumentException] when [text] is not a valid [SemVer].
+ */
+ @JvmStatic
+ fun parse(text: String): SemVer = requireNotNull(parseOrNull(text)) {
+ "\"$text\" is not a valid SemVer (https://semver.org/#semantic-versioning-200)."
+ }
+
+ /**
+ * Parses [text] into a strict [SemVer] (no prefix allowed), or returns `null` if [text] is not a valid [SemVer].
+ */
@JvmStatic
- @JvmOverloads
- fun fromGitTag(gitTag: GitTag, allowPrefixV: Boolean = true): SemVer = gitTag.toSemVer(allowPrefixV)
+ @Suppress("DestructuringDeclarationWithTooManyEntries")
+ fun parseOrNull(text: String): SemVer? {
+ val (major, minor, patch, preRelease, buildMetadata) =
+ SEM_VER_REGEX.matchEntire(text)?.destructured ?: return null
+ val majorInt = major.toIntOrNull()
+ val minorInt = minor.toIntOrNull()
+ val patchInt = patch.toIntOrNull()
+ return if (majorInt != null && minorInt != null && patchInt != null) {
+ SemVer(
+ major = majorInt,
+ minor = minorInt,
+ patch = patchInt,
+ preRelease = preRelease.ifEmpty { null },
+ buildMetadata = buildMetadata.ifEmpty { null },
+ )
+ } else {
+ null
+ }
+ }
}
}
-/**
- * Generates an Integer representation of the [SemVer] using positional notation.
- * @param maxDigitsPerComponent number of digits allocated to the MINOR and PATCH components.
- * @throws [IllegalArgumentException] when MINOR or PATCH version is out of range allowed by [maxDigitsPerComponent], or when the result is above [Int.MAX_VALUE].
- */
-@Suppress("MagicNumber")
-internal fun SemVer.toInt(maxDigitsPerComponent: Int): Int {
- require(maxDigitsPerComponent > 0)
- require(patch < 10.0.pow(maxDigitsPerComponent))
- require(minor < 10.0.pow(maxDigitsPerComponent))
- val result = major * 10.0.pow(maxDigitsPerComponent * 2) + minor * 10.0.pow(maxDigitsPerComponent) + patch
- require(result <= Int.MAX_VALUE)
- return result.toInt()
+private fun comparePreRelease(a: String, b: String): Int {
+ val aParts = a.split('.')
+ val bParts = b.split('.')
+ return aParts.zip(bParts)
+ .firstNotNullOfOrNull { (aPart, bPart) ->
+ comparePreReleaseIdentifier(aPart, bPart).takeIf { it != 0 }
+ }
+ ?: aParts.size.compareTo(bParts.size)
}
-/**
- * Tries to create a [SemVer] from a [GitTag] by parsing its `rawTagName` field.
- * @param allowPrefixV whether prefixing a semantic version with a āvā is allowed.
- * @throws [IllegalArgumentException] when the `rawTagName` of the [GitTag] is not a valid [SemVer].
- */
-@Suppress("DestructuringDeclarationWithTooManyEntries")
-fun GitTag.toSemVer(allowPrefixV: Boolean = true): SemVer {
- val result = requireNotNull(SEM_VER_REGEX.toRegex().matchEntire(rawTagName)) {
- "\"$rawTagName\" is not a valid SemVer."
- }
- val (prefixV, major, minor, patch, preRelease, buildMetadata) = result.destructured
- require(prefixV.isEmpty() || allowPrefixV) {
- "\"$rawTagName\" is not a valid SemVer as prefix \"v\" is not allowed unless `allowPrefixV` is set to true."
+private fun comparePreReleaseIdentifier(a: String, b: String): Int {
+ val aIsNumeric = a.all { it.isDigit() }
+ val bIsNumeric = b.all { it.isDigit() }
+ return when {
+ a == b -> 0
+ aIsNumeric && bIsNumeric -> a.toBigInteger().compareTo(b.toBigInteger())
+ aIsNumeric -> -1
+ bIsNumeric -> 1
+ else -> a.compareTo(b)
}
- return SemVer(
- major = major.toInt(),
- minor = minor.toInt(),
- patch = patch.toInt(),
- preRelease = preRelease.ifEmpty { null },
- buildMetadata = buildMetadata.ifEmpty { null }
- )
}
/**
- * SemVer regex from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
- * allowing an optional "v" prefix.
+ * Strict SemVer 2.0.0 regex from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
*/
-private const val SEM_VER_REGEX =
- "^(v)?(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?\$"
+private val SEM_VER_REGEX =
+ "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?\$".toRegex()
diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/VariantInfo.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/VariantInfo.kt
index 65cd67a1..dc3b5f95 100644
--- a/src/main/kotlin/io/github/reactivecircus/appversioning/VariantInfo.kt
+++ b/src/main/kotlin/io/github/reactivecircus/appversioning/VariantInfo.kt
@@ -2,7 +2,7 @@ package io.github.reactivecircus.appversioning
import java.io.Serializable
-class VariantInfo(
+class VariantInfo internal constructor(
val buildType: String?,
val flavorName: String,
val variantName: String
@@ -10,7 +10,8 @@ class VariantInfo(
val isDebugBuild: Boolean get() = buildType == "debug"
val isReleaseBuild: Boolean get() = buildType == "release"
- companion object {
+ internal companion object {
+ @Suppress("unused")
private const val serialVersionUID = 1L
}
}
diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/VersionContext.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/VersionContext.kt
new file mode 100644
index 00000000..8e2820a2
--- /dev/null
+++ b/src/main/kotlin/io/github/reactivecircus/appversioning/VersionContext.kt
@@ -0,0 +1,21 @@
+package io.github.reactivecircus.appversioning
+
+/**
+ * Context available to [VersionCodeSpec] and [VersionNameSpec].
+ */
+interface VersionContext {
+ /**
+ * The latest git tag (matching `tagFilter` if provided).
+ */
+ val tag: GitTag
+
+ /**
+ * The build variant the version is generated for.
+ */
+ val variant: VariantInfo
+}
+
+internal class DefaultVersionContext(
+ override val tag: GitTag,
+ override val variant: VariantInfo,
+) : VersionContext
diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/VersionSpecs.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/VersionSpecs.kt
new file mode 100644
index 00000000..88a598de
--- /dev/null
+++ b/src/main/kotlin/io/github/reactivecircus/appversioning/VersionSpecs.kt
@@ -0,0 +1,109 @@
+package io.github.reactivecircus.appversioning
+
+import groovy.lang.Closure
+import org.codehaus.groovy.runtime.ConvertedClosure
+import java.io.Serializable
+import java.lang.reflect.Proxy
+
+/**
+ * Specification for generating `versionCode` from a [VersionContext].
+ */
+fun interface VersionCodeSpec : Serializable {
+ fun VersionContext.versionCode(): Int
+
+ companion object {
+ /**
+ * Converts a SemVer compliant tag to an integer using positional notation:
+ * `versionCode = MAJOR * 10000 + MINOR * 100 + PATCH`.
+ */
+ val SemVer: VersionCodeSpec = VersionCodeSpec { tag.toVersionCodeBySemVer() }
+ }
+}
+
+/**
+ * Specification for generating `versionName` from a [VersionContext].
+ */
+fun interface VersionNameSpec : Serializable {
+ fun VersionContext.versionName(): String
+
+ companion object {
+ /**
+ * Uses the name of the latest git tag as the `versionName`.
+ */
+ val TagName: VersionNameSpec = VersionNameSpec { tag.name }
+ }
+}
+
+private fun GitTag.toVersionCodeBySemVer(): Int {
+ val prefix = if (name.startsWith("v")) "v" else ""
+ val semVer = checkNotNull(runCatching { toSemVer(prefix = prefix) }.getOrNull()) {
+ """
+ Could not generate versionCode as "$name" does not follow semantic versioning.
+ Please either ensure latest git tag follows semantic versioning, or provide a custom spec for generating versionCode using the `versionCode` property.
+ """.trimIndent()
+ }
+ return runCatching {
+ semVer.toVersionCode()
+ }.getOrElse {
+ throw IllegalStateException(
+ """
+ Could not generate versionCode from "$name" as the SemVer cannot be represented as an Integer.
+ This is usually because MAJOR or MINOR version is greater than 99, as by default maximum of 2 digits is allowed for MINOR and PATCH components of a SemVer tag.
+ Another reason might be that the overall positional notation of the SemVer (MAJOR * 10000 + MINOR * 100 + PATCH) is greater than the maximum value of an integer (2147483647).
+ As a workaround you can provide a custom spec for generating versionCode using the `versionCode` property.
+ """.trimIndent(),
+ it,
+ )
+ }
+}
+
+internal fun VersionCodeSpec.asSerializable(): VersionCodeSpec {
+ if (Proxy.isProxyClass(javaClass)) {
+ val handler = Proxy.getInvocationHandler(this)
+ if (handler is ConvertedClosure) {
+ @Suppress("UNCHECKED_CAST")
+ return (handler.delegate as Closure).toVersionCodeSpec()
+ }
+ }
+ return this
+}
+
+internal fun VersionNameSpec.asSerializable(): VersionNameSpec {
+ if (Proxy.isProxyClass(javaClass)) {
+ val handler = Proxy.getInvocationHandler(this)
+ if (handler is ConvertedClosure) {
+ @Suppress("UNCHECKED_CAST")
+ return (handler.delegate as Closure).toVersionNameSpec()
+ }
+ }
+ return this
+}
+
+internal fun Closure.toVersionCodeSpec(): VersionCodeSpec {
+ val holder = SerializableClosure(this)
+ return VersionCodeSpec { holder.call(this) }
+}
+
+internal fun Closure.toVersionNameSpec(): VersionNameSpec {
+ val holder = SerializableClosure(this)
+ return VersionNameSpec { holder.call(this).toString() }
+}
+
+private class SerializableClosure(
+ closure: Closure,
+) : Serializable {
+ private val dehydrated: Closure = closure.dehydrate()
+
+ fun call(context: VersionContext): T =
+ dehydrated.rehydrate(context, context, context)
+ .also { it.resolveStrategy = Closure.DELEGATE_FIRST }
+ .call(context)
+
+ @Suppress("unused")
+ private fun writeReplace(): Any = SerializableClosure(dehydrated)
+
+ companion object {
+ @Suppress("unused")
+ private const val serialVersionUID = 1L
+ }
+}
diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/internal/GitOps.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/internal/GitOps.kt
new file mode 100644
index 00000000..ed2895f9
--- /dev/null
+++ b/src/main/kotlin/io/github/reactivecircus/appversioning/internal/GitOps.kt
@@ -0,0 +1,52 @@
+package io.github.reactivecircus.appversioning.internal
+
+import java.io.File
+import java.util.concurrent.TimeUnit
+
+internal object GitOps {
+ fun ensureValidGitRepo(workingDir: File) {
+ val isValid = runCatching {
+ listOf("git", "rev-parse", "--is-inside-work-tree").execute(workingDir).toBoolean()
+ }.getOrDefault(false)
+ require(isValid)
+ }
+
+ fun listLocalTags(workingDir: File): List {
+ val output = listOf("git", "tag", "--list").execute(workingDir)
+ return if (output.isEmpty()) emptyList() else output.lines()
+ }
+
+ fun fetchRemoteTags(workingDir: File) {
+ listOf("git", "fetch", "--tags").execute(workingDir)
+ }
+
+ fun describeLatestTag(workingDir: File, pattern: String? = null): String? {
+ val command = buildList {
+ add("git")
+ add("describe")
+ if (pattern != null) {
+ add("--match")
+ add(pattern)
+ }
+ add("--tags")
+ add("--long")
+ }
+ return runCatching { command.execute(workingDir) }.getOrNull()
+ }
+}
+
+private fun List.execute(workingDir: File, timeoutInSeconds: Long = DEFAULT_COMMAND_TIMEOUT_SECONDS): String {
+ val process = ProcessBuilder(this)
+ .directory(workingDir)
+ .start()
+ if (!process.waitFor(timeoutInSeconds, TimeUnit.SECONDS)) {
+ process.destroy()
+ throw IllegalStateException("Execution timeout: $this")
+ }
+ if (process.exitValue() != 0) {
+ throw IllegalStateException("Execution failed with exit code: ${process.exitValue()}: $this")
+ }
+ return process.inputStream.bufferedReader().readText().trim()
+}
+
+private const val DEFAULT_COMMAND_TIMEOUT_SECONDS = 5L
diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfo.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfo.kt
index 600f634a..96309122 100644
--- a/src/main/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfo.kt
+++ b/src/main/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfo.kt
@@ -1,20 +1,16 @@
package io.github.reactivecircus.appversioning.tasks
-import groovy.lang.Closure
+import io.github.reactivecircus.appversioning.DefaultVersionContext
import io.github.reactivecircus.appversioning.GitTag
import io.github.reactivecircus.appversioning.VariantInfo
-import io.github.reactivecircus.appversioning.VersionCodeCustomizer
-import io.github.reactivecircus.appversioning.VersionNameCustomizer
-import io.github.reactivecircus.appversioning.internal.GitClient
+import io.github.reactivecircus.appversioning.VersionCodeSpec
+import io.github.reactivecircus.appversioning.VersionNameSpec
+import io.github.reactivecircus.appversioning.internal.GitOps
import io.github.reactivecircus.appversioning.toGitTag
-import io.github.reactivecircus.appversioning.toInt
-import io.github.reactivecircus.appversioning.toSemVer
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
-import org.gradle.api.logging.Logging
import org.gradle.api.provider.Property
-import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.IgnoreEmptyDirectories
import org.gradle.api.tasks.Input
@@ -27,18 +23,12 @@ import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.work.NormalizeLineEndings
-import org.gradle.workers.WorkAction
-import org.gradle.workers.WorkParameters
-import org.gradle.workers.WorkerExecutor
-import javax.inject.Inject
/**
* Generates app's versionCode and versionName based on git tags.
*/
@CacheableTask
-abstract class GenerateAppVersionInfo @Inject constructor(
- private val workerExecutor: WorkerExecutor
-) : DefaultTask() {
+abstract class GenerateAppVersionInfo : DefaultTask() {
@get:Optional
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
@@ -65,21 +55,11 @@ abstract class GenerateAppVersionInfo @Inject constructor(
@get:Input
abstract val tagFilter: Property
- @get:Optional
- @get:Input
- abstract val kotlinVersionCodeCustomizer: Property
-
- @get:Optional
@get:Input
- abstract val kotlinVersionNameCustomizer: Property
+ abstract val versionCodeSpec: Property
- @get:Optional
@get:Input
- abstract val groovyVersionCodeCustomizer: Property>
-
- @get:Optional
- @get:Input
- abstract val groovyVersionNameCustomizer: Property>
+ abstract val versionNameSpec: Property
@get:OutputFile
abstract val versionCodeFile: RegularFileProperty
@@ -91,175 +71,59 @@ abstract class GenerateAppVersionInfo @Inject constructor(
abstract val variantInfo: Property
@TaskAction
- fun generate() {
- workerExecutor.noIsolation().submit(GenerateAppVersionInfoWorkAction::class.java) {
- gitRefsDirectory.set(this@GenerateAppVersionInfo.gitRefsDirectory)
- gitHead.set(this@GenerateAppVersionInfo.gitHead)
- rootProjectDirectory.set(this@GenerateAppVersionInfo.rootProjectDirectory)
- rootProjectDisplayName.set(this@GenerateAppVersionInfo.rootProjectDisplayName)
- fetchTagsWhenNoneExistsLocally.set(this@GenerateAppVersionInfo.fetchTagsWhenNoneExistsLocally)
- tagFilter.set(this@GenerateAppVersionInfo.tagFilter)
- kotlinVersionCodeCustomizer.set(this@GenerateAppVersionInfo.kotlinVersionCodeCustomizer)
- kotlinVersionNameCustomizer.set(this@GenerateAppVersionInfo.kotlinVersionNameCustomizer)
- groovyVersionCodeCustomizer.set(this@GenerateAppVersionInfo.groovyVersionCodeCustomizer)
- groovyVersionNameCustomizer.set(this@GenerateAppVersionInfo.groovyVersionNameCustomizer)
- versionCodeFile.set(this@GenerateAppVersionInfo.versionCodeFile)
- versionNameFile.set(this@GenerateAppVersionInfo.versionNameFile)
- variantInfo.set(this@GenerateAppVersionInfo.variantInfo)
- }
- }
-
- companion object {
- const val TASK_NAME_PREFIX = "generateAppVersionInfo"
- const val TASK_DESCRIPTION_PREFIX = "Generates app's versionCode and versionName based on git tags"
- const val VERSION_CODE_FALLBACK = 0
- const val VERSION_NAME_FALLBACK = ""
- }
-}
-
-private interface GenerateAppVersionInfoWorkParameters : WorkParameters {
- val gitRefsDirectory: DirectoryProperty
- val gitHead: RegularFileProperty
- val rootProjectDirectory: DirectoryProperty
- val rootProjectDisplayName: Property
- val fetchTagsWhenNoneExistsLocally: Property
- val tagFilter: Property
- val kotlinVersionCodeCustomizer: Property
- val kotlinVersionNameCustomizer: Property
- val groovyVersionCodeCustomizer: Property>
- val groovyVersionNameCustomizer: Property>
- val versionCodeFile: RegularFileProperty
- val versionNameFile: RegularFileProperty
- val variantInfo: Property
-}
-
-private abstract class GenerateAppVersionInfoWorkAction @Inject constructor(
- private val providers: ProviderFactory
-) : WorkAction {
- private val logger = Logging.getLogger(GenerateAppVersionInfo::class.java)
-
- @Suppress("LongMethod")
- override fun execute() {
- val gitRefsDirectory = parameters.gitRefsDirectory
- val gitHead = parameters.gitHead
- val rootProjectDirectory = parameters.rootProjectDirectory
- val rootProjectDisplayName = parameters.rootProjectDisplayName
- val fetchTagsWhenNoneExistsLocally = parameters.fetchTagsWhenNoneExistsLocally
- val tagFilter = parameters.tagFilter
- val kotlinVersionCodeCustomizer = parameters.kotlinVersionCodeCustomizer
- val kotlinVersionNameCustomizer = parameters.kotlinVersionNameCustomizer
- val groovyVersionCodeCustomizer = parameters.groovyVersionCodeCustomizer
- val groovyVersionNameCustomizer = parameters.groovyVersionNameCustomizer
- val versionCodeFile = parameters.versionCodeFile
- val versionNameFile = parameters.versionNameFile
- val variantInfo = parameters.variantInfo
-
+ internal fun generate() {
check(gitRefsDirectory.isPresent && gitHead.isPresent) {
"Android App Versioning Gradle Plugin works with git tags but ${rootProjectDisplayName.get()} is not a git root directory, and a valid gitRootDirectory is not provided."
}
- val gitClient = GitClient.open(rootProjectDirectory.get().asFile)
+ val rootProjectDir = rootProjectDirectory.get().asFile
+ GitOps.ensureValidGitRepo(rootProjectDir)
- val gitTag: GitTag = gitClient.describeLatestTag(
- tagFilter.orNull
+ val gitTag: GitTag = GitOps.describeLatestTag(
+ workingDir = rootProjectDir,
+ pattern = tagFilter.orNull,
)?.toGitTag() ?: if (fetchTagsWhenNoneExistsLocally.get()) {
- val tagsList = gitClient.listLocalTags()
+ val tagsList = GitOps.listLocalTags(rootProjectDir)
if (tagsList.isEmpty()) {
logger.warn("No git tags found. Fetching tags from remote.")
- gitClient.fetchRemoteTags()
+ GitOps.fetchRemoteTags(rootProjectDir)
}
- gitClient.describeLatestTag(tagFilter.orNull)?.toGitTag()
+ GitOps.describeLatestTag(
+ workingDir = rootProjectDir,
+ pattern = tagFilter.orNull,
+ )?.toGitTag()
} else {
null
} ?: run {
logger.warn(
"""
- No git tags found. Falling back to version code ${GenerateAppVersionInfo.VERSION_CODE_FALLBACK} and version name "${GenerateAppVersionInfo.VERSION_NAME_FALLBACK}".
+ No git tags found. Falling back to version code $VERSION_CODE_FALLBACK and version name "$VERSION_NAME_FALLBACK".
If you want to fallback to the versionCode and versionName set via the DSL or manifest, or stop generating versionCode and versionName from Git tags:
appVersioning {
enabled.set(false)
}
""".trimIndent()
)
- versionCodeFile.get().asFile.writeText(GenerateAppVersionInfo.VERSION_CODE_FALLBACK.toString())
- versionNameFile.get().asFile.writeText(GenerateAppVersionInfo.VERSION_NAME_FALLBACK)
+ versionCodeFile.get().asFile.writeText(VERSION_CODE_FALLBACK.toString())
+ versionNameFile.get().asFile.writeText(VERSION_NAME_FALLBACK)
return
}
- val versionCode: Int = generateVersionCodeFromGitTag(
- variantInfo,
- gitTag,
- kotlinVersionCodeCustomizer,
- groovyVersionCodeCustomizer
- )
+ val context = DefaultVersionContext(gitTag, variantInfo.get())
+
+ val versionCode = with(versionCodeSpec.get()) { context.versionCode() }
versionCodeFile.get().asFile.writeText(versionCode.toString())
logger.quiet("Generated app version code: $versionCode.")
- val versionName: String = when {
- kotlinVersionNameCustomizer.isPresent -> kotlinVersionNameCustomizer.get().invoke(
- gitTag,
- providers,
- variantInfo.get()
- )
-
- groovyVersionNameCustomizer.isPresent -> groovyVersionNameCustomizer.get().call(
- gitTag,
- providers,
- variantInfo.get()
- )
-
- else -> gitTag.toString()
- }
+ val versionName = with(versionNameSpec.get()) { context.versionName() }
versionNameFile.get().asFile.writeText(versionName)
logger.quiet("Generated app version name: \"$versionName\".")
}
- private fun generateVersionCodeFromGitTag(
- variantInfo: Property,
- gitTag: GitTag,
- kotlinVersionCodeCustomizer: Property,
- groovyVersionCodeCustomizer: Property>
- ): Int = when {
- kotlinVersionCodeCustomizer.isPresent -> kotlinVersionCodeCustomizer.get().invoke(
- gitTag,
- providers,
- variantInfo.get()
- )
-
- groovyVersionCodeCustomizer.isPresent -> groovyVersionCodeCustomizer.get().call(
- gitTag,
- providers,
- variantInfo.get()
- )
-
- else -> {
- // no custom rule for generating versionCode has been provided, attempt calculation based on SemVer
- val semVer = runCatching {
- gitTag.toSemVer()
- }.getOrNull()
- checkNotNull(semVer) {
- """
- Could not generate versionCode as "${gitTag.rawTagName}" does not follow semantic versioning.
- Please either ensure latest git tag follows semantic versioning, or provide a custom rule for generating versionCode using the `overrideVersionCode` lambda.
- """.trimIndent()
- }
- runCatching {
- semVer.toInt(maxDigitsPerComponent = MAX_DIGITS_PER_SEM_VER_COMPONENT)
- }.getOrElse {
- Int.MAX_VALUE
- throw IllegalStateException(
- """
- Could not generate versionCode from "${gitTag.rawTagName}" as the SemVer cannot be represented as an Integer.
- This is usually because MAJOR or MINOR version is greater than 99, as by default maximum of 2 digits is allowed for MINOR and PATCH components of a SemVer tag.
- Another reason might be that the overall positional notation of the SemVer (MAJOR * 10000 + MINOR * 100 + PATCH) is greater than the maximum value of an integer (2147483647).
- As a workaround you can provide a custom rule for generating versionCode using the `overrideVersionCode` lambda.
- """.trimIndent()
- )
- }
- }
- }
-
- companion object {
- private const val MAX_DIGITS_PER_SEM_VER_COMPONENT = 2
+ internal companion object {
+ const val TASK_NAME_PREFIX = "generateAppVersionInfo"
+ const val TASK_DESCRIPTION_PREFIX = "Generates app's versionCode and versionName based on git tags"
+ const val VERSION_CODE_FALLBACK = 0
+ const val VERSION_NAME_FALLBACK = ""
}
}
diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfo.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfo.kt
index 8d45f86c..2d28d276 100644
--- a/src/main/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfo.kt
+++ b/src/main/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfo.kt
@@ -27,7 +27,7 @@ abstract class PrintAppVersionInfo : DefaultTask() {
abstract val buildVariantName: Property
@TaskAction
- fun print() {
+ internal fun print() {
if (versionCodeFile.isPresent && versionNameFile.isPresent) {
logger.quiet(
"""
@@ -45,7 +45,7 @@ abstract class PrintAppVersionInfo : DefaultTask() {
}
}
- companion object {
+ internal companion object {
const val TASK_NAME_PREFIX = "printAppVersionInfo"
const val TASK_DESCRIPTION_PREFIX = "Prints the versionCode and versionName generated by Android App Versioning plugin (if available) to the console"
}
diff --git a/src/test/kotlin/io/github/reactivecircus/appversioning/GitTagTest.kt b/src/test/kotlin/io/github/reactivecircus/appversioning/GitTagTest.kt
index f0964391..b6aeb1f8 100644
--- a/src/test/kotlin/io/github/reactivecircus/appversioning/GitTagTest.kt
+++ b/src/test/kotlin/io/github/reactivecircus/appversioning/GitTagTest.kt
@@ -30,7 +30,7 @@ class GitTagTest {
}
@Test
- fun `GitTag#toString() returns rawTagName`() {
+ fun `GitTag#toString() returns GitTag#name`() {
val gitTag = GitTag("1.0.0-alpha03", 1, "36e7453")
assertEquals("1.0.0-alpha03", gitTag.toString())
}
diff --git a/src/test/kotlin/io/github/reactivecircus/appversioning/SemVerTest.kt b/src/test/kotlin/io/github/reactivecircus/appversioning/SemVerTest.kt
index 8b8153d2..da976c04 100644
--- a/src/test/kotlin/io/github/reactivecircus/appversioning/SemVerTest.kt
+++ b/src/test/kotlin/io/github/reactivecircus/appversioning/SemVerTest.kt
@@ -3,175 +3,113 @@ package io.github.reactivecircus.appversioning
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
class SemVerTest {
@Test
- fun `SemVer can be converted to integer representation using positional notation`() {
- assertEquals(3, SemVer(0, 0, 3).toInt(maxDigitsPerComponent = 2))
- assertEquals(103, SemVer(0, 1, 3).toInt(maxDigitsPerComponent = 2))
- assertEquals(20103, SemVer(2, 1, 3).toInt(maxDigitsPerComponent = 2))
- assertEquals(99, SemVer(0, 0, 99).toInt(maxDigitsPerComponent = 2))
- assertEquals(9999, SemVer(0, 99, 99).toInt(maxDigitsPerComponent = 2))
- assertEquals(1009999, SemVer(100, 99, 99).toInt(maxDigitsPerComponent = 2))
+ fun `SemVer can be converted to versionCode using positional notation`() {
+ assertEquals(3, SemVer(0, 0, 3).toVersionCode())
+ assertEquals(103, SemVer(0, 1, 3).toVersionCode())
+ assertEquals(20103, SemVer(2, 1, 3).toVersionCode())
+ assertEquals(99, SemVer(0, 0, 99).toVersionCode())
+ assertEquals(9999, SemVer(0, 99, 99).toVersionCode())
+ assertEquals(1009999, SemVer(100, 99, 99).toVersionCode())
+ assertEquals(1002003, SemVer(1, 2, 3).toVersionCode(digitsPerComponent = 3))
}
@Test
- fun `converting SemVer to integer throws exception when maxDigitsPerComponent is not positive`() {
- assertFailsWith { SemVer(1, 2, 3).toInt(-1) }
- assertFailsWith { SemVer(1, 2, 3).toInt(0) }
+ fun `toVersionCode throws exception when digitsPerComponent is not positive`() {
+ assertFailsWith { SemVer(1, 2, 3).toVersionCode(digitsPerComponent = -1) }
+ assertFailsWith { SemVer(1, 2, 3).toVersionCode(digitsPerComponent = 0) }
}
@Test
- fun `converting SemVer to integer throws exception when a version component is out of range`() {
- assertFailsWith { SemVer(1, 2, 10).toInt(1) }
- assertFailsWith { SemVer(1, 10, 3).toInt(1) }
- assertFailsWith { SemVer(1, 2, 100).toInt(2) }
- assertFailsWith { SemVer(1, 100, 3).toInt(2) }
+ fun `toVersionCode throws exception when a version component is out of range`() {
+ assertFailsWith { SemVer(1, 2, 10).toVersionCode(digitsPerComponent = 1) }
+ assertFailsWith { SemVer(1, 10, 3).toVersionCode(digitsPerComponent = 1) }
+ assertFailsWith { SemVer(1, 2, 100).toVersionCode(digitsPerComponent = 2) }
+ assertFailsWith { SemVer(1, 100, 3).toVersionCode(digitsPerComponent = 2) }
}
@Test
- fun `converting SemVer to integer throws exception when result is out of range`() {
- assertEquals(Int.MAX_VALUE, SemVer(0, 0, Int.MAX_VALUE).toInt(maxDigitsPerComponent = 10))
- assertFailsWith { SemVer(0, 1, Int.MAX_VALUE).toInt(10) }
+ fun `toVersionCode throws exception when result is out of range`() {
+ assertEquals(Int.MAX_VALUE, SemVer(0, 0, Int.MAX_VALUE).toVersionCode(digitsPerComponent = 10))
+ assertFailsWith { SemVer(0, 1, Int.MAX_VALUE).toVersionCode(digitsPerComponent = 10) }
}
@Test
- fun `SemVer compliant GitTag can be converted to a SemVer`() {
- assertEquals(
- SemVer(0, 0, 4),
- GitTag("0.0.4", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 2, 3),
- GitTag("1.2.3", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(10, 20, 30),
- GitTag("10.20.30", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 1, 2, "prerelease", "meta"),
- GitTag("1.1.2-prerelease+meta", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 1, 2, buildMetadata = "meta"),
- GitTag("1.1.2+meta", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 1, 2, buildMetadata = "meta-valid"),
- GitTag("1.1.2+meta-valid", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 0, 0, preRelease = "alpha"),
- GitTag("1.0.0-alpha", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 0, 0, preRelease = "beta"),
- GitTag("1.0.0-beta", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 0, 0, preRelease = "alpha.beta"),
- GitTag("1.0.0-alpha.beta", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 0, 0, preRelease = "alpha.beta.1"),
- GitTag("1.0.0-alpha.beta.1", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 0, 0, preRelease = "alpha.1"),
- GitTag("1.0.0-alpha.1", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 0, 0, preRelease = "alpha0.valid"),
- GitTag("1.0.0-alpha0.valid", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 0, 0, preRelease = "alpha.0valid"),
- GitTag("1.0.0-alpha.0valid", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 0, 0, preRelease = "alpha-a.b-c-somethinglong", buildMetadata = "build.1-aef.1-its-okay"),
- GitTag("1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 0, 0, preRelease = "rc.1", buildMetadata = "build.1"),
- GitTag("1.0.0-rc.1+build.1", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 0, 0, preRelease = "alpha.beta.1"),
- GitTag("1.0.0-alpha.beta.1", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(2, 0, 0, preRelease = "rc.1", buildMetadata = "build.123"),
- GitTag("2.0.0-rc.1+build.123", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 2, 3, preRelease = "beta"),
- GitTag("1.2.3-beta", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(10, 2, 3, preRelease = "DEV-SNAPSHOT"),
- GitTag("10.2.3-DEV-SNAPSHOT", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 2, 3, preRelease = "SNAPSHOT-123"),
- GitTag("1.2.3-SNAPSHOT-123", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 0, 0),
- GitTag("1.0.0", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(2, 0, 0),
- GitTag("2.0.0", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 1, 7),
- GitTag("1.1.7", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(2, 0, 0, buildMetadata = "build.1848"),
- GitTag("2.0.0+build.1848", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(2, 0, 1, preRelease = "alpha.1227"),
- GitTag("2.0.1-alpha.1227", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 0, 0, preRelease = "alpha", buildMetadata = "beta"),
- GitTag("1.0.0-alpha+beta", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 2, 3, preRelease = "---RC-SNAPSHOT.12.9.1--.12", buildMetadata = "788"),
- GitTag("1.2.3----RC-SNAPSHOT.12.9.1--.12+788", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 2, 3, preRelease = "---R-S.12.9.1--.12", buildMetadata = "meta"),
- GitTag("1.2.3----R-S.12.9.1--.12+meta", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 2, 3, preRelease = "---RC-SNAPSHOT.12.9.1--.12"),
- GitTag("1.2.3----RC-SNAPSHOT.12.9.1--.12", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 0, 0, buildMetadata = "0.build.1-rc.10000aaa-kk-0.1"),
- GitTag("1.0.0+0.build.1-rc.10000aaa-kk-0.1", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(999999999, 99999999, 9999999),
- GitTag("999999999.99999999.9999999", 0, "9c28ad3").toSemVer()
- )
- assertEquals(
- SemVer(1, 0, 0, preRelease = "0A.is.legal"),
- GitTag("1.0.0-0A.is.legal", 0, "9c28ad3").toSemVer()
- )
+ fun `SemVer#parse and SemVer#parseOrNull parse valid strict SemVer strings`() {
+ val validSemVers = mapOf(
+ "0.0.4" to SemVer(0, 0, 4),
+ "1.2.3" to SemVer(1, 2, 3),
+ "10.20.30" to SemVer(10, 20, 30),
+ "1.1.2-prerelease+meta" to SemVer(1, 1, 2, "prerelease", "meta"),
+ "1.1.2+meta" to SemVer(1, 1, 2, buildMetadata = "meta"),
+ "1.1.2+meta-valid" to SemVer(1, 1, 2, buildMetadata = "meta-valid"),
+ "1.0.0-alpha" to SemVer(1, 0, 0, preRelease = "alpha"),
+ "1.0.0-beta" to SemVer(1, 0, 0, preRelease = "beta"),
+ "1.0.0-alpha.beta" to SemVer(1, 0, 0, preRelease = "alpha.beta"),
+ "1.0.0-alpha.beta.1" to SemVer(1, 0, 0, preRelease = "alpha.beta.1"),
+ "1.0.0-alpha.1" to SemVer(1, 0, 0, preRelease = "alpha.1"),
+ "1.0.0-alpha0.valid" to SemVer(1, 0, 0, preRelease = "alpha0.valid"),
+ "1.0.0-alpha.0valid" to SemVer(1, 0, 0, preRelease = "alpha.0valid"),
+ "1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay" to SemVer(
+ 1,
+ 0,
+ 0,
+ preRelease = "alpha-a.b-c-somethinglong",
+ buildMetadata = "build.1-aef.1-its-okay",
+ ),
+ "1.0.0-rc.1+build.1" to SemVer(1, 0, 0, preRelease = "rc.1", buildMetadata = "build.1"),
+ "2.0.0-rc.1+build.123" to SemVer(2, 0, 0, preRelease = "rc.1", buildMetadata = "build.123"),
+ "1.2.3-beta" to SemVer(1, 2, 3, preRelease = "beta"),
+ "10.2.3-DEV-SNAPSHOT" to SemVer(10, 2, 3, preRelease = "DEV-SNAPSHOT"),
+ "1.2.3-SNAPSHOT-123" to SemVer(1, 2, 3, preRelease = "SNAPSHOT-123"),
+ "1.0.0" to SemVer(1, 0, 0),
+ "2.0.0" to SemVer(2, 0, 0),
+ "1.1.7" to SemVer(1, 1, 7),
+ "2.0.0+build.1848" to SemVer(2, 0, 0, buildMetadata = "build.1848"),
+ "2.0.1-alpha.1227" to SemVer(2, 0, 1, preRelease = "alpha.1227"),
+ "1.0.0-alpha+beta" to SemVer(1, 0, 0, preRelease = "alpha", buildMetadata = "beta"),
+ "1.2.3----RC-SNAPSHOT.12.9.1--.12+788" to SemVer(
+ 1,
+ 2,
+ 3,
+ preRelease = "---RC-SNAPSHOT.12.9.1--.12",
+ buildMetadata = "788",
+ ),
+ "1.2.3----R-S.12.9.1--.12+meta" to SemVer(
+ 1,
+ 2,
+ 3,
+ preRelease = "---R-S.12.9.1--.12",
+ buildMetadata = "meta",
+ ),
+ "1.2.3----RC-SNAPSHOT.12.9.1--.12" to SemVer(1, 2, 3, preRelease = "---RC-SNAPSHOT.12.9.1--.12"),
+ "1.0.0+0.build.1-rc.10000aaa-kk-0.1" to SemVer(
+ 1,
+ 0,
+ 0,
+ buildMetadata = "0.build.1-rc.10000aaa-kk-0.1",
+ ),
+ "999999999.99999999.9999999" to SemVer(999999999, 99999999, 9999999),
+ "1.0.0-0A.is.legal" to SemVer(1, 0, 0, preRelease = "0A.is.legal"),
+ )
+ validSemVers.forEach { (text, expected) ->
+ assertEquals(expected, SemVer.parse(text))
+ assertEquals(expected, SemVer.parseOrNull(text))
+ assertEquals(expected, GitTag(text, 0, "9c28ad3").toSemVer())
+ }
}
@Test
- fun `converting non-SemVer compliant GitTag to SemVer throws exception`() {
+ fun `SemVer#parse throws and SemVer#parseOrNull returns null for non-SemVer strings`() {
listOf(
"1",
"1.2",
+ "v1.2.3",
"1.2.3-0123",
"1.2.3-0123.0123",
"1.1.2+.123",
@@ -200,7 +138,6 @@ class SemVerTest {
"01.1.1",
"1.01.1",
"1.1.01",
- "1.2",
"1.2.3.DEV",
"1.2-SNAPSHOT",
"1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788",
@@ -209,30 +146,57 @@ class SemVerTest {
"+justmeta",
"9.8.7+meta+meta",
"9.8.7-whatever+meta+meta",
- "99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12"
+ "99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12",
).forEach { version ->
+ assertNull(SemVer.parseOrNull(version))
+ assertFailsWith { SemVer.parse(version) }
assertFailsWith { GitTag(version, 0, "9c28ad3").toSemVer() }
}
}
@Test
- fun `SemVer compliant GitTag with a prefix v can be converted to a GitTag when allowPrefixV is true`() {
- assertEquals(SemVer(1, 2, 3), GitTag("v1.2.3", 3, "9c28ad3").toSemVer(allowPrefixV = true))
+ fun `GitTag#toSemVer strips prefix when prefix matches and throws when prefix does not match`() {
+ assertEquals(SemVer(1, 2, 3), GitTag("v1.2.3", 3, "9c28ad3").toSemVer(prefix = "v"))
+ assertEquals(
+ SemVer(1, 2, 3, preRelease = "rc01"),
+ GitTag("android-1.2.3-rc01", 0, "9c28ad3").toSemVer(prefix = "android-"),
+ )
+ assertFailsWith {
+ GitTag("1.2.3", 0, "9c28ad3").toSemVer(prefix = "v")
+ }
+ assertFailsWith {
+ GitTag("ios-1.2.3", 0, "9c28ad3").toSemVer(prefix = "android-")
+ }
}
@Test
- fun `converting SemVer compliant GitTag with a prefix v throws exception when allowPrefixV is false`() {
- assertFailsWith { GitTag("v1.2.3", 3, "9c28ad3").toSemVer(allowPrefixV = false) }
- }
+ fun `SemVer implements Comparable following SemVer 2_0_0 precedence`() {
+ val ordered = listOf(
+ "1.0.0-alpha",
+ "1.0.0-alpha.1",
+ "1.0.0-alpha.beta",
+ "1.0.0-beta",
+ "1.0.0-beta.2",
+ "1.0.0-beta.11",
+ "1.0.0-rc.1",
+ "1.0.0",
+ "2.0.0",
+ "2.1.0",
+ "2.1.1",
+ ).map(SemVer::parse)
- @Test
- fun `SemVer can be created from fromGitTag() companion function`() {
- val gitTag = GitTag("1.2.3", 0, "9c28ad3")
- assertEquals(SemVer.fromGitTag(gitTag), gitTag.toSemVer())
+ for (i in 0.. ordered[i], "Expected ${ordered[i + 1]} > ${ordered[i]}")
+ }
+
+ // Build metadata does not affect version precedence
+ assertEquals(0, SemVer.parse("1.0.0+build1").compareTo(SemVer.parse("1.0.0+build2")))
+ assertNotEquals(SemVer.parse("1.0.0+build1"), SemVer.parse("1.0.0+build2"))
}
@Test
- fun `SemVer#toString() returns the SemVer in string representation`() {
+ fun `SemVer#toString() returns the SemVer in canonical string representation`() {
listOf(
"0.0.4",
"1.2.3",
@@ -264,9 +228,9 @@ class SemVerTest {
"1.2.3----RC-SNAPSHOT.12.9.1--.12",
"1.0.0+0.build.1-rc.10000aaa-kk-0.1",
"999999999.99999999.9999999",
- "1.0.0-0A.is.legal"
+ "1.0.0-0A.is.legal",
).forEach { version ->
- assertEquals(version, GitTag(version, 0, "9c28ad3").toSemVer().toString())
+ assertEquals(version, SemVer.parse(version).toString())
}
}
}
diff --git a/src/test/kotlin/io/github/reactivecircus/appversioning/VersionSpecsTest.kt b/src/test/kotlin/io/github/reactivecircus/appversioning/VersionSpecsTest.kt
new file mode 100644
index 00000000..c144b22c
--- /dev/null
+++ b/src/test/kotlin/io/github/reactivecircus/appversioning/VersionSpecsTest.kt
@@ -0,0 +1,67 @@
+package io.github.reactivecircus.appversioning
+
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class VersionSpecsTest {
+ private val variant = VariantInfo(buildType = "release", flavorName = "", variantName = "release")
+
+ @Test
+ fun `VersionCodeSpec#SemVer converts SemVer compliant tag to integer using positional notation`() {
+ assertEquals(1, evaluateVersionCode("0.0.1"))
+ assertEquals(100, evaluateVersionCode("0.1.0"))
+ assertEquals(10000, evaluateVersionCode("1.0.0"))
+ assertEquals(10099, evaluateVersionCode("1.0.99"))
+ assertEquals(19999, evaluateVersionCode("1.99.99"))
+ assertEquals(10203, evaluateVersionCode("1.2.3-alpha01+build.567"))
+ assertEquals(30000, evaluateVersionCode("v3.0.0-SNAPSHOT"))
+ }
+
+ @Test
+ fun `VersionCodeSpec#SemVer fails when tag is not SemVer compliant`() {
+ val exception = assertFailsWith {
+ evaluateVersionCode("1.2")
+ }
+ assertEquals(
+ """
+ Could not generate versionCode as "1.2" does not follow semantic versioning.
+ Please either ensure latest git tag follows semantic versioning, or provide a custom spec for generating versionCode using the `versionCode` property.
+ """.trimIndent(),
+ exception.message
+ )
+ }
+
+ @Test
+ fun `VersionCodeSpec#SemVer fails when SemVer component exceeds maximum digits`() {
+ val exception = assertFailsWith {
+ evaluateVersionCode("1.2.100")
+ }
+ assertEquals(
+ """
+ Could not generate versionCode from "1.2.100" as the SemVer cannot be represented as an Integer.
+ This is usually because MAJOR or MINOR version is greater than 99, as by default maximum of 2 digits is allowed for MINOR and PATCH components of a SemVer tag.
+ Another reason might be that the overall positional notation of the SemVer (MAJOR * 10000 + MINOR * 100 + PATCH) is greater than the maximum value of an integer (2147483647).
+ As a workaround you can provide a custom spec for generating versionCode using the `versionCode` property.
+ """.trimIndent(),
+ exception.message
+ )
+ }
+
+ @Test
+ fun `VersionNameSpec#TagName returns GitTag#name`() {
+ assertEquals("0.0.1", evaluateVersionName("0.0.1"))
+ assertEquals("v1.1.0-alpha01", evaluateVersionName("v1.1.0-alpha01"))
+ assertEquals("alpha", evaluateVersionName("alpha"))
+ }
+
+ private fun evaluateVersionCode(tagName: String, spec: VersionCodeSpec = VersionCodeSpec.SemVer): Int {
+ val context = DefaultVersionContext(GitTag(tagName, 0, "abcd123"), variant)
+ return with(spec) { context.versionCode() }
+ }
+
+ private fun evaluateVersionName(tagName: String, spec: VersionNameSpec = VersionNameSpec.TagName): String {
+ val context = DefaultVersionContext(GitTag(tagName, 0, "abcd123"), variant)
+ return with(spec) { context.versionName() }
+ }
+}