From d9e40c6d8e8a39d32429a6e1f297a925594a9541 Mon Sep 17 00:00:00 2001 From: Yang Date: Fri, 23 Oct 2026 16:31:34 +1100 Subject: [PATCH 01/11] 2.0 redesign. - New `versionCode` and and `versionName` APIs based on `fun interface` and `VersionContext` receiver. - Remove `ProviderFactory` from lambda param. - `SemVer` - new `parse`, `parseOrNull`, `toVersionCode(digitsPerComponent)` APIs. - `GitTag` - new `toSemVer(prefix)` API. - Move data classes to regular classes in public APIs. --- api/app-versioning.api | 99 +++-- gradle.properties | 2 +- .../AppVersioningPluginIntegrationTest.kt | 150 +++----- .../appversioning/fixtures/FixtureRunner.kt | 13 +- .../fixtures/ProjectTemplates.kt | 33 +- .../tasks/GenerateAppVersionInfoTest.kt | 345 +++++++++++------- .../tasks/PrintAppVersionInfoTest.kt | 51 +-- .../appversioning/AppVersioningExtension.kt | 84 ++--- .../appversioning/AppVersioningPlugin.kt | 8 +- .../reactivecircus/appversioning/GitTag.kt | 32 +- .../reactivecircus/appversioning/SemVer.kt | 141 ++++--- .../appversioning/VariantInfo.kt | 5 +- .../appversioning/VersionContext.kt | 21 ++ .../appversioning/VersionSpecs.kt | 109 ++++++ .../appversioning/internal/GitClient.kt | 3 +- .../tasks/GenerateAppVersionInfo.kt | 177 +-------- .../tasks/PrintAppVersionInfo.kt | 2 +- .../appversioning/SemVerTest.kt | 288 +++++++-------- .../appversioning/VersionSpecsTest.kt | 67 ++++ 19 files changed, 851 insertions(+), 779 deletions(-) create mode 100644 src/main/kotlin/io/github/reactivecircus/appversioning/VersionContext.kt create mode 100644 src/main/kotlin/io/github/reactivecircus/appversioning/VersionSpecs.kt create mode 100644 src/test/kotlin/io/github/reactivecircus/appversioning/VersionSpecsTest.kt diff --git a/api/app-versioning.api b/api/app-versioning.api index 5fdaccc1..7f1ae373 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 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,7 +63,27 @@ 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/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 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/VersionNameSpec$Companion { + public final fun getTagName ()Lio/github/reactivecircus/appversioning/VersionNameSpec; } public final class io/github/reactivecircus/appversioning/internal/CommitId { @@ -120,35 +119,22 @@ public final class io/github/reactivecircus/appversioning/internal/GitClient$Com } 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 fun ()V public final fun generate ()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; @@ -157,6 +143,3 @@ public abstract class io/github/reactivecircus/appversioning/tasks/PrintAppVersi public final fun print ()V } -public final class io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfo$Companion { -} - 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/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt index fd741731..648e490f 100644 --- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt +++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt @@ -8,8 +8,6 @@ import io.github.reactivecircus.appversioning.fixtures.BuildScriptLanguage import io.github.reactivecircus.appversioning.fixtures.LibraryProjectTemplate 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 @@ -33,7 +31,7 @@ class AppVersioningPluginIntegrationTest( withFixtureRunner( fixtureDir = fixtureDir, - subprojects = listOf(LibraryProjectTemplate()) + subprojects = listOf(LibraryProjectTemplate(buildScriptLanguage = buildScriptLanguage)) ).runAndExpectFailure( "help" ) { @@ -52,7 +50,7 @@ class AppVersioningPluginIntegrationTest( withFixtureRunner( fixtureDir = fixtureDir, - subprojects = listOf(AppProjectTemplate()) + subprojects = listOf(AppProjectTemplate(buildScriptLanguage = buildScriptLanguage)) ).runAndCheckResult( "tasks", "--group=versioning" @@ -60,22 +58,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." ) ) } @@ -96,42 +94,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." ) ) } @@ -148,70 +146,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." ) ) } @@ -228,7 +182,12 @@ class AppVersioningPluginIntegrationTest( """.trimIndent() withFixtureRunner( fixtureDir = fixtureDir, - subprojects = listOf(AppProjectTemplate(pluginExtension = extension)) + subprojects = listOf( + AppProjectTemplate( + buildScriptLanguage = buildScriptLanguage, + pluginExtension = extension, + ) + ) ).runAndCheckResult( "tasks", "--group=versioning" @@ -260,38 +219,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( - fixtureDir.root, - "app/build/outputs/app_versioning/release/version_code.txt" - ).readText() - val versionNameFileContent = File( + val apkOutputMetadata = 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,11 +232,13 @@ 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`() { + fun `plugin generates versionCode and versionName for all assembled APKs when splits-APKs and universal APK are enabled`() { GitClient.initialize(fixtureDir.root).apply { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) @@ -332,6 +264,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,6 +277,12 @@ 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() + ) } } @@ -375,6 +317,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 +329,8 @@ class AppVersioningPluginIntegrationTest( assertFalse(versionCodeFile.exists()) assertFalse(versionNameFile.exists()) + assertFalse(apkOutputMetadata.contains("\"versionCode\": 10000")) + assertFalse(apkOutputMetadata.contains("\"versionName\": \"1.0.0\"")) } } } 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..4712c49b 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/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/ProjectTemplates.kt b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/ProjectTemplates.kt index 643822c4..b45eee30 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() @@ -29,18 +42,10 @@ fun settingsFileContent( ${subprojects.map { it.projectName }.joinToString("\n") { "include(\":$it\")" }} """.trimIndent() -fun gradlePropertiesFileContent(enableConfigurationCache: Boolean): String { - val configurationCacheProperties = if (enableConfigurationCache) { - """ - org.gradle.isolated-projects=true - """.trimIndent() - } else { - "" - } - return """ - $configurationCacheProperties - """.trimIndent() -} +val gradlePropertiesFileContent = """ + org.gradle.configuration-cache=true + org.gradle.isolated-projects=true +""".trimIndent() enum class BuildScriptLanguage { Kts, 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..c18ed77e 100644 --- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt +++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt @@ -59,6 +59,30 @@ 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 { @@ -88,63 +112,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`() { + GitClient.initialize(fixtureDir.root).apply { 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 remoteGitClient = GitClient.initialize(remoteFolder.root) + val commitId = remoteGitClient.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) + remoteGitClient.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 gitClient = GitClient.initialize(fixtureDir.root).apply { + 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 +201,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 = gitClient.commit(message = "2nd commit.") + gitClient.tag(name = "v3.0.0-SNAPSHOT", message = "2nd tag", commitId = commitId2) runner.runAndCheckResult( "generateAppVersionInfoForRelease" @@ -163,7 +212,7 @@ class GenerateAppVersionInfoTest { } @Test - fun `GenerateAppVersionInfo fails when latest git tag is not SemVer-compliant and no custom versionCode generation rule is provided`() { + fun `GenerateAppVersionInfo fails when latest git tag is not SemVer-compliant and no custom versionCode spec is provided`() { GitClient.initialize(fixtureDir.root).apply { val commitId = commit(message = "1st commit.") tag(name = "1.2", message = "1st tag", commitId = commitId) @@ -180,7 +229,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,7 +237,7 @@ 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`() { + 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`() { GitClient.initialize(fixtureDir.root).apply { val commitId = commit(message = "1st commit.") tag(name = "1.2.100", message = "1st tag", commitId = commitId) @@ -207,7 +256,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() ) ) @@ -233,7 +282,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,13 +290,13 @@ 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`() { + fun `GenerateAppVersionInfo generates custom versionCode when custom versionCode spec is provided`() { GitClient.initialize(fixtureDir.root).apply { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) @@ -256,15 +305,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 +330,16 @@ class GenerateAppVersionInfoTest { } @Test - fun `GenerateAppVersionInfo fails when converting a non-SemVer compliant git tag to SemVer in a custom versionCode generation rule`() { + fun `GenerateAppVersionInfo fails when converting a non-SemVer compliant git tag to SemVer in a custom versionCode spec`() { GitClient.initialize(fixtureDir.root).apply { 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,12 +351,12 @@ 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`() { + fun `GenerateAppVersionInfo generates versionName directly from the latest git tag when no custom versionName spec is provided`() { val gitClient = GitClient.initialize(fixtureDir.root).apply { val commitId = commit(message = "1st commit.") tag(name = "0.0.1", message = "1st tag", commitId = commitId) @@ -318,7 +366,7 @@ class GenerateAppVersionInfoTest { val extensions = """ appVersioning { - overrideVersionCode { _, _, _ -> 0 } + versionCode.set { 0 } } """.trimIndent() @@ -353,7 +401,7 @@ class GenerateAppVersionInfoTest { } @Test - fun `GenerateAppVersionInfo generates custom versionName when custom versionName generation rule is provided`() { + fun `GenerateAppVersionInfo generates custom versionName when custom versionName spec is provided`() { GitClient.initialize(fixtureDir.root).apply { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) @@ -361,10 +409,10 @@ class GenerateAppVersionInfoTest { 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() @@ -404,12 +452,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() @@ -469,44 +515,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()) @@ -578,12 +620,12 @@ 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.") @@ -605,8 +647,8 @@ 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.") @@ -627,8 +669,8 @@ 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 @@ -675,7 +717,7 @@ class GenerateAppVersionInfoTest { } @Test - fun `overrideVersionCode and overrideVersionName configurations work in groovy`() { + fun `versionCode and versionName configurations work in groovy`() { GitClient.initialize(fixtureDir.root).apply { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) @@ -693,21 +735,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.rawTagName}${suffix}" } } """.trimIndent() @@ -740,7 +775,63 @@ class GenerateAppVersionInfoTest { } @Test - fun `GenerateAppVersionInfo is incremental without custom versionCode and versionName generation rules`() { + fun `versionCode and versionName configurations with external Provider work in groovy`() { + GitClient.initialize(fixtureDir.root).apply { + 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.rawTagName}" + } + } + """.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`() { GitClient.initialize(fixtureDir.root).apply { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) @@ -765,22 +856,20 @@ class GenerateAppVersionInfoTest { } @Test - fun `GenerateAppVersionInfo is incremental with custom versionCode and versionName generation rules`() { + fun `GenerateAppVersionInfo is incremental with custom versionCode and versionName specs`() { GitClient.initialize(fixtureDir.root).apply { 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() @@ -804,7 +893,7 @@ class GenerateAppVersionInfoTest { } @Test - fun `GenerateAppVersionInfo is cacheable without custom versionCode and versionName generation rules`() { + fun `GenerateAppVersionInfo is cacheable without custom versionCode and versionName specs`() { GitClient.initialize(fixtureDir.root).apply { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) @@ -833,22 +922,20 @@ class GenerateAppVersionInfoTest { } @Test - fun `GenerateAppVersionInfo is cacheable with custom versionCode and versionName generation rules`() { + fun `GenerateAppVersionInfo is cacheable with custom versionCode and versionName specs`() { GitClient.initialize(fixtureDir.root).apply { 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() 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..7aecd4e3 100644 --- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfoTest.kt +++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfoTest.kt @@ -76,56 +76,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`() { + fun `PrintAppVersionInfo prints latest generated versionCode and versionName (or warning message if not available) to the console for each project with AppVersioningPlugin applied`() { GitClient.initialize(fixtureDir.root).apply { 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 +154,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." diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningExtension.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningExtension.kt index 81c01cba..8affb562 100644 --- a/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningExtension.kt +++ b/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningExtension.kt @@ -1,9 +1,9 @@ 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]. @@ -52,79 +52,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..29d60cc2 100644 --- a/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningPlugin.kt +++ b/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningPlugin.kt @@ -76,10 +76,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") ) @@ -147,8 +145,6 @@ class AppVersioningPlugin : Plugin { private fun String.capitalize(): String { return replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } } - - companion object } 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..aab173f4 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( +class GitTag internal constructor( val rawTagName: String, val commitsSinceLatestTag: Int, - val commitHash: String + val commitHash: String, ) { - override fun toString() = rawTagName + /** + * Parses this tag's [rawTagName] into a [SemVer], stripping [prefix] first if non-empty. + * + * @param prefix expected prefix before the semantic version (default is `""`). + * @throws [IllegalArgumentException] when [rawTagName] does not start with [prefix] or the remaining string is not a valid [SemVer]. + */ + @JvmOverloads + fun toSemVer(prefix: String = ""): SemVer { + require(rawTagName.startsWith(prefix)) { + "Tag \"$rawTagName\" does not start with expected prefix \"$prefix\"." + } + return SemVer.parse(rawTagName.removePrefix(prefix)) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GitTag) return false + return rawTagName == other.rawTagName && + commitsSinceLatestTag == other.commitsSinceLatestTag && + commitHash == other.commitHash + } + + override fun hashCode(): Int = Objects.hash(rawTagName, commitsSinceLatestTag, commitHash) + + override fun toString(): String = rawTagName } /** 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..fa5cc6ac 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..9dfc6936 --- /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.rawTagName } + } +} + +private fun GitTag.toVersionCodeBySemVer(): Int { + val prefix = if (rawTagName.startsWith("v")) "v" else "" + val semVer = checkNotNull(runCatching { toSemVer(prefix = prefix) }.getOrNull()) { + """ + Could not generate versionCode as "$rawTagName" 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 "$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 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/GitClient.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/internal/GitClient.kt index a624f112..da72bf65 100644 --- a/src/main/kotlin/io/github/reactivecircus/appversioning/internal/GitClient.kt +++ b/src/main/kotlin/io/github/reactivecircus/appversioning/internal/GitClient.kt @@ -5,7 +5,8 @@ import java.util.concurrent.TimeUnit class GitClient private constructor(private val projectDir: File) { fun listLocalTags(): List { - return listOf("git", "tag", "--list").execute(projectDir).lines() + val output = listOf("git", "tag", "--list").execute(projectDir) + return if (output.isEmpty()) emptyList() else output.lines() } fun fetchRemoteTags() { 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..c668f487 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.VersionCodeSpec +import io.github.reactivecircus.appversioning.VersionNameSpec import io.github.reactivecircus.appversioning.internal.GitClient 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 @@ -92,68 +72,6 @@ abstract class GenerateAppVersionInfo @Inject constructor( @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 - 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." } @@ -174,92 +92,33 @@ private abstract class GenerateAppVersionInfoWorkAction @Inject constructor( } ?: 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..a5aa5ac7 100644 --- a/src/main/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfo.kt +++ b/src/main/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfo.kt @@ -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/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..5e88443d --- /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 rawTagName`() { + 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() } + } +} From 87fc4b96e6cd92c49ab5bf05cb9d54ebd381139e Mon Sep 17 00:00:00 2001 From: Yang Date: Fri, 23 Oct 2026 16:33:35 +1100 Subject: [PATCH 02/11] Update README. --- README.md | 312 +++++++++-------------------------------- docs/images/sample.png | Bin 118938 -> 0 bytes 2 files changed, 64 insertions(+), 248 deletions(-) delete mode 100644 docs/images/sample.png diff --git a/README.md b/README.md index 8287e6f4..a04905a3 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,7 @@ A Gradle Plugin for lazily generating Android app's `versionCode` & `versionName` from Git tags. -![App Versioning](docs/images/sample.png) - -Android Gradle Plugin 4.0 and 4.1 introduced some [new experimental APIs](https://medium.com/androiddevelopers/new-apis-in-the-android-gradle-plugin-f5325742e614) to support **lazily** computing and setting the `versionCode` and `versionName` for an APK or App Bundle. This plugin builds on top of these APIs to support the common app versioning use cases based on Git. - -This [blogpost](https://dev.to/ychescale9/git-based-android-app-versioning-with-agp-4-0-24ip) should provide more context around Git-based app versioning in general and why this plugin needs to build on top of the new variant APIs introduced in AGP 4.0 / 4.1 which are currently **incubating**. +Android Gradle Plugin 4.0 and 4.1 introduced [new 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 common Git-based app versioning use cases with full Configuration Cache and Isolated Projects support. ## Android Gradle Plugin version compatibility @@ -26,7 +22,7 @@ Version `0.10.0` of the plugin is the final version that's compatible with AGP * ## 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 +33,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 @@ -76,20 +61,20 @@ plugins { 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 (`VersionCodeSpec.SemVer`), 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` (or `v1.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` (`VersionNameSpec.TagName`) is the raw name of the latest Git tag. ``` > Task :app:generateAppVersionInfoForRelease @@ -97,44 +82,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 with `this: VersionContext` (`tag`, `variant`) } - overrideVersionName { gitTag, providers, variantInfo -> - // TODO generate a String from the given gitTag, providers, build variant + versionName.set { + // generate a String with `this: VersionContext` (`tag`, `variant`) } } ``` -`GitTag` is a type-safe representation of a tag encapsulating the `rawTagName`, `commitsSinceLatestTag` and `commitHash`, provided by the plugin. - -`providers` is a `ProviderFactory` instance which is a Gradle API that can be useful for [reading environment variables and system properties lazily](https://docs.gradle.org/current/javadoc/org/gradle/api/provider/ProviderFactory.html). - -`VariantInfo` is an object that encapsulates the build variant information including `buildType`, `flavorName`, and `variantName`. +Inside the lambda receiver (`VersionContext`): +- `tag` (`GitTag`) encapsulates `rawTagName`, `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. +By 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), strip a custom tag prefix, or add an offset / commit count:
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) + tag.commitsSinceLatestTag } } ``` @@ -144,109 +123,40 @@ 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.commitsSinceLatestTag } } ```
+`tag.toSemVer(prefix = "")` parses `GitTag.rawTagName` (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. +#### Combining variant info and external providers -If a Git tag is not fully [SemVer compliant](https://semver.org/#semantic-versioning-specification-semver) (e.g. `1.2`), calling `gitTag.toSemVer()` will throw an exception. In that case we'll need to find another way to compute the `versionCode`. - -#### Using timestamp for version code - -Since the key characteristic for `versionCode` is that it must **monotonically increase** with each app release, a common approach is to use the Epoch / Unix timestamp for `versionCode`: +Because `versionCode` and `versionName` are standard Gradle `Property` and `Property` instances, you can map external `Provider`s (such as `providers.environmentVariable(...)` or `providers.gradleProperty(...)`) directly into a spec so Gradle tracks them as task inputs:
Kotlin ```kt -import java.time.Instant -appVersioning { - overrideVersionCode { _, _, _ -> - Instant.now().epochSecond.toInt() - } -} -``` - -
- -
Groovy - -```groovy -appVersioning { - overrideVersionCode { gitTag, providers, variantInfo -> - Instant.now().epochSecond.intValue() - } -} -``` - -
- - -This will generate a monotonically increasing version code every time the `generateAppVersionInfoForRelease` task is run: - -``` -Generated app version code: 1599750437. -``` - -#### Using environment variable - -We can also add a `BUILD_NUMBER` environment variable provided by CI to the `versionCode` or `versionName`. To do this, use the `providers` lambda parameter to create a provider that's only queried during execution: - -
Kotlin - -```kt -import io.github.reactivecircus.appversioning.toSemVer -appVersioning { - overrideVersionCode { gitTag, providers, _ -> - val buildNumber = providers - .environmentVariable("BUILD_NUMBER") - .getOrElse("0").toInt() - val semVer = gitTag.toSemVer() - semVer.major * 10000 + semVer.minor * 100 + semVer.patch + buildNumber - } -} -``` - -
- -
Groovy +import io.github.reactivecircus.appversioning.VersionCodeSpec -```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 - } -} -``` +val buildNumber = providers.environmentVariable("BUILD_NUMBER").map(String::toInt).orElse(0) -
- -`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})" + versionCode.set( + buildNumber.map { buildNum -> + VersionCodeSpec { + val offset = if (variant.flavorName == "paid") 1 else 0 + tag.toSemVer().toVersionCode() + buildNum + offset + } + } + ) + versionName.set { + val suffix = if (variant.isDebugBuild) " (${variant.variantName})" else "" + "${tag.rawTagName} (${tag.commitHash})$suffix" } } ``` @@ -256,69 +166,22 @@ appVersioning {
Groovy ```groovy -appVersioning { - overrideVersionName { gitTag, providers, variantInfo -> - // a custom versionName combining the tag name, commitHash and an environment variable - def buildNumber = providers - .environmentVariable("BUILD_NUMBER") - .getOrElse("0") as Integer - "${gitTag.rawTagName} - #$buildNumber (${gitTag.commitHash})".toString() - } -} -``` - -
+import io.github.reactivecircus.appversioning.VersionCodeSpec -#### Custom rules based on build variants +def buildNumber = providers.environmentVariable("BUILD_NUMBER").map { it.toInteger() }.orElse(0) -Sometimes you might want to customize `versionCode` or `versionName` based on the build variants (product flavor, build type). To do this, use the `variantInfo` lambda parameter to query the build variant information when generating custom `versionCode` or `verrsionName`: - -
Kotlin - -```kt -import io.github.reactivecircus.appversioning.toSemVer appVersioning { - overrideVersionCode { gitTag, _, _ -> - // add 1 to the versionCode for builds with the "paid" product flavor - val offset = if (variantInfo.flavorName == "paid") 1 else 0 - val semVer = gitTag.toSemVer() - semVer.major * 10000 + semVer.minor * 100 + semVer.patch + offset - } - overrideVersionName { gitTag, _, variantInfo -> - // append build variant to the versionName for debug builds - val suffix = if (variantInfo.isDebugBuild) " (${variantInfo.variantName})" else "" - gitTag.toString() + suffix - } -} -``` - -
- -
Groovy - -```groovy -import io.github.reactivecircus.appversioning.SemVer -appVersioning { - overrideVersionCode { gitTag, providers, variantInfo -> - // add 1 to the versionCode for builds with the "paid" product flavor - def offset - if (variantInfo.flavorName == "paid") { - offset = 1 - } else { - offset = 0 + versionCode.set( + buildNumber.map { buildNum -> + { + def offset = variant.flavorName == "paid" ? 1 : 0 + tag.toSemVer().toVersionCode() + buildNum + offset + } as VersionCodeSpec } - def semVer = SemVer.fromGitTag(gitTag) - semVer.major * 10000 + semVer.minor * 100 + semVer.patch + offset - } - overrideVersionName { gitTag, providers, variantInfo -> - // append build variant to the versionName for debug builds - def suffix - if (variantInfo.debugBuild == true) { - suffix = " (" + variantInfo.variantName + ")" - } else { - suffix = "" - } - gitTag.toString() + suffix + ) + versionName { + def suffix = variant.debugBuild ? " (${variant.variantName})" : "" + "${tag.rawTagName} (${tag.commitHash})${suffix}" } } ``` @@ -329,23 +192,7 @@ appVersioning { By default the plugin uses the latest tag in the current branch for `versionCode` and `versionName` generation. -Sometimes it's useful to be able to use the latest tag that follows a specific glob pattern. - -For example a codebase might build and publish 3 different apps separately using the following tag pattern: - -`..[-]+` - -where `app-identifier` is the build metadata component in **SemVer**. - -Some of the possible tags are: - -``` -1.5.8+app-a -2.29.0-rc01+app-b -10.87.9-alpha04+app-c -``` - -To configure the plugin to generate version info specific to `app-b`: +To use the latest tag matching a specific glob pattern (for example `..[-]+` in a monorepo): ```kt appVersioning { @@ -357,38 +204,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 +235,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 +275,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/docs/images/sample.png b/docs/images/sample.png deleted file mode 100644 index cc4a010a86777b0e111c5904b9dfebd9679a9f4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118938 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 z�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 Y_CLFq@O1CZ9l*~uBXh&D%Xee{A0D Date: Fri, 23 Oct 2026 18:40:53 +1100 Subject: [PATCH 03/11] Simplify functional test configs with testkit, split Git APIs for prod and functional tests, clean up public APIs. --- api/app-versioning.api | 34 ------ build.gradle.kts | 37 +++--- gradle/libs.versions.toml | 3 + .../AppVersioningPluginIntegrationTest.kt | 18 +-- .../appversioning/fixtures/GitRepo.kt} | 56 ++------- .../tasks/GenerateAppVersionInfoTest.kt | 108 +++++++++--------- .../tasks/PrintAppVersionInfoTest.kt | 10 +- .../appversioning/AppVersioningExtension.kt | 1 - .../appversioning/VariantInfo.kt | 2 +- .../appversioning/VersionSpecs.kt | 4 +- .../appversioning/internal/GitOps.kt | 52 +++++++++ .../tasks/GenerateAppVersionInfo.kt | 21 ++-- .../tasks/PrintAppVersionInfo.kt | 2 +- 13 files changed, 164 insertions(+), 184 deletions(-) rename src/{main/kotlin/io/github/reactivecircus/appversioning/internal/GitClient.kt => functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/GitRepo.kt} (51%) create mode 100644 src/main/kotlin/io/github/reactivecircus/appversioning/internal/GitOps.kt diff --git a/api/app-versioning.api b/api/app-versioning.api index 7f1ae373..e263a356 100644 --- a/api/app-versioning.api +++ b/api/app-versioning.api @@ -86,41 +86,8 @@ public final class io/github/reactivecircus/appversioning/VersionNameSpec$Compan public final fun getTagName ()Lio/github/reactivecircus/appversioning/VersionNameSpec; } -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/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 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 abstract class io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfo : org/gradle/api/DefaultTask { public fun ()V - public final fun generate ()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; @@ -140,6 +107,5 @@ public abstract class io/github/reactivecircus/appversioning/tasks/PrintAppVersi 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 } diff --git a/build.gradle.kts b/build.gradle.kts index 682edd7d..8a985b4a 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,18 @@ 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 } -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 +82,15 @@ val fixtureAgpVersion: Provider = providers dependencies { compileOnly(libs.agp.build.api) - testImplementation(kotlin("test")) + testImplementation(kotlin("test-junit")) + functionalTestImplementation(kotlin("test-junit")) + functionalTestImplementation(libs.burst) fixtureClasspath(libs.agp.build.gradle.flatMap { dependency -> fixtureAgpVersion.map { version -> "${dependency.group}:${dependency.name}:$version" } }) + detektPlugins(libs.plugin.detektKtlintWrapper) } detekt { @@ -116,6 +108,3 @@ tasks.withType().configureEach { html.outputLocation.set(file("build/reports/detekt/${project.name}.html")) } } - - -dependencies.add("detektPlugins", libs.plugin.detektKtlintWrapper.get()) 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 648e490f..1b56a618 100644 --- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt +++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt @@ -6,8 +6,8 @@ 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.LibraryProjectTemplate +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 @@ -27,7 +27,7 @@ class AppVersioningPluginIntegrationTest( @Test fun `plugin cannot be applied to project without Android App plugin`() { - GitClient.initialize(fixtureDir.root) + initGitRepo(fixtureDir.root) withFixtureRunner( fixtureDir = fixtureDir, @@ -46,7 +46,7 @@ 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, @@ -81,7 +81,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( @@ -137,7 +137,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 { @@ -173,7 +173,7 @@ class AppVersioningPluginIntegrationTest( @Test fun `plugin tasks are not registered when plugin is disabled`() { - GitClient.initialize(fixtureDir.root) + initGitRepo(fixtureDir.root) val extension = """ appVersioning { @@ -200,7 +200,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) } @@ -239,7 +239,7 @@ class AppVersioningPluginIntegrationTest( @Test fun `plugin generates versionCode and versionName for all assembled APKs when splits-APKs and universal APK are enabled`() { - GitClient.initialize(fixtureDir.root).apply { + initGitRepo(fixtureDir.root) { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) } @@ -288,7 +288,7 @@ class AppVersioningPluginIntegrationTest( @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) } 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 51% 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 da72bf65..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,30 +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 { - val output = listOf("git", "tag", "--list").execute(projectDir) - return if (output.isEmpty()) emptyList() else output.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 { @@ -37,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) { @@ -55,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) { @@ -64,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/tasks/GenerateAppVersionInfoTest.kt b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt index c18ed77e..3697e402 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,8 @@ 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.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 @@ -85,7 +85,7 @@ class GenerateAppVersionInfoTest { @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) } @@ -113,7 +113,7 @@ class GenerateAppVersionInfoTest { @Test fun `GenerateAppVersionInfo generates versionCode and versionName when bareGitRepoDirectory is provided`() { - GitClient.initialize(fixtureDir.root).apply { + initGitRepo(fixtureDir.root) { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) } @@ -144,14 +144,14 @@ class GenerateAppVersionInfoTest { fun `GenerateAppVersionInfo fetches remote tags when fetchTagsWhenNoneExistsLocally is enabled and no local tags exist`() { val remoteFolder = TemporaryFolder().apply { create() } try { - val remoteGitClient = GitClient.initialize(remoteFolder.root) - val commitId = remoteGitClient.commit(message = "1st commit.") + val remoteGitRepo = initGitRepo(remoteFolder.root) + val commitId = remoteGitRepo.commit(message = "1st commit.") ProcessBuilder("git", "clone", "--no-tags", remoteFolder.root.absolutePath, fixtureDir.root.absolutePath) .start() .waitFor() - remoteGitClient.tag(name = "1.2.3", message = "remote tag", commitId = commitId) + remoteGitRepo.tag(name = "1.2.3", message = "remote tag", commitId = commitId) val extension = """ appVersioning { @@ -183,7 +183,7 @@ 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 spec is provided`() { - val gitClient = GitClient.initialize(fixtureDir.root).apply { + val gitRepo = initGitRepo(fixtureDir.root) { val commitId = commit(message = "1st commit.") tag(name = "1.2.3-alpha01+build.567", message = "1st tag", commitId = commitId) } @@ -201,8 +201,8 @@ class GenerateAppVersionInfoTest { assertEquals("10203", versionCodeFile.readText()) } - val commitId2 = gitClient.commit(message = "2nd commit.") - gitClient.tag(name = "v3.0.0-SNAPSHOT", message = "2nd tag", commitId = commitId2) + val commitId2 = gitRepo.commit(message = "2nd commit.") + gitRepo.tag(name = "v3.0.0-SNAPSHOT", message = "2nd tag", commitId = commitId2) runner.runAndCheckResult( "generateAppVersionInfoForRelease" @@ -213,7 +213,7 @@ class GenerateAppVersionInfoTest { @Test fun `GenerateAppVersionInfo fails when latest git tag is not SemVer-compliant and no custom versionCode spec is provided`() { - GitClient.initialize(fixtureDir.root).apply { + initGitRepo(fixtureDir.root) { val commitId = commit(message = "1st commit.") tag(name = "1.2", message = "1st tag", commitId = commitId) } @@ -238,7 +238,7 @@ 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 spec is provided`() { - GitClient.initialize(fixtureDir.root).apply { + initGitRepo(fixtureDir.root) { val commitId = commit(message = "1st commit.") tag(name = "1.2.100", message = "1st tag", commitId = commitId) } @@ -265,7 +265,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.") } @@ -297,7 +297,7 @@ class GenerateAppVersionInfoTest { @Test fun `GenerateAppVersionInfo generates custom versionCode when custom versionCode spec is provided`() { - GitClient.initialize(fixtureDir.root).apply { + initGitRepo(fixtureDir.root) { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) } @@ -331,7 +331,7 @@ class GenerateAppVersionInfoTest { @Test fun `GenerateAppVersionInfo fails when converting a non-SemVer compliant git tag to SemVer in a custom versionCode spec`() { - GitClient.initialize(fixtureDir.root).apply { + initGitRepo(fixtureDir.root) { val commitId = commit(message = "1st commit.") tag(name = "1.2", message = "1st tag", commitId = commitId) } @@ -357,7 +357,7 @@ class GenerateAppVersionInfoTest { @Test fun `GenerateAppVersionInfo generates versionName directly from the latest git tag when no custom versionName spec is provided`() { - val gitClient = GitClient.initialize(fixtureDir.root).apply { + val gitRepo = initGitRepo(fixtureDir.root) { val commitId = commit(message = "1st commit.") tag(name = "0.0.1", message = "1st tag", commitId = commitId) } @@ -381,8 +381,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" @@ -390,8 +390,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" @@ -402,7 +402,7 @@ class GenerateAppVersionInfoTest { @Test fun `GenerateAppVersionInfo generates custom versionName when custom versionName spec is provided`() { - GitClient.initialize(fixtureDir.root).apply { + initGitRepo(fixtureDir.root) { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) } @@ -429,7 +429,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) } @@ -493,7 +493,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) } @@ -557,7 +557,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.") } @@ -609,8 +609,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" @@ -628,12 +628,12 @@ class GenerateAppVersionInfoTest { 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" @@ -651,11 +651,11 @@ class GenerateAppVersionInfoTest { 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" @@ -674,8 +674,8 @@ class GenerateAppVersionInfoTest { } // 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" @@ -693,11 +693,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" @@ -718,7 +718,7 @@ class GenerateAppVersionInfoTest { @Test fun `versionCode and versionName configurations work in groovy`() { - GitClient.initialize(fixtureDir.root).apply { + initGitRepo(fixtureDir.root) { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) commit(message = "2nd commit.") @@ -776,7 +776,7 @@ class GenerateAppVersionInfoTest { @Test fun `versionCode and versionName configurations with external Provider work in groovy`() { - GitClient.initialize(fixtureDir.root).apply { + initGitRepo(fixtureDir.root) { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) commit(message = "2nd commit.") @@ -832,7 +832,7 @@ class GenerateAppVersionInfoTest { @Test fun `GenerateAppVersionInfo is incremental without custom versionCode and versionName specs`() { - GitClient.initialize(fixtureDir.root).apply { + initGitRepo(fixtureDir.root) { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) } @@ -857,7 +857,7 @@ class GenerateAppVersionInfoTest { @Test fun `GenerateAppVersionInfo is incremental with custom versionCode and versionName specs`() { - GitClient.initialize(fixtureDir.root).apply { + initGitRepo(fixtureDir.root) { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) commit(message = "2nd commit.") @@ -894,7 +894,7 @@ class GenerateAppVersionInfoTest { @Test fun `GenerateAppVersionInfo is cacheable without custom versionCode and versionName specs`() { - GitClient.initialize(fixtureDir.root).apply { + initGitRepo(fixtureDir.root) { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) } @@ -923,7 +923,7 @@ class GenerateAppVersionInfoTest { @Test fun `GenerateAppVersionInfo is cacheable with custom versionCode and versionName specs`() { - GitClient.initialize(fixtureDir.root).apply { + initGitRepo(fixtureDir.root) { val commitId = commit(message = "1st commit.") tag(name = "1.2.3", message = "1st tag", commitId = commitId) commit(message = "2nd commit.") @@ -964,7 +964,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) } @@ -990,7 +990,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) } @@ -1020,7 +1020,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) } @@ -1042,7 +1042,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" @@ -1053,11 +1053,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, @@ -1080,7 +1080,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", @@ -1093,7 +1093,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.") @@ -1117,7 +1117,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" @@ -1128,7 +1128,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.") @@ -1156,7 +1156,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 7aecd4e3..265a0090 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,8 @@ package io.github.reactivecircus.appversioning.tasks import io.github.reactivecircus.appversioning.fixtures.AppProjectTemplate +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 +18,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 +55,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) } @@ -77,7 +77,7 @@ class PrintAppVersionInfoTest { @Test fun `PrintAppVersionInfo prints latest generated versionCode and versionName (or warning message if not available) to the console for each project with AppVersioningPlugin applied`() { - GitClient.initialize(fixtureDir.root).apply { + initGitRepo(fixtureDir.root) { commit(message = "Initial commit.") val commitId1 = commit(message = "appA release.") tag(name = "0.1.0+appA", message = "1st tag", commitId = commitId1) @@ -165,7 +165,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) } diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningExtension.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningExtension.kt index 8affb562..4a094d89 100644 --- a/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningExtension.kt +++ b/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningExtension.kt @@ -8,7 +8,6 @@ import org.gradle.api.provider.Property /** * Extension for [AppVersioningPlugin]. */ -@Suppress("unused") open class AppVersioningExtension internal constructor(objects: ObjectFactory) { /** * Whether to enable the plugin. diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/VariantInfo.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/VariantInfo.kt index fa5cc6ac..dc3b5f95 100644 --- a/src/main/kotlin/io/github/reactivecircus/appversioning/VariantInfo.kt +++ b/src/main/kotlin/io/github/reactivecircus/appversioning/VariantInfo.kt @@ -11,7 +11,7 @@ class VariantInfo internal constructor( val isReleaseBuild: Boolean get() = buildType == "release" internal companion object { - @Suppress("Unused") + @Suppress("unused") private const val serialVersionUID = 1L } } diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/VersionSpecs.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/VersionSpecs.kt index 9dfc6936..be2cafb9 100644 --- a/src/main/kotlin/io/github/reactivecircus/appversioning/VersionSpecs.kt +++ b/src/main/kotlin/io/github/reactivecircus/appversioning/VersionSpecs.kt @@ -99,11 +99,11 @@ private class SerializableClosure( .also { it.resolveStrategy = Closure.DELEGATE_FIRST } .call(context) - @Suppress("Unused") + @Suppress("unused") private fun writeReplace(): Any = SerializableClosure(dehydrated) companion object { - @Suppress("Unused") + @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 c668f487..96309122 100644 --- a/src/main/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfo.kt +++ b/src/main/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfo.kt @@ -5,7 +5,7 @@ import io.github.reactivecircus.appversioning.GitTag import io.github.reactivecircus.appversioning.VariantInfo import io.github.reactivecircus.appversioning.VersionCodeSpec import io.github.reactivecircus.appversioning.VersionNameSpec -import io.github.reactivecircus.appversioning.internal.GitClient +import io.github.reactivecircus.appversioning.internal.GitOps import io.github.reactivecircus.appversioning.toGitTag import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty @@ -71,22 +71,27 @@ abstract class GenerateAppVersionInfo : DefaultTask() { abstract val variantInfo: Property @TaskAction - fun generate() { + 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 { 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 a5aa5ac7..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( """ From a6f517aaa3dde3b9502b9c66be7f62ff102bd336 Mon Sep 17 00:00:00 2001 From: Yang Date: Fri, 23 Oct 2026 18:48:45 +1100 Subject: [PATCH 04/11] Rename `rawTagName` to `name`. --- README.md | 10 ++++----- api/app-versioning.api | 2 +- .../tasks/GenerateAppVersionInfoTest.kt | 4 ++-- .../reactivecircus/appversioning/GitTag.kt | 22 +++++++++---------- .../appversioning/VersionSpecs.kt | 8 +++---- .../appversioning/GitTagTest.kt | 2 +- .../appversioning/VersionSpecsTest.kt | 2 +- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index a04905a3..93e7c928 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ versionCode = MAJOR * 10000 + MINOR * 100 + PATCH For example, for a tag `1.3.1` (or `v1.3.1`), the generated `versionCode` is `1 * 10000 + 3 * 100 + 1 = 10301`. -The default `versionName` (`VersionNameSpec.TagName`) is the raw name of the latest Git tag. +The default `versionName` (`VersionNameSpec.TagName`) is the name of the latest Git tag. ``` > Task :app:generateAppVersionInfoForRelease @@ -99,7 +99,7 @@ appVersioning { ``` Inside the lambda receiver (`VersionContext`): -- `tag` (`GitTag`) encapsulates `rawTagName`, `commitsSinceLatestTag`, and `commitHash`. +- `tag` (`GitTag`) encapsulates `name`, `commitsSinceLatestTag`, and `commitHash`. - `variant` (`VariantInfo`) encapsulates `buildType`, `flavorName`, `variantName`, `isDebugBuild`, and `isReleaseBuild`. #### SemVer-based version code @@ -132,7 +132,7 @@ appVersioning {
-`tag.toSemVer(prefix = "")` parses `GitTag.rawTagName` (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)`. +`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)`. #### Combining variant info and external providers @@ -156,7 +156,7 @@ appVersioning { ) versionName.set { val suffix = if (variant.isDebugBuild) " (${variant.variantName})" else "" - "${tag.rawTagName} (${tag.commitHash})$suffix" + "${tag.name} (${tag.commitHash})$suffix" } } ``` @@ -181,7 +181,7 @@ appVersioning { ) versionName { def suffix = variant.debugBuild ? " (${variant.variantName})" : "" - "${tag.rawTagName} (${tag.commitHash})${suffix}" + "${tag.name} (${tag.commitHash})${suffix}" } } ``` diff --git a/api/app-versioning.api b/api/app-versioning.api index e263a356..0b9e4b34 100644 --- a/api/app-versioning.api +++ b/api/app-versioning.api @@ -21,7 +21,7 @@ public final class io/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; 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 3697e402..626d224d 100644 --- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt +++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt @@ -742,7 +742,7 @@ class GenerateAppVersionInfoTest { } versionName { def suffix = variant.debugBuild ? " (${variant.variantName})" : "" - "Version ${tag.rawTagName}${suffix}" + "Version ${tag.name}${suffix}" } } """.trimIndent() @@ -805,7 +805,7 @@ class GenerateAppVersionInfoTest { } ) versionName { - "Version ${tag.rawTagName}" + "Version ${tag.name}" } } """.trimIndent() diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/GitTag.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/GitTag.kt index aab173f4..e3961e82 100644 --- a/src/main/kotlin/io/github/reactivecircus/appversioning/GitTag.kt +++ b/src/main/kotlin/io/github/reactivecircus/appversioning/GitTag.kt @@ -6,35 +6,35 @@ import java.util.Objects * Type-safe representation of a git tag. */ class GitTag internal constructor( - val rawTagName: String, + val name: String, val commitsSinceLatestTag: Int, val commitHash: String, ) { /** - * Parses this tag's [rawTagName] into a [SemVer], stripping [prefix] first if non-empty. + * 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 [rawTagName] does not start with [prefix] or the remaining string is not a valid [SemVer]. + * @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(rawTagName.startsWith(prefix)) { - "Tag \"$rawTagName\" does not start with expected prefix \"$prefix\"." + require(name.startsWith(prefix)) { + "Tag \"$name\" does not start with expected prefix \"$prefix\"." } - return SemVer.parse(rawTagName.removePrefix(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 rawTagName == other.rawTagName && + return name == other.name && commitsSinceLatestTag == other.commitsSinceLatestTag && commitHash == other.commitHash } - override fun hashCode(): Int = Objects.hash(rawTagName, commitsSinceLatestTag, commitHash) + override fun hashCode(): Int = Objects.hash(name, commitsSinceLatestTag, commitHash) - override fun toString(): String = rawTagName + override fun toString(): String = name } /** @@ -44,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/VersionSpecs.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/VersionSpecs.kt index be2cafb9..88a598de 100644 --- a/src/main/kotlin/io/github/reactivecircus/appversioning/VersionSpecs.kt +++ b/src/main/kotlin/io/github/reactivecircus/appversioning/VersionSpecs.kt @@ -30,15 +30,15 @@ fun interface VersionNameSpec : Serializable { /** * Uses the name of the latest git tag as the `versionName`. */ - val TagName: VersionNameSpec = VersionNameSpec { tag.rawTagName } + val TagName: VersionNameSpec = VersionNameSpec { tag.name } } } private fun GitTag.toVersionCodeBySemVer(): Int { - val prefix = if (rawTagName.startsWith("v")) "v" else "" + val prefix = if (name.startsWith("v")) "v" else "" val semVer = checkNotNull(runCatching { toSemVer(prefix = prefix) }.getOrNull()) { """ - Could not generate versionCode as "$rawTagName" does not follow semantic versioning. + 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() } @@ -47,7 +47,7 @@ private fun GitTag.toVersionCodeBySemVer(): Int { }.getOrElse { throw IllegalStateException( """ - Could not generate versionCode from "$rawTagName" as the SemVer cannot be represented as an Integer. + 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. 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/VersionSpecsTest.kt b/src/test/kotlin/io/github/reactivecircus/appversioning/VersionSpecsTest.kt index 5e88443d..c144b22c 100644 --- a/src/test/kotlin/io/github/reactivecircus/appversioning/VersionSpecsTest.kt +++ b/src/test/kotlin/io/github/reactivecircus/appversioning/VersionSpecsTest.kt @@ -49,7 +49,7 @@ class VersionSpecsTest { } @Test - fun `VersionNameSpec#TagName returns rawTagName`() { + 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")) From c0add1b2e13fdd9b49c76be036fa94a6e073315b Mon Sep 17 00:00:00 2001 From: Yang Date: Fri, 23 Oct 2026 19:50:09 +1100 Subject: [PATCH 05/11] Add CC tests. --- .../AppVersioningPluginIntegrationTest.kt | 94 +++++++++++++++++++ .../tasks/GenerateAppVersionInfoTest.kt | 4 + .../tasks/PrintAppVersionInfoTest.kt | 2 + 3 files changed, 100 insertions(+) diff --git a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt index 1b56a618..f251e3a0 100644 --- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt +++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt @@ -333,4 +333,98 @@ class AppVersioningPluginIntegrationTest( 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", + ) { + assertTrue(output.contains("Configuration cache entry stored.")) + assertEquals(TaskOutcome.SUCCESS, task(":app-a:generateAppVersionInfoForRelease")?.outcome) + assertEquals(TaskOutcome.SUCCESS, task(":app-b:generateAppVersionInfoForRelease")?.outcome) + } + + runner.runAndCheckResult( + "generateAppVersionInfoForRelease", + ) { + assertTrue(output.contains("Configuration cache entry reused.")) + assertEquals(TaskOutcome.UP_TO_DATE, task(":app-a:generateAppVersionInfoForRelease")?.outcome) + assertEquals(TaskOutcome.UP_TO_DATE, task(":app-b:generateAppVersionInfoForRelease")?.outcome) + } + + runner.runAndCheckResult( + "printAppVersionInfoForRelease", + ) { + assertTrue(output.contains("Configuration cache entry 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", + ) { + assertTrue(output.contains("Configuration cache entry 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/tasks/GenerateAppVersionInfoTest.kt b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt index 626d224d..554bf6be 100644 --- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt +++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt @@ -845,12 +845,14 @@ class GenerateAppVersionInfoTest { runner.runAndCheckResult( "generateAppVersionInfoForRelease" ) { + assertTrue(output.contains("Configuration cache entry stored.")) assertEquals(TaskOutcome.SUCCESS, task(":app:generateAppVersionInfoForRelease")?.outcome) } runner.runAndCheckResult( "generateAppVersionInfoForRelease" ) { + assertTrue(output.contains("Configuration cache entry reused.")) assertEquals(TaskOutcome.UP_TO_DATE, task(":app:generateAppVersionInfoForRelease")?.outcome) } } @@ -882,12 +884,14 @@ class GenerateAppVersionInfoTest { runner.runAndCheckResult( "generateAppVersionInfoForRelease" ) { + assertTrue(output.contains("Configuration cache entry stored.")) assertEquals(TaskOutcome.SUCCESS, task(":app:generateAppVersionInfoForRelease")?.outcome) } runner.runAndCheckResult( "generateAppVersionInfoForRelease" ) { + assertTrue(output.contains("Configuration cache entry reused.")) assertEquals(TaskOutcome.UP_TO_DATE, task(":app:generateAppVersionInfoForRelease")?.outcome) } } 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 265a0090..82bf0e0b 100644 --- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfoTest.kt +++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfoTest.kt @@ -184,6 +184,7 @@ class PrintAppVersionInfoTest { runner.runAndCheckResult( "printAppVersionInfoForRelease" ) { + assertTrue(output.contains("Configuration cache entry stored.")) assertEquals(TaskOutcome.SUCCESS, task(":app:printAppVersionInfoForRelease")?.outcome) assertTrue( output.contains( @@ -201,6 +202,7 @@ class PrintAppVersionInfoTest { runner.runAndCheckResult( "printAppVersionInfoForRelease" ) { + assertTrue(output.contains("Configuration cache entry reused.")) assertEquals(TaskOutcome.SUCCESS, task(":app:printAppVersionInfoForRelease")?.outcome) assertTrue( output.contains( From e338849886a541ddd331a8ff42fb81cdee484e83 Mon Sep 17 00:00:00 2001 From: Yang Date: Sat, 24 Oct 2026 01:17:22 +1100 Subject: [PATCH 06/11] Add migration guide, add AGP compatibility table. --- README.md | 21 +- docs/migration-guide.md | 239 ++++++++++++++++++ .../tasks/GenerateAppVersionInfoTest.kt | 4 +- 3 files changed, 253 insertions(+), 11 deletions(-) create mode 100644 docs/migration-guide.md diff --git a/README.md b/README.md index 93e7c928..848c578a 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,22 @@ A Gradle Plugin for lazily generating Android app's `versionCode` & `versionName` from Git tags. -Android Gradle Plugin 4.0 and 4.1 introduced [new 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 common Git-based app versioning use cases with full Configuration Cache and Isolated Projects support. +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 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 @@ -57,6 +60,8 @@ 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: 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/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt index 554bf6be..dc7df1d3 100644 --- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt +++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt @@ -799,9 +799,7 @@ class GenerateAppVersionInfoTest { appVersioning { versionCode.set( buildNumber.map { buildNum -> - { - tag.toSemVer().toVersionCode() * 100 + buildNum - } as VersionCodeSpec + { tag.toSemVer().toVersionCode() * 100 + buildNum } as VersionCodeSpec } ) versionName { From 541f3d333057128a2ee48573103920c648027f64 Mon Sep 17 00:00:00 2001 From: Yang Date: Sat, 24 Oct 2026 01:25:33 +1100 Subject: [PATCH 07/11] Polish. --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 848c578a..a10474ae 100644 --- a/README.md +++ b/README.md @@ -71,15 +71,15 @@ The plugin offers 2 Gradle tasks for each build variant: ### Default behavior -Without any configuration, the plugin fetches the latest Git tag in the repository, parses it as a **SemVer** string (`VersionCodeSpec.SemVer`), and computes `versionCode` using [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 ``` -For example, for a tag `1.3.1` (or `v1.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` (`VersionNameSpec.TagName`) is the name of the latest Git tag. +The default `versionName` is the name of the latest Git tag. ``` > Task :app:generateAppVersionInfoForRelease @@ -94,11 +94,11 @@ You can customize how `versionCode` and `versionName` are computed by configurin ```kt appVersioning { versionCode.set { - // generate an Int with `this: VersionContext` (`tag`, `variant`) + // generate an Int } versionName.set { - // generate a String with `this: VersionContext` (`tag`, `variant`) + // generate a String } } ``` @@ -109,7 +109,7 @@ Inside the lambda receiver (`VersionContext`): #### SemVer-based version code -By default, `VersionCodeSpec.SemVer` reserves 2 digits for each of the **MAJOR**, **MINOR**, and **PATCH** components in a SemVer tag (accepting an optional `"v"` prefix). +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 (up to 999 per component), strip a custom tag prefix, or add an offset / commit count: From 05b4a6167d03f7fc4035cb59cc76566c832e6e27 Mon Sep 17 00:00:00 2001 From: Yang Date: Sat, 24 Oct 2026 01:58:32 +1100 Subject: [PATCH 08/11] Add sample code snippet. --- README.md | 8 +++++--- docs/images/sample.png | Bin 0 -> 170038 bytes 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 docs/images/sample.png diff --git a/README.md b/README.md index a10474ae..f12582eb 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ A Gradle Plugin for lazily generating Android app's `versionCode` & `versionName` from Git tags. +![App Versioning](docs/images/sample.png) + 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 plugin builds on top of these APIs to support common Git-based app versioning use cases with full Configuration Cache and Isolated Projects support. @@ -111,14 +113,14 @@ Inside the lambda receiver (`VersionContext`): 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 (up to 999 per component), strip a custom tag prefix, or add an offset / commit count: +To allocate 3 digits per component instead (up to 999 per component) or strip a custom tag prefix:
Kotlin ```kt appVersioning { versionCode.set { - tag.toSemVer(prefix = "android-").toVersionCode(digitsPerComponent = 3) + tag.commitsSinceLatestTag + tag.toSemVer(prefix = "android-").toVersionCode(digitsPerComponent = 3) } } ``` @@ -130,7 +132,7 @@ appVersioning { ```groovy appVersioning { versionCode { - tag.toSemVer("android-").toVersionCode(3) + tag.commitsSinceLatestTag + tag.toSemVer("android-").toVersionCode(3) } } ``` diff --git a/docs/images/sample.png b/docs/images/sample.png new file mode 100644 index 0000000000000000000000000000000000000000..19da8a53cca8d517ad2ad1deed9366889cb50d5b GIT binary patch literal 170038 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

sDw1j%NR)?x=O9RvM^v)>vEDxEgM?c~ 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%0ACcz$s(8_Hs=^}|&)Yj2loLcX_4jlK=Cb=Ft< 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
)=Y!o5BY|lsL5$ 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 literal 0 HcmV?d00001 From 9f22c8667184fcc9805c3754b7ebbb04c3cea968 Mon Sep 17 00:00:00 2001 From: Yang Date: Sat, 24 Oct 2026 02:13:03 +1100 Subject: [PATCH 09/11] CHANGELOG. --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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. From c352741eddb2fec735e11f5640aa33d267e373e7 Mon Sep 17 00:00:00 2001 From: Yang Date: Sun, 25 Oct 2026 14:30:08 +1100 Subject: [PATCH 10/11] Enable cc and isolated projects in tests for AGP 9.2.0+, add min AGP version check. --- build.gradle.kts | 2 + .../AppVersioningPluginIntegrationTest.kt | 10 ++-- .../ConfigurationCacheCompatibility.kt | 49 +++++++++++++++++++ .../appversioning/fixtures/FixtureRunner.kt | 2 +- .../fixtures/ProjectTemplates.kt | 13 +++-- .../tasks/GenerateAppVersionInfoTest.kt | 10 ++-- .../tasks/PrintAppVersionInfoTest.kt | 6 ++- .../appversioning/AppVersioningPlugin.kt | 8 +++ 8 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/ConfigurationCacheCompatibility.kt diff --git a/build.gradle.kts b/build.gradle.kts index 8a985b4a..b2ac342a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -69,6 +69,7 @@ tasks.withType().configureEach { tasks.functionalTest { failFast = true + systemProperty("fixtureAgpVersion", fixtureAgpVersion.get()) } tasks.test { @@ -85,6 +86,7 @@ dependencies { 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" diff --git a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt index f251e3a0..1c9895aa 100644 --- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt +++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt @@ -5,7 +5,9 @@ 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 org.gradle.testkit.runner.TaskOutcome @@ -394,7 +396,7 @@ class AppVersioningPluginIntegrationTest( runner.runAndCheckResult( "generateAppVersionInfoForRelease", ) { - assertTrue(output.contains("Configuration cache entry stored.")) + assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Stored) assertEquals(TaskOutcome.SUCCESS, task(":app-a:generateAppVersionInfoForRelease")?.outcome) assertEquals(TaskOutcome.SUCCESS, task(":app-b:generateAppVersionInfoForRelease")?.outcome) } @@ -402,7 +404,7 @@ class AppVersioningPluginIntegrationTest( runner.runAndCheckResult( "generateAppVersionInfoForRelease", ) { - assertTrue(output.contains("Configuration cache entry reused.")) + assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Reused) assertEquals(TaskOutcome.UP_TO_DATE, task(":app-a:generateAppVersionInfoForRelease")?.outcome) assertEquals(TaskOutcome.UP_TO_DATE, task(":app-b:generateAppVersionInfoForRelease")?.outcome) } @@ -410,7 +412,7 @@ class AppVersioningPluginIntegrationTest( runner.runAndCheckResult( "printAppVersionInfoForRelease", ) { - assertTrue(output.contains("Configuration cache entry stored.")) + assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Stored) assertEquals(TaskOutcome.SUCCESS, task(":app-a:printAppVersionInfoForRelease")?.outcome) assertEquals(TaskOutcome.SUCCESS, task(":app-b:printAppVersionInfoForRelease")?.outcome) assertTrue(output.contains("versionCode: 10001")) @@ -422,7 +424,7 @@ class AppVersioningPluginIntegrationTest( runner.runAndCheckResult( "printAppVersionInfoForRelease", ) { - assertTrue(output.contains("Configuration cache entry reused.")) + 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 4712c49b..137094ef 100644 --- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/FixtureRunner.kt +++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/FixtureRunner.kt @@ -75,7 +75,7 @@ private fun TemporaryFolder.buildFixture(gradleRoot: File, subprojects: List 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 b45eee30..3a10b773 100644 --- a/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/ProjectTemplates.kt +++ b/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/ProjectTemplates.kt @@ -42,10 +42,15 @@ fun settingsFileContent( ${subprojects.map { it.projectName }.joinToString("\n") { "include(\":$it\")" }} """.trimIndent() -val gradlePropertiesFileContent = """ - org.gradle.configuration-cache=true - org.gradle.isolated-projects=true -""".trimIndent() +fun gradlePropertiesFileContent(enableConfigurationCache: Boolean): String { + return """ + org.gradle.parallel=true + org.gradle.configureondemand=true + org.gradle.caching=true + org.gradle.configuration-cache=$enableConfigurationCache + org.gradle.isolated-projects=$enableConfigurationCache + """.trimIndent() +} enum class BuildScriptLanguage { Kts, 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 dc7df1d3..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,6 +4,8 @@ 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 org.gradle.testkit.runner.TaskOutcome @@ -843,14 +845,14 @@ class GenerateAppVersionInfoTest { runner.runAndCheckResult( "generateAppVersionInfoForRelease" ) { - assertTrue(output.contains("Configuration cache entry stored.")) + assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Stored) assertEquals(TaskOutcome.SUCCESS, task(":app:generateAppVersionInfoForRelease")?.outcome) } runner.runAndCheckResult( "generateAppVersionInfoForRelease" ) { - assertTrue(output.contains("Configuration cache entry reused.")) + assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Reused) assertEquals(TaskOutcome.UP_TO_DATE, task(":app:generateAppVersionInfoForRelease")?.outcome) } } @@ -882,14 +884,14 @@ class GenerateAppVersionInfoTest { runner.runAndCheckResult( "generateAppVersionInfoForRelease" ) { - assertTrue(output.contains("Configuration cache entry stored.")) + assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Stored) assertEquals(TaskOutcome.SUCCESS, task(":app:generateAppVersionInfoForRelease")?.outcome) } runner.runAndCheckResult( "generateAppVersionInfoForRelease" ) { - assertTrue(output.contains("Configuration cache entry reused.")) + assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Reused) assertEquals(TaskOutcome.UP_TO_DATE, task(":app:generateAppVersionInfoForRelease")?.outcome) } } 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 82bf0e0b..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,6 +3,8 @@ 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 org.gradle.testkit.runner.TaskOutcome @@ -184,7 +186,7 @@ class PrintAppVersionInfoTest { runner.runAndCheckResult( "printAppVersionInfoForRelease" ) { - assertTrue(output.contains("Configuration cache entry stored.")) + assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Stored) assertEquals(TaskOutcome.SUCCESS, task(":app:printAppVersionInfoForRelease")?.outcome) assertTrue( output.contains( @@ -202,7 +204,7 @@ class PrintAppVersionInfoTest { runner.runAndCheckResult( "printAppVersionInfoForRelease" ) { - assertTrue(output.contains("Configuration cache entry reused.")) + assertHasConfigurationCacheEntryLog(ConfigurationCacheEntryLog.Reused) assertEquals(TaskOutcome.SUCCESS, task(":app:printAppVersionInfoForRelease")?.outcome) assertTrue( output.contains( diff --git a/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningPlugin.kt b/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningPlugin.kt index 29d60cc2..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) @@ -145,6 +149,10 @@ class AppVersioningPlugin : Plugin { private fun String.capitalize(): String { return replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } } + + private companion object { + private val MIN_AGP_VERSION = AndroidPluginVersion(9, 0, 0) + } } private const val APP_VERSIONING_TASK_GROUP = "versioning" From 66fbadccf244fe6d86a49a35a9b4528a20f4daa1 Mon Sep 17 00:00:00 2001 From: Yang Date: Sun, 25 Oct 2026 15:32:11 +1100 Subject: [PATCH 11/11] README. --- README.md | 72 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f12582eb..60840fad 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ versionCode = MAJOR * 10000 + MINOR * 100 + PATCH For example, for a tag `1.3.1`, the generated `versionCode` is `1 * 10000 + 3 * 100 + 1 = 10301`. -The default `versionName` is the name of the latest Git tag. +The default `versionName` generated is the name of the latest Git tag. ``` > Task :app:generateAppVersionInfoForRelease @@ -141,9 +141,9 @@ appVersioning { `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)`. -#### Combining variant info and external providers +#### Using environment variables or Gradle properties -Because `versionCode` and `versionName` are standard Gradle `Property` and `Property` instances, you can map external `Provider`s (such as `providers.environmentVariable(...)` or `providers.gradleProperty(...)`) directly into a spec so Gradle tracks them as task inputs: +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 @@ -156,15 +156,10 @@ appVersioning { versionCode.set( buildNumber.map { buildNum -> VersionCodeSpec { - val offset = if (variant.flavorName == "paid") 1 else 0 - tag.toSemVer().toVersionCode() + buildNum + offset + tag.toSemVer().toVersionCode() + buildNum } } ) - versionName.set { - val suffix = if (variant.isDebugBuild) " (${variant.variantName})" else "" - "${tag.name} (${tag.commitHash})$suffix" - } } ``` @@ -181,12 +176,49 @@ appVersioning { versionCode.set( buildNumber.map { buildNum -> { - def offset = variant.flavorName == "paid" ? 1 : 0 - tag.toSemVer().toVersionCode() + buildNum + offset + tag.toSemVer().toVersionCode() + buildNum } as VersionCodeSpec } ) +} +``` + +
+ +#### 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 `variant` (`VariantInfo`) from the `VersionContext` receiver when generating custom `versionCode` or `versionName`: + +
Kotlin + +```kt +appVersioning { + versionCode.set { + // add 1 to the versionCode for builds with the "paid" product flavor + val offset = if (variant.flavorName == "paid") 1 else 0 + tag.toSemVer().toVersionCode() + offset + } + versionName.set { + // append build variant to the versionName for debug builds + val suffix = if (variant.isDebugBuild) " (${variant.variantName})" else "" + "${tag.name} (${tag.commitHash})$suffix" + } +} +``` + +
+ +
Groovy + +```groovy +appVersioning { + versionCode { + // add 1 to the versionCode for builds with the "paid" product flavor + def offset = variant.flavorName == "paid" ? 1 : 0 + tag.toSemVer().toVersionCode() + offset + } versionName { + // append build variant to the versionName for debug builds def suffix = variant.debugBuild ? " (${variant.variantName})" : "" "${tag.name} (${tag.commitHash})${suffix}" } @@ -199,7 +231,23 @@ appVersioning { By default the plugin uses the latest tag in the current branch for `versionCode` and `versionName` generation. -To use the latest tag matching a specific glob pattern (for example `..[-]+` in a monorepo): +Sometimes it's useful to be able to use the latest tag that follows a specific glob pattern. + +For example a codebase might build and publish 3 different apps separately using the following tag pattern: + +`..[-]+` + +where `app-identifier` is the build metadata component in **SemVer**. + +Some of the possible tags are: + +``` +1.5.8+app-a +2.29.0-rc01+app-b +10.87.9-alpha04+app-c +``` + +To configure the plugin to generate version info specific to `app-b`: ```kt appVersioning {