diff --git a/.claude/settings.json b/.claude/settings.json index 3c7da68..44a3ac2 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -14,6 +14,7 @@ "Bash(git switch:*)", "Bash(git checkout:*)", "Bash(git add:*)", + "Bash(git commit:*)", "Bash(git restore:*)", "Bash(git stash:*)", "Bash(git fetch:*)", @@ -48,7 +49,15 @@ "Bash(gh release create:*)" ], "ask": [ - "Bash(git commit:*)", + "Bash(git push --force*)", + "Bash(git push * --force*)", + "Bash(git push -f*)", + "Bash(git push * -f*)", + "Bash(git push -uf*)", + "Bash(git push * -uf*)", + "Bash(git push * +*)", + "Bash(git push --mirror*)", + "Bash(git push * --mirror*)", "Bash(git rebase:*)", "Bash(git merge:*)", "Bash(git cherry-pick:*)", diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 649df9a..3bad11f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,7 +30,7 @@ on: jobs: publish: name: Publish to Maven repositories - uses: SpineEventEngine/config/.github/workflows/publishing.yml@219fef6ddd42c1e236dddf04526fb2a64409de6a + uses: SpineEventEngine/config/.github/workflows/publishing.yml@a0cbbbfe66c12475224563439fb6deb3da71ba58 permissions: contents: read packages: write diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 94ca8cd..aeaa51d 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -77,7 +77,7 @@ val grGitVersion = "5.3.3" * This version may change from the [version of Kotlin][io.spine.dependency.lib.Kotlin.version] * used by the project. */ -val kotlinEmbeddedVersion = "2.4.10" +val kotlinEmbeddedVersion = "2.4.21" /** * The version of Guava used in `buildSrc`. @@ -85,7 +85,7 @@ val kotlinEmbeddedVersion = "2.4.10" * Always use the same version as the one specified in [io.spine.dependency.lib.Guava]. * Otherwise, when testing Gradle plugins, clashes may occur. */ -val guavaVersion = "33.7.1-jre" +val guavaVersion = "33.7.2-jre" /** * The version of ErrorProne Gradle plugin. @@ -132,7 +132,7 @@ val kotestJvmPluginVersion = "0.4.11" /** * @see [io.spine.dependency.test.Kover] */ -val koverVersion = "0.9.9" +val koverVersion = "0.9.11" /** * The version of the Shadow Plugin. @@ -175,7 +175,7 @@ val junitVersion = "6.1.3" * * @see [io.spine.dependency.test.Kotest] */ -val kotestVersion = "6.2.4" +val kotestVersion = "6.2.5" configurations.all { resolutionStrategy { diff --git a/buildSrc/src/main/kotlin/DokkaExts.kt b/buildSrc/src/main/kotlin/DokkaExts.kt index 2bb2cc2..d1483b6 100644 --- a/buildSrc/src/main/kotlin/DokkaExts.kt +++ b/buildSrc/src/main/kotlin/DokkaExts.kt @@ -14,6 +14,7 @@ import io.spine.dependency.local.DokkaTools import io.spine.gradle.SpineTaskGroup +import io.spine.gradle.github.pages.TaskName import io.spine.gradle.publish.getOrCreate import java.io.File import java.time.LocalDate @@ -21,14 +22,20 @@ import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.execution.TaskExecutionGraph +import org.gradle.api.publish.maven.tasks.PublishToMavenLocal +import org.gradle.api.publish.maven.tasks.PublishToMavenRepository import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.bundling.Jar import org.gradle.kotlin.dsl.DependencyHandlerScope +import org.gradle.kotlin.dsl.property +import org.gradle.kotlin.dsl.withType import org.jetbrains.dokka.gradle.DokkaExtension import org.jetbrains.dokka.gradle.engine.parameters.DokkaSourceSetSpec import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier import org.jetbrains.dokka.gradle.engine.plugins.DokkaHtmlPluginParameters +import org.jetbrains.dokka.gradle.tasks.DokkaBaseTask /** * To exclude pieces of code annotated with `@Internal` from the documentation @@ -205,18 +212,127 @@ fun Project.htmlDocsJar(): TaskProvider = tasks.getOrCreate("htmlDocsJar") /** * Tells if this task belongs to the execution graph that contains - * the `publish` and `dokkaGenerate` tasks. + * the `publish` or `dokkaGenerate` tasks. * * This predicate could be useful for disabling publishing tasks * when doing, e.g., `publishToMavenLocal` for the purpose of the * integration tests that (of course) do not test the documentation * generation process and its results. + * + * The predicate reaches the task graph through [Task.getProject]. Called from an `onlyIf` + * spec or a task action, it does so at execution time, which Gradle deprecates and + * the configuration cache does not support. Also, the predicate holds whenever the graph + * contains a Dokka task, since Dokka 2.x names its tasks `dokkaGenerate…`. + * [skipDokkaWhenPublishingToMavenLocal] avoids both problems. */ +@Deprecated(message = "Please use `Project.skipDokkaWhenPublishingToMavenLocal()` instead.") fun Task.isInPublishingGraph(): Boolean = project.gradle.taskGraph.allTasks.any { it.name == "publish" || it.name.contains("dokkaGenerate") } +/** + * Configures the Dokka tasks of this project to be skipped in a build that publishes + * to Maven Local only. + * + * Such a build usually feeds integration tests, which need the published code, + * but not its documentation. So the Dokka tasks are skipped when the execution graph + * contains a [PublishToMavenLocal] task and no task that ships the documentation + * elsewhere: `publish`, a [PublishToMavenRepository] task, or `updateGitHubPages`. + * They still run if a task named on the command line selects a documentation task of + * the graph: one that has `dokka` in its name, ignoring case, or a documentation JAR, + * `javadocJar` or `htmlDocsJar`. The name may be the full one, e.g., `dokkaGenerate`, + * or a camel-case abbreviation, e.g., `dGPJ` for `dokkaGeneratePublicationJavadoc`. + * Without task names, a documentation request cannot be ruled out, so the tasks run too. + * This is the case of an included build, as Gradle does not pass it the tasks named + * on the command line. + * + * Any other build runs the Dokka tasks in its graph. When the tasks are skipped, + * the documentation JARs they feed, such as `javadocJar`, contain only a manifest, + * or the output of an earlier Dokka run. + * + * The `onlyIf` spec added to the tasks is evaluated at execution time, when it must not + * call [Task.getProject] to reach the task graph: Gradle deprecates that, and + * the configuration cache does not support it. So the graph is scanned at configuration + * time, once it is ready, and the spec reads the result from a property, which + * a configuration cache entry stores along with the tasks. + */ +fun Project.skipDokkaWhenPublishingToMavenLocal() { + // The project path is dropped, so that, e.g., `:dokka-extensions:build` + // is not taken for a Dokka request. + val requestedNames = gradle.startParameter.taskNames + .map { it.substringAfterLast(':') } + .filter { it.isNotEmpty() } + val docsNeeded = objects.property() + gradle.taskGraph.whenReady { + val docsRequested = requestedNames.isEmpty() || selectsDocsTask(requestedNames) + docsNeeded.set(docsRequested || !publishesToMavenLocalOnly()) + } + tasks.withType().configureEach { + onlyIf("documentation is needed beyond Maven Local") { docsNeeded.get() } + } +} + +/** + * The names of the lifecycle tasks that ship the documentation beyond Maven Local. + */ +private val docsShippingTaskNames = setOf("publish", TaskName.updateGitHubPages) + +/** + * Tells if this graph publishes to Maven Local and does not contain a task that ships + * the documentation elsewhere. + */ +private fun TaskExecutionGraph.publishesToMavenLocalOnly(): Boolean { + val tasks = allTasks + return tasks.any { it is PublishToMavenLocal } + && tasks.none { it is PublishToMavenRepository || it.name in docsShippingTaskNames } +} + +/** + * The names of the tasks that pack the documentation into JARs. + */ +private val docsJarTaskNames = setOf("javadocJar", "htmlDocsJar") + +/** + * Tells if any of the given names, taken from the command line, selects + * a documentation task of this graph. + * + * A documentation task is one that has `dokka` in its name, ignoring case, + * or a documentation JAR. The command line keeps a task name as typed, so the name + * may be a camel-case abbreviation that Gradle resolves, e.g., `dGPJ` + * for `dokkaGeneratePublicationJavadoc`. + */ +private fun TaskExecutionGraph.selectsDocsTask(names: List): Boolean { + val docsTaskNames = allTasks.map { it.name }.filter { + it.contains("dokka", ignoreCase = true) || it in docsJarTaskNames + } + return names.any { name -> + val pattern = abbreviationPattern(name) + docsTaskNames.any { pattern.containsMatchIn(it) } + } +} + +/** + * Creates the pattern for the task names that the given camel-case abbreviation selects. + * + * Follows the matching of Gradle: each word of the abbreviation starts a word of + * the task name, in the same order, and the task name may go on after the last one. + * Unlike Gradle, which prefers exact and case-sensitive matches, the pattern + * ignores case, so it may select more tasks, never fewer. + */ +private fun abbreviationPattern(abbreviation: String): Regex { + val words = abbreviation.split(wordStart).filter { it.isNotEmpty() } + val pattern = words.joinToString(separator = "[\\p{javaLowerCase}\\p{Digit}]*", prefix = "^") { + Regex.escape(it) + } + return Regex(pattern, RegexOption.IGNORE_CASE) +} + +/** + * Matches the position before an upper-case letter, where a word of a camel-case name starts. + */ +private val wordStart = Regex("(?=\\p{javaUpperCase})") + /** * Disables Dokka and Javadoc tasks in this `Project`. * diff --git a/buildSrc/src/main/kotlin/dokka-setup.gradle.kts b/buildSrc/src/main/kotlin/dokka-setup.gradle.kts index 9255d03..c66e9f2 100644 --- a/buildSrc/src/main/kotlin/dokka-setup.gradle.kts +++ b/buildSrc/src/main/kotlin/dokka-setup.gradle.kts @@ -23,11 +23,7 @@ dependencies { useDokkaWithSpineExtensions() } -tasks.withType().configureEach { - onlyIf { - isInPublishingGraph() - } -} +skipDokkaWhenPublishingToMavenLocal() // The Dokka Javadoc format does not support Kotlin Multiplatform source sets, so its // publication task fails for KMP modules ("No source set found for /jvmMain"). diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/build/AnimalSniffer.kt b/buildSrc/src/main/kotlin/io/spine/dependency/build/AnimalSniffer.kt index 7abd1d6..4891a7d 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/build/AnimalSniffer.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/build/AnimalSniffer.kt @@ -17,6 +17,6 @@ package io.spine.dependency.build // https://www.mojohaus.org/animal-sniffer/animal-sniffer-maven-plugin/ @Suppress("unused", "ConstPropertyName") object AnimalSniffer { - private const val version = "1.27" + private const val version = "1.28" const val lib = "org.codehaus.mojo:animal-sniffer-annotations:$version" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/build/CheckStyle.kt b/buildSrc/src/main/kotlin/io/spine/dependency/build/CheckStyle.kt index f2bb226..b493760 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/build/CheckStyle.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/build/CheckStyle.kt @@ -25,10 +25,19 @@ object CheckStyle { /** * The version to be used in the project. * - * `10.12.1` is the last version in `10.12.0`, which does not introduce - * capability conflict over `google-collections` with Guava. + * `12.3.1` is the last release that runs on Java 17, which our builds use + * to run Checkstyle. `13.0.0` and later require Java 21. + * + * `12.3.1` depends on Doxia, which Checkstyle uses only to generate its own + * website. Through Doxia, it brings two versions each of `commons-lang3` and + * `httpcore`. `forceVersions()` enables `failOnVersionConflict()` in the + * `checkstyle` configuration too, so Gradle would reject the graph. + * [CheckStyleConfig][io.spine.gradle.checkstyle.CheckStyleConfig] therefore + * excludes the `org.apache.maven.doxia` group from the `checkstyle` configuration. + * Releases from `13.11.0` on no longer depend on Doxia, so the exclusion can go + * once we run Checkstyle on Java 21. * * @see Checkstyle */ - const val version = "10.12.1" + const val version = "12.3.1" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/build/CheckerFramework.kt b/buildSrc/src/main/kotlin/io/spine/dependency/build/CheckerFramework.kt index b125eeb..85f2603 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/build/CheckerFramework.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/build/CheckerFramework.kt @@ -17,7 +17,7 @@ package io.spine.dependency.build // https://checkerframework.org/ @Suppress("unused", "ConstPropertyName") object CheckerFramework { - private const val version = "4.2.2" + private const val version = "4.3.1" const val annotations = "org.checkerframework:checker-qual:$version" @Suppress("unused") val dataflow = listOf( diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/build/Ksp.kt b/buildSrc/src/main/kotlin/io/spine/dependency/build/Ksp.kt index 58d40cd..5f5f65c 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/build/Ksp.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/build/Ksp.kt @@ -23,7 +23,7 @@ import io.spine.dependency.Dependency */ @Suppress("unused") object Ksp : Dependency() { - override val version = "2.3.11" + override val version = "2.3.12" val dogfoodingVersion = version override val group = "com.google.devtools.ksp" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/build/PluginPublishPlugin.kt b/buildSrc/src/main/kotlin/io/spine/dependency/build/PluginPublishPlugin.kt index 0f8a48c..5ebe6a4 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/build/PluginPublishPlugin.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/build/PluginPublishPlugin.kt @@ -25,6 +25,6 @@ package io.spine.dependency.build */ @Suppress("ConstPropertyName") object PluginPublishPlugin { - const val version = "2.1.1" + const val version = "2.2.1" const val id = "com.gradle.plugin-publish" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/build/Pmd.kt b/buildSrc/src/main/kotlin/io/spine/dependency/build/Pmd.kt index 94a5fdc..0ca4a21 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/build/Pmd.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/build/Pmd.kt @@ -19,5 +19,5 @@ package io.spine.dependency.build // https://github.com/pmd/pmd/releases @Suppress("unused", "ConstPropertyName") object Pmd { - const val version = "7.27.0" + const val version = "7.28.0" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/ApacheHttp.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/ApacheHttp.kt index 06ac48e..28b509e 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/ApacheHttp.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/ApacheHttp.kt @@ -18,5 +18,5 @@ package io.spine.dependency.lib object ApacheHttp { // https://hc.apache.org/downloads.cgi - const val core = "org.apache.httpcomponents:httpcore:4.4.14" + const val core = "org.apache.httpcomponents:httpcore:4.4.16" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Asm.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Asm.kt index fa21f8b..216e038 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Asm.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Asm.kt @@ -17,7 +17,7 @@ package io.spine.dependency.lib // https://asm.ow2.io/ @Suppress("unused", "ConstPropertyName") object Asm { - private const val version = "9.10.1" + private const val version = "9.11" const val group = "org.ow2.asm" const val lib = "$group:asm:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/BouncyCastle.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/BouncyCastle.kt index e65d153..88bea96 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/BouncyCastle.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/BouncyCastle.kt @@ -17,5 +17,31 @@ package io.spine.dependency.lib // https://www.bouncycastle.org/java.html @Suppress("unused", "ConstPropertyName") object BouncyCastle { - const val libPkcsJdk15 = "org.bouncycastle:bcpkix-jdk15on:1.68" + + /** + * The PKIX library, pinned to `bcpkix-jdk15on:1.70`, the last release of + * the `-jdk15on` artifacts. + * + * Newer releases are published as `bcpkix-jdk18on`. They carry the same + * `org.bouncycastle` classes, yet Gradle sees them as a different module + * and resolves no conflict between the two. + * + * The [IntelliJ Platform][IntelliJ.version] stays at `213.7172.53` for now. + * Its `ide-impl` module depends on the `-jdk15on` artifacts through two paths: + * + * - `marketplace-zip-signer:0.1.3` requires `bcpkix-jdk15on:1.64`. + * - `remote-dev-util` requires `bcpg-jdk15on:1.69`, which requires + * `bcprov-jdk15on:1.69`. + * + * Staying on `-jdk15on` lets Gradle resolve `bcpkix-jdk15on` and + * `bcprov-jdk15on` to `1.70`, the version of this library. With + * `bcpkix-jdk18on` instead, a project that also has the IntelliJ Platform + * on its classpath would get two copies of the BouncyCastle classes. + * + * The two paths also disagree on the version. That is why `tool-base` + * leaves BouncyCastle out of the POM of its IntelliJ Platform uber JAR. + * + * Revisit this pin when [IntelliJ.version] changes. + */ + const val libPkcsJdk15 = "org.bouncycastle:bcpkix-jdk15on:1.70" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Caffeine.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Caffeine.kt index 48e8d64..8e1d907 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Caffeine.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Caffeine.kt @@ -26,6 +26,6 @@ package io.spine.dependency.lib */ @Suppress("unused") object Caffeine { - private const val version = "3.2.4" + private const val version = "3.3.1" const val lib = "com.github.ben-manes.caffeine:caffeine:$version" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/CommonsCodec.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/CommonsCodec.kt index 8bfe3b2..e2d45c2 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/CommonsCodec.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/CommonsCodec.kt @@ -17,6 +17,6 @@ package io.spine.dependency.lib // https://commons.apache.org/proper/commons-codec/changes-report.html @Suppress("unused", "ConstPropertyName") object CommonsCodec { - private const val version = "1.22.1" + private const val version = "1.23.0" const val lib = "commons-codec:commons-codec:$version" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Firebase.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Firebase.kt index e28a240..435861d 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Firebase.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Firebase.kt @@ -17,6 +17,6 @@ package io.spine.dependency.lib // https://firebase.google.com/docs/admin/setup#java @Suppress("unused", "ConstPropertyName") object Firebase { - private const val adminVersion = "9.10.0" + private const val adminVersion = "9.11.0" const val admin = "com.google.firebase:firebase-admin:$adminVersion" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/GoogleApis.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/GoogleApis.kt index 78d2a4b..8664637 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/GoogleApis.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/GoogleApis.kt @@ -21,26 +21,26 @@ package io.spine.dependency.lib object GoogleApis { // https://github.com/googleapis/google-api-java-client - const val client = "com.google.api-client:google-api-client:1.32.2" + const val client = "com.google.api-client:google-api-client:2.9.1" // https://github.com/googleapis/api-common-java - const val common = "com.google.api:api-common:2.64.0" + const val common = "com.google.api:api-common:2.70.0" // https://github.com/googleapis/java-common-protos - const val commonProtos = "com.google.api.grpc:proto-google-common-protos:2.72.0" + const val commonProtos = "com.google.api.grpc:proto-google-common-protos:2.78.0" // https://github.com/googleapis/gax-java - const val gax = "com.google.api:gax:2.80.0" + const val gax = "com.google.api:gax:2.87.0" // https://github.com/googleapis/java-iam - const val protoAim = "com.google.api.grpc:proto-google-iam-v1:1.2.0" + const val protoAim = "com.google.api.grpc:proto-google-iam-v1:1.73.0" // https://github.com/googleapis/google-oauth-java-client - const val oAuthClient = "com.google.oauth-client:google-oauth-client:1.32.1" + const val oAuthClient = "com.google.oauth-client:google-oauth-client:1.39.0" // https://github.com/googleapis/google-auth-library-java object AuthLibrary { - const val version = "1.51.0" + const val version = "1.54.0" const val credentials = "com.google.auth:google-auth-library-credentials:$version" const val oAuth2Http = "com.google.auth:google-auth-library-oauth2-http:$version" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/GoogleCloud.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/GoogleCloud.kt index ff3de8e..4df086b 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/GoogleCloud.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/GoogleCloud.kt @@ -21,14 +21,14 @@ package io.spine.dependency.lib object GoogleCloud { // https://github.com/googleapis/google-cloud-java/tree/main/sdk-platform-java/java-core - const val core = "com.google.cloud:google-cloud-core:2.71.0" + const val core = "com.google.cloud:google-cloud-core:2.77.0" // https://github.com/googleapis/google-cloud-java/tree/main/java-pubsub/proto-google-cloud-pubsub-v1 - const val pubSubGrpcApi = "com.google.api.grpc:proto-google-cloud-pubsub-v1:1.151.0" + const val pubSubGrpcApi = "com.google.api.grpc:proto-google-cloud-pubsub-v1:1.157.0" // https://github.com/googleapis/google-cloud-java/tree/main/java-trace - const val trace = "com.google.cloud:google-cloud-trace:2.93.0" + const val trace = "com.google.cloud:google-cloud-trace:2.99.0" // https://github.com/googleapis/google-cloud-java/tree/main/java-datastore - const val datastore = "com.google.cloud:google-cloud-datastore:2.31.2" + const val datastore = "com.google.cloud:google-cloud-datastore:3.7.0" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Grpc.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Grpc.kt index bc201a3..7f89c29 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Grpc.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Grpc.kt @@ -20,7 +20,7 @@ import io.spine.dependency.DependencyWithBom @Suppress("unused") object Grpc : DependencyWithBom() { - override val version = "1.83.1" + override val version = "1.85.0" override val group = "io.grpc" override val bom = "$group:grpc-bom:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Guava.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Guava.kt index 3acff5b..8f9eb3b 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Guava.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Guava.kt @@ -25,7 +25,7 @@ package io.spine.dependency.lib */ @Suppress("unused", "ConstPropertyName") object Guava { - private const val version = "33.7.1-jre" + private const val version = "33.7.2-jre" const val group = "com.google.guava" const val lib = "$group:guava:$version" const val testLib = "$group:guava-testlib:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/IntelliJ.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/IntelliJ.kt index e8118ef..ea3ce8e 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/IntelliJ.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/IntelliJ.kt @@ -31,6 +31,9 @@ object IntelliJ { * This is the version used by Kotlin compiler `1.9.21`. * Advance this version with caution because it may break the setup of * IntelliJ platform standalone execution. + * + * This version also keeps [BouncyCastle.libPkcsJdk15] on the `-jdk15on` + * artifacts. Revisit that pin when changing this version. */ const val version = "213.7172.53" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/JacksonV2.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/JacksonV2.kt index d811d6d..f79deac 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/JacksonV2.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/JacksonV2.kt @@ -38,7 +38,7 @@ import io.spine.dependency.DependencyWithBom @Suppress("unused") object JacksonV2 : DependencyWithBom() { override val group = "com.fasterxml.jackson" - override val version = "2.22.2" + override val version = "2.22.3" // https://github.com/FasterXML/jackson-bom override val bom = "$group:jackson-bom:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/JavaX.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/JavaX.kt index 4c96a34..517b076 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/JavaX.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/JavaX.kt @@ -21,5 +21,5 @@ object JavaX { const val annotationGroup = "javax.annotation" const val annotations = "$annotationGroup:javax.annotation-api:1.3.2" - const val servletApi = "javax.servlet:javax.servlet-api:3.1.0" + const val servletApi = "javax.servlet:javax.servlet-api:4.0.1" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Kotlin.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Kotlin.kt index 5f63d1f..4b49e3a 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Kotlin.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Kotlin.kt @@ -26,7 +26,7 @@ object Kotlin : DependencyWithBom() { * This is the version of Kotlin we use for writing code that does not depend on Gradle. */ @Suppress("MemberVisibilityCanBePrivate") // used directly from the outside. - const val runtimeVersion = "2.4.10" + const val runtimeVersion = "2.4.21" override val version = runtimeVersion override val group = "org.jetbrains.kotlin" @@ -43,7 +43,7 @@ object Kotlin : DependencyWithBom() { * [embedded into Gradle](https://docs.gradle.org/current/userguide/compatibility.html#kotlin), * which the Gradle version determines on its own. */ - const val embeddedVersion = "2.4.10" + const val embeddedVersion = "2.4.21" /** * The version of the JetBrains annotations library, which is a transitive diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/KotlinPoet.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/KotlinPoet.kt index 9cd9664..c7fcf35 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/KotlinPoet.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/KotlinPoet.kt @@ -17,7 +17,7 @@ package io.spine.dependency.lib // https://github.com/square/kotlinpoet @Suppress("unused", "ConstPropertyName") object KotlinPoet { - private const val version = "2.3.0" + private const val version = "2.4.0" const val lib = "com.squareup:kotlinpoet:$version" const val ksp = "com.squareup:kotlinpoet-ksp:$version" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Netty.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Netty.kt index f494047..53f6b40 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Netty.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Netty.kt @@ -17,7 +17,7 @@ package io.spine.dependency.lib @Suppress("unused", "ConstPropertyName") object Netty { // https://github.com/netty/netty/tags - private const val version = "4.2.17.Final" + private const val version = "4.2.19.Final" const val common = "io.netty:netty-common:$version" const val buffer = "io.netty:netty-buffer:$version" const val transport = "io.netty:netty-transport:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Okio.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Okio.kt index 9bf98f7..1487b5d 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Okio.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Okio.kt @@ -21,6 +21,6 @@ package io.spine.dependency.lib */ @Suppress("unused", "ConstPropertyName") object Okio { - private const val version = "3.18.1" + private const val version = "3.18.2" const val lib = "com.squareup.okio:okio:$version" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/PalantirJavaFormat.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/PalantirJavaFormat.kt index bb71e7a..12e4630 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/PalantirJavaFormat.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/PalantirJavaFormat.kt @@ -24,7 +24,7 @@ import io.spine.dependency.Dependency object PalantirJavaFormat : Dependency() { override val group = "com.palantir.javaformat" - override val version = "2.97.0" + override val version = "2.102.0" override val modules: List = listOf("$group:palantir-java-format") val lib = artifact(modules[0]) diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Protobuf.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Protobuf.kt index 5d83e2d..9478184 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Protobuf.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Protobuf.kt @@ -21,7 +21,7 @@ package io.spine.dependency.lib ) object Protobuf { const val group = "com.google.protobuf" - const val version = "4.36.0" + const val version = "4.36.2" /** * The Java library with Protobuf data types. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Slf4J.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Slf4J.kt index b329ff6..c3478a5 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Slf4J.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Slf4J.kt @@ -28,7 +28,7 @@ package io.spine.dependency.lib // https://search.maven.org/artifact/org.slf4j/slf4j-api @Suppress("unused", "ConstPropertyName") object Slf4J { - private const val version = "2.0.18" + private const val version = "2.0.20" const val lib = "org.slf4j:slf4j-api:$version" const val jdk14 = "org.slf4j:slf4j-jdk14:$version" const val reload4j = "org.slf4j:slf4j-reload4j:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt index c82cb3e..bef7362 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt @@ -27,7 +27,7 @@ typealias CoreJava = CoreJvm @Suppress("ConstPropertyName", "unused") object CoreJvm { const val group = Spine.group - const val version = "2.0.0-SNAPSHOT.561" + const val version = "2.0.0-SNAPSHOT.567" const val coreArtifact = "spine-core" const val clientArtifact = "spine-client" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt index 1df9ce5..5db1a81 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt @@ -34,12 +34,12 @@ object CoreJvmCompiler { /** * The version used in the build classpath. */ - const val dogfoodingVersion = "2.0.0-SNAPSHOT.094" + const val dogfoodingVersion = "2.0.0-SNAPSHOT.097" /** * The version to be used for integration tests. */ - const val version = "2.0.0-SNAPSHOT.094" + const val version = "2.0.0-SNAPSHOT.097" /** * The ID of the Gradle plugin. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Reflect.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Reflect.kt index d45e1a2..ae8e17f 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Reflect.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Reflect.kt @@ -21,7 +21,7 @@ package io.spine.dependency.local */ @Suppress("ConstPropertyName") object Reflect { - const val version = "2.0.0-SNAPSHOT.200" + const val version = "2.0.0-SNAPSHOT.201" const val group = Spine.group const val artifact = "spine-reflect" const val lib = "$group:$artifact:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/TestLib.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/TestLib.kt index b07ddad..bf26e25 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/TestLib.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/TestLib.kt @@ -21,7 +21,7 @@ package io.spine.dependency.local */ @Suppress("ConstPropertyName") object TestLib { - const val version = "2.0.0-SNAPSHOT.213" + const val version = "2.0.0-SNAPSHOT.214" const val group = Spine.toolsGroup const val artifact = "base-testlib" const val lib = "$group:$artifact:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt index 9acd345..63ecfca 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt @@ -29,8 +29,8 @@ package io.spine.dependency.local @Suppress("ConstPropertyName", "unused") object ToolBase { const val group = Spine.toolsGroup - const val version = "2.0.0-SNAPSHOT.423" - const val dogfoodingVersion = "2.0.0-SNAPSHOT.423" + const val version = "2.0.0-SNAPSHOT.425" + const val dogfoodingVersion = "2.0.0-SNAPSHOT.425" const val archive = "$group:archive:$version" const val code = "$group:code:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/storage/H2.kt b/buildSrc/src/main/kotlin/io/spine/dependency/storage/H2.kt index b970875..a7e81ff 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/storage/H2.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/storage/H2.kt @@ -23,6 +23,6 @@ package io.spine.dependency.storage */ @Suppress("unused", "ConstPropertyName") object H2 { - private const val version = "2.4.240" + private const val version = "2.5.252" const val lib = "com.h2database:h2:$version" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/storage/PostgreSql.kt b/buildSrc/src/main/kotlin/io/spine/dependency/storage/PostgreSql.kt index fbfa0e9..9f47ac8 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/storage/PostgreSql.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/storage/PostgreSql.kt @@ -23,6 +23,6 @@ package io.spine.dependency.storage */ @Suppress("unused", "ConstPropertyName") object PostgreSql { - private const val version = "42.7.13" + private const val version = "42.7.14" const val connector = "org.postgresql:postgresql:$version" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/test/Kotest.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/Kotest.kt index 0f1bf46..b0bb899 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/test/Kotest.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/Kotest.kt @@ -23,7 +23,7 @@ package io.spine.dependency.test */ @Suppress("unused", "ConstPropertyName") object Kotest { - const val version = "6.2.4" + const val version = "6.2.5" const val group = "io.kotest" const val gradlePluginId = "io.kotest" const val assertions = "$group:kotest-assertions-core:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/test/KotlinCompileTesting.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/KotlinCompileTesting.kt index 4d54ce0..1976493 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/test/KotlinCompileTesting.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/KotlinCompileTesting.kt @@ -21,7 +21,7 @@ package io.spine.dependency.test */ @Suppress("unused", "ConstPropertyName") object KotlinCompileTesting { - private const val version = "0.13.0" + private const val version = "0.14.0" private const val group = "dev.zacsweers.kctfork" const val libCore = "$group:core:$version" const val libKsp = "$group:ksp:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/test/Kover.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/Kover.kt index cbaa2c8..9c984b4 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/test/Kover.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/Kover.kt @@ -17,7 +17,7 @@ package io.spine.dependency.test // https://github.com/Kotlin/kotlinx-kover @Suppress("unused", "ConstPropertyName") object Kover { - const val version = "0.9.9" + const val version = "0.9.11" const val id = "org.jetbrains.kotlinx.kover" const val classpath = "org.jetbrains.kotlinx:kover-gradle-plugin:$version" } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/test/TestKitTruth.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/TestKitTruth.kt index 76b3f7a..69fabe7 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/test/TestKitTruth.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/TestKitTruth.kt @@ -19,11 +19,16 @@ package io.spine.dependency.test /** * Gradle TestKit extension for Google Truth. * - * @see TestKit source code + * @see TestKit source code * @see Usage description */ @Suppress("unused", "ConstPropertyName") object TestKitTruth { - private const val version = "1.20.0" - const val lib = "com.autonomousapps:testkit-truth:$version" + private const val version = "1.7.1" + + /** + * The artifact has been published as `gradle-testkit-truth` since `1.3`. + * Earlier versions, up to `1.1`, were published as `testkit-truth`. + */ + const val lib = "com.autonomousapps:gradle-testkit-truth:$version" } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/checkstyle/CheckStyleConfig.kt b/buildSrc/src/main/kotlin/io/spine/gradle/checkstyle/CheckStyleConfig.kt index 6ae2115..b823268 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/checkstyle/CheckStyleConfig.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/checkstyle/CheckStyleConfig.kt @@ -18,6 +18,7 @@ import io.spine.dependency.build.CheckStyle import org.gradle.api.Project import org.gradle.api.plugins.quality.CheckstyleExtension import org.gradle.api.plugins.quality.CheckstylePlugin +import org.gradle.kotlin.dsl.exclude import org.gradle.kotlin.dsl.the /** @@ -51,6 +52,12 @@ object CheckStyleConfig { configDirectory.set(configDir) } + // Checkstyle needs Doxia only to generate its own website, and Doxia's + // dependencies conflict with the versions we force. See `CheckStyle.version`. + project.configurations.named("checkstyle") { + exclude(group = "org.apache.maven.doxia") + } + project.afterEvaluate { // Disables checking the test sources and test fixtures. arrayOf( diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt index 6b4b98d..f43f019 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt @@ -20,9 +20,13 @@ import io.spine.gradle.repo.Repository import java.net.URI import org.gradle.api.DefaultTask import org.gradle.api.GradleException -import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault /** * A task that verifies the project version is fit to be published. @@ -36,11 +40,19 @@ import org.gradle.api.tasks.TaskAction * parallel PRs that bumped to the same value, regardless of what is (or is not yet) * published. * 2. [checkNotPublished] — the [version] must not already exist in the target Maven - * repository, so a publication cannot overwrite an immutable artifact. + * repository for any of the [artifacts][artifactPaths] the project publishes, so + * a publication cannot overwrite an immutable artifact. * * The two checks are complementary; neither subsumes the other. + * + * Neither the task action nor the `onlyIf` spec that gates it may access + * [project][org.gradle.api.Task.getProject]: Gradle deprecates that at execution time, and + * the configuration cache does not support it. Therefore, [version] is captured when the + * task is created, [IncrementGuard] sets [rootDir] and [artifactPaths] when it registers + * the task, and it sets [publishesToMavenLocal] once the task graph is ready. */ -open class CheckVersionIncrement : DefaultTask() { +@DisableCachingByDefault(because = "Queries a remote Maven repository and produces no outputs.") +abstract class CheckVersionIncrement : DefaultTask() { /** * The Maven repository in which to look for published artifacts. @@ -54,6 +66,44 @@ open class CheckVersionIncrement : DefaultTask() { @Input val version: String = project.version as String + /** + * The root directory of the build, which holds `version.gradle.kts`. + * + * Only its location matters — the task reads `version.gradle.kts` and runs `git` there. + * Hence, it is not an [input directory][org.gradle.api.tasks.InputDirectory], which + * would fingerprint the whole build tree. + */ + @get:Internal + abstract val rootDir: DirectoryProperty + + /** + * The paths to the artifacts of the project in a Maven repository, such as + * `io/spine/spine-base`, one per Maven publication of the project. + * + * Empty for a project that publishes nothing. + */ + @get:Input + abstract val artifactPaths: SetProperty + + /** + * Tells whether the build is going to publish this task's project to Maven Local. + * + * Integration tests in this and sibling projects consume freshly built artifacts + * from `~/.m2`. Publishing them under a version that already exists would let those + * tests pick up a stale artifact, so a local build verifies the version increment + * before any local publication of the project runs. + * + * Only this task's own project counts: a sibling module's local publish in the same + * invocation must not trigger this module's check (see + * [IncrementGuard.localPublishPlanned]). + * + * [IncrementGuard] scans the task graph once it is ready and sets this property, which + * the `onlyIf` spec it adds to the task then reads. The value affects only whether + * the task runs, not what it verifies, so it is not an [input][Input]. + */ + @get:Internal + abstract val publishesToMavenLocal: Property + @TaskAction fun checkVersion() { checkIncrementedAgainstBase() @@ -132,7 +182,8 @@ open class CheckVersionIncrement : DefaultTask() { * [VersionGradleFile.contentInBase] when the base ref itself cannot be resolved. */ private fun baseVersionToCompare(baseRef: String): String? { - val headContent = VersionGradleFile.contentUnder(project.rootDir) + val root = rootDir.get().asFile + val headContent = VersionGradleFile.contentUnder(root) val key = headContent?.let { VersionGradleFile.keyForValue(it, version) } if (key == null) { logger.warn( @@ -141,7 +192,7 @@ open class CheckVersionIncrement : DefaultTask() { ) return null } - val baseContent = VersionGradleFile.contentInBase(project.rootDir, baseRef) + val baseContent = VersionGradleFile.contentInBase(root, baseRef) val baseVersion = baseContent?.let { VersionGradleFile.valueForKey(it, key) } if (baseVersion == null) { logger.info( @@ -154,29 +205,76 @@ open class CheckVersionIncrement : DefaultTask() { /** * Verifies that the current [version] has not been published to the target Maven - * repository yet. + * repository yet, for any of the [artifactPaths]. * * Both the `releases` and `snapshots` repositories are checked; artifacts in either * may not be overwritten. */ private fun checkNotPublished() { - val artifact = "${project.artifactPath()}/${MavenMetadata.FILE_NAME}" - val snapshots = repository.target(snapshots = true) - checkInRepo(snapshots, artifact) + val paths = artifactPaths.get() + if (paths.isEmpty()) { + logger.info( + "The project has no Maven publications; " + + "skipping the check for an already published version." + ) + return + } + paths.forEach(::checkArtifactNotPublished) + } - if (!repository.hasOneTarget()) { - checkInRepo(repository.target(snapshots = false), artifact) + /** + * Verifies that the current [version] of the artifact at the given [path] has not been + * published to the target Maven repository yet. + * + * Each repository is checked as soon as its metadata is fetched, so a failure to reach + * one repository does not hide the version found in another one checked before it. + * + * When no repository has the metadata of the artifact, the version cannot be verified, + * and a warning is logged. This is expected before the first publication of the artifact. + * For an artifact published before, it means that [path] does not match the published one. + */ + private fun checkArtifactNotPublished(path: String) { + val artifact = "$path/${MavenMetadata.FILE_NAME}" + val repositories = targetRepositories() + var found = false + for (repoUrl in repositories) { + val metadata = fetch(repoUrl, artifact) ?: continue + found = true + checkNotListed(path = path, repoUrl = repoUrl, metadata = metadata) + } + if (!found) { + logger.warn( + "No `${MavenMetadata.FILE_NAME}` is found for `$path` in " + + "${repositories.joinToString { "`$it`" }}. Either the artifact has never " + + "been published, or the path does not match the published artifact. " + + "`$name` cannot tell whether the version `$version` is already published." + ) + } + } + + /** + * Returns the URLs of the target repositories: the `snapshots` one, and + * the `releases` one unless it is the same destination. + */ + private fun targetRepositories(): List { + val snapshots = repository.target(snapshots = true) + if (repository.hasOneTarget()) { + return listOf(snapshots) } + return listOf(snapshots, repository.target(snapshots = false)) } - private fun checkInRepo(repoUrl: String, artifact: String) { - val metadata = fetch(repoUrl, artifact) - val versions = metadata?.versioning?.versions - val versionExists = versions?.contains(version) ?: false - if (versionExists) { + /** + * Throws if the [metadata] of the artifact at the given [path], fetched from [repoUrl], + * lists the current [version]. + */ + private fun checkNotListed(path: String, repoUrl: String, metadata: MavenMetadata) { + val versions = metadata.versioning.versions + if (version in versions) { throw GradleException( """ - The version `$version` is already published to the Maven repository `$repoUrl`. + The version `$version` of `$path` is already published + to the Maven repository `$repoUrl`. Try incrementing the library version. All available versions are: ${versions.joinToString(separator = ", ")}. @@ -190,29 +288,4 @@ open class CheckVersionIncrement : DefaultTask() { val url = URI.create("$repository/$artifact").toURL() return MavenMetadata.fetchAndParse(url) } - - private fun Project.artifactPath(): String { - val group = this.group as String - val name = "${artifactPrefix()}${this.name}" - - val pathElements = ArrayList(group.split('.')) - pathElements.add(name) - val path = pathElements.joinToString(separator = "/") - return path - } - - /** - * Returns the artifact prefix used for the publishing of this project. - * - * All current Spine modules should be using `SpinePublishing`. - * Therefore, the corresponding extension should be present in the root project. - * However, just in case, we define the "standard" prefix here as well. - * - * This value MUST be the same as defined by the defaults in `SpinePublishing`. - */ - private fun Project.artifactPrefix(): String { - val ext = rootProject.extensions.findByType(SpinePublishing::class.java) - val result = ext?.artifactPrefix ?: SpinePublishing.DEFAULT_PREFIX - return result - } } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt index 29b7dad..121cc20 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt @@ -21,6 +21,7 @@ import io.spine.gradle.SpineTaskGroup import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.Task +import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.tasks.PublishToMavenLocal /** @@ -143,8 +144,27 @@ class IncrementGuard : Plugin { group = SpineTaskGroup.name description = "Verifies that the project version was incremented before publishing" repository = CloudArtifactRegistry.repository + rootDir.set(target.rootDir) + // Resolved lazily: publications and their coordinates are configured + // after the task is created, some of them in `afterEvaluate`. + artifactPaths.set(target.provider { target.publishedArtifactPaths() }) onlyIf { - mustVerify(shouldCheckVersion(), Build.ci, it.publishesToMavenLocal()) + mustVerify( + ciPullRequest = shouldCheckVersion(), + onCi = Build.ci, + localPublish = publishesToMavenLocal.get() + ) + } + } + + // The `onlyIf` spec above runs at execution time, when it must not call `Task.project` + // to reach the task graph. So the graph is scanned for this project's Maven Local + // publishing at configuration time, once it is known. The result is kept in a task + // property, which a configuration cache entry stores along with the task. + target.gradle.taskGraph.whenReady { + val graph = this + checkVersion.configure { + publishesToMavenLocal.set(localPublishPlanned(graph.allTasks, target)) } } @@ -181,18 +201,28 @@ class IncrementGuard : Plugin { } /** - * Tells whether the current build is going to publish this task's project to - * Maven Local. + * Obtains the paths in a Maven repository to the artifacts this project publishes, + * such as `io/spine/spine-base`. + * + * The paths are taken from the coordinates of the Maven publications of the project, + * rather than computed from its name. So they cover the + * [tool artifact prefix][SpinePublishing.toolArtifactPrefix], an `artifactId` set by + * a module with [custom publishing][SpinePublishing.modulesWithCustomPublishing], and + * every publication of a module that has several, such as a Kotlin Multiplatform one. * - * Integration tests in this and sibling projects consume freshly built artifacts - * from `~/.m2`. Publishing them under a version that already exists would let those - * tests pick up a stale artifact, so the version increment must be verified before - * any local publication runs. + * Plugin markers are included. A marker is a publication of its own, which may be + * uploaded without the plugin it points to, e.g., by a publish that failed halfway. * - * Only this task's own project is considered: a sibling module's local publish in - * the same invocation must not trigger this module's check. The predicate is - * evaluated lazily as a task `onlyIf` spec, by which point the execution - * [task graph][org.gradle.api.execution.TaskExecutionGraph] is fully populated. + * The result is empty for a project that publishes nothing. + */ +private fun Project.publishedArtifactPaths(): Set = + mavenPublications() + .map { it.repositoryPath } + .toSet() + +/** + * The path to the artifact of this publication in a Maven repository, + * such as `io/spine/spine-base`. */ -private fun Task.publishesToMavenLocal(): Boolean = - IncrementGuard.localPublishPlanned(project.gradle.taskGraph.allTasks, project) +private val MavenPublication.repositoryPath: String + get() = "${groupId.replace('.', '/')}/$artifactId" diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt index 1884c54..1d4bb96 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt @@ -16,16 +16,22 @@ package io.spine.gradle.publish import DocumentationSettings import LicenseSettings +import io.spine.gradle.SpineTaskGroup import io.spine.gradle.artifactId import io.spine.gradle.isSnapshot import io.spine.gradle.repo.Repository import io.spine.gradle.report.pom.InceptionYear +import java.util.Locale +import java.util.concurrent.Callable import org.gradle.api.Project import org.gradle.api.artifacts.dsl.RepositoryHandler +import org.gradle.api.artifacts.repositories.MavenArtifactRepository import org.gradle.api.invocation.BuildInvocationDetails import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.publish.maven.tasks.PublishToMavenRepository import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.support.serviceOf +import org.gradle.kotlin.dsl.withType /** * The name of the Maven Publishing Gradle plugin. @@ -276,18 +282,55 @@ sealed class PublicationHandler( } /** - * Adds a Maven repository to the project specifying credentials, if they are - * [available][Repository.credentials] from the root project. + * Adds a Maven repository to the project. + * + * The credentials of the repository, if they are [available][Repository.credentials] + * from the root project, are supplied only when a task publishing to it is about + * to run (see [supplyCredentials]). */ private fun RepositoryHandler.register(project: Project, repository: Repository) { val isSnapshot = project.version.toString().isSnapshot() - val credentials = repository.credentials(project.rootProject) - maven { + val target = maven { name = repository.name(isSnapshot) url = project.uri(repository.target(isSnapshot)) - credentials { - username = credentials?.username - password = credentials?.password + } + project.supplyCredentials(target, repository) +} + +/** + * Registers a supplier task that sets the credentials of the [target] repository + * to those of the [source] one, and makes each task publishing to [target] + * depend on the supplier task. + * + * Obtaining the credentials may start an external process, e.g., `gcloud` + * for [CloudArtifactRegistry]. Gradle deprecates starting one at configuration time, + * and builds that do not publish should not obtain the credentials at all. + * An action of the publishing task cannot supply them, either: the task validates + * them before running any of its actions. Hence, the publishing task depends on + * the supplier task, which sets them on the repository rather than producing an output. + */ +private fun Project.supplyCredentials(target: MavenArtifactRepository, source: Repository) { + // Captured at configuration time: the action must not reach the project + // through its task, as Gradle deprecates calling `Task.project` at execution time. + val rootProject = rootProject + val repoName = target.name.replaceFirstChar { it.titlecase(Locale.ROOT) } + val supplier = tasks.register("supply${repoName}Credentials") { + group = SpineTaskGroup.name + description = "Obtains the credentials for publishing to the `${target.name}` repository" + notCompatibleWithConfigurationCache( + "Gradle does not support explicit repository credentials with the configuration cache." + ) + doLast { + val credentials = source.credentials(rootProject) + target.credentials { + username = credentials?.username + password = credentials?.password + } } } + tasks.withType().configureEach { + // The `maven-publish` plugin may assign the repository after this action runs, + // so the dependency is resolved later, when Gradle builds the task graph. + dependsOn(Callable { if (repository === target) supplier else null }) + } } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt index 04df443..5a876d2 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt @@ -203,7 +203,19 @@ private fun TaskContainer.registerCheckCredentialsTask( } } +/** + * Adds an action that fails this task if any of the [destinations] has no credentials. + * + * The action obtains the credentials with the [Project] of this task. The project is + * captured when this function runs, at configuration time, because Gradle deprecates + * calling [Task.getProject] at execution time. The configuration cache cannot store + * a project, though, so the task is not compatible with it. + */ private fun Task.doLastCredentialsCheck(destinations: Set) { + val project = project + notCompatibleWithConfigurationCache( + "Obtaining the credentials of a repository requires the `Project`." + ) doLast { if (logger.isDebugEnabled) { val isSnapshot = project.version.toString().isSnapshot() diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt index 74e0d3c..c552dd4 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt @@ -111,14 +111,21 @@ fun RepositoryHandler.standardToSpineSdk() { mavenCentral { excludeSpine() } gradlePluginPortal { excludeSpine() } - spineArtifacts() - + // + // Spine repositories. Gradle fetches a module's artifacts only from the repository + // that served its metadata. If that repository drops the artifacts after a build + // cached the metadata, later builds fail instead of falling through to the next one. + // + // Google Artifact Registry retains old versions, so it comes first. `CloudRepo` + // serves only earlier versions. GitHub Packages comes last: a scheduled workflow + // keeps only the newest snapshots of each package there. + // @Suppress("DEPRECATION") // Still use `CloudRepo` for earlier versions. val spineRepos = listOf( - Repos.spine, - Repos.spineSnapshots, Repos.artifactRegistry, - Repos.artifactRegistrySnapshots + Repos.artifactRegistrySnapshots, + Repos.spine, + Repos.spineSnapshots ) spineRepos @@ -130,6 +137,8 @@ fun RepositoryHandler.standardToSpineSdk() { } } + spineArtifacts() + // IntelliJ Platform repositories. `intellijReleases` serves the platform // artifacts (`com.jetbrains.intellij.*`); `intellijDependencies` serves the // repackaged third-party dependencies (`org.jetbrains.intellij.deps.*` and diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt index 4c39315..8e747f3 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt @@ -29,6 +29,10 @@ import org.gradle.api.Project * @param credentialsFile The path to the file that contains the credentials for the registry. * @param credentialValues The function to obtain an instance of [Credentials] from * a Gradle [Project], if [credentialsFile] is not specified. + * Publishing calls it only at execution time, so it may start an external process. + * It may be called several times for each published module, but never concurrently. + * It must not wait for the state of any project, e.g., resolve a configuration: + * the calls hold a lock that the tasks of all projects share. */ data class Repository( private val name: String, @@ -40,6 +44,11 @@ data class Repository( companion object { val regex = Regex("[A-Za-z0-9_\\-.]+") + + /** + * Serializes obtaining the credentials of all repositories (see [credentials]). + */ + private val credentialsLock = Any() } init { @@ -76,13 +85,22 @@ data class Repository( * If the credentials are represented by a `.properties` file, reads the file and parses * the credentials. The file must have properties `user.name` and `user.password`, which store * the username and the password for the Maven repository auth. + * + * Call this function from a task action rather than at configuration time: + * obtaining the credentials may start an external process, such as `gcloud`. + * + * The calls are serialized. Tasks of several projects may call it at once, and + * obtaining the credentials may write files that other calls share, e.g., + * the GitHub Packages token unpacked into the build directory of the given project. */ - fun credentials(project: Project): Credentials? = when { - credentialValues != null -> credentialValues.invoke(project) - credentialsFile != null -> credsFromFile(credentialsFile, project) - else -> throw IllegalArgumentException( - "Credentials file or a supplier function should be passed." - ) + fun credentials(project: Project): Credentials? = synchronized(credentialsLock) { + when { + credentialValues != null -> credentialValues.invoke(project) + credentialsFile != null -> credsFromFile(credentialsFile, project) + else -> throw IllegalArgumentException( + "Credentials file or a supplier function should be passed." + ) + } } private fun credsFromFile(fileName: String, project: Project): Credentials? { diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/checkstyle/CheckStyleConfigSpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/checkstyle/CheckStyleConfigSpec.kt new file mode 100644 index 0000000..8b7669f --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/checkstyle/CheckStyleConfigSpec.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.gradle.checkstyle + +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.shouldBe +import io.spine.dependency.build.CheckStyle +import org.gradle.api.Project +import org.gradle.api.plugins.quality.CheckstyleExtension +import org.gradle.kotlin.dsl.the +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`CheckStyleConfig` should") +internal class CheckStyleConfigSpec { + + private val project: Project = ProjectBuilder.builder().build().also { + CheckStyleConfig.applyTo(it) + } + + @Test + fun `use the declared Checkstyle version`() { + project.the().toolVersion shouldBe CheckStyle.version + } + + @Test + fun `exclude the whole Doxia group from the Checkstyle classpath`() { + val rules = project.configurations.getByName("checkstyle").excludeRules + rules.map { it.group to it.module } shouldContain ("org.apache.maven.doxia" to null) + } +} diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/docs/DokkaSetupIgTest.kt b/buildSrc/src/test/kotlin/io/spine/gradle/docs/DokkaSetupIgTest.kt new file mode 100644 index 0000000..6ec5eff --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/docs/DokkaSetupIgTest.kt @@ -0,0 +1,295 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.gradle.docs + +import io.kotest.assertions.withClue +import io.kotest.matchers.file.shouldExist +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import java.io.File +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome.SKIPPED +import org.gradle.testkit.runner.TaskOutcome.SUCCESS +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +/** + * The path of the probe task that the build under test declares. + */ +private const val PROBE_PATH = ":dokka-sample:dokkaGeneratePublicationProbe" + +/** + * Verifies when the `dokka-setup` plugin lets Dokka tasks run in a real build + * that uses the configuration cache. + * + * Every case runs the build twice: the first run stores the configuration cache entry, + * and the second one reuses it. The task must reach the same outcome both times, so the + * decision whether to run cannot rely on configuration that a reused entry skips. + * + * Dokka's own tasks resolve the Dokka engine from Maven Central, which an offline + * build cannot reach. So the build declares a probe task that extends + * [DokkaBaseTask][org.jetbrains.dokka.gradle.tasks.DokkaBaseTask] and does nothing. + * The plugin gates it like any other Dokka task. The probe is named like + * `dokkaGeneratePublicationJavadoc`: the Dokka 2.x tasks that generate documentation + * have `dokkaGenerate` in their names, so a gate that relied on task names would never + * skip them. + * + * The build publishes a `javadocJar` that depends on the probe, just as the `javadocJar` + * that `spinePublishing` creates depends on `dokkaGeneratePublicationJavadoc`. The build + * publishes to a file-based repository and to its own Maven Local repository, so it never + * touches `~/.m2`. The `updateGitHubPages` task of the build stands in for the one that + * copies the Dokka output to GitHub Pages. + * + * The project with the probe is named `dokka-sample`, like the modules of `dokka-tools`, + * so that the tests cover `dokka` in the path of a requested task, which must not count + * as a Dokka request. + */ +@DisplayName("`dokka-setup` should, with the configuration cache,") +internal class DokkaSetupIgTest { + + @TempDir + lateinit var projectDir: File + + @BeforeEach + fun createBuild() { + writeBuild(projectDir, "documented-sample") + } + + @Test + fun `skip a Dokka task when the build publishes to Maven Local only`() { + runStoringThenReusing(":dokka-sample:publishToMavenLocal") { + it.probeOutcome shouldBe SKIPPED + // The javadoc JAR is still published, which is enough for integration tests. + mavenLocal.resolve("io/spine/sample/dokka-sample/1.0.0/dokka-sample-1.0.0-javadoc.jar") + .shouldExist() + } + } + + @Test + fun `run a Dokka task when the build publishes to a remote repository`() { + runStoringThenReusing(":dokka-sample:publish") { + it.probeOutcome shouldBe SUCCESS + } + } + + @Test + fun `run a Dokka task when the build publishes to both Maven Local and a remote repository`() { + runStoringThenReusing(":dokka-sample:publishToMavenLocal", ":dokka-sample:publish") { + it.probeOutcome shouldBe SUCCESS + } + } + + @Test + fun `run a Dokka task when a repository publication task runs along with Maven Local`() { + runStoringThenReusing( + ":dokka-sample:publishToMavenLocal", + ":dokka-sample:publishDocsPublicationToRemoteRepository" + ) { + it.probeOutcome shouldBe SUCCESS + } + } + + @Test + fun `run a Dokka task when the build updates GitHub Pages and publishes to Maven Local`() { + runStoringThenReusing( + ":dokka-sample:publishToMavenLocal", + ":dokka-sample:updateGitHubPages" + ) { + it.probeOutcome shouldBe SUCCESS + } + } + + @Test + fun `run a Dokka task when it is requested along with publishing to Maven Local`() { + runStoringThenReusing(":dokka-sample:publishToMavenLocal", PROBE_PATH) { + it.probeOutcome shouldBe SUCCESS + } + } + + @Test + fun `run a Dokka task requested by an abbreviation along with publishing to Maven Local`() { + // Gradle selects `dokkaGeneratePublicationProbe` by this camel-case abbreviation. + runStoringThenReusing(":dokka-sample:publishToMavenLocal", ":dokka-sample:dGPP") { + it.probeOutcome shouldBe SUCCESS + } + } + + @Test + fun `run a Dokka task when a docs JAR is requested along with publishing to Maven Local`() { + runStoringThenReusing(":dokka-sample:publishToMavenLocal", ":dokka-sample:javadocJar") { + it.probeOutcome shouldBe SUCCESS + } + } + + @Test + fun `run a Dokka task requested from an included build along with Maven Local`() { + // Gradle passes an included build no task names, so it cannot see the request. + val included = "included" + writeBuild(file(included), included) + file("settings.gradle.kts").appendText("\nincludeBuild(\"$included\")\n") + val probe = ":$included$PROBE_PATH" + runStoringThenReusing(":$included:dokka-sample:publishToMavenLocal", probe) { + it.task(probe)?.outcome shouldBe SUCCESS + } + } + + @Test + fun `run a Dokka task when the build does not publish`() { + runStoringThenReusing(":dokka-sample:javadocJar") { + it.probeOutcome shouldBe SUCCESS + } + } + + @Test + fun `run a Dokka task when the build generates documentation`() { + runStoringThenReusing(PROBE_PATH) { + it.probeOutcome shouldBe SUCCESS + } + } + + /** + * Writes a build named [name] into [dir], with the probe in its `dokka-sample` project. + */ + private fun writeBuild(dir: File, name: String) { + dir.mkdirs() + dir.resolve("settings.gradle.kts").writeText( + """ + rootProject.name = "$name" + include("dokka-sample") + """.trimIndent() + ) + // The standard library would be resolved from Maven Central otherwise, + // which an offline build cannot reach. + dir.resolve("gradle.properties").writeText("kotlin.stdlib.default.dependency=false\n") + dir.resolve("build.gradle.kts").writeText( + """ + buildscript { + dependencies { + classpath(files( + ${buildSrcClasspath()} + )) + } + } + """.trimIndent() + ) + dir.resolve("dokka-sample").mkdirs() + dir.resolve("dokka-sample/build.gradle.kts").writeText( + """ + import org.jetbrains.dokka.gradle.internal.InternalDokkaGradlePluginApi + import org.jetbrains.dokka.gradle.tasks.DokkaBaseTask + + plugins { + kotlin("jvm") + `maven-publish` + id("dokka-setup") + } + + @OptIn(InternalDokkaGradlePluginApi::class) + abstract class DocsProbe : DokkaBaseTask() { + + @TaskAction + fun probe() = Unit + } + + group = "io.spine.sample" + version = "1.0.0" + + val docsProbe = tasks.register("dokkaGeneratePublicationProbe") + + val javadocJar = tasks.register("javadocJar") { + archiveClassifier.set("javadoc") + from(layout.buildDirectory.dir("docs-probe")) + dependsOn(docsProbe) + } + + publishing { + publications.create("docs") { + artifact(javadocJar) + } + repositories.maven { + name = "remote" + url = uri(rootDir.resolve("remote-repo")) + } + } + + tasks.register("updateGitHubPages") { + dependsOn(docsProbe) + } + """.trimIndent() + ) + } + + private val BuildResult.probeOutcome + get() = task(PROBE_PATH)?.outcome + + /** + * The Maven Local repository of the build, which replaces `~/.m2/repository`. + */ + private val mavenLocal: File + get() = file("maven-local") + + /** + * Runs the given [tasks] twice with the configuration cache, and checks the result of + * each run with [verify]. + * + * The first run stores the cache entry, and the second one reuses it. + */ + private fun runStoringThenReusing(vararg tasks: String, verify: (BuildResult) -> Unit) { + val stored = runGradle(tasks) + withClue("The run that stores the configuration cache entry") { + stored.output shouldContain "Configuration cache entry stored" + verify(stored) + } + val reused = runGradle(tasks) + withClue("The run that reuses the configuration cache entry") { + reused.output shouldContain "Reusing configuration cache" + verify(reused) + } + } + + private fun runGradle(tasks: Array): BuildResult = + GradleRunner.create() + .withProjectDir(projectDir) + .withArguments( + *tasks, + "--configuration-cache", + "--offline", + "--stacktrace", + "-Dmaven.repo.local=${mavenLocal.absolutePath}" + ) + .build() + + private fun file(relativePath: String): File = projectDir.resolve(relativePath) + + /** + * Renders the classpath with the production classes of `buildSrc` as + * arguments of `files(...)`, for injection into the build script classpath + * of the generated build. + * + * The classpath comes from the `test` task in `buildSrc/build.gradle.kts`. + */ + private fun buildSrcClasspath(): String { + val classpath = requireNotNull(System.getProperty("buildSrc.classpath")) { + "The `buildSrc.classpath` system property is not set." + + " It is supplied by the `test` task in `buildSrc/build.gradle.kts`." + } + return classpath.split(File.pathSeparator).joinToString(",\n") { + " \"${File(it).invariantSeparatorsPath}\"" + } + } +} diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/publish/CheckVersionIncrementIgTest.kt b/buildSrc/src/test/kotlin/io/spine/gradle/publish/CheckVersionIncrementIgTest.kt new file mode 100644 index 0000000..2d440b0 --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/publish/CheckVersionIncrementIgTest.kt @@ -0,0 +1,179 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.gradle.publish + +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain +import java.io.File +import org.gradle.testkit.runner.GradleRunner +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +/** + * Runs `checkVersionIncrement` in a real build via Gradle TestKit. + * + * The build publishes a module under a custom `artifactId`, as `:compiler-plugins` of + * `core-jvm-compiler` does, and checks it against a Maven repository in a local directory. + * The environment of a pull request to `master` makes the task run. + * + * Unlike the tests built with `ProjectBuilder`, this one sees the output of the build, + * which carries the warnings of the task. + * + * The builds run without the [ciVariables] of the test JVM, like those of + * [IncrementGuardIgTest], which explains why a TestKit build must not see `CI`. + */ +@DisplayName("`checkVersionIncrement` should") +internal class CheckVersionIncrementIgTest { + + @TempDir + lateinit var projectDir: File + + @Test + fun `fail when the version of a publication is already published`() { + repoDir.writeMetadata(repository = "snapshots", path = artifactPath, version) + writeBuild() + + val output = runner().buildAndFail().output + + output shouldContain "The version `$version` of `$artifactPath` is already published" + } + + @Test + fun `pass without a warning when the version is not published yet`() { + repoDir.writeMetadata(repository = "snapshots", path = artifactPath, previousVersion) + writeBuild() + + val output = runner().build().output + + output shouldNotContain "No `${MavenMetadata.FILE_NAME}` is found" + } + + @Test + fun `warn when no repository has the metadata of a publication`() { + writeBuild() + + val output = runner().build().output + + output shouldContain "No `${MavenMetadata.FILE_NAME}` is found for `$artifactPath`" + output shouldContain + "cannot tell whether the version `$version` is already published" + } + + /** + * The directory holding the `snapshots` and `releases` repositories. + */ + private val repoDir: File + get() = projectDir.resolve("repo") + + private fun runner(): GradleRunner = + GradleRunner.create() + .withProjectDir(projectDir) + .withEnvironment(pullRequestEnvironment()) + .withArguments(IncrementGuard.taskName, "--stacktrace") + + /** + * Returns the environment of a build of a pull request to `master`, outside + * the `Version Guard` workflow, so that the task runs and skips the comparison + * with the base branch. + */ + private fun pullRequestEnvironment(): Map = + System.getenv() - ciVariables + mapOf( + "GITHUB_EVENT_NAME" to "pull_request", + "GITHUB_BASE_REF" to "master", + ) + + private fun writeBuild() { + val repoUrl = repoDir.toURI().toString().removeSuffix("/") + file("settings.gradle.kts").writeText( + """ + rootProject.name = "compiler-plugins" + """.trimIndent() + ) + file("build.gradle.kts").writeText( + """ + buildscript { + dependencies { + classpath(files( + ${buildSrcClasspath()} + )) + } + } + + import io.spine.gradle.publish.CheckVersionIncrement + import io.spine.gradle.publish.IncrementGuard + import io.spine.gradle.repo.Repository + + plugins { + `maven-publish` + } + + group = "$group" + version = "$version" + + apply() + + publishing { + publications { + create("fatJar") { + artifactId = "$artifactId" + } + } + } + + tasks.named(IncrementGuard.taskName) { + repository = Repository( + name = "local", + releases = "$repoUrl/releases", + snapshots = "$repoUrl/snapshots", + ) + } + """.trimIndent() + ) + } + + private fun file(relativePath: String): File = projectDir.resolve(relativePath) + + /** + * Renders the classpath with the production classes of `buildSrc` as + * arguments of `files(...)`, for injection into the build script classpath + * of the generated build. + * + * The classpath comes from the `test` task in `buildSrc/build.gradle.kts`. + */ + private fun buildSrcClasspath(): String { + val classpath = requireNotNull(System.getProperty("buildSrc.classpath")) { + "The `buildSrc.classpath` system property is not set." + + " It is supplied by the `test` task in `buildSrc/build.gradle.kts`." + } + return classpath.split(File.pathSeparator).joinToString(",\n") { + " \"${File(it).invariantSeparatorsPath}\"" + } + } + + private companion object { + + const val group = "io.spine.tools" + const val version = "2.0.0-SNAPSHOT.094" + const val previousVersion = "2.0.0-SNAPSHOT.093" + + /** + * The `artifactId` of the publication, unrelated to the name of the project. + */ + const val artifactId = "core-jvm-plugins" + + const val artifactPath = "io/spine/tools/$artifactId" + } +} diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardIgTest.kt b/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardIgTest.kt new file mode 100644 index 0000000..c4fea07 --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardIgTest.kt @@ -0,0 +1,201 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.gradle.publish + +import io.kotest.assertions.withClue +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import java.io.File +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome.SKIPPED +import org.gradle.testkit.runner.TaskOutcome.SUCCESS +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +/** + * Verifies when `checkVersionIncrement` runs in a real build that uses the + * configuration cache. + * + * Every case runs the build twice: the first run stores the configuration cache entry, + * and the second one reuses it. The task must reach the same outcome both times, so the + * decision whether to run cannot rely on configuration that a reused entry skips. + * + * The builds run without the `CI` and GitHub Actions variables of the test JVM, so the + * outcome is the same on a workstation and on CI. The Maven repository the task queries + * is a missing local directory, so the task finds no published versions and passes + * without network access. + * + * [io.spine.gradle.Build.ci] is read once per class loader, and a TestKit daemon may reuse + * the build script class loader across builds. Therefore, any TestKit build in this test + * run that reads `Build.ci` with `CI` set — e.g., by running `checkVersionIncrement` + * without removing `CI` from its environment — would leak that value into these builds. + */ +@DisplayName("`checkVersionIncrement` should, with the configuration cache,") +internal class IncrementGuardIgTest { + + @TempDir + lateinit var projectDir: File + + @BeforeEach + fun createBuild() { + file("settings.gradle.kts").writeText( + """ + rootProject.name = "guarded-sample" + include("lib", "app") + """.trimIndent() + ) + file("lib").mkdirs() + file("app").mkdirs() + file("build.gradle.kts").writeText( + """ + buildscript { + dependencies { + classpath(files( + ${buildSrcClasspath()} + )) + } + } + + import io.spine.gradle.publish.CheckVersionIncrement + import io.spine.gradle.publish.IncrementGuard + import io.spine.gradle.repo.Repository + + allprojects { + group = "io.spine.sample" + version = "1.0.0" + } + + subprojects { + apply(plugin = "maven-publish") + apply() + configure { + publications { + create("maven") + } + } + val unpublished = rootDir.resolve("repository").toURI().toString() + tasks.withType().configureEach { + repository = Repository( + name = "unpublished", + releases = unpublished, + snapshots = unpublished + ) + } + } + """.trimIndent() + ) + } + + @Test + fun `run when its project publishes to Maven Local`() { + runStoringThenReusing(":lib:publishToMavenLocal") { + it.task(":lib:checkVersionIncrement")?.outcome shouldBe SUCCESS + } + } + + @Test + fun `skip when only a sibling project publishes to Maven Local`() { + runStoringThenReusing(":lib:checkVersionIncrement", ":app:publishToMavenLocal") { + it.task(":lib:checkVersionIncrement")?.outcome shouldBe SKIPPED + it.task(":app:checkVersionIncrement")?.outcome shouldBe SUCCESS + } + } + + @Test + fun `skip on a local build that does not publish`() { + runStoringThenReusing(":lib:checkVersionIncrement") { + it.task(":lib:checkVersionIncrement")?.outcome shouldBe SKIPPED + } + } + + @Test + fun `run on a pull request to a protected branch`() { + val pullRequest = mapOf( + "GITHUB_EVENT_NAME" to "pull_request", + "GITHUB_BASE_REF" to "master" + ) + + runStoringThenReusing(":lib:checkVersionIncrement", env = pullRequest) { + it.task(":lib:checkVersionIncrement")?.outcome shouldBe SUCCESS + } + } + + /** + * Runs the given [tasks] twice with the configuration cache, and checks the result of + * each run with [verify]. + * + * The first run stores the cache entry, and the second one reuses it. The [env] + * variables are added to the environment of the test JVM, from which the [ciVariables] + * are removed. + */ + private fun runStoringThenReusing( + vararg tasks: String, + env: Map = emptyMap(), + verify: (BuildResult) -> Unit + ) { + val stored = runGradle(tasks, env) + withClue("The run that stores the configuration cache entry") { + stored.output shouldContain "Configuration cache entry stored" + verify(stored) + } + val reused = runGradle(tasks, env) + withClue("The run that reuses the configuration cache entry") { + reused.output shouldContain "Reusing configuration cache" + verify(reused) + } + } + + private fun runGradle(tasks: Array, env: Map): BuildResult { + val environment = System.getenv() - ciVariables + env + return GradleRunner.create() + .withProjectDir(projectDir) + .withEnvironment(environment) + .withArguments( + *tasks, + "--configuration-cache", + "-Dmaven.repo.local=${file("m2").absolutePath}", + "--stacktrace" + ) + .build() + } + + private fun file(relativePath: String): File = projectDir.resolve(relativePath) + + /** + * Renders the classpath with the production classes of `buildSrc` as + * arguments of `files(...)`, for injection into the build script classpath + * of the generated build. + * + * The classpath comes from the `test` task in `buildSrc/build.gradle.kts`. + */ + private fun buildSrcClasspath(): String { + val classpath = requireNotNull(System.getProperty("buildSrc.classpath")) { + "The `buildSrc.classpath` system property is not set." + + " It is supplied by the `test` task in `buildSrc/build.gradle.kts`." + } + return classpath.split(File.pathSeparator).joinToString(",\n") { + " \"${File(it).invariantSeparatorsPath}\"" + } + } +} + +/** + * The environment variables that select the CI behavior of [IncrementGuard] + * and [CheckVersionIncrement]. + */ +internal val ciVariables = setOf("CI", "GITHUB_EVENT_NAME", "GITHUB_BASE_REF", "VERSION_GUARD") diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt b/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt index bbb1b3d..9bcbc4e 100644 --- a/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt +++ b/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt @@ -14,20 +14,31 @@ package io.spine.gradle.publish +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldNotContain import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain import io.spine.gradle.publish.IncrementGuard.Companion.localPublishPlanned import io.spine.gradle.publish.IncrementGuard.Companion.mustVerify import io.spine.gradle.publish.IncrementGuard.Companion.shouldCheckVersion import io.spine.gradle.publish.IncrementGuard.Companion.shouldCompareToBase +import io.spine.gradle.repo.Repository +import java.io.File +import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.Task +import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.tasks.PublishToMavenLocal +import org.gradle.api.publish.maven.tasks.PublishToMavenRepository +import org.gradle.kotlin.dsl.create import org.gradle.testfixtures.ProjectBuilder import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir @DisplayName("`IncrementGuard` should") class IncrementGuardTest { @@ -156,6 +167,17 @@ class IncrementGuardTest { localPublishPlanned(listOf(appPublish), lib) shouldBe false } + + @Test + fun `but not when the project publishes only to a remote repository`() { + val project = guardedProject() + val remotePublish = project.tasks.register( + "publishFooPublicationToCloudRepository", + PublishToMavenRepository::class.java + ).get() + + localPublishPlanned(listOf(remotePublish), project) shouldBe false + } } @Nested @@ -188,8 +210,266 @@ class IncrementGuardTest { check.dependencyNames() shouldNotContain IncrementGuard.taskName } } + + @Nested + inner class `configure 'checkVersionIncrement' with` { + + @Test + fun `the root directory of the build`() { + val root = ProjectBuilder.builder().build() + val sub = ProjectBuilder.builder().withParent(root).withName("sub").build() + sub.pluginManager.apply(IncrementGuard::class.java) + + sub.checkVersionTask().rootDir.get().asFile shouldBe root.rootDir + } + + @Test + fun `the artifact path of each Maven publication`() { + val project = publishingProject() + project.publication( + "kotlinMultiplatform", + groupId = "io.spine", + artifactId = "spine-logging" + ) + project.publication("jvm", groupId = "io.spine", artifactId = "spine-logging-jvm") + + project.checkVersionTask().artifactPaths.get() shouldBe + setOf("io/spine/spine-logging", "io/spine/spine-logging-jvm") + } + + /** + * The case of `:compiler-plugins` in `core-jvm-compiler`, which publishes + * `core-jvm-plugins`. A path computed from the module name does not exist, + * so the check would pass even for an already published version. + */ + @Test + fun `the artifact path of a publication with a custom artifact ID`() { + val project = publishingProject("compiler-plugins") + project.publication( + "fatJar", + groupId = "io.spine.tools", + artifactId = "core-jvm-plugins" + ) + + project.checkVersionTask().artifactPaths.get() shouldBe + setOf("io/spine/tools/core-jvm-plugins") + } + + @Test + fun `the artifact path under the tool artifact prefix`() { + val root = ProjectBuilder.builder().build() + root.extensions.create(SpinePublishing.extensionName, root).run { + toolArtifactPrefix = "core-jvm-" + modulesWithCustomPublishing = setOf("guarded-tool") + } + val tool = ProjectBuilder.builder().withParent(root).withName("guarded-tool").build() + tool.group = "io.spine.tools" + tool.pluginManager.apply("maven-publish") + tool.pluginManager.apply(IncrementGuard::class.java) + val task = tool.checkVersionTask() + tool.publications.create("mavenJava") + CustomPublicationHandler.serving(tool, emptySet()).apply() + + task.artifactPaths.get() shouldBe + setOf("io/spine/tools/core-jvm-guarded-tool") + } + + @Test + fun `the coordinates set after the task is created`() { + val project = publishingProject() + val task = project.checkVersionTask() + val publication = project.publication( + "mavenJava", + groupId = "io.spine", + artifactId = "base" + ) + publication.artifactId = "spine-base" + + task.artifactPaths.get() shouldBe setOf("io/spine/spine-base") + } + + /** + * A marker may be uploaded without the plugin it points to, + * so it must be checked on its own. + */ + @Test + fun `the artifact path of a plugin marker`() { + val project = publishingProject() + project.publication( + "pluginMaven", + groupId = "io.spine.tools", + artifactId = "core-jvm-gradle-plugin" + ) + project.publication( + "coreJvmPluginMarkerMaven", + groupId = "io.spine.core-jvm", + artifactId = "io.spine.core-jvm.gradle.plugin" + ) + + project.checkVersionTask().artifactPaths.get() shouldBe setOf( + "io/spine/tools/core-jvm-gradle-plugin", + "io/spine/core-jvm/io.spine.core-jvm.gradle.plugin" + ) + } + + @Test + fun `no artifact paths for a project that does not publish`() { + val project = ProjectBuilder.builder().build() + project.pluginManager.apply(IncrementGuard::class.java) + + project.checkVersionTask().artifactPaths.get().shouldBeEmpty() + } + } + + @Nested + inner class `make 'checkVersionIncrement' fail` { + + @TempDir + lateinit var repoDir: File + + /** + * Only the publication checked last has the version published, so + * the task must look past the artifacts that do not. + */ + @Test + fun `when the version of any publication is already published`() { + val project = publishingProject() + project.version = publishedVersion + project.publication( + "fatJar", + groupId = "io.spine.tools", + artifactId = "core-jvm-plugins" + ) + project.publication( + "mavenJava", + groupId = "io.spine.tools", + artifactId = "core-jvm-gradle-plugin" + ) + repoDir.writeMetadata( + repository = "snapshots", + path = "io/spine/tools/core-jvm-plugins", + "2.0.0-SNAPSHOT.093" + ) + repoDir.writeMetadata( + repository = "snapshots", + path = "io/spine/tools/core-jvm-gradle-plugin", + publishedVersion + ) + val task = project.checkVersionTask() + task.repository = repoDir.asRepository() + + val exception = shouldThrow { task.checkVersion() } + + exception.message shouldContain "already published" + exception.message shouldContain "io/spine/tools/core-jvm-gradle-plugin" + } + + @Test + fun `when the version is already published to the releases repository`() { + val project = publishingProject() + project.version = "2.0.0" + project.publication("mavenJava", groupId = "io.spine", artifactId = "spine-base") + repoDir.writeMetadata(repository = "releases", path = "io/spine/spine-base", "2.0.0") + val task = project.checkVersionTask() + task.repository = repoDir.asRepository() + + val exception = shouldThrow { task.checkVersion() } + + exception.message shouldContain "already published" + } + + /** + * The unreadable metadata in `releases` stands for an unreachable repository, + * such as a response with the 401 or 5xx code. + */ + @Test + fun `when the version is published to a repository checked before a failing one`() { + val project = publishingProject() + project.version = publishedVersion + project.publication("mavenJava", groupId = "io.spine", artifactId = "spine-base") + repoDir.writeMetadata( + repository = "snapshots", + path = "io/spine/spine-base", + publishedVersion + ) + repoDir.resolve("releases/io/spine/spine-base/${MavenMetadata.FILE_NAME}").run { + parentFile.mkdirs() + writeText("Not a Maven metadata document.") + } + val task = project.checkVersionTask() + task.repository = repoDir.asRepository() + + val exception = shouldThrow { task.checkVersion() } + + exception.message shouldContain "already published" + } + } + + @Nested + inner class `make 'checkVersionIncrement' pass` { + + @TempDir + lateinit var repoDir: File + + @Test + fun `when the version is not published yet`() { + val project = publishingProject() + project.version = "2.0.0-SNAPSHOT.095" + project.publication( + "fatJar", + groupId = "io.spine.tools", + artifactId = "core-jvm-plugins" + ) + repoDir.writeMetadata( + repository = "snapshots", + path = "io/spine/tools/core-jvm-plugins", + publishedVersion + ) + val task = project.checkVersionTask() + task.repository = repoDir.asRepository() + + shouldNotThrowAny { task.checkVersion() } + } + + /** + * The task logs a warning in this case: it cannot tell a new artifact from + * a path that does not match the published one. + * + * @see CheckVersionIncrementIgTest for the check of the warning + */ + @Test + fun `when the artifact has never been published`() { + val project = publishingProject() + project.version = publishedVersion + project.publication( + "mavenJava", + groupId = "io.spine.tools", + artifactId = "core-jvm-new-module" + ) + val task = project.checkVersionTask() + task.repository = repoDir.asRepository() + + shouldNotThrowAny { task.checkVersion() } + } + + @Test + fun `for a project that does not publish`() { + val project = ProjectBuilder.builder().build() + project.version = publishedVersion + project.pluginManager.apply(IncrementGuard::class.java) + val task = project.checkVersionTask() + task.repository = repoDir.asRepository() + + shouldNotThrowAny { task.checkVersion() } + } + } } +/** + * The version listed as published by the metadata written with [writeMetadata]. + */ +private const val publishedVersion = "2.0.0-SNAPSHOT.094" + /** * Creates a project with the `base` plugin (for the `check` task), the * `maven-publish` plugin (for [PublishToMavenLocal] tasks), and [IncrementGuard] @@ -203,6 +483,76 @@ private fun guardedProject(): Project { return project } +/** + * Creates a project with the given [name], which has the `maven-publish` plugin and + * [IncrementGuard] applied. + */ +private fun publishingProject(name: String = "guarded"): Project { + val project = ProjectBuilder.builder().withName(name).build() + project.pluginManager.apply("maven-publish") + project.pluginManager.apply(IncrementGuard::class.java) + return project +} + +/** + * Creates a Maven publication with the given [name] and coordinates in this project. + */ +private fun Project.publication( + name: String, + groupId: String, + artifactId: String +): MavenPublication = publications.create(name) { + this.groupId = groupId + this.artifactId = artifactId +} + +/** + * Obtains the [CheckVersionIncrement] task that [IncrementGuard] registered in this project. + * + * The task captures the project version when it is created, so the version must be + * set before this call. + */ +private fun Project.checkVersionTask(): CheckVersionIncrement = + tasks.named(IncrementGuard.taskName, CheckVersionIncrement::class.java).get() + +/** + * Writes `maven-metadata.xml` listing the given [versions] of the artifact at [path] + * into the [repository] subdirectory of this directory. + * + * The [repository] is either `snapshots` or `releases`, as in [asRepository]. + */ +internal fun File.writeMetadata(repository: String, path: String, vararg versions: String) { + val (groupPath, artifactId) = path.split('/').let { it.dropLast(1) to it.last() } + val versionElements = versions.joinToString(separator = "") { "$it" } + val file = resolve("$repository/$path/${MavenMetadata.FILE_NAME}") + file.parentFile.mkdirs() + file.writeText( + """ + + + ${groupPath.joinToString(separator = ".")} + $artifactId + + $versionElements + + + """.trimIndent() + ) +} + +/** + * Represents this directory as a Maven repository with separate destinations + * for snapshots and releases, stored in the subdirectories of the same names. + */ +private fun File.asRepository(): Repository { + val url = toURI().toString().removeSuffix("/") + return Repository( + name = "local", + releases = "$url/releases", + snapshots = "$url/snapshots", + ) +} + /** * Obtains the names of the tasks this task directly depends on. */ diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/publish/PublishingCredentialsIgTest.kt b/buildSrc/src/test/kotlin/io/spine/gradle/publish/PublishingCredentialsIgTest.kt new file mode 100644 index 0000000..bfb5e70 --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/publish/PublishingCredentialsIgTest.kt @@ -0,0 +1,337 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.gradle.publish + +import com.sun.net.httpserver.HttpExchange +import com.sun.net.httpserver.HttpServer +import io.kotest.assertions.withClue +import io.kotest.matchers.file.shouldExist +import io.kotest.matchers.file.shouldNotExist +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain +import java.io.File +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.URI +import java.util.Base64 +import java.util.concurrent.CopyOnWriteArrayList +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +/** + * Verifies when the credentials of a publishing destination are obtained, + * against a real build run via Gradle TestKit. + * + * Obtaining the credentials may start an external process — `gcloud` for + * [CloudArtifactRegistry]. Gradle deprecates starting one at configuration time, + * so the credentials must be obtained only by a build that publishes, and only + * when it is about to. + * + * Each [Destination] of the fixture obtains its own credentials with a function + * that records each call in a file, and points at its own [ProbeRepository], + * an HTTP server that accepts uploads only with credentials. + */ +@DisplayName("Credentials of a publishing destination should") +internal class PublishingCredentialsIgTest { + + @TempDir + lateinit var projectDir: File + + private val destinations = listOf( + Destination(name = "Probe", username = "publisher", password = "secret"), + Destination(name = "Other", username = "deployer", password = "token"), + ) + + @BeforeEach + fun createBuild() { + file("settings.gradle.kts").writeText( + """ + rootProject.name = "credentials-sample" + include("lib") + """.trimIndent() + ) + file("build.gradle.kts").writeText( + """ + buildscript { + dependencies { + classpath(files( + ${buildSrcClasspath()} + )) + } + } + + import io.spine.gradle.publish.spinePublishing + import io.spine.gradle.repo.Credentials + import io.spine.gradle.repo.Repository + + allprojects { + group = "io.spine.sample" + version = "1.0.0" + } + + subprojects { + apply(plugin = "java-library") + apply(plugin = "maven-publish") + + // `javadocJar` of `spinePublishing` is wired to Dokka, which + // this fixture does not apply: the credentials are under test. + tasks.register("dokkaGeneratePublicationJavadoc") + + // The probe repositories are served over plain HTTP. + the().repositories + .withType() + .configureEach { isAllowInsecureProtocol = true } + } + + spinePublishing { + modules = setOf("lib") + destinations = setOf( + ${destinations.joinToString(",\n") { it.declaration() }} + ) + } + """.trimIndent() + ) + file("lib/src/main/java/io/spine/sample/Sample.java").apply { + parentFile.mkdirs() + writeText("package io.spine.sample;\n\npublic class Sample {}\n") + } + } + + @AfterEach + fun stopRepositories() { + destinations.forEach { it.repository.close() } + } + + @Test + fun `not be obtained by a build that does not publish`() { + val stored = runGradle("help", "--configuration-cache") + withClue("The run that stores the configuration cache entry") { + stored.output shouldContain "Configuration cache entry stored" + } + val reused = runGradle("help", "--configuration-cache") + withClue("The run that reuses the configuration cache entry") { + reused.output shouldContain "Reusing configuration cache" + } + + file(requestLog).shouldNotExist() + } + + @Test + fun `be sent with each upload to its own repository`() { + runGradle(publishTask) + + shouldSendOwnCredentials() + } + + /** + * Gradle does not support explicit repository credentials with the configuration + * cache, so a build that publishes with them falls back to running without it + * instead of failing. + */ + @Test + fun `be sent with each upload when the configuration cache is requested`() { + runGradle(publishTask, "--configuration-cache") + + shouldSendOwnCredentials() + } + + /** + * Gradle deprecates calling `Task.project` at execution time, which is + * when the check obtains the credentials. + */ + @Test + fun `be checked by 'checkCredentials' without reaching the project through its task`() { + val result = runGradle(checkTask, "--warning-mode", "all") + + result.output shouldNotContain "Invocation of Task.project at execution time" + file(requestLog).shouldExist() + } + + /** + * Only the last destination lacks credentials, so a check that stops + * at the first one cannot pass. + */ + @Test + fun `be required by 'checkCredentials' for every destination`() { + val lacking = destinations.last().name + val result = gradle(checkTask, "-P$withoutCredentials=$lacking").buildAndFail() + + result.output shouldContain "No valid credentials for repository `$lacking`" + } + + /** + * Asserts that each repository accepted uploads only with the credentials + * of its own destination. + * + * Checking the log of credential requests here shows that the fixture writes it, + * so the `help` case, which expects no log, cannot pass vacuously. + */ + private fun shouldSendOwnCredentials() { + file(requestLog).shouldExist() + destinations.forEach { + withClue(it.name) { + it.repository.authorizations.toSet() shouldBe setOf(it.authorization) + } + } + } + + private fun runGradle(vararg arguments: String): BuildResult = gradle(*arguments).build() + + private fun gradle(vararg arguments: String): GradleRunner = + GradleRunner.create() + .withProjectDir(projectDir) + .withArguments(*arguments, "--stacktrace") + + private fun file(relativePath: String): File = projectDir.resolve(relativePath) + + /** + * Renders the classpath with the production classes of `buildSrc` as + * arguments of `files(...)`, for injection into the build script classpath + * of the generated build. + * + * The classpath comes from the `test` task in `buildSrc/build.gradle.kts`. + */ + private fun buildSrcClasspath(): String { + val classpath = requireNotNull(System.getProperty("buildSrc.classpath")) { + "The `buildSrc.classpath` system property is not set." + + " It is supplied by the `test` task in `buildSrc/build.gradle.kts`." + } + return classpath.split(File.pathSeparator).joinToString(",\n") { + " \"${File(it).invariantSeparatorsPath}\"" + } + } + + /** + * A publishing destination of the fixture, served by its own [repository]. + */ + private class Destination(val name: String, val username: String, val password: String) { + + val repository = ProbeRepository() + + /** + * The `Authorization` header that carries the credentials of this destination. + */ + val authorization: String = + "Basic " + Base64.getEncoder().encodeToString("$username:$password".toByteArray()) + + /** + * Declares this destination in the build script of the fixture. + * + * The lines are indented like those of [buildSrcClasspath], so that + * the build script keeps its indentation once it is trimmed. + */ + fun declaration(): String = + """ + Repository( + name = "$name", + releases = "${repository.url}", + snapshots = "${repository.url}", + credentialValues = { project -> + project.rootDir.resolve("$requestLog").appendText("$name\n") + if (project.findProperty("$withoutCredentials") == "$name") null + else Credentials("$username", "$password") + } + ) + """.trimIndent().prependIndent(" ") + } + + private companion object { + + /** + * The file to which the credentials functions of the fixture append + * a line each time they are called. + */ + const val requestLog = "credential-requests.log" + + /** + * The project property that names the destination whose credentials function + * finds no credentials. + */ + const val withoutCredentials = "withoutCredentials" + + /** + * Publishes to every destination and checks their credentials. + */ + const val publishTask = ":lib:publish" + + /** + * Checks that every destination has credentials. + */ + const val checkTask = ":lib:checkCredentials" + } +} + +/** + * A Maven repository that accepts an upload only with credentials. + * It is served over HTTP from the loopback interface. + * + * Gradle does not send the credentials until challenged, so the repository answers + * an upload without them with `401`, asking for Basic authentication. + * It has no artifacts to serve, so it answers any other request with `404`. + */ +private class ProbeRepository : AutoCloseable { + + private val server: HttpServer = + HttpServer.create(InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0) + + private val accepted = CopyOnWriteArrayList() + + /** + * The `Authorization` headers of the accepted uploads. + */ + val authorizations: List + get() = accepted.toList() + + /** + * The URL of this repository. + * + * The loopback address may be an IPv6 one, which [URI] encloses in square brackets. + */ + val url: String + get() { + val address = server.address + return URI("http", null, address.hostString, address.port, "/maven", null, null) + .toString() + } + + init { + server.createContext("/") { exchange -> exchange.use { respond(it) } } + server.start() + } + + private fun respond(exchange: HttpExchange) { + exchange.requestBody.readBytes() + val authorization = exchange.requestHeaders.getFirst("Authorization") + when { + exchange.requestMethod != "PUT" -> exchange.sendResponseHeaders(404, -1) + authorization == null -> { + exchange.responseHeaders.add("WWW-Authenticate", "Basic realm=\"probe\"") + exchange.sendResponseHeaders(401, -1) + } + else -> { + accepted.add(authorization) + exchange.sendResponseHeaders(201, -1) + } + } + } + + override fun close() = server.stop(0) +} diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/repo/RepositoriesSpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/repo/RepositoriesSpec.kt new file mode 100644 index 0000000..df120ff --- /dev/null +++ b/buildSrc/src/test/kotlin/io/spine/gradle/repo/RepositoriesSpec.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2026 CodeMatters, Lda. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package io.spine.gradle.repo + +import io.kotest.matchers.collections.shouldContainExactly +import io.spine.gradle.publish.CloudRepo +import io.spine.gradle.publish.PublishingRepos +import java.net.URI +import org.gradle.api.artifacts.repositories.MavenArtifactRepository +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`standardToSpineSdk()` should") +internal class RepositoriesSpec { + + @Test + fun `query Spine repositories from Artifact Registry to GitHub Packages`() { + val project = ProjectBuilder.builder().build() + project.repositories.standardToSpineSdk() + + val registry = PublishingRepos.cloudArtifactRegistry + @Suppress("DEPRECATION") // Still queried for earlier versions. + val cloudRepo = CloudRepo.published + val spineRepos = listOf( + registry.target(snapshots = false), + registry.target(snapshots = true), + cloudRepo.target(snapshots = false), + cloudRepo.target(snapshots = true), + "https://maven.pkg.github.com/SpineEventEngine/*" + ).map { URI(it) } + + val queried = project.repositories + .filterIsInstance() + .map { it.url } + .filter { it in spineRepos } + queried shouldContainExactly spineRepos + } +} diff --git a/config b/config index 219fef6..a0cbbbf 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 219fef6ddd42c1e236dddf04526fb2a64409de6a +Subproject commit a0cbbbfe66c12475224563439fb6deb3da71ba58 diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index 817192b..a8ff08d 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine:spine-reflect:2.0.0-SNAPSHOT.201` +# Dependencies of `io.spine:spine-reflect:2.0.0-SNAPSHOT.202` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -21,7 +21,7 @@ * **License:** [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.guava. **Name** : guava. **Version** : 33.7.1-jre. +1. **Group** : com.google.guava. **Name** : guava. **Version** : 33.7.2-jre. * **Project URL:** [https://github.com/google/guava](https://github.com/google/guava) * **License:** [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -33,17 +33,17 @@ * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.36.0. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.36.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.36.0. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.36.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.36.0. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.36.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) @@ -52,15 +52,15 @@ * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -167,11 +167,11 @@ 1. **Group** : com.google.errorprone. **Name** : error_prone_type_annotations. **Version** : 2.42.0. * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.flogger. **Name** : flogger. **Version** : 0.7.4. +1. **Group** : com.google.flogger. **Name** : flogger. **Version** : 0.9. * **Project URL:** [https://github.com/google/flogger](https://github.com/google/flogger) * **License:** [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.flogger. **Name** : flogger-system-backend. **Version** : 0.7.4. +1. **Group** : com.google.flogger. **Name** : flogger-system-backend. **Version** : 0.9. * **Project URL:** [https://github.com/google/flogger](https://github.com/google/flogger) * **License:** [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -183,12 +183,12 @@ * **License:** [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.guava. **Name** : guava. **Version** : 33.7.1-jre. +1. **Group** : com.google.guava. **Name** : guava. **Version** : 33.7.2-jre. * **Project URL:** [https://github.com/google/guava](https://github.com/google/guava) * **License:** [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.guava. **Name** : guava-testlib. **Version** : 33.7.1-jre. +1. **Group** : com.google.guava. **Name** : guava-testlib. **Version** : 33.7.2-jre. * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) 1. **Group** : com.google.guava. **Name** : listenablefuture. **Version** : 9999.0-empty-to-avoid-conflict-with-guava. @@ -198,17 +198,17 @@ * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.36.0. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.36.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.36.0. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.36.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.36.0. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.36.2. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) @@ -225,21 +225,16 @@ 1. **Group** : com.google.truth.extensions. **Name** : truth-proto-extension. **Version** : 1.4.5. * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.puppycrawl.tools. **Name** : checkstyle. **Version** : 10.12.1. +1. **Group** : com.puppycrawl.tools. **Name** : checkstyle. **Version** : 12.3.1. * **Project URL:** [https://checkstyle.org/](https://checkstyle.org/) - * **License:** [LGPL-2.1+](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt) + * **License:** [LGPL-2.1-or-later](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt) 1. **Group** : com.soywiz.korlibs.korte. **Name** : korte-jvm. **Version** : 4.0.10. * **Project URL:** [https://github.com/korlibs/korge-next](https://github.com/korlibs/korge-next) * **License:** [MIT](https://raw.githubusercontent.com/korlibs/korge-next/master/korge/LICENSE.txt) -1. **Group** : commons-beanutils. **Name** : commons-beanutils. **Version** : 1.9.4. - * **Project URL:** [https://commons.apache.org/proper/commons-beanutils/](https://commons.apache.org/proper/commons-beanutils/) - * **License:** [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) - * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : commons-codec. **Name** : commons-codec. **Version** : 1.22.1. - * **Project URL:** [https://commons.apache.org/proper/commons-codec/](https://commons.apache.org/proper/commons-codec/) +1. **Group** : commons-beanutils. **Name** : commons-beanutils. **Version** : 1.11.0. + * **Project URL:** [https://commons.apache.org/proper/commons-beanutils](https://commons.apache.org/proper/commons-beanutils) * **License:** [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -256,7 +251,7 @@ * **Project URL:** [https://github.com/drewhamilton/Poko](https://github.com/drewhamilton/Poko) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : info.picocli. **Name** : picocli. **Version** : 4.7.4. +1. **Group** : info.picocli. **Name** : picocli. **Version** : 4.7.7. * **Project URL:** [https://picocli.info](https://picocli.info) * **License:** [The Apache Software License, version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -373,27 +368,27 @@ * **Project URL:** [https://detekt.dev](https://detekt.dev) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : io.kotest. **Name** : kotest-assertions-core. **Version** : 6.2.4. +1. **Group** : io.kotest. **Name** : kotest-assertions-core. **Version** : 6.2.5. * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.kotest. **Name** : kotest-assertions-core-jvm. **Version** : 6.2.4. +1. **Group** : io.kotest. **Name** : kotest-assertions-core-jvm. **Version** : 6.2.5. * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.kotest. **Name** : kotest-assertions-shared. **Version** : 6.2.4. +1. **Group** : io.kotest. **Name** : kotest-assertions-shared. **Version** : 6.2.5. * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.kotest. **Name** : kotest-assertions-shared-jvm. **Version** : 6.2.4. +1. **Group** : io.kotest. **Name** : kotest-assertions-shared-jvm. **Version** : 6.2.5. * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.kotest. **Name** : kotest-common. **Version** : 6.2.4. +1. **Group** : io.kotest. **Name** : kotest-common. **Version** : 6.2.5. * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.kotest. **Name** : kotest-common-jvm. **Version** : 6.2.4. +1. **Group** : io.kotest. **Name** : kotest-common-jvm. **Version** : 6.2.5. * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) @@ -409,24 +404,19 @@ * **Project URL:** [http://www.saxonica.com/](http://www.saxonica.com/) * **License:** [Mozilla Public License Version 2.0](http://www.mozilla.org/MPL/2.0/) -1. **Group** : net.sf.saxon. **Name** : Saxon-HE. **Version** : 12.2. +1. **Group** : net.sf.saxon. **Name** : Saxon-HE. **Version** : 12.9. * **Project URL:** [http://www.saxonica.com/](http://www.saxonica.com/) * **License:** [Mozilla Public License Version 2.0](http://www.mozilla.org/MPL/2.0/) -1. **Group** : net.sourceforge.pmd. **Name** : pmd-ant. **Version** : 7.27.0. +1. **Group** : net.sourceforge.pmd. **Name** : pmd-ant. **Version** : 7.28.0. * **License:** [BSD-style](http://pmd.sourceforge.net/license.html) -1. **Group** : net.sourceforge.pmd. **Name** : pmd-core. **Version** : 7.27.0. +1. **Group** : net.sourceforge.pmd. **Name** : pmd-core. **Version** : 7.28.0. * **License:** [BSD-style](http://pmd.sourceforge.net/license.html) -1. **Group** : net.sourceforge.pmd. **Name** : pmd-java. **Version** : 7.27.0. +1. **Group** : net.sourceforge.pmd. **Name** : pmd-java. **Version** : 7.28.0. * **License:** [BSD-style](http://pmd.sourceforge.net/license.html) -1. **Group** : org.antlr. **Name** : antlr4-runtime. **Version** : 4.11.1. - * **Project URL:** [https://www.antlr.org/](https://www.antlr.org/) - * **License:** [https://www.antlr.org/license.html](https://www.antlr.org/license.html) - * **License:** [BSD-3-Clause](https://www.antlr.org/license.html) - 1. **Group** : org.antlr. **Name** : antlr4-runtime. **Version** : 4.13.2. * **Project URL:** [https://www.antlr.org/](https://www.antlr.org/) * **License:** [https://www.antlr.org/license.html](https://www.antlr.org/license.html) @@ -437,25 +427,11 @@ * **License:** [https://www.apache.org/licenses/LICENSE-2.0.txt](https://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.apache.httpcomponents.client5. **Name** : httpclient5. **Version** : 5.1.3. - * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.apache.httpcomponents.core5. **Name** : httpcore5. **Version** : 5.1.3. - * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.apache.httpcomponents.core5. **Name** : httpcore5-h2. **Version** : 5.1.3. - * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) - 1. **Group** : org.apiguardian. **Name** : apiguardian-api. **Version** : 1.1.2. * **Project URL:** [https://github.com/apiguardian-team/apiguardian](https://github.com/apiguardian-team/apiguardian) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.checkerframework. **Name** : checker-compat-qual. **Version** : 2.5.3. - * **Project URL:** [https://checkerframework.org](https://checkerframework.org) - * **License:** [GNU General Public License, version 2 (GPL2), with the classpath exception](http://www.gnu.org/software/classpath/license.html) - * **License:** [The MIT License](http://opensource.org/licenses/MIT) - -1. **Group** : org.checkerframework. **Name** : checker-qual. **Version** : 4.2.2. +1. **Group** : org.checkerframework. **Name** : checker-qual. **Version** : 4.3.1. * **Project URL:** [https://checkerframework.org/](https://checkerframework.org/) * **License:** MIT * **License:** [The MIT License](https://opensource.org/licenses/MIT) @@ -556,19 +532,23 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.4.21. + * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-compat. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-compat. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-cri-impl. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-cri-impl. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -576,15 +556,15 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -592,11 +572,11 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -608,27 +588,31 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.4.21. + * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.0.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -644,6 +628,10 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.4.21. + * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.0.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -668,7 +656,7 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-tooling-core. **Version** : 2.4.10. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-tooling-core. **Version** : 2.4.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -787,19 +775,19 @@ * **License:** The Apache License, Version 2.0 * **License:** [The Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.ow2.asm. **Name** : asm. **Version** : 9.10.1. +1. **Group** : org.ow2.asm. **Name** : asm. **Version** : 9.11. * **Project URL:** [http://asm.ow2.io/](http://asm.ow2.io/) * **License:** [BSD-3-Clause](https://asm.ow2.io/LICENSE.txt) * **License:** [BSD-3-Clause](https://asm.ow2.io/license.html) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.ow2.asm. **Name** : asm-commons. **Version** : 9.10.1. +1. **Group** : org.ow2.asm. **Name** : asm-commons. **Version** : 9.11. * **Project URL:** [http://asm.ow2.io/](http://asm.ow2.io/) * **License:** [BSD-3-Clause](https://asm.ow2.io/LICENSE.txt) * **License:** [BSD-3-Clause](https://asm.ow2.io/license.html) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.ow2.asm. **Name** : asm-tree. **Version** : 9.10.1. +1. **Group** : org.ow2.asm. **Name** : asm-tree. **Version** : 9.11. * **Project URL:** [http://asm.ow2.io/](http://asm.ow2.io/) * **License:** [BSD-3-Clause](https://asm.ow2.io/LICENSE.txt) * **License:** [BSD-3-Clause](https://asm.ow2.io/license.html) @@ -823,15 +811,16 @@ * **Project URL:** [http://www.slf4j.org](http://www.slf4j.org) * **License:** [MIT License](http://www.opensource.org/licenses/mit-license.php) +1. **Group** : org.slf4j. **Name** : slf4j-simple. **Version** : 2.0.17. + * **Project URL:** [http://www.slf4j.org](http://www.slf4j.org) + * **License:** [https://opensource.org/license/mit](https://opensource.org/license/mit) + * **License:** [MIT](https://opensource.org/license/mit) + 1. **Group** : org.snakeyaml. **Name** : snakeyaml-engine. **Version** : 2.7. * **Project URL:** [https://bitbucket.org/snakeyaml/snakeyaml-engine](https://bitbucket.org/snakeyaml/snakeyaml-engine) * **License:** [http://www.apache.org/licenses/LICENSE-2.0.txt](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.xmlresolver. **Name** : xmlresolver. **Version** : 5.1.2. - * **Project URL:** [https://github.com/xmlresolver/xmlresolver](https://github.com/xmlresolver/xmlresolver) - * **License:** [Apache License version 2.0](https://www.apache.org/licenses/LICENSE-2.0) - 1. **Group** : org.xmlresolver. **Name** : xmlresolver. **Version** : 5.3.3. * **Project URL:** [https://github.com/xmlresolver/xmlresolver](https://github.com/xmlresolver/xmlresolver) * **License:** [Apache License version 2.0](https://www.apache.org/licenses/LICENSE-2.0) @@ -839,6 +828,6 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Oct 07 18:48:43 WEST 2026** using +This report was generated on **Thu Oct 15 20:10:06 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/docs/dependencies/pom.xml b/docs/dependencies/pom.xml index a820239..a889f6d 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine reflect -2.0.0-SNAPSHOT.201 +2.0.0-SNAPSHOT.202 2015 @@ -26,43 +26,43 @@ all modules and does not describe the project structure per-subproject. com.google.guava guava - 33.7.1-jre + 33.7.2-jre compile com.google.protobuf protobuf-java - 4.36.0 + 4.36.2 compile com.google.protobuf protobuf-java-util - 4.36.0 + 4.36.2 compile com.google.protobuf protobuf-kotlin - 4.36.0 + 4.36.2 compile org.jetbrains.kotlin kotlin-bom - 2.4.10 + 2.4.21 compile org.jetbrains.kotlin kotlin-reflect - 2.4.10 + 2.4.21 compile org.jetbrains.kotlin kotlin-stdlib - 2.4.10 + 2.4.21 compile @@ -92,25 +92,25 @@ all modules and does not describe the project structure per-subproject. org.checkerframework checker-qual - 4.2.2 + 4.3.1 provided com.google.guava guava-testlib - 33.7.1-jre + 33.7.2-jre test io.kotest kotest-assertions-core - 6.2.4 + 6.2.5 test io.spine.tools base-testlib - 2.0.0-SNAPSHOT.213 + 2.0.0-SNAPSHOT.214 test @@ -151,7 +151,7 @@ all modules and does not describe the project structure per-subproject. com.puppycrawl.tools checkstyle - 10.12.1 + 12.3.1 io.gitlab.arturbosch.detekt @@ -166,12 +166,12 @@ all modules and does not describe the project structure per-subproject. net.sourceforge.pmd pmd-ant - 7.27.0 + 7.28.0 net.sourceforge.pmd pmd-java - 7.27.0 + 7.28.0 org.jacoco @@ -216,22 +216,22 @@ all modules and does not describe the project structure per-subproject. org.jetbrains.kotlin kotlin-build-tools-compat - 2.4.10 + 2.4.21 org.jetbrains.kotlin kotlin-build-tools-impl - 2.4.10 + 2.4.21 org.jetbrains.kotlin kotlin-klib-commonizer-embeddable - 2.4.10 + 2.4.21 org.jetbrains.kotlin kotlin-scripting-compiler-embeddable - 2.4.10 + 2.4.21 diff --git a/gradle.properties b/gradle.properties index 7c2bb5f..78a5789 100644 --- a/gradle.properties +++ b/gradle.properties @@ -43,3 +43,8 @@ kotlin.tests.individualTaskReports=true # For details please see: # https://kotlinlang.org/docs/dokka-migration.html#enable-migration-helpers org.jetbrains.dokka.experimental.gradle.pluginMode=V2EnabledWithHelpers + +# Let the IDE fetch the project models in parallel during a Gradle sync. +# Since Gradle 9.4, `org.gradle.parallel` above covers only the build execution. +# This affects only the models built for an IDE, not the tasks of a build. +org.gradle.tooling.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index eddabd2..9fa7f28 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ad7845b..2e7bb9f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/gradlew.bat b/gradlew.bat index a51ec4f..3185a43 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -26,6 +26,33 @@ @rem Set local scope for the variables, and ensure extensions are enabled setlocal EnableExtensions +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet + set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused @@ -45,13 +72,14 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. "%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -59,13 +87,14 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. "%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :execute @rem Setup the command line @@ -75,8 +104,9 @@ echo location of your Java installation. 1>&2 @rem Execute gradlew @rem endlocal doesn't take effect until after the line is parsed and variables are expanded @rem which allows us to clear the local environment before executing the java command -endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel +@rem This label must not be changed. We rely on old scripts being able to jump to this point. :exitWithErrorLevel @rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts "%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/version.gradle.kts b/version.gradle.kts index 0d1e631..bd66ba4 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -12,4 +12,4 @@ * and limitations under the License. */ -extra.set("versionToPublish", "2.0.0-SNAPSHOT.201") +extra.set("versionToPublish", "2.0.0-SNAPSHOT.202")