+ get() = listOf(
+ lib,
+ javaExtensions,
+ kotlinExtensions,
+ testLib
+ ).map {
+ it.split(":").let { (g, artifact) -> "$g:$artifact" }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt
new file mode 100644
index 000000000..54c6ef906
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.dependency.local
+
+/**
+ * Artifacts of the `tool-base` module.
+ *
+ * @see tool-base
+ */
+@Suppress("ConstPropertyName", "unused")
+object ToolBase {
+ const val group = Spine.toolsGroup
+ const val version = "2.0.0-SNAPSHOT.400"
+ const val dogfoodingVersion = "2.0.0-SNAPSHOT.400"
+
+ const val lib = "$group:tool-base:$version"
+ const val classicCodegen = "$group:classic-codegen:$version"
+ const val pluginBase = "$group:plugin-base:$version"
+ const val pluginTestlib = "$group:plugin-testlib:$version"
+
+ const val intellijPlatform = "$group:intellij-platform:$version"
+ const val intellijPlatformJava = "$group:intellij-platform-java:$version"
+
+ const val psi = "$group:psi:$version"
+ const val psiJavaArtifactName = "psi-java"
+ const val psiJava = "$group:$psiJavaArtifactName:$version"
+
+ const val rootGradlePlugins = "$group:root-gradle-plugins:$version"
+ const val gradlePluginApi = "$group:gradle-plugin-api:$version"
+ const val gradlePluginApiTestFixtures = "$group:gradle-plugin-api-test-fixtures:$version"
+
+ const val jvmTools = "$group:jvm-tools:$version"
+ const val jvmToolPluginDogfooding = "$group:jvm-tool-plugins-all:$dogfoodingVersion"
+ const val jvmToolPlugins = "$group:jvm-tool-plugins-all:$version"
+
+ const val protobufSetupPlugins = "$group:protobuf-setup-plugins:$version"
+
+ object JavadocFilter {
+ const val group = ToolBase.group
+ const val version = "2.0.0-SNAPSHOT.75"
+ const val artifact = "$group:spine-javadoc-filter:$version"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt
new file mode 100644
index 000000000..10f9f0dba
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.dependency.local
+
+/**
+ * Dependencies on Spine Validation SDK.
+ *
+ * See [`SpineEventEngine/validation`](https://github.com/SpineEventEngine/validation/).
+ */
+@Suppress("ConstPropertyName", "unused")
+object Validation {
+ /**
+ * The version of the Validation library artifacts.
+ */
+ const val version = "2.0.0-SNAPSHOT.446"
+
+ const val group = Spine.toolsGroup
+ private const val prefix = "validation"
+
+ const val gradlePluginModule = "$group:$prefix-gradle-plugin"
+ const val gradlePluginLib = "$gradlePluginModule:$version"
+
+ const val runtimeModule = "${Spine.group}:spine-$prefix-jvm-runtime"
+
+ fun runtime(version: String) = "$runtimeModule:$version"
+ val runtime = runtime(version)
+
+ @Deprecated("Use `runtime` instead.", ReplaceWith("runtime"))
+ const val oldRuntime = "io.spine.validation:spine-validation-java-runtime:2.0.0-SNAPSHOT.354"
+
+ const val javaModule = "$group:$prefix-java"
+ const val java = "$javaModule:$version"
+ const val javaBundleModule = "$group:$prefix-java-bundle"
+
+ /** Obtains the artifact for the `java-bundle` artifact of the given version. */
+ fun javaBundle(version: String) = "$javaBundleModule:$version"
+
+ val javaBundle = javaBundle(version)
+
+ const val model = "$group:$prefix-model:$version"
+
+ const val configModule = "$group:$prefix-configuration"
+ const val context = "$group:$prefix-context:$version"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/AssertK.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/AssertK.kt
similarity index 83%
rename from buildSrc/src/main/kotlin/io/spine/internal/dependency/AssertK.kt
rename to buildSrc/src/main/kotlin/io/spine/dependency/test/AssertK.kt
index 8d42b6193..760c11610 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/AssertK.kt
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/AssertK.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,15 +24,16 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.dependency
+package io.spine.dependency.test
/**
* Assertion library for tests in Kotlin
*
* [AssertK](https://github.com/willowtreeapps/assertk)
*/
-@Suppress("unused")
+@Deprecated("Please use Kotest assertions instead.")
+@Suppress("unused", "ConstPropertyName")
object AssertK {
- private const val version = "0.25"
- const val libJvm = "com.willowtreeapps.assertk:assertk-jvm:${version}"
+ private const val version = "0.28.1"
+ const val libJvm = "com.willowtreeapps.assertk:assertk-jvm:$version"
}
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/test/Hamcrest.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/Hamcrest.kt
new file mode 100644
index 000000000..e540c2cdb
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/Hamcrest.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.dependency.test
+
+/**
+ * The dependency on the Hamcrest, which is transitive for us.
+ *
+ * If you need assertions in Java, please use Google [Truth] instead.
+ * For Kotlin, please use [Kotest].
+ */
+@Suppress("unused", "ConstPropertyName")
+object Hamcrest {
+ // https://github.com/hamcrest/JavaHamcrest/releases
+ private const val version = "3.0"
+ const val core = "org.hamcrest:hamcrest-core:$version"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/test/JUnit.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/JUnit.kt
new file mode 100644
index 000000000..11e3b9926
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/JUnit.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.dependency.test
+
+import io.spine.dependency.Dependency
+import io.spine.dependency.DependencyWithBom
+
+// https://junit.org/
+@Suppress("unused", "ConstPropertyName")
+object JUnit : DependencyWithBom() {
+
+ override val version = "6.1.0"
+ override val group: String = "org.junit"
+
+ /**
+ * The BOM of JUnit.
+ *
+ * This one should be forced in a project via:
+ *
+ * ```kotlin
+ * dependencies {
+ * testImplementation(enforcedPlatform(JUnit.bom))
+ * }
+ * ```
+ * The version of JUnit is forced automatically by
+ * the [BomsPlugin][io.spine.dependency.boms.BomsPlugin]
+ * when it is applied to the project.
+ */
+ override val bom = "$group:junit-bom:$version"
+
+ private const val legacyVersion = "4.13.2"
+
+ // https://github.com/apiguardian-team/apiguardian
+ private const val apiGuardianVersion = "1.1.2"
+
+ // https://github.com/junit-pioneer/junit-pioneer
+ private const val pioneerVersion = "2.3.0"
+ const val pioneer = "org.junit-pioneer:junit-pioneer:$pioneerVersion"
+
+ const val legacy = "junit:junit:$legacyVersion"
+
+ object Jupiter : Dependency() {
+ override val version = JUnit.version
+ override val group = "org.junit.jupiter"
+ private const val infix = "junit-jupiter"
+
+ // We do not use versions because they are forced via BOM.
+ val api = "$group:$infix-api"
+ val params = "$group:$infix-params"
+ val engine = "$group:$infix-engine"
+
+ override val modules = listOf(api, params, engine)
+ }
+
+ /**
+ * The same as [Jupiter.artifacts].
+ */
+ override val modules = Jupiter.modules
+
+ object Platform : Dependency() {
+
+ /**
+ * The version of the platform is defined by JUnit BOM.
+ *
+ * So when we use JUnit as a platform, this property should be picked up
+ * for the dependencies automatically.
+ */
+ override val version: String = "6.0.0"
+ override val group = "org.junit.platform"
+
+ private const val infix = "junit-platform"
+ val commons = "$group:$infix-commons"
+ val launcher = "$group:$infix-launcher"
+ val engine = "$group:$infix-engine"
+ val suiteApi = "$group:$infix-suite-api"
+
+ override val modules = listOf(commons, launcher, engine, suiteApi)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/AutoValue.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/Jacoco.kt
similarity index 78%
rename from buildSrc/src/main/kotlin/io/spine/internal/dependency/AutoValue.kt
rename to buildSrc/src/main/kotlin/io/spine/dependency/test/Jacoco.kt
index 5b630bf19..478ae1b96 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/AutoValue.kt
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/Jacoco.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,10 +24,14 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.dependency
+package io.spine.dependency.test
-// https://github.com/google/auto
-object AutoValue {
- private const val version = "1.9"
- const val annotations = "com.google.auto.value:auto-value-annotations:${version}"
+/**
+ * Code coverage library for Java.
+ *
+ * @see Releases
+ */
+@Suppress("ConstPropertyName")
+object Jacoco {
+ const val version = "0.8.15"
}
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/test/Kotest.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/Kotest.kt
new file mode 100644
index 000000000..f857bcd88
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/Kotest.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+@file:Suppress("unused")
+
+package io.spine.dependency.test
+
+/**
+ * Testing framework for Kotlin.
+ *
+ * @see Kotest site
+ */
+@Suppress("unused", "ConstPropertyName")
+object Kotest {
+ const val version = "6.1.11"
+ const val group = "io.kotest"
+ const val gradlePluginId = "io.kotest"
+ const val assertions = "$group:kotest-assertions-core:$version"
+ const val runnerJUnit5 = "$group:kotest-runner-junit5:$version"
+ const val runnerJUnit5Jvm = "$group:kotest-runner-junit5-jvm:$version"
+ const val frameworkEngine = "$group:kotest-framework-engine:$version"
+ const val common = "$group:kotest-common:$version"
+
+ /**
+ * @deprecated Use `frameworkEngine` instead.
+ */
+ @Deprecated("Use `frameworkEngine` instead.", ReplaceWith("frameworkEngine"))
+ const val frameworkApi = "$group:kotest-framework-api:$version"
+ /**
+ * @deprecated The dependency was merged into the core framework.
+ */
+ @Deprecated("The dependency was merged into the core framework.")
+ const val datatest = "$group:kotest-framework-datatest:$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
new file mode 100644
index 000000000..b3381893e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/KotlinCompileTesting.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.dependency.test
+
+/**
+ * A library for in-process compilation of Kotlin and Java code compilation.
+ *
+ * @see GitHub repo
+ */
+@Suppress("unused", "ConstPropertyName")
+object KotlinCompileTesting {
+ private const val version = "0.13.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
new file mode 100644
index 000000000..93ef593b4
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/Kover.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.dependency.test
+
+// https://github.com/Kotlin/kotlinx-kover
+@Suppress("unused", "ConstPropertyName")
+object Kover {
+ const val version = "0.9.8"
+ 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/internal/dependency/Jetty.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/OpenTest4J.kt
similarity index 73%
rename from buildSrc/src/main/kotlin/io/spine/internal/dependency/Jetty.kt
rename to buildSrc/src/main/kotlin/io/spine/dependency/test/OpenTest4J.kt
index 123e02eee..1e22fb344 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Jetty.kt
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/OpenTest4J.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,13 +24,15 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.dependency
+package io.spine.dependency.test
/**
- * https://www.eclipse.org/jetty/
- * https://github.com/eclipse/jetty.project
+ * The dependency on the OpenTest4j library, which is transitive for us.
*/
-object Jetty {
- const val orbitServletJsp = "org.eclipse.jetty.orbit:javax.servlet.jsp:2.2.0.v201112011158"
- const val toolchainSchemas = "org.eclipse.jetty.toolchain:jetty-schemas:3.1"
+@Suppress("unused", "ConstPropertyName")
+object OpenTest4J {
+
+ // https://github.com/ota4j-team/opentest4j/releases
+ private const val version = "1.3.0"
+ const val lib = "org.opentest4j:opentest4j:$version"
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/AnimalSniffer.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/SystemLambda.kt
similarity index 75%
rename from buildSrc/src/main/kotlin/io/spine/internal/dependency/AnimalSniffer.kt
rename to buildSrc/src/main/kotlin/io/spine/dependency/test/SystemLambda.kt
index 068ae59ce..5f0eef8c6 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/AnimalSniffer.kt
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/SystemLambda.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,10 +24,12 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.dependency
+package io.spine.dependency.test
-// https://www.mojohaus.org/animal-sniffer/animal-sniffer-maven-plugin/
-object AnimalSniffer {
- private const val version = "1.21"
- const val lib = "org.codehaus.mojo:animal-sniffer-annotations:${version}"
+// https://github.com/stefanbirkner/system-lambda
+@Suppress("unused", "ConstPropertyName")
+object SystemLambda {
+ const val version = "1.2.1"
+ const val group = "com.github.stefanbirkner"
+ const val lib = "$group:system-lambda:$version"
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/TestKitTruth.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/TestKitTruth.kt
similarity index 70%
rename from buildSrc/src/main/kotlin/io/spine/internal/dependency/TestKitTruth.kt
rename to buildSrc/src/main/kotlin/io/spine/dependency/test/TestKitTruth.kt
index 8d572d9b2..bfec2113c 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/TestKitTruth.kt
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/TestKitTruth.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,19 +24,18 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.dependency
+@file:Suppress("MaxLineLength")
+
+package io.spine.dependency.test
/**
* Gradle TestKit extension for Google Truth.
*
- * Source code:
- * https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin/tree/main/testkit-truth
- *
- * Usage description:
- * https://dev.to/autonomousapps/gradle-all-the-way-down-testing-your-gradle-plugin-with-gradle-testkit-2hmc
+ * @see TestKit source code
+ * @see Usage description
*/
-@Suppress("unused")
+@Suppress("unused", "ConstPropertyName")
object TestKitTruth {
- private const val version = "1.1"
+ private const val version = "1.20.0"
const val lib = "com.autonomousapps:testkit-truth:$version"
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Truth.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/Truth.kt
similarity index 75%
rename from buildSrc/src/main/kotlin/io/spine/internal/dependency/Truth.kt
rename to buildSrc/src/main/kotlin/io/spine/dependency/test/Truth.kt
index 48c1f2df5..3fc5a5e95 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Truth.kt
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/Truth.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,14 +24,15 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.dependency
+package io.spine.dependency.test
// https://github.com/google/truth
+@Suppress("unused", "ConstPropertyName")
object Truth {
- private const val version = "1.1.3"
+ private const val version = "1.4.5"
val libs = listOf(
- "com.google.truth:truth:${version}",
- "com.google.truth.extensions:truth-java8-extension:${version}",
- "com.google.truth.extensions:truth-proto-extension:${version}"
+ "com.google.truth:truth:$version",
+ "com.google.truth.extensions:truth-java8-extension:$version",
+ "com.google.truth.extensions:truth-proto-extension:$version"
)
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/markup/MarkdownDocument.kt b/buildSrc/src/main/kotlin/io/spine/docs/MarkdownDocument.kt
similarity index 93%
rename from buildSrc/src/main/kotlin/io/spine/internal/markup/MarkdownDocument.kt
rename to buildSrc/src/main/kotlin/io/spine/docs/MarkdownDocument.kt
index a70889677..23937bdaa 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/markup/MarkdownDocument.kt
+++ b/buildSrc/src/main/kotlin/io/spine/docs/MarkdownDocument.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,20 +24,16 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.markup
+package io.spine.docs
import java.io.File
-/**
- * Shortcuts for the Markdown syntax.
- */
-
/**
* A virtual document written in Markdown.
*
* After it's finished, end-users would typically write it to a [real file][writeToFile].
*/
-@SuppressWarnings("detekt.complexity.TooManyFunctions") /* By design. */
+@Suppress("TooManyFunctions")
class MarkdownDocument {
private val builder: StringBuilder = StringBuilder()
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/Build.kt b/buildSrc/src/main/kotlin/io/spine/gradle/Build.kt
similarity index 90%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/Build.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/Build.kt
index c42a3c08f..327031e05 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/Build.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/Build.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle
+package io.spine.gradle
@Suppress("unused")
object Build {
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/Clean.kt b/buildSrc/src/main/kotlin/io/spine/gradle/Clean.kt
similarity index 92%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/Clean.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/Clean.kt
index ad5c1c59f..380e835a1 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/Clean.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/Clean.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle
+package io.spine.gradle
import java.io.File
import java.nio.file.Files
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/Runtime.kt b/buildSrc/src/main/kotlin/io/spine/gradle/Cli.kt
similarity index 53%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/Runtime.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/Cli.kt
index eced40e43..9424ea1ff 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/Runtime.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/Cli.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,22 +24,18 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle
+@file:Suppress("unused")
+
+package io.spine.gradle
-import com.google.common.base.Joiner
-import io.spine.internal.dependency.Flogger
import java.io.File
import java.io.InputStream
import java.io.StringWriter
+import java.lang.ProcessBuilder.Redirect.PIPE
import java.util.*
-object Runtime {
- @Suppress("unused")
- val flogger = Flogger.Runtime
-}
-
/**
- * Executor of CLI commands.
+ * Executes a process from Gradle code.
*
* Uses the passed [workingFolder] as the directory in which the commands are executed.
*/
@@ -48,49 +44,56 @@ class Cli(private val workingFolder: File) {
/**
* Executes the given terminal command and retrieves the command output.
*
- * {@link Runtime#exec(String[], String[], File) Executes} the given {@code String} array as
- * a CLI command. If the execution is successful, returns the command output. Throws
- * an {@link IllegalStateException} otherwise.
+ * [Executes][Runtime.exec] the given `String` array as a CLI command.
*
- * @param command the command to execute
- * @return the command line output
- * @throws IllegalStateException upon an execution error
+ * If the execution is successful, returns the command output.
+ * Throws an [IllegalStateException] otherwise.
+ *
+ * @param command the command to execute.
+ * @return the command line output.
+ * @throws IllegalStateException if the execution fails.
*/
fun execute(vararg command: String): String {
val outWriter = StringWriter()
val errWriter = StringWriter()
- val process = ProcessBuilder(*command)
- .directory(workingFolder)
- .redirectOutput(ProcessBuilder.Redirect.PIPE)
- .redirectError(ProcessBuilder.Redirect.PIPE)
- .start()
+ val process = ProcessBuilder(*command).apply {
+ directory(workingFolder)
+ redirectOutput(PIPE)
+ redirectError(PIPE)
+ }.start()
- process.inputStream!!.pourTo(outWriter)
- process.errorStream!!.pourTo(errWriter)
+ val outReader = process.inputStream!!.pourTo(outWriter)
+ val errReader = process.errorStream!!.pourTo(errWriter)
val exitCode = process.waitFor()
+ // `waitFor()` returns on process exit but does not wait for the reader
+ // threads to finish draining the pipes; join them so the buffers hold
+ // the complete output before it is read below.
+ outReader.join()
+ errReader.join()
if (exitCode == 0) {
return outWriter.toString()
} else {
- val cmdAsString = Joiner.on(" ").join(command.iterator())
- val errorMsg = "Command `$cmdAsString` finished with exit code $exitCode:" +
- " ${System.lineSeparator()}$errWriter" +
- " ${System.lineSeparator()}$outWriter."
+ val commandLine = command.joinToString(" ")
+ val nl = System.lineSeparator()
+ val errorMsg = "Command `$commandLine` finished with exit code $exitCode:" +
+ "$nl$errWriter" +
+ "$nl$outWriter."
throw IllegalStateException(errorMsg)
}
}
}
/**
- * Asynchronously reads all lines from this [InputStream] and appends them
- * to the passed [StringWriter].
+ * Starts a background thread that reads all lines from this [InputStream] and
+ * appends them to [dest], returning the thread so the caller can [join][Thread.join]
+ * it once the process has exited, ensuring the buffer holds the complete output.
*/
-fun InputStream.pourTo(dest: StringWriter) {
+private fun InputStream.pourTo(dest: StringWriter): Thread =
Thread {
val sc = Scanner(this)
while (sc.hasNextLine()) {
dest.append(sc.nextLine())
}
- }.start()
-}
+ }.also { it.start() }
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/ConfigTester.kt b/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt
similarity index 82%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/ConfigTester.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt
index 8a3db510f..14e0ef8c2 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/ConfigTester.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -26,7 +26,7 @@
@file:Suppress("unused") /* Some constants may be used throughout the Spine repos. */
-package io.spine.internal.gradle
+package io.spine.gradle
import java.io.File
import java.net.URI
@@ -39,8 +39,8 @@ import org.gradle.api.tasks.TaskContainer
* A tool to execute the Gradle `build` task in selected Git repositories
* with the local version of [config] contents.
*
- * Checks out the content of selected repositories into the specified [tempFolder]. The folder
- * is created if it does not exist. By default, uses `./tmp` as a temp folder.
+ * Checks out the content of selected repositories into the specified [tempFolder].
+ * The folder is created if it does not exist. By default, uses `./tmp` as a temp folder.
*
* Replaces the `config` and `buildSrc` folders in the checked out repository by the local versions
* of code. If the repository-under-test already contains its own `buildSrc` or `config` folders,
@@ -60,6 +60,15 @@ class ConfigTester(
private val tasks: TaskContainer,
private val tempFolder: File = File("./tmp")
) {
+
+ companion object {
+
+ /**
+ * Gradle build timeout.
+ */
+ private const val BUILD_TIMEOUT_MINUTES = 30L
+ }
+
private val buildSrc: Path = config.resolve("buildSrc")
/**
@@ -89,6 +98,8 @@ class ConfigTester(
val tasksPerRepo = repos.map { testWithConfig(it) }
tasks.register(taskName) {
+ group = SpineTaskGroup.name
+ description = "Builds every configured downstream repository against this `config`"
for (repoTaskName in tasksPerRepo) {
dependsOn(repoTaskName)
}
@@ -110,6 +121,8 @@ class ConfigTester(
runGradleName: String
) {
tasks.register(executeBuildName) {
+ group = SpineTaskGroup.name
+ description = "Checks out `${gitRepo.name}` and overlays local `config` and `buildSrc`"
doLast {
println(" *** Testing `config` and `config/buildSrc` with `${gitRepo.name}`. ***")
val ignoredFolder = tempFolder.toPath()
@@ -125,6 +138,8 @@ class ConfigTester(
gitRepo: GitRepository,
) {
tasks.register(runGradleName, RunBuild::class.java) {
+ group = SpineTaskGroup.name
+ description = "Runs the Gradle build of `${gitRepo.name}` against the local `config`"
doFirst {
println("`${gitRepo.name}`: starting Gradle build...")
}
@@ -132,7 +147,7 @@ class ConfigTester(
println("*** `${gitRepo.name}`: Gradle build completed. ***")
}
directory = gitRepo.prepareCheckout(tempFolder).absolutePath
- maxDurationMins = 30
+ maxDurationMins = BUILD_TIMEOUT_MINUTES
}
}
@@ -186,7 +201,7 @@ class GitRepository(
* to the specified [destinationFolder].
*
* The source code is put to the sub-folder named after the repository.
- * E.g. for `https://github.com/acme-org/foobar` the code is placed under
+ * E.g., for `https://github.com/acme-org/foobar` the code is placed under
* the `destinationFolder/foobar` folder.
*
* If the supplied folder does not exist, it is created.
@@ -235,7 +250,7 @@ class GitRepository(
class ClonedRepo(
/**
- * Origin Git repository which is cloned.
+ * Origin Git repository that is cloned.
*/
private val repo: GitRepository,
@@ -254,7 +269,7 @@ class ClonedRepo(
* The original `buildSrc` folder, if it exists in this cloned repo, is renamed
* to `buildSrc-original`.
*
- * Optionally, takes an [ignoredFolder] which will be excluded from the [source] paths
+ * Optionally, takes an [ignoredFolder] that will be excluded from the [source] paths
* when copying.
*
*
@@ -274,7 +289,7 @@ class ClonedRepo(
* The original `config` folder, if it exists in this cloned repo, is renamed
* to `config-original`.
*
- * Optionally, takes an [ignoredFolder] which will be excluded from the [source] paths
+ * Optionally, takes an [ignoredFolder] that will be excluded from the [source] paths
* when copying.
*
* Returns this instance of `ClonedRepo`, for call chaining.
@@ -302,31 +317,31 @@ class ClonedRepo(
@Suppress("TooGenericExceptionCaught")
private fun copyFolder(sourceFolder: Path, ignoredFolder: Path?, destinationFolder: Path) {
try {
- Files.walk(sourceFolder).forEach { file: Path ->
- if (ignoredFolder != null) {
- if (file.toAbsolutePath().startsWith(ignoredFolder.toAbsolutePath())) {
- return@forEach
- }
- }
- try {
- val destination = destinationFolder.resolve(sourceFolder.relativize(file))
- if (Files.isDirectory(file)) {
- if (!Files.exists(destination)) Files.createDirectory(destination)
- return@forEach
- }
- Files.copy(file, destination)
- } catch (e: Exception) {
- throw IllegalStateException(
- "Error copying folder `$sourceFolder` to `$destinationFolder`.", e
- )
- }
- }
+ copyRecursively(sourceFolder, ignoredFolder, destinationFolder)
} catch (e: Exception) {
throw IllegalStateException(
"Error copying folder `$sourceFolder` to `$destinationFolder`.", e
)
}
}
+
+ private fun copyRecursively(sourceFolder: Path, ignoredFolder: Path?, destinationFolder: Path) {
+ fun Path.isIgnored(): Boolean = ignoredFolder
+ ?.let { toAbsolutePath().startsWith(it.toAbsolutePath()) }
+ ?: false
+
+ val flattenedTree = Files.walk(sourceFolder).filter { it.isIgnored().not() }
+ val filesToDestinations = flattenedTree.map { file ->
+ val destination = destinationFolder.resolve(sourceFolder.relativize(file))
+ file to destination
+ }
+
+ val directories = filesToDestinations.filter { Files.isDirectory(it.first) }
+ directories.forEach { Files.createDirectories(it.second) }
+
+ val files = filesToDestinations.filter { Files.isDirectory(it.first).not() }
+ files.forEach { Files.copy(it.first, it.second) }
+ }
}
/**
@@ -341,7 +356,7 @@ object SpineRepos {
val base: URI = library("base")
val baseTypes: URI = library("base-types")
- val coreJava: URI = library("core-java")
+ val coreJvm: URI = library("core-jvm")
val web: URI = library("web")
private fun library(repo: String) = URI(libsOrg + repo)
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/ProjectExtensions.kt b/buildSrc/src/main/kotlin/io/spine/gradle/ProjectExtensions.kt
similarity index 75%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/ProjectExtensions.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/ProjectExtensions.kt
index a2e34b3a0..afccc24b5 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/ProjectExtensions.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/ProjectExtensions.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,9 +24,10 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle
+package io.spine.gradle
-import io.spine.internal.gradle.publish.SpinePublishing
+import io.spine.gradle.publish.SpinePublishing
+import java.io.File
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
@@ -36,8 +37,13 @@ import org.gradle.kotlin.dsl.findByType
import org.gradle.kotlin.dsl.getByType
/**
- * This file contains extension methods and properties for the Gradle `Project`.
+ * Logs the result of the function using the project logger at `INFO` level.
*/
+fun Project.log(message: () -> String) {
+ if (logger.isInfoEnabled) {
+ logger.info(message.invoke())
+ }
+}
/**
* Obtains the Java plugin extension of the project.
@@ -67,16 +73,18 @@ fun Project.applyPlugin(cls: Class>) {
* the generic parameter `T`.
*/
@Suppress("UNCHECKED_CAST") /* See the method docs. */
-fun Project.findTask(name: String): T {
+fun Project.getTask(name: String): T {
val task = this.tasks.findByName(name)
- return task!! as T
+ ?: error("Unable to find a task named `$name` in the project `${this.name}`.")
+ return task as T
}
/**
* Obtains Maven artifact ID of this [Project].
*
- * The method checks if [SpinePublishing] extension is configured upon this project. If yes,
- * returns [SpinePublishing.artifactId] for the project. Otherwise, a project's name is returned.
+ * The property getter checks if [SpinePublishing] extension is configured upon this project.
+ * If yes, it returns [SpinePublishing.artifactId] for the project.
+ * Otherwise, a project's name is returned.
*/
val Project.artifactId: String
get() {
@@ -90,3 +98,9 @@ val Project.artifactId: String
val artifactId = spinePublishing?.artifactId(this)
return artifactId ?: name
}
+
+/**
+ * Returns project's build directory as [File].
+ */
+val Project.buildDirectory: File
+ get() = layout.buildDirectory.get().asFile
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/RunBuild.kt b/buildSrc/src/main/kotlin/io/spine/gradle/RunBuild.kt
similarity index 89%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/RunBuild.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/RunBuild.kt
index 03cef7017..aa2759dec 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/RunBuild.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/RunBuild.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle
+package io.spine.gradle
/**
* Runs the `build` task via Gradle Wrapper.
@@ -32,6 +32,6 @@ package io.spine.internal.gradle
open class RunBuild : RunGradle() {
init {
- task("build")
+ task("clean", "build")
}
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/RunGradle.kt b/buildSrc/src/main/kotlin/io/spine/gradle/RunGradle.kt
similarity index 84%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/RunGradle.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/RunGradle.kt
index 12ad01112..5d05b4493 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/RunGradle.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/RunGradle.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,9 +24,10 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle
+package io.spine.gradle
import java.io.File
+import java.io.FileOutputStream
import java.util.concurrent.TimeUnit
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
@@ -35,7 +36,7 @@ import org.gradle.api.tasks.TaskAction
import org.gradle.internal.os.OperatingSystem
/**
- * A Gradle task which runs another Gradle build.
+ * A Gradle task that runs another Gradle build.
*
* Launches Gradle wrapper under a given [directory] with the specified [taskNames] names.
* The `clean` task is also run if current build includes a `clean` task.
@@ -46,8 +47,16 @@ import org.gradle.internal.os.OperatingSystem
@Suppress("unused")
open class RunGradle : DefaultTask() {
+ companion object {
+
+ /**
+ * Default Gradle build timeout.
+ */
+ private const val BUILD_TIMEOUT_MINUTES: Long = 10
+ }
+
/**
- * Path to the directory which contains a Gradle wrapper script.
+ * Path to the directory that contains a Gradle wrapper script.
*/
@Internal
lateinit var directory: String
@@ -61,7 +70,7 @@ open class RunGradle : DefaultTask() {
* For how many minutes to wait for the Gradle build to complete.
*/
@Internal
- var maxDurationMins: Long = 10
+ var maxDurationMins: Long = BUILD_TIMEOUT_MINUTES
/**
* Names of Gradle properties to copy into the launched build.
@@ -91,16 +100,20 @@ open class RunGradle : DefaultTask() {
}
@TaskAction
- private fun execute() {
+ public fun execute() {
// Ensure build error output log.
// Since we're executing this task in another process, we redirect error output to
- // the file under the `build` directory.
- val buildDir = File(directory, "build")
+ // the file under the `_out` directory. Using the `build` directory for this purpose
+ // proved to cause problems under Windows when executing the `clean` command, which
+ // fails because another process holds files.
+ val buildDir = File(directory, "_out")
if (!buildDir.exists()) {
buildDir.mkdir()
}
val errorOut = File(buildDir, "error-out.txt")
+ errorOut.truncate()
val debugOut = File(buildDir, "debug-out.txt")
+ debugOut.truncate()
val command = buildCommand()
val process = startProcess(command, errorOut, debugOut)
@@ -154,7 +167,7 @@ open class RunGradle : DefaultTask() {
}
private fun buildScript(): String {
- val runsOnWindows = OperatingSystem.current().isWindows()
+ val runsOnWindows = OperatingSystem.current().isWindows
return if (runsOnWindows) "gradlew.bat" else "gradlew"
}
@@ -166,3 +179,10 @@ open class RunGradle : DefaultTask() {
.redirectOutput(debugOut)
.start()
}
+
+private fun File.truncate() {
+ val stream = FileOutputStream(this)
+ stream.use {
+ it.channel.truncate(0)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/SpineTaskGroup.kt b/buildSrc/src/main/kotlin/io/spine/gradle/SpineTaskGroup.kt
new file mode 100644
index 000000000..073fe5d3e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/SpineTaskGroup.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle
+
+/**
+ * The Gradle task group used by every custom task registered or
+ * configured by Spine SDK code.
+ *
+ * Setting `group = SpineTaskGroup.name` on every Spine-specific task
+ * keeps them listed together under `spine` in `./gradlew tasks` and
+ * in the IntelliJ IDEA Gradle tool window. See
+ * `.agents/skills/gradle-review/spine-task-conventions.md` in the
+ * `config` repository for the full convention and rationale.
+ *
+ * Example:
+ * ```
+ * tasks.register("generateSpineModel") {
+ * group = SpineTaskGroup.name
+ * description = "Generates Spine model classes from .proto definitions"
+ * }
+ * ```
+ */
+object SpineTaskGroup {
+ const val name = "spine"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/StringExtensions.kt b/buildSrc/src/main/kotlin/io/spine/gradle/StringExtensions.kt
similarity index 91%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/StringExtensions.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/StringExtensions.kt
index ae19007b0..a18589209 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/StringExtensions.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/StringExtensions.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle
+package io.spine.gradle
/**
* Returns `true` if the version of a project contains `snapshot` (in any case),
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/TaskName.kt b/buildSrc/src/main/kotlin/io/spine/gradle/TaskName.kt
similarity index 93%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/TaskName.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/TaskName.kt
index e6967b47c..7c7abd129 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/TaskName.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/TaskName.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle
+package io.spine.gradle
import kotlin.reflect.KClass
import org.gradle.api.Task
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/base/Tasks.kt b/buildSrc/src/main/kotlin/io/spine/gradle/base/Tasks.kt
similarity index 91%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/base/Tasks.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/base/Tasks.kt
index 10290712d..c77b79560 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/base/Tasks.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/base/Tasks.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.base
+package io.spine.gradle.base
import org.gradle.api.Task
import org.gradle.api.tasks.Delete
@@ -77,8 +77,8 @@ val TaskContainer.assemble: TaskProvider
*
* Intended to build everything, including running all tests, producing the production artifacts
* and generating documentation. One will probably rarely attach concrete tasks directly
- * to `build` as [assemble][io.spine.internal.gradle.base.assemble] and
- * [check][io.spine.internal.gradle.base.check] are typically more appropriate.
+ * to `build` as [assemble][io.spine.gradle.base.assemble] and
+ * [check][io.spine.gradle.base.check] are typically more appropriate.
*
* @see
* Tasks | The Base Plugin
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/checkstyle/CheckStyleConfig.kt b/buildSrc/src/main/kotlin/io/spine/gradle/checkstyle/CheckStyleConfig.kt
similarity index 80%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/checkstyle/CheckStyleConfig.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/checkstyle/CheckStyleConfig.kt
index e3a0ef29a..bb2a1811a 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/checkstyle/CheckStyleConfig.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/checkstyle/CheckStyleConfig.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,11 +24,10 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.checkstyle
+package io.spine.gradle.checkstyle
-import io.spine.internal.dependency.CheckStyle
+import io.spine.dependency.build.CheckStyle
import org.gradle.api.Project
-import org.gradle.api.plugins.quality.Checkstyle
import org.gradle.api.plugins.quality.CheckstyleExtension
import org.gradle.api.plugins.quality.CheckstylePlugin
import org.gradle.kotlin.dsl.the
@@ -57,7 +56,7 @@ object CheckStyleConfig {
plugin(CheckstylePlugin::class.java)
}
- val configDir = project.rootDir.resolve("config/quality/")
+ val configDir = project.rootDir.resolve("buildSrc/quality/")
with(project.the()) {
toolVersion = CheckStyle.version
@@ -65,9 +64,13 @@ object CheckStyleConfig {
}
project.afterEvaluate {
- // Disables checking the test sources.
- val checkstyleTest = project.tasks.findByName("checkstyleTest") as Checkstyle
- checkstyleTest.enabled = false
+ // Disables checking the test sources and test fixtures.
+ arrayOf(
+ "checkstyleTest",
+ "checkstyleTestFixtures"
+ ).forEach {
+ task -> tasks.findByName(task)?.enabled = false
+ }
}
}
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartContext.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/DartContext.kt
similarity index 92%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartContext.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/dart/DartContext.kt
index 2f33b2819..c32c10f17 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartContext.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/DartContext.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.dart
+package io.spine.gradle.dart
import org.gradle.api.Project
import org.gradle.api.tasks.Exec
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartEnvironment.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/DartEnvironment.kt
similarity index 95%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartEnvironment.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/dart/DartEnvironment.kt
index 7b10f64cc..0e622e2cf 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartEnvironment.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/DartEnvironment.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.dart
+package io.spine.gradle.dart
import java.io.File
import org.apache.tools.ant.taskdefs.condition.Os
@@ -54,7 +54,7 @@ interface DartEnvironment {
val projectName: String
/**
- * A directory which all artifacts are generated into.
+ * A directory that all artifacts are generated into.
*
* Default value: "$projectDir/build".
*/
@@ -73,7 +73,7 @@ interface DartEnvironment {
.resolve(projectName)
/**
- * A directory which contains integration test Dart sources.
+ * A directory that contains integration test Dart sources.
*
* Default value: "$projectDir/integration-test".
*/
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartExtension.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/DartExtension.kt
similarity index 93%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartExtension.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/dart/DartExtension.kt
index cf585a9f2..002531d9b 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartExtension.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/DartExtension.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,10 +24,10 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.dart
+package io.spine.gradle.dart
-import io.spine.internal.gradle.dart.task.DartTasks
-import io.spine.internal.gradle.dart.plugin.DartPlugins
+import io.spine.gradle.dart.plugin.DartPlugins
+import io.spine.gradle.dart.task.DartTasks
import org.gradle.api.Project
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.findByType
@@ -69,7 +69,7 @@ import org.gradle.kotlin.dsl.findByType
* There are two ways to modify the environment:
*
* 1. Modify [DartEnvironment] interface directly. Go with this option when it is a global change
- * that should affect all projects which use this extension.
+ * that should affect all projects that use this extension.
* 2. Use [DartExtension.environment] scope — for temporary and custom overridings.
*
* An example of a property overriding:
@@ -92,7 +92,7 @@ import org.gradle.kotlin.dsl.findByType
* be named after a task it registers or a task group if several tasks are registered at once.
* Then this extension is called in a project's `build.gradle.kts`.
*
- * `DartTasks` and `DartPlugins` scopes extend [DartContext] which provides access
+ * `DartTasks` and `DartPlugins` scopes extend [DartContext] that provides access
* to the current [DartEnvironment] and shortcuts for running `pub` tool.
*
* Below is the simplest example of how to create a primitive `printPubVersion` task.
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/plugin/DartPlugins.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/plugin/DartPlugins.kt
similarity index 89%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/plugin/DartPlugins.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/dart/plugin/DartPlugins.kt
index 061e2add2..9d76a77f9 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/plugin/DartPlugins.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/plugin/DartPlugins.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,10 +24,10 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.dart.plugin
+package io.spine.gradle.dart.plugin
-import io.spine.internal.gradle.dart.DartContext
-import io.spine.internal.gradle.dart.DartEnvironment
+import io.spine.gradle.dart.DartContext
+import io.spine.gradle.dart.DartEnvironment
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionContainer
import org.gradle.api.plugins.PluginContainer
@@ -63,8 +63,8 @@ import org.gradle.api.tasks.TaskContainer
* And here's how to apply it in `build.gradle.kts`:
*
* ```
- * import io.spine.internal.gradle.dart.dart
- * import io.spine.internal.gradle.dart.plugins.fooBar
+ * import io.spine.gradle.dart.dart
+ * import io.spine.gradle.dart.plugins.fooBar
*
* // ...
*
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/plugin/Protobuf.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/plugin/Protobuf.kt
similarity index 78%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/plugin/Protobuf.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/dart/plugin/Protobuf.kt
index 350f0fbd3..bfd503fd1 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/plugin/Protobuf.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/plugin/Protobuf.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,14 +24,12 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.dart.plugin
+package io.spine.gradle.dart.plugin
-import com.google.protobuf.gradle.builtins
+import com.google.protobuf.gradle.ProtobufExtension
import com.google.protobuf.gradle.id
-import com.google.protobuf.gradle.plugins
-import com.google.protobuf.gradle.protobuf
import com.google.protobuf.gradle.remove
-import io.spine.internal.dependency.Protobuf
+import io.spine.dependency.lib.Protobuf
/**
* Applies `protobuf` plugin and configures `GenerateProtoTask` to work with a Dart module.
@@ -42,9 +40,10 @@ fun DartPlugins.protobuf() {
plugins.apply(Protobuf.GradlePlugin.id)
- project.protobuf {
- generateProtoTasks.all().forEach { task ->
- task.apply {
+ val protobufExtension = project.extensions.getByType(ProtobufExtension::class.java)
+ protobufExtension.apply {
+ generateProtoTasks.all().configureEach {
+ apply {
plugins { id("dart") }
builtins { remove("java") }
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/Build.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/Build.kt
similarity index 87%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/Build.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/dart/task/Build.kt
index 79c387c36..3f1ca120c 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/Build.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/Build.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,14 +24,15 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.dart.task
+package io.spine.gradle.dart.task
-import io.spine.internal.gradle.TaskName
-import io.spine.internal.gradle.base.assemble
-import io.spine.internal.gradle.base.check
-import io.spine.internal.gradle.base.clean
-import io.spine.internal.gradle.named
-import io.spine.internal.gradle.register
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.TaskName
+import io.spine.gradle.base.assemble
+import io.spine.gradle.base.check
+import io.spine.gradle.base.clean
+import io.spine.gradle.named
+import io.spine.gradle.register
import org.gradle.api.tasks.Delete
import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.TaskContainer
@@ -49,8 +50,8 @@ import org.gradle.api.tasks.TaskProvider
* An example of how to apply it in `build.gradle.kts`:
*
* ```
- * import io.spine.internal.gradle.dart.dart
- * import io.spine.internal.gradle.dart.task.build
+ * import io.spine.gradle.dart.dart
+ * import io.spine.gradle.dart.task.build
*
* // ...
*
@@ -98,7 +99,7 @@ private fun DartTasks.resolveDependencies(): TaskProvider =
register(resolveDependenciesName) {
description = "Fetches dependencies declared via `pubspec.yaml`."
- group = DartTasks.Group.build
+ group = SpineTaskGroup.name
mustRunAfter(cleanPackageIndex)
@@ -125,7 +126,7 @@ private fun DartTasks.cleanPackageIndex(): TaskProvider =
register(cleanPackageIndexName) {
description = "Deletes the resolved `.packages` and `package_config.json` files."
- group = DartTasks.Group.build
+ group = SpineTaskGroup.name
delete(
packageIndex,
@@ -147,7 +148,7 @@ private fun DartTasks.testDart(): TaskProvider =
register(testDartName) {
description = "Runs Dart tests declared in the `./test` directory."
- group = DartTasks.Group.build
+ group = SpineTaskGroup.name
dependsOn(resolveDependencies)
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/DartTasks.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/DartTasks.kt
similarity index 76%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/DartTasks.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/dart/task/DartTasks.kt
index 5ebe92725..65f94e230 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/DartTasks.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/DartTasks.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,10 +24,10 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.dart.task
+package io.spine.gradle.dart.task
-import io.spine.internal.gradle.dart.DartContext
-import io.spine.internal.gradle.dart.DartEnvironment
+import io.spine.gradle.dart.DartContext
+import io.spine.gradle.dart.DartEnvironment
import org.gradle.api.Project
import org.gradle.api.tasks.TaskContainer
@@ -38,12 +38,11 @@ import org.gradle.api.tasks.TaskContainer
*
* 1. Access to the current [DartContext].
* 2. Project's [TaskContainer].
- * 3. Default task groups.
*
* Supposing, one needs to create a new task that would participate in building. Let the task name
* be `testDart`. To do that, several steps should be completed:
*
- * 1. Define the task name and type using [TaskName][io.spine.internal.gradle.TaskName].
+ * 1. Define the task name and type using [TaskName][io.spine.gradle.TaskName].
* 2. Create a public typed reference for the task upon [TaskContainer]. It would facilitate
* referencing to the new task, so that external tasks could depend on it. This reference
* should be documented.
@@ -53,9 +52,10 @@ import org.gradle.api.tasks.TaskContainer
* Here's an example of `testDart()` extension:
*
* ```
- * import io.spine.internal.gradle.named
- * import io.spine.internal.gradle.register
- * import io.spine.internal.gradle.TaskName
+ * import io.spine.gradle.SpineTaskGroup
+ * import io.spine.gradle.named
+ * import io.spine.gradle.register
+ * import io.spine.gradle.TaskName
* import org.gradle.api.Task
* import org.gradle.api.tasks.TaskContainer
* import org.gradle.api.tasks.Exec
@@ -75,8 +75,8 @@ import org.gradle.api.tasks.TaskContainer
* fun DartTasks.testDart() =
* register(testDartName) {
*
- * description = "Runs Dart tests declared in the `./test` directory."
- * group = DartTasks.Group.build
+ * description = "Runs Dart tests declared in the `./test` directory"
+ * group = SpineTaskGroup.name
*
* // ...
* }
@@ -85,8 +85,8 @@ import org.gradle.api.tasks.TaskContainer
* And here's how to apply it in `build.gradle.kts`:
*
* ```
- * import io.spine.internal.gradle.dart.dart
- * import io.spine.internal.gradle.dart.task.testDart
+ * import io.spine.gradle.dart.dart
+ * import io.spine.gradle.dart.task.testDart
*
* // ...
*
@@ -102,14 +102,3 @@ import org.gradle.api.tasks.TaskContainer
*/
class DartTasks(dartEnv: DartEnvironment, project: Project)
: DartContext(dartEnv, project), TaskContainer by project.tasks
-{
- /**
- * Default task groups for tasks that participate in building a Dart module.
- *
- * @see [org.gradle.api.Task.getGroup]
- */
- internal object Group {
- const val build = "Dart/Build"
- const val publish = "Dart/Publish"
- }
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/IntegrationTest.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/IntegrationTest.kt
similarity index 81%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/IntegrationTest.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/dart/task/IntegrationTest.kt
index f12b96fd5..21aa28b65 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/IntegrationTest.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/IntegrationTest.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,11 +24,12 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.dart.task
+package io.spine.gradle.dart.task
-import io.spine.internal.gradle.TaskName
-import io.spine.internal.gradle.named
-import io.spine.internal.gradle.register
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.TaskName
+import io.spine.gradle.named
+import io.spine.gradle.register
import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
@@ -40,7 +41,7 @@ private val integrationTestName = TaskName.of("integrationTest", Exec::class)
*
* The task runs integration tests of the `spine-dart` library against a sample
* Spine-based application. The tests are run in Chrome browser because they use `WebFirebaseClient`
- * which only works in web environment.
+ * that only works in web environment.
*
* A sample Spine-based application is run from the `test-app` module before integration
* tests start and is stopped as the tests complete.
@@ -57,9 +58,9 @@ val TaskContainer.integrationTest: TaskProvider
* Here's an example of how to apply it in `build.gradle.kts`:
*
* ```
- * import io.spine.internal.gradle.dart.dart
- * import io.spine.internal.gradle.task.build
- * import io.spine.internal.gradle.task.integrationTest
+ * import io.spine.gradle.dart.dart
+ * import io.spine.gradle.task.build
+ * import io.spine.gradle.task.integrationTest
*
* // ...
*
@@ -71,9 +72,13 @@ val TaskContainer.integrationTest: TaskProvider
* }
* ```
*/
+@Suppress("unused")
fun DartTasks.integrationTest() =
register(integrationTestName) {
+ group = SpineTaskGroup.name
+ description = "Runs integration tests of `spine-dart` against a sample application"
+
dependsOn(
resolveDependencies,
":test-app:appBeforeIntegrationTest"
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/Publish.kt b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/Publish.kt
similarity index 86%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/Publish.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/dart/task/Publish.kt
index 92eb2904b..c1d72c023 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/Publish.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/dart/task/Publish.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,13 +24,14 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.dart.task
+package io.spine.gradle.dart.task
-import io.spine.internal.gradle.TaskName
-import io.spine.internal.gradle.base.assemble
-import io.spine.internal.gradle.publish.publish
-import io.spine.internal.gradle.named
-import io.spine.internal.gradle.register
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.TaskName
+import io.spine.gradle.base.assemble
+import io.spine.gradle.named
+import io.spine.gradle.publish.publish
+import io.spine.gradle.register
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.TaskContainer
@@ -51,9 +52,9 @@ import org.gradle.api.tasks.TaskProvider
* Usage example:
*
* ```
- * import io.spine.internal.gradle.dart.dart
- * import io.spine.internal.gradle.dart.task.build
- * import io.spine.internal.gradle.dart.task.publish
+ * import io.spine.gradle.dart.dart
+ * import io.spine.gradle.dart.task.build
+ * import io.spine.gradle.dart.task.publish
*
* // ...
*
@@ -83,7 +84,7 @@ private val stagePubPublicationName = TaskName.of("stagePubPublication", Copy::c
* Locates `stagePubPublication` in this [TaskContainer].
*
* The task prepares the Dart package for Pub publication in the
- * [publication directory][io.spine.internal.gradle.dart.DartEnvironment.publicationDir].
+ * [publication directory][io.spine.gradle.dart.DartEnvironment.publicationDir].
*/
val TaskContainer.stagePubPublication: TaskProvider
get() = named(stagePubPublicationName)
@@ -92,7 +93,7 @@ private fun DartTasks.stagePubPublication(): TaskProvider =
register(stagePubPublicationName) {
description = "Prepares the Dart package for Pub publication."
- group = DartTasks.Group.publish
+ group = SpineTaskGroup.name
dependsOn(assemble)
@@ -110,7 +111,7 @@ private fun DartTasks.stagePubPublication(): TaskProvider =
into(publicationDir)
doLast {
- logger.debug("Pub publication is prepared in directory `$publicationDir`.")
+ logger.debug("Pub publication is prepared in directory `{}`.", publicationDir)
}
}
@@ -128,7 +129,7 @@ private fun DartTasks.publishToPub(): TaskProvider =
register(publishToPubName) {
description = "Publishes the prepared publication to Pub."
- group = DartTasks.Group.publish
+ group = SpineTaskGroup.name
dependsOn(stagePubPublication)
@@ -160,7 +161,7 @@ private fun DartTasks.activateLocally(): TaskProvider =
register(activateLocallyName) {
description = "Activates this package locally."
- group = DartTasks.Group.publish
+ group = SpineTaskGroup.name
dependsOn(stagePubPublication)
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/docs/UpdatePluginVersion.kt b/buildSrc/src/main/kotlin/io/spine/gradle/docs/UpdatePluginVersion.kt
new file mode 100644
index 000000000..0277bcd28
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/docs/UpdatePluginVersion.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.docs
+
+import java.io.File
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.TaskAction
+
+/**
+ * Updates the version of a Gradle plugin in `build.gradle.kts` files.
+ *
+ * The task searches for plugin declarations in the format
+ * `id("plugin-id") version "version-number"` and replaces
+ * the version number with the one found in the version script file.
+ *
+ * @property directory
+ * The directory to scan recursively for `build.gradle.kts` files.
+ * @property version
+ * The version number to set for the plugin.
+ * @property pluginId
+ * The ID of the plugin whose version should be updated.
+ * @property kotlinVersion
+ * Optional. If set, updates the version of the Kotlin plugin declared with
+ * `kotlin("…") version "…"` syntax in the `plugins` block.
+ * This option works in combination with the [version] and [pluginId] properties.
+ */
+abstract class UpdatePluginVersion : DefaultTask() {
+
+ @get:InputDirectory
+ abstract val directory: DirectoryProperty
+
+ @get:Input
+ abstract val version: Property
+
+ @get:Input
+ abstract val pluginId: Property
+
+ @get:Input
+ @get:Optional
+ abstract val kotlinVersion: Property
+
+ /**
+ * Updates plugin versions in build files within the path in the [directory].
+ */
+ @TaskAction
+ fun update() {
+ val rootDir = directory.get().asFile
+
+ val kotlinVersionSet = kotlinVersion.isPresent
+ val kotlinVer = kotlinVersion.orNull
+ val id = pluginId.get()
+ val ver = version.get()
+
+ rootDir.walkTopDown()
+ .filter { it.name == "build.gradle.kts" }
+ .forEach { file ->
+ if (kotlinVersionSet && kotlinVer != null) {
+ updateKotlinPluginVersion(file, kotlinVer)
+ }
+ updatePluginVersion(file, id, ver)
+ }
+ }
+
+ @Suppress("MemberNameEqualsClassName")
+ private fun updatePluginVersion(file: File, id: String, version: String) {
+ val content = file.readText()
+ val pluginId = Regex.escape(id)
+ // Regex to match: id("pluginId") version "version-number"
+ val regex = """id\("$pluginId"\)\s+version\s+"([^"]+)"""".toRegex()
+
+ if (regex.containsMatchIn(content)) {
+ val updatedContent = regex.replace(content) {
+ "id(\"$id\") version \"$version\""
+ }
+ if (content != updatedContent) {
+ file.writeText(updatedContent)
+ logger.info("Updated version of '$id' in `${file.absolutePath}`.")
+ }
+ }
+ }
+
+ private fun updateKotlinPluginVersion(file: File, kotlinVersion: String) {
+ val content = file.readText()
+ // Regex to match Kotlin plugin declarations like: kotlin("jvm") version "1.9.0"
+ val regex = """kotlin\("([^"]+)"\)\s+version\s+"([^"]+)"""".toRegex()
+ if (regex.containsMatchIn(content)) {
+ val updatedContent = regex.replace(content) { matchResult ->
+ val plugin = matchResult.groupValues[1]
+ "kotlin(\"$plugin\") version \"$kotlinVersion\""
+ }
+ if (content != updatedContent) {
+ file.writeText(updatedContent)
+ logger.info("Updated Kotlin plugin version in `${file.absolutePath}`.")
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/fs/LazyTempPath.kt b/buildSrc/src/main/kotlin/io/spine/gradle/fs/LazyTempPath.kt
similarity index 82%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/fs/LazyTempPath.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/fs/LazyTempPath.kt
index 595e07a19..0344819f2 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/fs/LazyTempPath.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/fs/LazyTempPath.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.fs
+package io.spine.gradle.fs
import java.io.File
import java.net.URI
@@ -42,29 +42,22 @@ import java.nio.file.WatchService
* After the first usage, the instances of this type delegate all calls to the internally
* created instance of [Path] created with [createTempDirectory].
*/
+@Suppress("TooManyFunctions")
class LazyTempPath(private val prefix: String) : Path {
- private lateinit var tempPath: Path
+ private val delegate: Path by lazy { createTempDirectory(prefix) }
- private val delegate: Path
- get() {
- if (!::tempPath.isInitialized) {
- tempPath = createTempDirectory(prefix)
- }
- return tempPath
- }
-
- override fun compareTo(other: Path?): Int = delegate.compareTo(other)
+ override fun compareTo(other: Path): Int = delegate.compareTo(other)
override fun iterator(): MutableIterator = delegate.iterator()
override fun register(
- watcher: WatchService?,
- events: Array>?,
+ watcher: WatchService,
+ events: Array>,
vararg modifiers: WatchEvent.Modifier?
): WatchKey = delegate.register(watcher, events, *modifiers)
- override fun register(watcher: WatchService?, vararg events: WatchEvent.Kind<*>?): WatchKey =
+ override fun register(watcher: WatchService, vararg events: WatchEvent.Kind<*>): WatchKey =
delegate.register(watcher, *events)
override fun getFileSystem(): FileSystem = delegate.fileSystem
@@ -108,7 +101,9 @@ class LazyTempPath(private val prefix: String) : Path {
override fun toAbsolutePath(): Path = delegate.toAbsolutePath()
- override fun toRealPath(vararg options: LinkOption?): Path = delegate.toRealPath(*options)
+ override fun toRealPath(vararg options: LinkOption): Path = delegate.toRealPath(*options)
override fun toFile(): File = delegate.toFile()
+
+ override fun toString(): String = delegate.toString()
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/Branch.kt b/buildSrc/src/main/kotlin/io/spine/gradle/git/Branch.kt
similarity index 77%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/Branch.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/git/Branch.kt
index c9c7e7db9..a10a65f81 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/Branch.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/git/Branch.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,13 +24,20 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.github.pages
+package io.spine.gradle.git
/**
- * Names of branches involved when updating documentation.
+ * Branch names.
*/
object Branch {
- /** The branch to use when pushing the updates to the documentation. */
- const val ghPages = "gh-pages"
+ /**
+ * The default branch.
+ */
+ const val master = "master"
+
+ /**
+ * The branch used for publishing documentation to GitHub Pages.
+ */
+ const val documentation = "gh-pages"
}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/git/Repository.kt b/buildSrc/src/main/kotlin/io/spine/gradle/git/Repository.kt
new file mode 100644
index 000000000..8ce4d72a5
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/git/Repository.kt
@@ -0,0 +1,331 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.git
+
+import com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly
+import io.spine.gradle.Cli
+import io.spine.gradle.fs.LazyTempPath
+import java.util.concurrent.TimeUnit.MILLISECONDS
+import org.gradle.api.Project
+
+/**
+ * Interacts with a real Git repository.
+ *
+ * Clones the repository with the provided SSH URL in a temporal folder. Provides
+ * functionality to configure a user, checkout branches, commit changes and push them
+ * to the remote repository.
+ *
+ * It is assumed that before using this class an appropriate SSH key that has
+ * sufficient rights to perform described above operations was registered
+ * in `ssh-agent`.
+ *
+ * NOTE: This class creates a temporal folder, so it holds resources. For the proper
+ * release of resources please use the provided functionality inside a `use` block or
+ * call the `close` method manually.
+ *
+ * @property project The Gradle project in which context the repo operations are held.
+ * @property sshUrl The GitHub SSH URL to the underlying repository.
+ * @property user Current user configuration.
+ * This configuration determines what ends up in the `author` and `committer` fields of a commit.
+ * @property currentBranch The currently checked-out branch.
+ */
+@Suppress("TooManyFunctions") // A cohesive wrapper over many small `git` commands.
+class Repository private constructor(
+ private val project: Project,
+ private val sshUrl: String,
+ private var user: UserInfo,
+ private var currentBranch: String,
+) : AutoCloseable {
+
+ /**
+ * Path to the temporal folder for a clone of the underlying repository.
+ */
+ val location = LazyTempPath("repoTemp")
+
+ /**
+ * Clones the repository with [the SSH url][sshUrl] into the [temporal folder][location].
+ */
+ private fun clone() {
+ repoExecute("git", "clone", sshUrl, ".")
+ }
+
+ /**
+ * Executes a command in the [location].
+ */
+ private fun repoExecute(vararg command: String): String {
+ val cmd = command.toList().joinToString(" ")
+ val msg = "[Repo (${project.path})] Executing command: `$cmd`."
+ System.err.println(msg)
+ return Cli(location.toFile()).execute(*command)
+ }
+
+ /**
+ * Checks out the branch by its name.
+ *
+ * IMPORTANT. The branch must exist in the upstream repository.
+ * Use [checkoutOrCreate] to check out a branch that may not exist yet.
+ */
+ fun checkout(branch: String) {
+ repoExecute("git", "checkout", branch)
+ repoExecute("git", "pull")
+
+ currentBranch = branch
+ }
+
+ /**
+ * Checks out the [branch], creating it in the remote repository if it does
+ * not exist yet.
+ *
+ * If the branch is already present on the remote, it is [checked out][checkout]
+ * as usual. Otherwise, it is created as an orphan branch seeded with
+ * [initialFiles] and pushed to the remote, so that subsequent commits with the
+ * documentation have a branch to append to.
+ *
+ * Creating the branch on the fly makes the very first documentation publication
+ * of a repository self-sufficient: the [documentation branch][Branch.documentation]
+ * no longer needs to be created manually beforehand.
+ *
+ * @param branch the name of the branch to check out or create.
+ * @param initialFiles the files — paths relative to the repository root mapped
+ * to their content — to add to the initial commit when the branch is created.
+ * Ignored when the branch already exists.
+ */
+ fun checkoutOrCreate(branch: String, initialFiles: Map = emptyMap()) {
+ if (remoteHasBranch(branch)) {
+ // `remoteHasBranch` queries the remote directly via `git ls-remote`,
+ // which does not populate `refs/remotes/origin/*`. In a parallel
+ // build another module may have created the branch after this clone,
+ // so fetch first to make the `origin/$branch` ref available;
+ // otherwise `git checkout` cannot guess it and fails with a
+ // pathspec error.
+ repoExecute("git", "fetch", "origin")
+ checkout(branch)
+ } else {
+ createOrphanBranch(branch, initialFiles)
+ }
+ }
+
+ /**
+ * Tells whether the remote repository has a branch with the given [name].
+ *
+ * Queries the fully-qualified ref `refs/heads/$name` rather than the bare
+ * [name]: `git ls-remote` treats a bare name as a tail glob and would also
+ * match a namespaced branch such as `feature/$name`. Relies on `git ls-remote`
+ * returning an empty output with a zero exit code when the branch is absent,
+ * so the check does not raise an exception.
+ */
+ private fun remoteHasBranch(name: String): Boolean {
+ val output = repoExecute("git", "ls-remote", "--heads", "origin", "refs/heads/$name")
+ return output.isNotBlank()
+ }
+
+ /**
+ * Creates the [branch] as an orphan branch seeded with [initialFiles] and
+ * pushes it to the remote.
+ *
+ * `git switch --orphan` starts a new history with an empty working tree, so
+ * the source code of the default branch does not leak into the created branch.
+ * The [initialFiles] are written into this clean tree and staged before the
+ * initial commit, which stays `--allow-empty` to support seeding no files.
+ */
+ private fun createOrphanBranch(branch: String, initialFiles: Map) {
+ repoExecute("git", "switch", "--orphan", branch)
+ initialFiles.forEach { (path, content) ->
+ location.toFile().resolve(path).writeText(content)
+ repoExecute("git", "add", path)
+ }
+ repoExecute(
+ "git",
+ "commit",
+ "--allow-empty",
+ "--message=Initialize the `$branch` branch."
+ )
+ currentBranch = branch
+ pushNewBranch(branch)
+ }
+
+ /**
+ * Pushes the just-created [branch] to the remote, setting up the upstream tracking.
+ *
+ * If the push is rejected because a concurrently running publication created
+ * the branch first (e.g., another module publishing documentation in the same
+ * parallel build), the remote branch is [adopted][adoptRemoteBranch] instead.
+ * Otherwise, the failure is genuine, and the original exception is rethrown.
+ */
+ private fun pushNewBranch(branch: String) {
+ try {
+ repoExecute("git", "push", "--set-upstream", "origin", branch)
+ } catch (e: IllegalStateException) {
+ // `Cli.execute` surfaces every non-zero `git` exit as an
+ // `IllegalStateException`, so this branch handles a rejected push.
+ // If the branch now exists on the remote, another module won the
+ // creation race and we adopt its branch; otherwise the failure is
+ // genuine and is rethrown.
+ repoExecute("git", "fetch", "origin")
+ if (!remoteHasBranch(branch)) {
+ throw e
+ }
+ adoptRemoteBranch(branch)
+ }
+ }
+
+ /**
+ * Discards the local orphan branch in favour of the same-named branch that
+ * already exists on the remote, keeping the local branch in sync with it.
+ */
+ private fun adoptRemoteBranch(branch: String) {
+ repoExecute("git", "reset", "--hard", "origin/$branch")
+ repoExecute("git", "branch", "--set-upstream-to=origin/$branch", branch)
+ }
+
+ /**
+ * Configures the username and the email of the user.
+ *
+ * Overwrites `user.name` and `user.email` settings locally in [location] with
+ * values from [user]. These settings determine what ends up in author and
+ * committer fields of a commit.
+ */
+ fun configureUser(user: UserInfo) {
+ repoExecute("git", "config", "user.name", user.name)
+ repoExecute("git", "config", "user.email", user.email)
+
+ this.user = user
+ }
+
+ /**
+ * Stages all changes and commits with the provided message.
+ */
+ fun commitAllChanges(message: String) {
+ stageAllChanges()
+ commit(message)
+ }
+
+ private fun stageAllChanges() {
+ repoExecute("git", "add", "--all")
+ }
+
+ private fun commit(message: String) {
+ repoExecute(
+ "git",
+ "commit",
+ "--allow-empty",
+ "--message=${message}"
+ )
+ }
+
+ /**
+ * Pushes the current branch of the repository to the remote.
+ *
+ * Performs a pull with rebase before pushing to ensure the local branch is up-to-date.
+ */
+ fun push() {
+ withRetries(description = "Pushing to $sshUrl, branch = '$currentBranch'") {
+ repoExecute("git", "pull", "--rebase")
+ repoExecute("git", "push")
+ }
+ }
+
+ override fun close() {
+ location.toFile().deleteRecursively()
+ }
+
+ companion object Factory {
+
+ /**
+ * Clones the repository with the provided SSH URL in a temporal folder.
+ *
+ * Configures the username and the email of the Git user.
+ * See [configureUser] documentation for more information.
+ *
+ * Performs checkout of the branch in case it was passed.
+ * By default, [master][Branch.master] is checked out. A non-default branch
+ * that does not exist yet is created and seeded with [initialFiles].
+ *
+ * @throws IllegalArgumentException if SSH URL is an empty string.
+ */
+ fun clone(
+ project: Project,
+ sshUrl: String,
+ user: UserInfo,
+ branch: String = Branch.master,
+ initialFiles: Map = emptyMap(),
+ ): Repository {
+ require(sshUrl.isNotBlank()) { "SSH URL cannot be an empty string." }
+
+ val repo = Repository(project, sshUrl, user, branch)
+ repo.clone()
+ repo.configureUser(user)
+
+ if (branch != Branch.master) {
+ repo.checkoutOrCreate(branch, initialFiles)
+ }
+
+ return repo
+ }
+ }
+}
+
+/**
+ * Executes a given operation with retries using exponential backoff strategy.
+ *
+ * If the operation fails, it will be retried up to the specified number of times
+ * with increasing delays between attempts.
+ * The delay increases exponentially but is capped at the specified maximum value.
+ *
+ * If all retries fail, the exception from the final attempt will be thrown to the caller.
+ *
+ * @param T the type of value returned by the operation
+ * @param times the maximum number of attempts to execute the operation (default: 3)
+ * @param initialDelay the delay before the first retry in milliseconds (default: 100ms)
+ * @param maxDelay the maximum delay between retries in milliseconds (default: 2000ms)
+ * @param factor the multiplier used to increase delay after each failure (default: 2.0)
+ * @param description a description of the operation for error reporting (default: empty string)
+ * @param block the operation to execute
+ * @return the result of the successful operation execution
+ */
+@Suppress("TooGenericExceptionCaught", "LongParameterList")
+private fun withRetries(
+ times: Int = 5,
+ initialDelay: Long = 2000, // ms
+ maxDelay: Long = 20000, // ms
+ factor: Double = 2.0,
+ description: String = "",
+ block: () -> T
+): T {
+ var currentDelay = initialDelay
+ repeat(times - 1) {
+ try {
+ return block()
+ } catch (e: Exception) {
+ System.err.println("'$description' failed. " +
+ "Message: '${e.message}'. Retrying in $currentDelay ms.")
+ }
+ sleepUninterruptibly(currentDelay, MILLISECONDS)
+ currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
+ }
+ return block()
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/git/UserInfo.kt b/buildSrc/src/main/kotlin/io/spine/gradle/git/UserInfo.kt
new file mode 100644
index 000000000..bc9f08d3a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/git/UserInfo.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.git
+
+/**
+ * Contains information about a Git user.
+ *
+ * Determines the author and committer fields of a commit.
+ *
+ * @constructor throws an [IllegalArgumentException] if the name or the email
+ * is an empty string.
+ */
+data class UserInfo(val name: String, val email: String) {
+ init {
+ require(name.isNotBlank()) { "Name cannot be an empty string." }
+ require(email.isNotBlank()) { "Email cannot be an empty string." }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/AuthorEmail.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/AuthorEmail.kt
similarity index 85%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/AuthorEmail.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/github/pages/AuthorEmail.kt
index cac6e41a5..f0d839f89 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/AuthorEmail.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/AuthorEmail.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.github.pages
+package io.spine.gradle.github.pages
/**
* An author of updates to GitHub pages.
@@ -45,10 +45,8 @@ class AuthorEmail(val value: String) {
*/
fun fromVar() : AuthorEmail {
val envValue = System.getenv(environmentVariable)
- if (envValue.isNullOrEmpty()) {
- throw IllegalStateException(
- "Unable to obtain an author from `${environmentVariable}`."
- )
+ check(envValue != null && envValue.isNotBlank()) {
+ "Unable to obtain an author from `${environmentVariable}`."
}
return AuthorEmail(envValue)
}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/RepositoryExtensions.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/RepositoryExtensions.kt
new file mode 100644
index 000000000..682d6478c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/RepositoryExtensions.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.github.pages
+
+import io.spine.gradle.git.Branch
+import io.spine.gradle.git.Repository
+import io.spine.gradle.git.UserInfo
+import io.spine.gradle.repo.RepoSlug
+import org.gradle.api.Project
+
+/**
+ * Clones the current project repository with the branch dedicated to publishing
+ * documentation to GitHub Pages checked out.
+ *
+ * The repository's GitHub SSH URL is derived from the `REPO_SLUG` environment
+ * variable. The [branch][Branch.documentation] dedicated to publishing documentation
+ * is automatically checked out in this repository, and created if it does not exist
+ * yet. A freshly created branch is seeded with a `CNAME` file so that GitHub Pages
+ * serves the documentation under the `spine.io` custom domain. Also, the username
+ * and the email of the git user are automatically configured.
+ *
+ * The username is set to `"UpdateGitHubPages Plugin"`, and the email is derived from
+ * the `FORMAL_GIT_HUB_PAGES_AUTHOR` environment variable.
+ *
+ * @throws org.gradle.api.GradleException if any of the environment variables described above
+ * is not set.
+ */
+internal fun Repository.Factory.forPublishingDocumentation(project: Project): Repository {
+ val host = RepoSlug.fromVar().gitHost()
+
+ val username = "UpdateGitHubPages Plugin"
+ val userEmail = AuthorEmail.fromVar().toString()
+ val user = UserInfo(username, userEmail)
+
+ val branch = Branch.documentation
+
+ // When the `gh-pages` branch is created from scratch, seed it with a `CNAME`
+ // file so that GitHub Pages serves the documentation under the `spine.io`
+ // custom domain.
+ val initialFiles = mapOf("CNAME" to "spine.io\n")
+
+ return clone(project, host, user, branch, initialFiles)
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/SshKey.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/SshKey.kt
new file mode 100644
index 000000000..653a9f950
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/SshKey.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.github.pages
+
+import io.spine.gradle.Cli
+import java.io.File
+import org.gradle.api.GradleException
+import org.gradle.api.logging.Logger
+
+/**
+ * Registers the SSH key for further operations with GitHub Pages.
+ *
+ * @property rootProjectFolder The folder of the project for which we build the documentation.
+ * @property logger The logger for placing diagnostic messages of this class.
+ */
+internal class SshKey(
+ private val rootProjectFolder: File,
+ private val logger: Logger
+) {
+
+ private fun log(message: () -> String) {
+ if (logger.isInfoEnabled) {
+ logger.info("[SshKey] " + message())
+ }
+ }
+
+ /**
+ * Creates an SSH key with the credentials and registers it by invoking the
+ * `register-ssh-key.sh` script.
+ */
+ fun register() {
+ log { "Registering using ${rootProjectFolder.absolutePath}." }
+ val gitHubAccessKey = gitHubKey()
+ log { "Obtained the key file at ${gitHubAccessKey.absolutePath}." }
+ val sshConfigFile = sshConfigFile()
+ log { "Located the SSH config file at ${sshConfigFile.absolutePath}." }
+ sshConfigFile.appendPublisher(gitHubAccessKey)
+ log { "SSH config file appended." }
+
+ execute(
+ "${rootProjectFolder.absolutePath}/config/scripts/register-ssh-key.sh",
+ gitHubAccessKey.absolutePath
+ )
+ log { "The SSH key registered." }
+ }
+
+ /**
+ * Locates `deploy_key_rsa` in the [rootProjectFolder] and returns it as a [File].
+ *
+ * A CI instance comes with an RSA key. However, of course, the default key has
+ * no privileges in Spine repositories. Thus, we add our own RSA key —
+ * `deploy_rsa_key`. It must have `write` rights in the associated repository.
+ * Also, we don't want that key to be used for anything else but GitHub Pages
+ * publishing.
+ *
+ * Thus, we configure the SSH agent to use the `deploy_rsa_key` only for specific
+ * references, namely in `github-publish`.
+ *
+ * @throws GradleException if `deploy_key_rsa` is not found.
+ */
+ private fun gitHubKey(): File {
+ val gitHubAccessKey = File("${rootProjectFolder.absolutePath}/deploy_key_rsa")
+
+ if (!gitHubAccessKey.exists()) {
+ throw GradleException(
+ "File $gitHubAccessKey does not exist. It should be encrypted" +
+ " in the repository and decrypted on CI."
+ )
+ }
+ return gitHubAccessKey
+ }
+
+ private fun sshConfigFile(): File {
+ val sshConfigFile = File("${System.getProperty("user.home")}/.ssh/config")
+
+ if (!sshConfigFile.exists()) {
+ val parentDir = sshConfigFile.canonicalFile.parentFile
+ parentDir.mkdirs()
+ sshConfigFile.createNewFile()
+ }
+
+ return sshConfigFile
+ }
+
+ private fun File.appendPublisher(privateKey: File) {
+ val nl = System.lineSeparator()
+ this.appendText(
+ nl +
+ "Host github-publish" + nl +
+ " HostName github.com" + nl +
+ " User git" + nl +
+ " IdentityFile ${privateKey.absolutePath}" + nl
+ )
+ }
+
+ /**
+ * Executes a command in the project [rootProjectFolder].
+ */
+ private fun execute(vararg command: String): String = Cli(rootProjectFolder).execute(*command)
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileExtension.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/TaskName.kt
similarity index 65%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileExtension.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/github/pages/TaskName.kt
index 97ab594e4..efea9e27a 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileExtension.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/TaskName.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,26 +24,24 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.coverage
+package io.spine.gradle.github.pages
-/**
- * File extensions.
- */
-internal enum class FileExtension(val value: String) {
+object TaskName {
/**
- * Extension of a Java source file.
+ * The name of the task that updates the GitHub Pages.
*/
- JAVA_SOURCE(".java"),
+ const val updateGitHubPages = "updateGitHubPages"
/**
- * Extension of a Java compiled file.
+ * The name of the helper task to gather the generated Javadoc format
+ * documentation generated by Dokka before updating GitHub Pages.
*/
- COMPILED_CLASS(".class");
+ const val copyJavadocDocs = "copyJavadocDocs"
/**
- * The number of symbols in the extension.
+ * The name of the helper task to gather HTML documentation
+ * generated by Dokka before updating GitHub Pages.
*/
- val length: Int
- get() = this.value.length
+ const val copyHtmlDocs = "copyHtmlDocs"
}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/Update.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/Update.kt
new file mode 100644
index 000000000..785162e8b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/Update.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.github.pages
+
+import io.spine.gradle.git.Repository
+import java.io.File
+import java.nio.file.Path
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.FileCollection
+import org.gradle.api.logging.Logger
+
+/**
+ * Performs the update of GitHub pages.
+ */
+fun Task.updateGhPages(project: Project) {
+ val plugin = project.plugins.getPlugin(UpdateGitHubPages::class.java)
+
+ SshKey(plugin.rootFolder, logger).register()
+
+ val repository = Repository.forPublishingDocumentation(project)
+
+ val updateJavadocFormat =
+ UpdateJavadocFormat(project, plugin.javadocOutputFolder, repository, logger)
+
+ val updateHtmlFormat =
+ UpdateHtmlFormat(project, plugin.htmlOutputFolder, repository, logger)
+
+ repository.use {
+ updateJavadocFormat.run()
+ updateHtmlFormat.run()
+ repository.push()
+ }
+}
+
+private abstract class UpdateDocumentation(
+ private val project: Project,
+ private val docsSourceFolder: Path,
+ private val repository: Repository,
+ private val logger: Logger
+) {
+
+ /**
+ * The folder under the repository's root(`/`) for storing documentation.
+ *
+ * The value should not contain any leading or trailing file separators.
+ *
+ * The absolute path to the project's documentation is made by appending its
+ * name to the end, making `/docsDestinationFolder/project.name`.
+ */
+ protected abstract val docsDestinationFolder: String
+
+ /**
+ * The name of the format of the documentation to update.
+ *
+ * This name will appear in logs as part of a message.
+ */
+ protected abstract val formatName: String
+
+ private val mostRecentFolder by lazy {
+ File("${repository.location}/${docsDestinationFolder}/${project.name}")
+ }
+
+ private fun log(message: () -> String) {
+ if (logger.isDebugEnabled) {
+ logger.debug(message())
+ }
+ }
+
+ fun run() {
+ val module = project.name
+ log { "Update of the `$formatName` documentation for the module `$module` started." }
+
+ val documentation = replaceMostRecentDocs()
+ copyIntoVersionDir(documentation)
+
+ val version = project.version
+ val updateMessage =
+ "Update `$formatName` documentation for the module" +
+ " `$module` with the version `$version`."
+ repository.commitAllChanges(updateMessage)
+
+ log { "Update of the `$formatName` documentation for `$module` successfully finished." }
+ }
+
+ private fun replaceMostRecentDocs(): ConfigurableFileCollection {
+ val generatedDocs = project.files(docsSourceFolder)
+
+ log { "Replacing the most recent `$formatName` documentation in `$mostRecentFolder`." }
+ copyDocs(generatedDocs, mostRecentFolder)
+
+ return generatedDocs
+ }
+
+ private fun copyDocs(source: FileCollection, destination: File) {
+ destination.mkdir()
+ project.copy {
+ from(source)
+ into(destination)
+ }
+ }
+
+ private fun copyIntoVersionDir(generatedDocs: ConfigurableFileCollection) {
+ val versionedDocDir = File("$mostRecentFolder/v/${project.version}")
+
+ log { "Storing the new version of `$formatName` documentation in `${versionedDocDir}`." }
+ copyDocs(generatedDocs, versionedDocDir)
+ }
+}
+
+private class UpdateJavadocFormat(
+ project: Project,
+ docsSourceFolder: Path,
+ repository: Repository,
+ logger: Logger
+) : UpdateDocumentation(project, docsSourceFolder, repository, logger) {
+
+ override val docsDestinationFolder: String
+ get() = "javadoc"
+ override val formatName: String
+ get() = "javadoc"
+}
+
+private class UpdateHtmlFormat(
+ project: Project,
+ docsSourceFolder: Path,
+ repository: Repository,
+ logger: Logger
+) : UpdateDocumentation(project, docsSourceFolder, repository, logger) {
+
+ override val docsDestinationFolder: String
+ get() = "reference"
+ override val formatName: String
+ get() = "html"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt
new file mode 100644
index 000000000..0382e8364
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.github.pages
+
+import dokkaHtmlTask
+import dokkaJavadocTask
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.fs.LazyTempPath
+import io.spine.gradle.github.pages.TaskName.copyHtmlDocs
+import io.spine.gradle.github.pages.TaskName.copyJavadocDocs
+import io.spine.gradle.github.pages.TaskName.updateGitHubPages
+import java.io.File
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.tasks.Copy
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+
+/**
+ * Registers the `updateGitHubPages` task that performs the update of
+ * the GitHub Pages with the documentation generated in Javadoc and HTML format
+ * for a particular Gradle project.
+ *
+ * The generated documentation is appended to the `spine.io` site
+ * via GitHub pages by pushing commits to the `gh-pages` branch.
+ *
+ * In order to work, the script needs a `deploy_key_rsa` private RSA key file in the
+ * repository root. It is recommended to encrypt it in the repository and then decrypt
+ * it on CI upon publication. Also, the script uses the `FORMAL_GIT_HUB_PAGES_AUTHOR`
+ * environment variable to set the author email for the commits. The `gh-pages`
+ * branch is created automatically if it does not exist yet.
+ *
+ * NOTE: when changing the value of "FORMAL_GIT_HUB_PAGES_AUTHOR", one also must change
+ * the SSH private (encrypted `deploy_key_rsa`) and the public
+ * ("GitHub Pages publisher" on GitHub) keys.
+ *
+ * Another requirement is an environment variable `REPO_SLUG`, which is set by the CI
+ * environment, such as `Publish` GitHub Actions workflow. It points to the repository
+ * for which the update is executed. E.g.:
+ *
+ * ```
+ * REPO_SLUG: SpineEventEngine/base
+ * ```
+ *
+ * @see UpdateGitHubPagesExtension for the extension that is used to configure
+ * this plugin
+ */
+class UpdateGitHubPages : Plugin {
+
+ /**
+ * Root folder of the repository, to which this `Project` belongs.
+ */
+ internal lateinit var rootFolder: File
+
+ /**
+ * The external inputs to include into the publishing.
+ *
+ * The inputs are evaluated according to [Copy.from] specification.
+ */
+ private lateinit var includedInputs: Set
+
+ /**
+ * Path to the temp folder used to gather the Javadoc format output (generated by Dokka)
+ * before submitting it to the GitHub Pages update.
+ */
+ internal val javadocOutputFolder = LazyTempPath("javadoc")
+
+ /**
+ * Path to the temp folder used to gather the HTML documentation
+ * generated by Dokka before submitting it to the GitHub Pages update.
+ */
+ internal val htmlOutputFolder = LazyTempPath("html")
+
+ /**
+ * Applies the plugin to the specified [project].
+ *
+ * If the project version says it is a snapshot, the plugin registers a no-op task.
+ *
+ * Even in such a case, the extension object is still created in the given project
+ * to allow customization of the parameters in its build script for later usage
+ * when the project version changes to non-snapshot.
+ */
+ override fun apply(project: Project) {
+ val extension = UpdateGitHubPagesExtension.createIn(project)
+ project.afterEvaluate {
+ registerTasks(extension)
+ }
+ }
+
+ /**
+ * Registers `updateGitHubPages` task that performs no actual update, but prints
+ * the message telling the update is skipped, since the project is in
+ * its `SNAPSHOT` version.
+ */
+ @Suppress("unused")
+ private fun Project.registerNoOpTask() {
+ tasks.register(updateGitHubPages) {
+ group = SpineTaskGroup.name
+ description = "Skips the GitHub Pages update for snapshot project versions"
+ doLast {
+ val project = this@registerNoOpTask
+ println(
+ "GitHub Pages update will be skipped since this project is a snapshot: " +
+ "`${project.name}-${project.version}`."
+ )
+ }
+ }
+ }
+
+ private fun Project.registerTasks(extension: UpdateGitHubPagesExtension) {
+ rootFolder = extension.rootFolder()
+ includedInputs = extension.includedInputs()
+
+ tasks.registerCopyJavadoc()
+ tasks.registerCopyDokka()
+
+ val updatePagesTask = tasks.registerUpdateTask()
+ updatePagesTask.configure {
+ dependsOn(copyJavadocDocs)
+ dependsOn(copyHtmlDocs)
+ }
+ }
+
+ private fun TaskContainer.registerCopyJavadoc() {
+ val inputs = composeJavadocInputs()
+
+ register(copyJavadocDocs, Copy::class.java) {
+ group = SpineTaskGroup.name
+ description = "Copies generated Javadoc into the GitHub Pages staging folder"
+ inputs.forEach { from(it) }
+ into(javadocOutputFolder)
+ }
+ }
+
+ private fun TaskContainer.composeJavadocInputs(): List {
+ val inputs = mutableListOf()
+ inputs.add(dokkaJavadocTask()!!)
+ inputs.addAll(includedInputs)
+ return inputs
+ }
+
+ private fun TaskContainer.registerCopyDokka() {
+ val inputs = composeDokkaInputs()
+
+ register(copyHtmlDocs, Copy::class.java) {
+ group = SpineTaskGroup.name
+ description = "Copies generated Dokka HTML docs into the GitHub Pages staging folder"
+ inputs.forEach { from(it) }
+ into(htmlOutputFolder)
+ }
+ }
+
+ private fun TaskContainer.composeDokkaInputs(): List {
+ val inputs = mutableListOf()
+
+ dokkaHtmlTask()?.let {
+ inputs.add(it)
+ }
+ inputs.addAll(includedInputs)
+
+ return inputs
+ }
+
+ private fun TaskContainer.registerUpdateTask(): TaskProvider {
+ return register(updateGitHubPages) {
+ group = SpineTaskGroup.name
+ description = "Publishes the generated documentation to the `gh-pages` branch"
+ doLast {
+ try {
+ updateGhPages(project)
+ } finally {
+ cleanup()
+ }
+ }
+ }
+ }
+
+ private fun cleanup() {
+ val folders = listOf(htmlOutputFolder, javadocOutputFolder)
+ folders.forEach {
+ it.toFile().deleteRecursively()
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/UpdateGitHubPagesExtension.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPagesExtension.kt
similarity index 53%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/UpdateGitHubPagesExtension.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPagesExtension.kt
index ec654fbf7..a849a836d 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/UpdateGitHubPagesExtension.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPagesExtension.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.github.pages
+package io.spine.gradle.github.pages
import java.io.File
import org.gradle.api.Project
@@ -38,63 +38,41 @@ import org.gradle.kotlin.dsl.property
* Configures the `updateGitHubPages` extension.
*/
@Suppress("unused")
-fun Project.updateGitHubPages(excludeInternalDocletVersion: String,
- action: UpdateGitHubPagesExtension.() -> Unit) {
+fun Project.updateGitHubPages(
+ action: UpdateGitHubPagesExtension.() -> Unit
+) {
apply()
val extension = extensions.getByType(UpdateGitHubPagesExtension::class)
- extension.excludeInternalDocletVersion = excludeInternalDocletVersion
extension.action()
}
/**
* The extension for configuring the [UpdateGitHubPages] plugin.
+ *
+ * @property rootFolder The root folder of the repository to which the updated `Project` belongs.
+ * @property includeInputs The external inputs, which output should be included
+ * into the GitHub Pages update. The values are interpreted according to
+ * [Copy.from][org.gradle.api.tasks.Copy.from] specification.
+ * This property is optional.
*/
-class UpdateGitHubPagesExtension
-private constructor(
-
- /**
- * Tells whether the types marked `@Internal` should be included into the doc generation.
- */
- val allowInternalJavadoc: Property,
-
- /**
- * The root folder of the repository to which the updated `Project` belongs.
- */
+class UpdateGitHubPagesExtension private constructor(
var rootFolder: Property,
-
- /**
- * The external inputs, which output should be included
- * into the GitHub Pages update.
- *
- * The values are interpreted according to [org.gradle.api.tasks.Copy.from] specification.
- *
- * This property is optional.
- */
var includeInputs: SetProperty
) {
-
- /**
- * The version of the
- * [ExcludeInternalDoclet][io.spine.internal.gradle.javadoc.ExcludeInternalDoclet]
- * used when updating documentation at GitHub Pages.
- *
- * This value is used when adding dependency on the doclet when the plugin tasks
- * are registered. Since the doclet dependency is required, its value passed as a parameter for
- * the extension, rather than a property.
- */
- internal lateinit var excludeInternalDocletVersion: String
-
internal companion object {
- /** The name of the extension. */
+ /**
+ * The name of the extension.
+ */
const val name = "updateGitHubPages"
- /** Creates a new extension and adds it to the passed project. */
+ /**
+ * Creates a new extension and adds it to the passed project.
+ */
fun createIn(project: Project): UpdateGitHubPagesExtension {
val factory = project.objects
val result = UpdateGitHubPagesExtension(
- allowInternalJavadoc = factory.property(Boolean::class),
rootFolder = factory.property(File::class),
includeInputs = factory.setProperty(Any::class.java)
)
@@ -104,25 +82,14 @@ private constructor(
}
/**
- * Returns `true` if the `@Internal`-annotated types should be included into the generated
- * documentation, `false` otherwise.
- */
- fun allowInternalJavadoc(): Boolean {
- return allowInternalJavadoc.get()
- }
-
- /**
- * Returns the local root folder of the repository, to which the handled Gradle Project belongs.
+ * Returns the local root folder of the repository, to which the handled Gradle
+ * Project belongs.
*/
- fun rootFolder(): File {
- return rootFolder.get()
- }
+ fun rootFolder(): File = rootFolder.get()
/**
- * Returns the external inputs, which results should be included
- * into the GitHub Pages update.
+ * Returns the external inputs, which results should be included into the
+ * GitHub Pages update.
*/
- fun includedInputs(): Set {
- return includeInputs.get()
- }
+ fun includedInputs(): Set = includeInputs.get()
}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/java/Linters.kt b/buildSrc/src/main/kotlin/io/spine/gradle/java/Linters.kt
new file mode 100644
index 000000000..459237c4a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/java/Linters.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.java
+
+import net.ltgt.gradle.errorprone.errorprone
+import org.gradle.api.Project
+import org.gradle.api.tasks.compile.JavaCompile
+import org.gradle.kotlin.dsl.invoke
+import org.gradle.kotlin.dsl.named
+
+/**
+ * Disables Java linters in this [Project].
+ *
+ * In particular, the following linters will be disabled:
+ *
+ * 1. CheckStyle.
+ * 2. PMD.
+ * 3. ErrorProne.
+ *
+ * Apply this configuration for modules that have original Flogger sources,
+ * which have not been migrated to Kotlin yet. They produce a lot of
+ * errors/warnings failing the build.
+ *
+ * Our own sources are mostly in Kotlin (as for `spine-logging` repo),
+ * so this action seems quite safe.
+ */
+// TODO:2023-09-22:yevhenii.nadtochii: Remove this piece of configuration.
+// See issue: https://github.com/SpineEventEngine/logging/issues/56
+fun Project.disableLinters() {
+ tasks {
+ named("checkstyleMain") { enabled = false }
+ named("pmdMain") { enabled = false }
+ named("compileJava") {
+ options.errorprone.enabled.set(false)
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/java/Tasks.kt b/buildSrc/src/main/kotlin/io/spine/gradle/java/Tasks.kt
similarity index 88%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/java/Tasks.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/java/Tasks.kt
index 8f2344feb..b2737f20c 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/java/Tasks.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/java/Tasks.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.java
+package io.spine.gradle.java
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
@@ -36,7 +36,7 @@ import org.gradle.kotlin.dsl.named
*
* Runs the unit tests using JUnit or TestNG.
*
- * Depends on `testClasses`, and all tasks which produce the test runtime classpath.
+ * Depends on `testClasses`, and all tasks that produce the test runtime classpath.
*
* @see
* Tasks | The Java Plugin
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javac/ErrorProne.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javac/ErrorProne.kt
similarity index 93%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javac/ErrorProne.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javac/ErrorProne.kt
index df31e581a..84c6bcb98 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javac/ErrorProne.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javac/ErrorProne.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javac
+package io.spine.gradle.javac
import net.ltgt.gradle.errorprone.errorprone
import org.gradle.api.tasks.compile.JavaCompile
@@ -51,6 +51,7 @@ import org.gradle.process.CommandLineArgumentProvider
*/
@Suppress("unused")
fun JavaCompile.configureErrorProne() {
+ options.compilerArgs.add("--should-stop=ifError=FLOW")
options.errorprone
.errorproneArgumentProviders
.add(ErrorProneConfig.ARGUMENTS)
@@ -82,6 +83,7 @@ private object ErrorProneConfig {
"-Xep:CheckReturnValue:OFF",
"-Xep:FloggerSplitLogStatement:OFF",
+ "-Xep:FloggerLogString:OFF"
)
}
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javac/Javac.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javac/Javac.kt
similarity index 94%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javac/Javac.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javac/Javac.kt
index 839252d63..44223cb61 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javac/Javac.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javac/Javac.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javac
+package io.spine.gradle.javac
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.process.CommandLineArgumentProvider
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/Encoding.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/Encoding.kt
similarity index 90%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/Encoding.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javadoc/Encoding.kt
index f71296834..052a1aabe 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/Encoding.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/Encoding.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javadoc
+package io.spine.gradle.javadoc
/**
* The encoding to use in Javadoc processing.
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/ExcludeInternalDoclet.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt
similarity index 81%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/ExcludeInternalDoclet.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt
index 72d7f6a1b..0732edc8c 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/ExcludeInternalDoclet.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,21 +24,24 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javadoc
+package io.spine.gradle.javadoc
-import io.spine.internal.gradle.javadoc.ExcludeInternalDoclet.Companion.taskName
-import io.spine.internal.gradle.sourceSets
+import io.spine.dependency.local.ToolBase
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.javadoc.ExcludeInternalDoclet.Companion.taskName
+import io.spine.gradle.sourceSets
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.external.javadoc.StandardJavadocDocletOptions
/**
- * The doclet which removes Javadoc for `@Internal` things in the Java code.
+ * The doclet that removes Javadoc for `@Internal` things in the Java code.
*/
-class ExcludeInternalDoclet(val version: String) {
+@Suppress("ConstPropertyName")
+class ExcludeInternalDoclet {
- private val dependency = "io.spine.tools:spine-javadoc-filter:${version}"
+ private val dependency = ToolBase.JavadocFilter.artifact
companion object {
@@ -54,7 +57,7 @@ class ExcludeInternalDoclet(val version: String) {
const val className = "io.spine.tools.javadoc.ExcludeInternalDoclet"
/**
- * The name of the helper task which configures the Javadoc processing
+ * The name of the helper task that configures the Javadoc processing
* to exclude `@Internal` types.
*/
const val taskName = "noInternalJavadoc"
@@ -65,7 +68,7 @@ class ExcludeInternalDoclet(val version: String) {
}
/**
- * Creates a custom Javadoc task for the [project] which excludes the types
+ * Creates a custom Javadoc task for the [project] that excludes the types
* annotated as `@Internal`.
*
* The task is registered under [taskName].
@@ -91,6 +94,9 @@ private fun Project.appendCustomJavadocTask(excludeInternalDoclet: Configuration
val javadocTask = tasks.javadocTask()
tasks.register(taskName, Javadoc::class.java) {
+ group = SpineTaskGroup.name
+ description = "Generates Javadoc that omits `@Internal` Java APIs"
+
source = sourceSets.getByName("main").allJava.filter {
!it.absolutePath.contains("generated")
}.asFileTree
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/JavadocConfig.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/JavadocConfig.kt
similarity index 80%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/JavadocConfig.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javadoc/JavadocConfig.kt
index f6002cad5..9616e99e2 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/JavadocConfig.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/JavadocConfig.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,13 +24,26 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javadoc
+package io.spine.gradle.javadoc
+import io.spine.gradle.javadoc.JavadocConfig.tags
import java.io.File
import org.gradle.api.JavaVersion
import org.gradle.api.Project
+import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.external.javadoc.StandardJavadocDocletOptions
+import productionModules
+
+/**
+ * Finds a [Javadoc] Gradle task by the passed name.
+ */
+fun TaskContainer.javadocTask(named: String) = this.getByName(named) as Javadoc
+
+/**
+ * Finds a default [Javadoc] Gradle task.
+ */
+fun TaskContainer.javadocTask() = this.getByName("javadoc") as Javadoc
/**
* Javadoc processing settings.
@@ -43,9 +56,9 @@ object JavadocConfig {
/**
* Link to the documentation for Java 11 Standard Library API.
*
- * OpenJDK SE 11 is used for the reference.
+ * Oracle JDK SE 11 is used for the reference.
*/
- private const val standardLibraryAPI = "https://cr.openjdk.java.net/~iris/se/11/latestSpec/api/"
+ private const val standardLibraryAPI = "https://docs.oracle.com/en/java/javase/11/docs/api/"
@Suppress("MemberVisibilityCanBePrivate") // opened to be visible from docs.
val tags = listOf(
@@ -58,17 +71,25 @@ object JavadocConfig {
fun applyTo(project: Project) {
val javadocTask = project.tasks.javadocTask()
+ if (!isProductionModule(project)) {
+ javadocTask.enabled = false
+ return
+ }
discardJavaModulesInLinks(javadocTask)
val docletOptions = javadocTask.options as StandardJavadocDocletOptions
configureDoclet(docletOptions)
}
+ private fun isProductionModule(project: Project) = project.run {
+ rootProject.productionModules.contains(this)
+ }
+
/**
- * Discards using of Java 9 modules in URL links generated by javadoc for our codebase.
+ * Discards using of Java 9 modules in URL links generated by Javadoc for our codebase.
*
* This fixes navigation to classes through the search results.
*
- * The issue appeared after migration to Java 11. When javadoc is generated for a project
+ * The issue appeared after migration to Java 11. When Javadoc is generated for a project
* that does not declare Java 9 modules, search results contain broken links with appended
* `undefined` prefix to the URL. This `undefined` was meant to be a name of a Java 9 module.
*
@@ -78,9 +99,9 @@ object JavadocConfig {
// We ask `Javadoc` task to modify "search.js" and override a method, responsible for
// the formation of URL prefixes. We can't specify the option "--no-module-directories",
- // because it leads to discarding of all module prefixes in generated links. That means,
- // links to the types from the standard library would not work, as they are declared
- // within modules since Java 9.
+ // because it leads to discarding of all module prefixes in generated links.
+ // That means links to the types from the standard library would not work,
+ // as they are declared within modules since Java 9.
val discardModulePrefix = """
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/JavadocTag.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/JavadocTag.kt
similarity index 90%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/JavadocTag.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javadoc/JavadocTag.kt
index 2a278bd92..a6224a533 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/JavadocTag.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/JavadocTag.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javadoc
+package io.spine.gradle.javadoc
/**
* The Javadoc tag.
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsContext.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsContext.kt
similarity index 86%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsContext.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsContext.kt
index 8754173f5..ec5d87263 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsContext.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsContext.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,10 +24,12 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javascript
+package io.spine.gradle.javascript
import java.io.File
import org.gradle.api.Project
+import org.gradle.kotlin.dsl.support.serviceOf
+import org.gradle.process.ExecOperations
/**
* Provides access to the current [JsEnvironment] and shortcuts for running `npm` tool.
@@ -47,8 +49,7 @@ open class JsContext(jsEnv: JsEnvironment, internal val project: Project)
*
* This [File] is used as a working directory.
*/
- fun File.npm(vararg args: String) = project.exec {
-
+ fun File.npm(vararg args: String) = project.serviceOf().exec {
workingDir(this@npm)
commandLine(npmExecutable)
args(*args)
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsEnvironment.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsEnvironment.kt
similarity index 97%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsEnvironment.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsEnvironment.kt
index f2d10aca0..7a649a899 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsEnvironment.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsEnvironment.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javascript
+package io.spine.gradle.javascript
import java.io.File
import org.apache.tools.ant.taskdefs.condition.Os
@@ -71,7 +71,7 @@ interface JsEnvironment {
get() = projectDir.resolve("test")
/**
- * A directory which all artifacts are generated into.
+ * A directory that all artifacts are generated into.
*
* Default value: "$projectDir/build".
*/
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsExtension.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsExtension.kt
similarity index 93%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsExtension.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsExtension.kt
index 4fe1d3ffa..a849c7609 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsExtension.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/JsExtension.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,10 +24,10 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javascript
+package io.spine.gradle.javascript
-import io.spine.internal.gradle.javascript.task.JsTasks
-import io.spine.internal.gradle.javascript.plugin.JsPlugins
+import io.spine.gradle.javascript.plugin.JsPlugins
+import io.spine.gradle.javascript.task.JsTasks
import org.gradle.api.Project
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.extra
@@ -70,7 +70,7 @@ import org.gradle.kotlin.dsl.findByType
* There are two ways to modify the environment:
*
* 1. Update [JsEnvironment] directly. Go with this option when it is a global change
- * that should affect all projects which use this extension.
+ * that should affect all projects that use this extension.
* 2. Use [JsExtension.environment] scope — for temporary and custom overridings.
*
* An example of a property overriding:
@@ -93,7 +93,7 @@ import org.gradle.kotlin.dsl.findByType
* named after a task it registers or a task group if several tasks are registered at once.
* Then this extension is called in a project's `build.gradle.kts`.
*
- * `JsTasks` and `JsPlugins` scopes extend [JsContext] which provides access
+ * `JsTasks` and `JsPlugins` scopes extend [JsContext] that provides access
* to the current [JsEnvironment] and shortcuts for running `npm` tool.
*
* Below is the simplest example of how to create a primitive `printNpmVersion` task.
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/Idea.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/plugin/Idea.kt
similarity index 91%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/Idea.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javascript/plugin/Idea.kt
index 51d312c84..087dd2556 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/Idea.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/plugin/Idea.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javascript.plugin
+package io.spine.gradle.javascript.plugin
import org.gradle.kotlin.dsl.configure
import org.gradle.plugins.ide.idea.model.IdeaModel
@@ -49,7 +49,7 @@ fun JsPlugins.idea() {
module {
sourceDirs.add(srcDir)
- testSourceDirs.add(testSrcDir)
+ testSources.from(testSrcDir)
excludeDirs.addAll(
listOf(
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/JsPlugins.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/plugin/JsPlugins.kt
similarity index 88%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/JsPlugins.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javascript/plugin/JsPlugins.kt
index 6a17820da..b478f649a 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/JsPlugins.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/plugin/JsPlugins.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,10 +24,10 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javascript.plugin
+package io.spine.gradle.javascript.plugin
-import io.spine.internal.gradle.javascript.JsContext
-import io.spine.internal.gradle.javascript.JsEnvironment
+import io.spine.gradle.javascript.JsContext
+import io.spine.gradle.javascript.JsEnvironment
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionContainer
import org.gradle.api.plugins.PluginContainer
@@ -63,8 +63,8 @@ import org.gradle.api.tasks.TaskContainer
* And here's how to apply it in `build.gradle.kts`:
*
* ```
- * import io.spine.internal.gradle.javascript.javascript
- * import io.spine.internal.gradle.javascript.plugins.fooBar
+ * import io.spine.gradle.javascript.javascript
+ * import io.spine.gradle.javascript.plugins.fooBar
*
* // ...
*
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/McJs.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/plugin/McJs.kt
similarity index 93%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/McJs.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javascript/plugin/McJs.kt
index 56b1263a9..2efa4a432 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/McJs.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/plugin/McJs.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javascript.plugin
+package io.spine.gradle.javascript.plugin
import org.gradle.api.Task
import org.gradle.api.tasks.TaskContainer
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/Protobuf.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/plugin/Protobuf.kt
similarity index 85%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/Protobuf.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javascript/plugin/Protobuf.kt
index d99d2a288..c1c2d6480 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/Protobuf.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/plugin/Protobuf.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,15 +24,12 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javascript.plugin
+package io.spine.gradle.javascript.plugin
-import com.google.protobuf.gradle.builtins
-import com.google.protobuf.gradle.generateProtoTasks
+import com.google.protobuf.gradle.ProtobufExtension
import com.google.protobuf.gradle.id
-import com.google.protobuf.gradle.protobuf
-import com.google.protobuf.gradle.protoc
import com.google.protobuf.gradle.remove
-import io.spine.internal.dependency.Protobuf
+import io.spine.dependency.lib.Protobuf
/**
* Applies and configures `protobuf` plugin to work with a JavaScript module.
@@ -50,9 +47,8 @@ fun JsPlugins.protobuf() {
apply(Protobuf.GradlePlugin.id)
}
- project.protobuf {
-
- generatedFilesBaseDir = projectDir.path
+ val protobufExt = project.extensions.getByType(ProtobufExtension::class.java)
+ protobufExt.apply {
protoc {
artifact = Protobuf.compiler
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Assemble.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Assemble.kt
similarity index 84%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Assemble.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Assemble.kt
index dbc770dfd..3bb145480 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Assemble.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Assemble.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,16 +24,17 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javascript.task
+package io.spine.gradle.javascript.task
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.google.protobuf.gradle.GenerateProtoTask
-import io.spine.internal.gradle.base.assemble
-import io.spine.internal.gradle.javascript.plugin.generateJsonParsers
-import io.spine.internal.gradle.named
-import io.spine.internal.gradle.register
-import io.spine.internal.gradle.TaskName
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.TaskName
+import io.spine.gradle.base.assemble
+import io.spine.gradle.javascript.plugin.generateJsonParsers
+import io.spine.gradle.named
+import io.spine.gradle.register
import org.gradle.api.Task
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
@@ -42,8 +43,8 @@ import org.gradle.kotlin.dsl.withType
/**
* Registers tasks for assembling JavaScript artifacts.
*
- * Please note, this task group depends on [mc-js][io.spine.internal.gradle.javascript.plugin.mcJs]
- * and [protobuf][io.spine.internal.gradle.javascript.plugin.protobuf]` plugins. Therefore,
+ * Please note, this task group depends on [mc-js][io.spine.gradle.javascript.plugin.mcJs]
+ * and [protobuf][io.spine.gradle.javascript.plugin.protobuf]` plugins. Therefore,
* these plugins should be applied in the first place.
*
* List of tasks to be created:
@@ -56,8 +57,8 @@ import org.gradle.kotlin.dsl.withType
* Here's an example of how to apply it in `build.gradle.kts`:
*
* ```
- * import io.spine.internal.gradle.javascript.javascript
- * import io.spine.internal.gradle.javascript.task.assemble
+ * import io.spine.gradle.javascript.javascript
+ * import io.spine.gradle.javascript.task.assemble
*
* // ...
*
@@ -105,7 +106,7 @@ private fun JsTasks.assembleJs() =
register(assembleJsName) {
description = "Assembles JavaScript sources into consumable artifacts."
- group = JsTasks.Group.assemble
+ group = SpineTaskGroup.name
dependsOn(
installNodePackages,
@@ -130,7 +131,7 @@ private fun JsTasks.compileProtoToJs() =
register(compileProtoToJsName) {
description = "Compiles Protobuf messages into JavaScript."
- group = JsTasks.Group.assemble
+ group = SpineTaskGroup.name
withType()
.forEach { dependsOn(it) }
@@ -141,7 +142,7 @@ private val installNodePackagesName = TaskName.of("installNodePackages")
/**
* Locates `installNodePackages` task in this [TaskContainer].
*
- * The task installs Node packages which this module depends on using `npm install` command.
+ * The task installs Node packages that this module depends on using `npm install` command.
*
* The `npm install` command is executed with the vulnerability check disabled since
* it cannot fail the task execution despite on vulnerabilities found.
@@ -158,7 +159,7 @@ private fun JsTasks.installNodePackages() =
register(installNodePackagesName) {
description = "Installs module`s Node dependencies."
- group = JsTasks.Group.assemble
+ group = SpineTaskGroup.name
inputs.file(packageJson)
outputs.dir(nodeModules)
@@ -175,7 +176,7 @@ private val updatePackageVersionName = TaskName.of("updatePackageVersion")
* Locates `updatePackageVersion` task in this [TaskContainer].
*
* The task sets the module's version in `package.json` to the value of
- * [moduleVersion][io.spine.internal.gradle.javascript.JsEnvironment.moduleVersion]
+ * [moduleVersion][io.spine.gradle.javascript.JsEnvironment.moduleVersion]
* specified in the current `JsEnvironment`.
*/
val TaskContainer.updatePackageVersion: TaskProvider
@@ -185,7 +186,7 @@ private fun JsTasks.updatePackageVersion() =
register(updatePackageVersionName) {
description = "Sets a module's version in `package.json`."
- group = JsTasks.Group.assemble
+ group = SpineTaskGroup.name
doLast {
val objectNode = ObjectMapper()
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Check.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Check.kt
similarity index 87%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Check.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Check.kt
index 4fdd69f92..9f46d845b 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Check.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Check.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,14 +24,15 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javascript.task
+package io.spine.gradle.javascript.task
-import io.spine.internal.gradle.base.check
-import io.spine.internal.gradle.java.test
-import io.spine.internal.gradle.javascript.isWindows
-import io.spine.internal.gradle.named
-import io.spine.internal.gradle.register
-import io.spine.internal.gradle.TaskName
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.TaskName
+import io.spine.gradle.base.check
+import io.spine.gradle.java.test
+import io.spine.gradle.javascript.isWindows
+import io.spine.gradle.named
+import io.spine.gradle.register
import org.gradle.api.Task
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
@@ -52,9 +53,9 @@ import org.gradle.api.tasks.TaskProvider
* Here's an example of how to apply it in `build.gradle.kts`:
*
* ```
- * import io.spine.internal.gradle.javascript.javascript
- * import io.spine.internal.gradle.javascript.task.assemble
- * import io.spine.internal.gradle.javascript.task.check
+ * import io.spine.gradle.javascript.javascript
+ * import io.spine.gradle.javascript.task.assemble
+ * import io.spine.gradle.javascript.task.check
*
* // ...
*
@@ -97,7 +98,7 @@ private fun JsTasks.checkJs() =
register(checkJsName) {
description = "Runs tests, audits NPM modules and creates a test-coverage report."
- group = JsTasks.Group.check
+ group = SpineTaskGroup.name
dependsOn(
auditNodePackages,
@@ -126,7 +127,7 @@ private fun JsTasks.auditNodePackages() =
register(auditNodePackagesName) {
description = "Audits the module's Node dependencies."
- group = JsTasks.Group.check
+ group = SpineTaskGroup.name
inputs.dir(nodeModules)
@@ -161,7 +162,7 @@ private fun JsTasks.coverageJs() =
register(coverageJsName) {
description = "Runs the JavaScript tests and collects the code coverage."
- group = JsTasks.Group.check
+ group = SpineTaskGroup.name
outputs.dir(nycOutput)
@@ -186,7 +187,7 @@ private fun JsTasks.testJs() =
register(testJsName) {
description = "Runs JavaScript tests."
- group = JsTasks.Group.check
+ group = SpineTaskGroup.name
doLast {
npm("run", "test")
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Clean.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Clean.kt
similarity index 84%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Clean.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Clean.kt
index 6658b830f..6042128fa 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Clean.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Clean.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,12 +24,13 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javascript.task
+package io.spine.gradle.javascript.task
-import io.spine.internal.gradle.base.clean
-import io.spine.internal.gradle.named
-import io.spine.internal.gradle.register
-import io.spine.internal.gradle.TaskName
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.TaskName
+import io.spine.gradle.base.clean
+import io.spine.gradle.named
+import io.spine.gradle.register
import org.gradle.api.tasks.Delete
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
@@ -48,9 +49,9 @@ import org.gradle.api.tasks.TaskProvider
* Here's an example of how to apply it in `build.gradle.kts`:
*
* ```
- * import io.spine.internal.gradle.javascript.javascript
- * import io.spine.internal.gradle.javascript.task.assemble
- * import io.spine.internal.gradle.javascript.task.clean
+ * import io.spine.gradle.javascript.javascript
+ * import io.spine.gradle.javascript.task.assemble
+ * import io.spine.gradle.javascript.task.clean
*
* // ...
*
@@ -87,7 +88,7 @@ private fun JsTasks.cleanJs() =
register(cleanJsName) {
description = "Cleans output of `assembleJs` task and output of its dependants."
- group = JsTasks.Group.clean
+ group = SpineTaskGroup.name
delete(
assembleJs.map { it.outputs },
@@ -114,7 +115,7 @@ private fun JsTasks.cleanGenerated() =
register(cleanGeneratedName) {
description = "Cleans generated code and reports."
- group = JsTasks.Group.clean
+ group = SpineTaskGroup.name
delete(
genProtoMain,
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/IntegrationTest.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/IntegrationTest.kt
similarity index 88%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/IntegrationTest.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/IntegrationTest.kt
index fadcc09cb..2db59a4f5 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/IntegrationTest.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/IntegrationTest.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,12 +24,13 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javascript.task
+package io.spine.gradle.javascript.task
-import io.spine.internal.gradle.base.build
-import io.spine.internal.gradle.named
-import io.spine.internal.gradle.register
-import io.spine.internal.gradle.TaskName
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.TaskName
+import io.spine.gradle.base.build
+import io.spine.gradle.named
+import io.spine.gradle.register
import org.gradle.api.Task
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
@@ -61,8 +62,8 @@ val TaskContainer.integrationTest: TaskProvider
* Here's an example of how to apply it in `build.gradle.kts`:
*
* ```
- * import io.spine.internal.gradle.javascript.javascript
- * import io.spine.internal.gradle.javascript.task.integrationTest
+ * import io.spine.gradle.javascript.javascript
+ * import io.spine.gradle.javascript.task.integrationTest
*
* // ...
*
@@ -74,6 +75,7 @@ val TaskContainer.integrationTest: TaskProvider
* }
* ```
*/
+@Suppress("unused")
fun JsTasks.integrationTest() {
linkSpineWebModule()
@@ -86,7 +88,7 @@ fun JsTasks.integrationTest() {
description = "Runs integration tests of the `spine-web` library " +
"against the sample application."
- group = JsTasks.Group.check
+ group = SpineTaskGroup.name
dependsOn(build, linkSpineWebModule, ":test-app:appBeforeIntegrationTest")
@@ -117,7 +119,7 @@ private fun JsTasks.linkSpineWebModule() =
register(linkSpineWebModuleName) {
description = "Install unpublished artifact of `spine-web` library as a module dependency."
- group = JsTasks.Group.assemble
+ group = SpineTaskGroup.name
dependsOn(":client-js:publishJsLocally")
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/JsTasks.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/JsTasks.kt
similarity index 73%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/JsTasks.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/JsTasks.kt
index b384fd3a5..3b9d814a1 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/JsTasks.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/JsTasks.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,10 +24,10 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javascript.task
+package io.spine.gradle.javascript.task
-import io.spine.internal.gradle.javascript.JsEnvironment
-import io.spine.internal.gradle.javascript.JsContext
+import io.spine.gradle.javascript.JsContext
+import io.spine.gradle.javascript.JsEnvironment
import org.gradle.api.Project
import org.gradle.api.tasks.TaskContainer
@@ -38,12 +38,11 @@ import org.gradle.api.tasks.TaskContainer
*
* 1. Access to the current [JsContext].
* 2. Project's [TaskContainer].
- * 3. Default task groups.
*
* Supposing, one needs to create a new task that would participate in building. Let the task name
* be `bundleJs`. To do that, several steps should be completed:
*
- * 1. Define the task name and type using [TaskName][io.spine.internal.gradle.TaskName].
+ * 1. Define the task name and type using [TaskName][io.spine.gradle.TaskName].
* 2. Create a public typed reference for the task upon [TaskContainer]. It would facilitate
* referencing to the new task, so that external tasks could depend on it. This reference
* should be documented.
@@ -53,9 +52,10 @@ import org.gradle.api.tasks.TaskContainer
* Here's an example of `bundleJs()` extension:
*
* ```
- * import io.spine.internal.gradle.named
- * import io.spine.internal.gradle.register
- * import io.spine.internal.gradle.TaskName
+ * import io.spine.gradle.SpineTaskGroup
+ * import io.spine.gradle.named
+ * import io.spine.gradle.register
+ * import io.spine.gradle.TaskName
* import org.gradle.api.Task
* import org.gradle.api.tasks.TaskContainer
* import org.gradle.api.tasks.Exec
@@ -75,8 +75,8 @@ import org.gradle.api.tasks.TaskContainer
* fun JsTasks.bundleJs() =
* register(bundleJsName) {
*
- * description = "Bundles JS sources using `webpack` tool."
- * group = JsTasks.Group.build
+ * description = "Bundles JS sources using `webpack` tool"
+ * group = SpineTaskGroup.name
*
* // ...
* }
@@ -85,8 +85,8 @@ import org.gradle.api.tasks.TaskContainer
* And here's how to apply it in `build.gradle.kts`:
*
* ```
- * import io.spine.internal.gradle.javascript.javascript
- * import io.spine.internal.gradle.javascript.task.bundleJs
+ * import io.spine.gradle.javascript.javascript
+ * import io.spine.gradle.javascript.task.bundleJs
*
* // ...
*
@@ -102,17 +102,3 @@ import org.gradle.api.tasks.TaskContainer
*/
class JsTasks(jsEnv: JsEnvironment, project: Project)
: JsContext(jsEnv, project), TaskContainer by project.tasks
-{
- /**
- * Default task groups for tasks that participate in building a JavaScript module.
- *
- * @see [org.gradle.api.Task.getGroup]
- */
- internal object Group {
- const val assemble = "JavaScript/Assemble"
- const val check = "JavaScript/Check"
- const val clean = "JavaScript/Clean"
- const val build = "JavaScript/Build"
- const val publish = "JavaScript/Publish"
- }
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/LicenseReport.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/LicenseReport.kt
similarity index 83%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/LicenseReport.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/LicenseReport.kt
index 99935edd6..8276bcc06 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/LicenseReport.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/LicenseReport.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,12 +24,13 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javascript.task
+package io.spine.gradle.javascript.task
-import io.spine.internal.gradle.named
-import io.spine.internal.gradle.register
-import io.spine.internal.gradle.report.license.generateLicenseReport
-import io.spine.internal.gradle.TaskName
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.TaskName
+import io.spine.gradle.named
+import io.spine.gradle.register
+import io.spine.gradle.report.license.generateLicenseReport
import org.gradle.api.Task
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
@@ -42,8 +43,8 @@ import org.gradle.api.tasks.TaskProvider
* Here's an example of how to apply it in `build.gradle.kts`:
*
* ```
- * import io.spine.internal.gradle.javascript.javascript
- * import io.spine.internal.gradle.javascript.task.clean
+ * import io.spine.gradle.javascript.javascript
+ * import io.spine.gradle.javascript.task.clean
*
* // ...
*
@@ -54,6 +55,7 @@ import org.gradle.api.tasks.TaskProvider
* }
* ```
*/
+@Suppress("unused")
fun JsTasks.licenseReport() {
npmLicenseReport().also {
generateLicenseReport.configure {
@@ -76,7 +78,7 @@ private fun JsTasks.npmLicenseReport() =
register(npmLicenseReportName) {
description = "Generates the report on NPM dependencies and their licenses."
- group = JsTasks.Group.build
+ group = SpineTaskGroup.name
doLast {
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Publish.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Publish.kt
similarity index 84%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Publish.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Publish.kt
index 486d831ce..c12650141 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Publish.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Publish.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,12 +24,13 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javascript.task
+package io.spine.gradle.javascript.task
-import io.spine.internal.gradle.publish.publish
-import io.spine.internal.gradle.named
-import io.spine.internal.gradle.register
-import io.spine.internal.gradle.TaskName
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.TaskName
+import io.spine.gradle.named
+import io.spine.gradle.publish.publish
+import io.spine.gradle.register
import org.gradle.api.Task
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
@@ -49,9 +50,9 @@ import org.gradle.api.tasks.TaskProvider
* Here's an example of how to apply it in `build.gradle.kts`:
*
* ```
- * import io.spine.internal.gradle.javascript.javascript
- * import io.spine.internal.gradle.javascript.task.assemble
- * import io.spine.internal.gradle.javascript.task.publish
+ * import io.spine.gradle.javascript.javascript
+ * import io.spine.gradle.javascript.task.assemble
+ * import io.spine.gradle.javascript.task.publish
*
* // ...
*
@@ -90,7 +91,7 @@ private fun JsTasks.transpileSources() =
register(transpileSourcesName) {
description = "Transpiles JavaScript sources using Babel before their publishing."
- group = JsTasks.Group.publish
+ group = SpineTaskGroup.name
doLast {
npm("run", "transpile-before-publish")
@@ -103,7 +104,7 @@ private val prepareJsPublicationName = TaskName.of("prepareJsPublication")
* Locates `prepareJsPublication` task in this [TaskContainer].
*
* This is a lifecycle task that prepares the NPM package in
- * [publicationDirectory][io.spine.internal.gradle.javascript.JsEnvironment.publicationDir]
+ * [publicationDirectory][io.spine.gradle.javascript.JsEnvironment.publicationDir]
* of the current `JsEnvironment`.
*/
val TaskContainer.prepareJsPublication: TaskProvider
@@ -113,7 +114,7 @@ private fun JsTasks.prepareJsPublication() =
register(prepareJsPublicationName) {
description = "Prepares the NPM package for publishing."
- group = JsTasks.Group.publish
+ group = SpineTaskGroup.name
// We need to copy two files into a destination directory without overwriting its content.
// Default `Copy` task is not used since it overwrites the content of a destination
@@ -153,7 +154,7 @@ private fun JsTasks.publishJsLocally() =
register(publishJsLocallyName) {
description = "Publishes the NPM package locally with `npm link`."
- group = JsTasks.Group.publish
+ group = SpineTaskGroup.name
doLast {
publicationDir.npm("link")
@@ -168,11 +169,11 @@ private val publishJsName = TaskName.of("publishJs")
* Locates `publishJs` task in this [TaskContainer].
*
* The task publishes the prepared NPM package from
- * [publicationDirectory][io.spine.internal.gradle.javascript.JsEnvironment.publicationDir]
+ * [publicationDirectory][io.spine.gradle.javascript.JsEnvironment.publicationDir]
* using `npm publish`.
*
* Please note, in order to publish an NMP package, a valid
- * [npmAuthToken][io.spine.internal.gradle.javascript.JsEnvironment.npmAuthToken] should be
+ * [npmAuthToken][io.spine.gradle.javascript.JsEnvironment.npmAuthToken] should be
* set. If no token is set, a default dummy value is quite enough for the local development.
*
* @see npm-publish | npm Docs
@@ -184,7 +185,7 @@ private fun JsTasks.publishJs() =
register(publishJsName) {
description = "Publishes the NPM package with `npm publish`."
- group = JsTasks.Group.publish
+ group = SpineTaskGroup.name
doLast {
publicationDir.npm("publish")
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Webpack.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Webpack.kt
similarity index 84%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Webpack.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Webpack.kt
index cbf3aaefb..5609c909c 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Webpack.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/javascript/task/Webpack.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,11 +24,12 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.javascript.task
+package io.spine.gradle.javascript.task
-import io.spine.internal.gradle.named
-import io.spine.internal.gradle.register
-import io.spine.internal.gradle.TaskName
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.TaskName
+import io.spine.gradle.named
+import io.spine.gradle.register
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
@@ -47,10 +48,10 @@ import org.gradle.api.tasks.TaskProvider
* Here's an example of how to apply it in `build.gradle.kts`:
*
* ```
- * import io.spine.internal.gradle.javascript.javascript
- * import io.spine.internal.gradle.javascript.task.assemble
- * import io.spine.internal.gradle.javascript.task.publish
- * import io.spine.internal.gradle.javascript.task.webpack
+ * import io.spine.gradle.javascript.javascript
+ * import io.spine.gradle.javascript.task.assemble
+ * import io.spine.gradle.javascript.task.publish
+ * import io.spine.gradle.javascript.task.webpack
*
* // ...
*
@@ -63,6 +64,7 @@ import org.gradle.api.tasks.TaskProvider
* }
* ```
*/
+@Suppress("unused")
fun JsTasks.webpack() {
assembleJs.configure {
@@ -92,6 +94,7 @@ private val copyBundledJsName = TaskName.of("copyBundledJs", Copy::class)
*
* The task copies bundled JavaScript sources to the publication directory.
*/
+@Suppress("unused")
val TaskContainer.copyBundledJs: TaskProvider
get() = named(copyBundledJsName)
@@ -99,7 +102,7 @@ private fun JsTasks.copyBundledJs() =
register(copyBundledJsName) {
description = "Copies bundled JavaScript sources to the NPM publication directory."
- group = JsTasks.Group.publish
+ group = SpineTaskGroup.name
from(assembleJs.map { it.outputs })
into(webpackPublicationDir)
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/kotlin/KotlinConfig.kt b/buildSrc/src/main/kotlin/io/spine/gradle/kotlin/KotlinConfig.kt
similarity index 76%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/kotlin/KotlinConfig.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/kotlin/KotlinConfig.kt
index 863ea4626..92b7a3874 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/kotlin/KotlinConfig.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/kotlin/KotlinConfig.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,20 +24,21 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.kotlin
+package io.spine.gradle.kotlin
import org.gradle.jvm.toolchain.JavaLanguageVersion
-import org.gradle.jvm.toolchain.JavaToolchainSpec
+import org.jetbrains.kotlin.gradle.dsl.JvmDefaultMode
+import org.jetbrains.kotlin.gradle.dsl.KotlinCommonCompilerOptions
+import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
/**
* Sets [Java toolchain](https://kotlinlang.org/docs/gradle.html#gradle-java-toolchains-support)
- * to the specified version (e.g. 11 or 8).
+ * to the specified version (e.g., 11 or 8).
*/
fun KotlinJvmProjectExtension.applyJvmToolchain(version: Int) {
jvmToolchain {
- (this as JavaToolchainSpec).languageVersion.set(JavaLanguageVersion.of(version))
+ languageVersion.set(JavaLanguageVersion.of(version))
}
}
@@ -53,12 +54,15 @@ fun KotlinJvmProjectExtension.applyJvmToolchain(version: String) =
* Opts-in to experimental features that we use in our codebase.
*/
@Suppress("unused")
-fun KotlinCompile.setFreeCompilerArgs() {
- kotlinOptions {
- freeCompilerArgs = listOf(
+fun KotlinCommonCompilerOptions.setFreeCompilerArgs() {
+ if (this is KotlinJvmCompilerOptions) {
+ jvmDefault.set(JvmDefaultMode.NO_COMPATIBILITY)
+ }
+ freeCompilerArgs.addAll(
+ listOf(
"-Xskip-prerelease-check",
- "-Xjvm-default=all",
- "-Xinline-classes",
+ "-Xexpect-actual-classes",
+ "-Xcontext-parameters",
"-opt-in=" +
"kotlin.contracts.ExperimentalContracts," +
"kotlin.io.path.ExperimentalPathApi," +
@@ -66,5 +70,5 @@ fun KotlinCompile.setFreeCompilerArgs() {
"kotlin.ExperimentalStdlibApi," +
"kotlin.experimental.ExperimentalTypeInference",
)
- }
+ )
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CheckVersionIncrement.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt
similarity index 71%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CheckVersionIncrement.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt
index aa4da982b..4c215f14b 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CheckVersionIncrement.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,12 +24,13 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.publish
+package io.spine.gradle.publish
import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
import com.fasterxml.jackson.dataformat.xml.XmlMapper
-import io.spine.internal.gradle.Repository
+import io.spine.gradle.repo.Repository
import java.io.FileNotFoundException
+import java.net.URI
import java.net.URL
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
@@ -38,7 +39,7 @@ import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
/**
- * A task which verifies that the current version of the library has not been published to the given
+ * A task that verifies that the current version of the library has not been published to the given
* Maven repository yet.
*/
open class CheckVersionIncrement : DefaultTask() {
@@ -56,12 +57,13 @@ open class CheckVersionIncrement : DefaultTask() {
val version: String = project.version as String
@TaskAction
- private fun fetchAndCheck() {
+ fun fetchAndCheck() {
val artifact = "${project.artifactPath()}/${MavenMetadata.FILE_NAME}"
- checkInRepo(repository.snapshots, artifact)
+ val snapshots = repository.target(snapshots = true)
+ checkInRepo(snapshots, artifact)
- if (repository.releases != repository.snapshots) {
- checkInRepo(repository.releases, artifact)
+ if (!repository.hasOneTarget()) {
+ checkInRepo(repository.target(snapshots = false), artifact)
}
}
@@ -70,31 +72,47 @@ open class CheckVersionIncrement : DefaultTask() {
val versions = metadata?.versioning?.versions
val versionExists = versions?.contains(version) ?: false
if (versionExists) {
- throw GradleException("""
- Version `$version` is already published to maven repository `$repoUrl`.
+ throw GradleException(
+ """
+ The version `$version` is already published to the Maven repository `$repoUrl`.
Try incrementing the library version.
- All available versions are: ${versions?.joinToString(separator = ", ")}.
-
- To disable this check, run Gradle with `-x $name`.
+ All available versions are: ${versions?.joinToString(separator = ", ")}.
+
+ To disable this check, run Gradle with `-x $name`.
""".trimIndent()
)
}
}
private fun fetch(repository: String, artifact: String): MavenMetadata? {
- val url = URL("$repository/$artifact")
+ val url = URI.create("$repository/$artifact").toURL()
return MavenMetadata.fetchAndParse(url)
}
private fun Project.artifactPath(): String {
val group = this.group as String
- val name = "spine-${this.name}"
+ 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
+ }
}
private data class MavenMetadata(var versioning: Versioning = Versioning()) {
@@ -119,7 +137,7 @@ private data class MavenMetadata(var versioning: Versioning = Versioning()) {
return try {
val metadata = mapper.readValue(url, MavenMetadata::class.java)
metadata
- } catch (e: FileNotFoundException) {
+ } catch (_: FileNotFoundException) {
null
}
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CloudArtifactRegistry.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CloudArtifactRegistry.kt
similarity index 88%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CloudArtifactRegistry.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/publish/CloudArtifactRegistry.kt
index 143ea25cc..67716d89c 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CloudArtifactRegistry.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CloudArtifactRegistry.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,12 +24,12 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.publish
+package io.spine.gradle.publish
import com.google.auth.oauth2.GoogleCredentials
import com.google.cloud.artifactregistry.auth.DefaultCredentialProvider
-import io.spine.internal.gradle.Credentials
-import io.spine.internal.gradle.Repository
+import io.spine.gradle.repo.Credentials
+import io.spine.gradle.repo.Repository
import java.io.IOException
import org.gradle.api.Project
@@ -51,13 +51,15 @@ import org.gradle.api.Project
* Ordering said hooks is a non-trivial operation and the result is usually quite fragile.
* Thus, we choose to do this small piece of configuration manually.
*/
+@Suppress("ConstPropertyName") // https://bit.ly/kotlin-prop-names
internal object CloudArtifactRegistry {
private const val spineRepoLocation = "https://europe-maven.pkg.dev/spine-event-engine"
val repository = Repository(
- releases = "${spineRepoLocation}/releases",
- snapshots = "${spineRepoLocation}/snapshots",
+ name = "CloudArtifactRegistry",
+ releases = "$spineRepoLocation/releases",
+ snapshots = "$spineRepoLocation/snapshots",
credentialValues = this::fetchGoogleCredentials
)
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CloudRepo.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CloudRepo.kt
similarity index 86%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CloudRepo.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/publish/CloudRepo.kt
index 48cd85855..624f5cb43 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CloudRepo.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CloudRepo.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,17 +24,18 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.publish
+package io.spine.gradle.publish
-import io.spine.internal.gradle.Repository
+import io.spine.gradle.repo.Repository
/**
* CloudRepo Maven repository.
*
* There is a special treatment for this repository. Usually, fetching and publishing of artifacts
* is performed via the same URL. But it is not true for CloudRepo. Fetching is performed via
- * public repository, and publishing via private one. Their URLs differ in `/public` infix.
+ * the public repository and publishing via the private one. Their URLs differ in `/public` infix.
*/
+@Deprecated(message = "Please use `PublishingRepos.cloudArtifactRegistry` instead.")
internal object CloudRepo {
private const val name = "CloudRepo"
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CustomPublicationHandler.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CustomPublicationHandler.kt
new file mode 100644
index 000000000..6791d21a6
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CustomPublicationHandler.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.publish
+
+import io.spine.gradle.repo.Repository
+import org.gradle.api.Project
+import org.gradle.api.publish.maven.MavenPublication
+
+/**
+ * A handler for custom publications, which are declared under the [publications]
+ * section of a module.
+ *
+ * Such publications should be treated differently than [StandardJavaPublicationHandler],
+ * which is created for a module. Instead, since the publications are already declared,
+ * this class only [assigns Maven coordinates][copyProjectAttributes].
+ *
+ * A module that declares custom publications must be specified in
+ * the [SpinePublishing.modulesWithCustomPublishing] property.
+ *
+ * If a module with [publications] declared locally is not specified as one with custom publishing,
+ * it may cause a name clash between an artifact produced by
+ * the [standard][org.gradle.api.publish.maven.MavenPublication] publication, and custom ones.
+ * To have both standard and custom publications, please specify custom artifact IDs or
+ * classifiers for each custom publication.
+ *
+ * @see StandardJavaPublicationHandler
+ */
+internal class CustomPublicationHandler private constructor(
+ project: Project,
+ destinations: Set
+) : PublicationHandler(project, destinations) {
+
+ override fun handlePublications() {
+ project.publications.forEach {
+ (it as MavenPublication).copyProjectAttributes()
+ }
+ }
+
+ companion object : HandlerFactory() {
+ override fun create(
+ project: Project,
+ destinations: Set,
+ vararg params: Any
+ ): CustomPublicationHandler = CustomPublicationHandler(project, destinations)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/GitHubPackages.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/GitHubPackages.kt
similarity index 62%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/GitHubPackages.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/publish/GitHubPackages.kt
index 3d5518d89..c25868fc1 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/GitHubPackages.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/GitHubPackages.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,10 +24,12 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.publish
+package io.spine.gradle.publish
-import io.spine.internal.gradle.Credentials
-import io.spine.internal.gradle.Repository
+import io.spine.gradle.repo.Credentials
+import io.spine.gradle.repo.Repository
+import io.spine.gradle.buildDirectory
+import net.lingala.zip4j.ZipFile
import org.gradle.api.Project
/**
@@ -40,12 +42,12 @@ internal object GitHubPackages {
*/
fun repository(repoName: String): Repository {
val githubActor: String = actor()
+ val url = "https://maven.pkg.github.com/SpineEventEngine/$repoName"
return Repository(
- name = "GitHub Packages",
- releases = "https://maven.pkg.github.com/SpineEventEngine/$repoName",
- snapshots = "https://maven.pkg.github.com/SpineEventEngine/$repoName",
- credentialValues = { project -> project.credentialsWithToken(githubActor) }
- )
+ name = "GitHub-Packages",
+ releases = url,
+ snapshots = url
+ ) { project -> project.credentialsWithToken(githubActor) }
}
private fun actor(): String {
@@ -73,22 +75,31 @@ private fun Project.credentialsWithToken(githubActor: String) = Credentials(
private fun Project.readGitHubToken(): String {
val githubToken: String? = System.getenv("GITHUB_TOKEN")
return if (githubToken.isNullOrEmpty()) {
- // Use the personal access token for the `developers@spine.io` account.
- // Only has the permission to read public GitHub packages.
- val targetDir = "${buildDir}/token"
- file(targetDir).mkdirs()
- val fileToUnzip = "${rootDir}/buildSrc/aus.weis"
-
- logger.info("GitHub Packages: reading token " +
- "by unzipping `$fileToUnzip` into `$targetDir`.")
- exec {
- // Unzip with password "123", allow overriding, quietly,
- // into the target dir, the given archive.
- commandLine("unzip", "-P", "123", "-oq", "-d", targetDir, fileToUnzip)
- }
- val file = file("$targetDir/token.txt")
- file.readText()
+ readTokenFromArchive()
} else {
githubToken
}
}
+
+/**
+ * Reads the personal access token for the `developers@spine.io` account.
+ * The token grants only read access to public GitHub packages.
+ *
+ * The token is extracted from the archive called `aus.weis` stored under `buildSrc`.
+ * The archive has such an unusual name to avoid scanning for tokens placed in repositories
+ * that is performed by GitHub. Since we do not violate any security, it is OK to
+ * use such a workaround.
+ */
+private fun Project.readTokenFromArchive(): String {
+ val targetDir = "$buildDirectory/token"
+ file(targetDir).mkdirs()
+ val fileToUnzip = "${rootDir}/buildSrc/aus.weis"
+
+ logger.info(
+ "GitHub Packages: reading token by unzipping `$fileToUnzip` into `$targetDir`."
+ )
+ ZipFile(fileToUnzip, "123".toCharArray()).extractAll(targetDir)
+ val file = file("$targetDir/token.txt")
+ val result = file.readText()
+ 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
new file mode 100644
index 000000000..24d6d0e2d
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+@file:Suppress("unused")
+
+package io.spine.gradle.publish
+
+import io.spine.gradle.SpineTaskGroup
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+
+/**
+ * Gradle plugin that adds a [CheckVersionIncrement] task.
+ *
+ * The task is called `checkVersionIncrement` inserted before the `check` task.
+ */
+class IncrementGuard : Plugin {
+
+ companion object {
+
+ const val taskName = "checkVersionIncrement"
+
+ /**
+ * Tells whether the version increment must be verified for the given
+ * GitHub Actions event and the base branch of the pull request.
+ *
+ * The version is guarded only for pull requests targeting a default or
+ * a release-line branch, i.e. a branch with the name ending with `master`
+ * or `main`. For example: `master`, `main`, `2.x-jdk8-master`, `2.x-jdk8-main`.
+ */
+ internal fun shouldCheckVersion(event: String?, baseBranch: String?): Boolean {
+ if (event != "pull_request" || baseBranch == null) {
+ return false
+ }
+ return baseBranch.endsWith("master") || baseBranch.endsWith("main")
+ }
+ }
+
+ /**
+ * Adds the [CheckVersionIncrement] task to the project.
+ *
+ * The task is created anyway, but it is enabled only if:
+ * 1. The project is built on GitHub CI, and
+ * 2. The job is a pull request targeting a default (`master` or `main`) or
+ * a release-line (e.g. `2.x-jdk8-master`) branch.
+ *
+ * It is the responsibility of a branch that aims to merge into a default
+ * (or otherwise protected) branch to bump the version. Auxiliary branches do not
+ * deal with the versions in the release cycle, so pull requests targeting them,
+ * direct pushes, and tag builds do not run the check. This also prevents unexpected
+ * CI fails when re-building `master` multiple times, creating git tags, and in other
+ * cases that go outside the "usual" development cycle.
+ */
+ override fun apply(target: Project) {
+ val tasks = target.tasks
+ tasks.register(taskName, CheckVersionIncrement::class.java) {
+ group = SpineTaskGroup.name
+ description = "Verifies that the project version was incremented before publishing"
+ repository = CloudArtifactRegistry.repository
+ tasks.getByName("check").dependsOn(this)
+
+ if (!shouldCheckVersion()) {
+ logger.info(
+ "The build does not represent a GitHub Actions pull request job " +
+ "targeting a default or a release-line branch, " +
+ "the `checkVersionIncrement` task is disabled."
+ )
+ this.enabled = false
+ }
+ }
+ }
+
+ /**
+ * Returns `true` if the current build is a GitHub Actions build of a pull request
+ * targeting a default (`master` or `main`) or a release-line branch,
+ * such as `2.x-jdk8-master`.
+ *
+ * Returns `false` for all other builds, including direct pushes, tag builds,
+ * and pull requests targeting auxiliary branches.
+ *
+ * @see
+ * List of default environment variables provided for GitHub Actions builds
+ */
+ private fun shouldCheckVersion(): Boolean {
+ val event = System.getenv("GITHUB_EVENT_NAME")
+ val baseBranch = System.getenv("GITHUB_BASE_REF")
+ return shouldCheckVersion(event, baseBranch)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/JarDsl.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/JarDsl.kt
new file mode 100644
index 000000000..37b8bfb01
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/JarDsl.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.publish
+
+/**
+ * A DSL element of [SpinePublishing] extension that allows enabling publishing
+ * of [testJar] artifact.
+ *
+ * This artifact contains compilation output of `test` source set. By default, it is not published.
+ *
+ * Take a look on [SpinePublishing.testJar] for a usage example.
+
+ * @see [artifacts]
+ */
+class TestJar {
+
+ /**
+ * Set of modules, for which a test JAR will be published.
+ */
+ var inclusions: Set = emptySet()
+
+ /**
+ * Enables test JAR publishing for all published modules.
+ */
+ var enabled = false
+}
+
+/**
+ * Flags for turning optional JAR artifacts in a project.
+ *
+ * @property sourcesJar Tells whether [sourcesJar] artifact should be published.
+ * Default value is `true`.
+ * @property publishTestJar Tells whether [testJar] artifact should be published.
+ */
+internal data class JarFlags(
+ val sourcesJar: Boolean = true,
+ val publishTestJar: Boolean,
+) {
+ internal companion object {
+ /**
+ * Creates an instance of [JarFlags] for the project with the given name,
+ * taking the setup parameters from JAR DSL elements.
+ */
+ fun create(projectName: String, testJar: TestJar): JarFlags {
+ val addTestJar = testJar.inclusions.contains(projectName) || testJar.enabled
+ return JarFlags(sourcesJar = true, addTestJar)
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/ProtoExts.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/ProtoExts.kt
new file mode 100644
index 000000000..a32e9698d
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/ProtoExts.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.publish
+
+import io.spine.gradle.sourceSets
+import java.io.File
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.file.FileTreeElement
+import org.gradle.api.file.SourceDirectorySet
+import org.gradle.api.tasks.bundling.Jar
+
+/**
+ * Tells whether there are any Proto sources in the "main" source set.
+ */
+fun Project.hasProto(): Boolean {
+ val protoSources = protoSources()
+ val result = protoSources.any {
+ it.exists()
+ && it.isDirectory
+ && it.listFiles()?.isNotEmpty() ?: false
+ }
+ return result
+}
+
+/**
+ * Locates directories with proto sources under the "main" source sets.
+ *
+ * Special treatment for Proto sources is needed, because they are not Java-related, and,
+ * thus, not included in `sourceSets["main"].allSource`.
+ */
+internal fun Project.protoSources(): Set {
+ val mainSourceSets = sourceSets.filter {
+ ss -> ss.name.endsWith("main", ignoreCase = true)
+ }
+
+ val protoExtensions = mainSourceSets.mapNotNull {
+ it.extensions.findByName("proto") as SourceDirectorySet?
+ }
+
+ val protoDirs = mutableSetOf()
+ protoExtensions.forEach {
+ protoDirs.addAll(it.srcDirs)
+ }
+
+ return protoDirs
+}
+
+/**
+ * Checks if the given file belongs to the Google `.proto` sources.
+ */
+internal fun FileTreeElement.isGoogleProtoSource(): Boolean {
+ val pathSegments = relativePath.segments
+ return pathSegments.isNotEmpty() && pathSegments[0].equals("google")
+}
+
+/**
+ * The reference to the `generateProto` task of a `main` source set.
+ */
+internal fun Project.generateProto(): Task? = tasks.findByName("generateProto")
+
+/**
+ * Makes this [Jar] task depend on the [generateProto] task, if it exists in the same project.
+ */
+internal fun Jar.dependOnGenerateProto() {
+ project.generateProto()?.let {
+ this.dependsOn(it)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt
new file mode 100644
index 000000000..c9fbdd651
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.publish
+
+import DocumentationSettings
+import LicenseSettings
+import io.spine.gradle.artifactId
+import io.spine.gradle.isSnapshot
+import io.spine.gradle.repo.Repository
+import io.spine.gradle.report.pom.InceptionYear
+import org.gradle.api.Project
+import org.gradle.api.artifacts.dsl.RepositoryHandler
+import org.gradle.api.invocation.BuildInvocationDetails
+import org.gradle.api.publish.maven.MavenPublication
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.support.serviceOf
+
+/**
+ * The name of the Maven Publishing Gradle plugin.
+ */
+private const val MAVEN_PUBLISH = "maven-publish"
+
+/**
+ * Abstract base for handlers of publications in a project
+ * with [spinePublishing] settings declared.
+ *
+ * @param project The project to which the handler is applied.
+ * @param destinations The repositories for publishing artifacts of this project.
+ * In a multi-module project the destinations can be re-defined by
+ * specifying custom values in
+ * the [`spinePublishing`][io.spine.gradle.publish.SpinePublishing.destinations]
+ * extension applied to the subproject.
+ */
+sealed class PublicationHandler(
+ protected val project: Project,
+ protected var destinations: Set
+) {
+ /**
+ * Remembers if the [apply] function was called by this handler.
+ */
+ private var applied: Boolean = false
+
+ /**
+ * Overwrites the [destinations] property with the given set.
+ */
+ fun publishTo(alternativeDestinations: Set) {
+ if (alternativeDestinations.isEmpty()) {
+ project.logger.info(
+ "The project ${project.path} is not going to be published because" +
+ " the publication handler `${this@PublicationHandler}`" +
+ " got an empty set of new `destinations`."
+ )
+ }
+ destinations = alternativeDestinations
+ }
+
+ /**
+ * Configures the publication of the associated [project].
+ */
+ fun apply() {
+ synchronized(project) {
+ if (applied) {
+ return
+ }
+ project.run {
+ // We apply the `maven-publish` plugin for modules with standard
+ // publishing automatically because they don't need custom DSL
+ // in their `build.gradle.kts` files.
+ // All the job is done by the `SpinePublishing` extension and
+ // `StandardPublicationHandler` instance associated with this project.
+ if (!hasCustomPublishing) {
+ apply(plugin = MAVEN_PUBLISH)
+ }
+ // And we do not apply the plugin for modules with custom publishing
+ // because they will need the `maven-publish` DSL to tune the publishing.
+ // Therefore, we only arrange the execution of our code when the plugin
+ // is applied.
+ pluginManager.withPlugin(MAVEN_PUBLISH) {
+ handlePublications()
+ registerDestinations()
+ configurePublishTask(destinations)
+ applied = true
+ }
+ }
+ }
+ }
+
+ /**
+ * Either handles publications already declared in the associated [project]
+ * or creates new ones.
+ */
+ abstract fun handlePublications()
+
+ /**
+ * Goes through the [destinations] and registers each as a repository for publishing
+ * in the given Gradle project.
+ */
+ private fun registerDestinations() {
+ val repositories = project.publishingExtension.repositories
+ destinations.forEach { destination ->
+ repositories.register(project, destination)
+ }
+ }
+
+ /**
+ * Copies the attributes of the [project] to this [MavenPublication].
+ *
+ * The following project attributes are copied:
+ * * [group][Project.getGroup];
+ * * [version][Project.getVersion];
+ * * [description][Project.getDescription].
+ *
+ * The [artifactId] is derived from the project
+ * [extension property][io.spine.gradle.artifactId] of the same name, combined with
+ * the platform-specific suffix already present in the publication's artifact ID.
+ * This preserves Kotlin Multiplatform suffixes such as `-jvm`.
+ *
+ * For example, if the project artifact ID is `spine-logging` and the publication's
+ * current artifact ID is `logging-jvm` (set by the KMP plugin), the resulting
+ * artifact ID will be `spine-logging-jvm`.
+ *
+ * The Apache Software License 2.0 is set as the only license
+ * under which the published artifact is distributed via [LicenseSettings]
+ *
+ * The source control management attributes are obtained from [DocumentationSettings].
+ *
+ * @see LicenseSettings
+ * @see DocumentationSettings
+ */
+ protected fun MavenPublication.copyProjectAttributes() {
+ groupId = project.group.toString()
+ // Add the proper prefix to the `artifactId`.
+ // The default `artifactId` is either `project.name` or
+ // the `project.name` with the platform suffix of a KMM distribution.
+ artifactId = if (artifactId.startsWith(project.name)) {
+ val platformSuffix = artifactId.removePrefix(project.name)
+ val replacedId = project.artifactId + platformSuffix
+ project.logger.info(
+ "The project `${project.name}` got modified artifact: `$replacedId`."
+ )
+ replacedId
+ } else {
+ project.logger.info(
+ "The `artifactId` for the project `${project.name}` stays: `$artifactId`."
+ )
+ // This is an unlikely case of `artifactId` being set to something unrelated
+ // to the project name. Let's keep it as is.
+ artifactId
+ }
+ version = project.version.toString()
+ pom.description.set(project.description)
+ pom.inceptionYear.set(InceptionYear.value)
+ pom.licenses {
+ license {
+ name.set(LicenseSettings.name)
+ url.set(LicenseSettings.url)
+ // It's either `"repo"` or `"manual"`.
+ // https://maven.apache.org/ref/3.9.15/maven-model/apidocs/org/apache/maven/model/License.html#setDistribution(java.lang.String)
+ distribution.set("repo")
+ }
+ }
+ pom.scm {
+ DocumentationSettings.run {
+ url.set(repoUrl(project))
+ connection.set(connectionUrl(project))
+ developerConnection.set(developerConnectionUrl(project))
+ }
+ }
+ }
+
+ /**
+ * The abstract base for factories producing instances of classes
+ * derived from [io.spine.gradle.publish.PublicationHandler].
+ *
+ * The factory maintains associations between a path of the project to
+ * its publication handler.
+ *
+ * If the handler already exists, its settings are updated when
+ * the [serving] factory method is called.
+ *
+ * Otherwise, a new handler is created and associated with the project.
+ *
+ * @param H The type of the publication handlers produced by this repository.
+ * @see serving
+ */
+ abstract class HandlerFactory {
+
+ /**
+ * Maps a project path suffixed with build start time to the associated publication handler.
+ *
+ * The suffix after the project path is needed to create a new handler
+ * for each build. We do not use Guava or other cache expecting the small amount
+ * of memory consumption of each publication handler.
+ */
+ private val handlers = mutableMapOf()
+
+ /**
+ * Computes the key for a publication handler taking the [project] and
+ * its build start time.
+ */
+ private fun createKey(project: Project): String {
+ val buildService = project.gradle.serviceOf()
+ val buildStartedMillis = buildService.buildStartedTime
+ val localTime = java.time.Instant.ofEpochMilli(buildStartedMillis)
+ val key = "${project.path}-at-$localTime"
+ return key
+ }
+
+ /**
+ * Obtains an instance of [PublicationHandler] for the given project.
+ *
+ * If the handler for the given [project] was already created, the handler
+ * gets new [destinations], [overwriting][publishTo] previously specified.
+ *
+ * @return the handler for the given project that would handle publishing to
+ * the specified [destinations].
+ */
+ fun serving(project: Project, destinations: Set, vararg params: Any): H {
+ synchronized(handlers) {
+ val key = createKey(project)
+ var handler = handlers[key]
+ if (handler == null) {
+ handler = create(project, destinations, *params)
+ handlers[key] = handler
+ } else {
+ handler.publishTo(destinations)
+ }
+ return handler
+ }
+ }
+
+ /**
+ * Creates a new publication handler for the given project.
+ *
+ * @param project The project to which the handler applies.
+ * @param destinations The repositories for publishing artifacts of this project.
+ * @param params Optional parameters to be passed as constructor parameters for
+ * classes of the type [H].
+ */
+ protected abstract fun create(
+ project: Project,
+ destinations: Set,
+ vararg params: Any
+ ): H
+ }
+}
+
+/**
+ * Adds a Maven repository to the project specifying credentials, if they are
+ * [available][Repository.credentials] from the root project.
+ */
+private fun RepositoryHandler.register(project: Project, repository: Repository) {
+ val isSnapshot = project.version.toString().isSnapshot()
+ val credentials = repository.credentials(project.rootProject)
+ maven {
+ name = repository.name(isSnapshot)
+ url = project.uri(repository.target(isSnapshot))
+ credentials {
+ username = credentials?.username
+ password = credentials?.password
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt
new file mode 100644
index 000000000..974542c3f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt
@@ -0,0 +1,364 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.publish
+
+import htmlDocsJar
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.isSnapshot
+import io.spine.gradle.repo.Repository
+import io.spine.gradle.sourceSets
+import java.util.*
+import org.gradle.api.InvalidUserDataException
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.file.DuplicatesStrategy
+import org.gradle.api.publish.PublicationContainer
+import org.gradle.api.publish.PublishingExtension
+import org.gradle.api.publish.maven.MavenPublication
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.api.tasks.bundling.Jar
+import org.gradle.kotlin.dsl.findByType
+import org.gradle.kotlin.dsl.get
+import org.gradle.kotlin.dsl.getByType
+import org.gradle.kotlin.dsl.named
+import org.gradle.kotlin.dsl.register
+import org.gradle.kotlin.dsl.withType
+
+/**
+ * Obtains [PublishingExtension] of this project.
+ */
+internal val Project.publishingExtension: PublishingExtension
+ get() = extensions.getByType()
+
+/**
+ * Obtains [PublicationContainer] of this project.
+ */
+internal val Project.publications: PublicationContainer
+ get() = publishingExtension.publications
+
+/**
+ * Obtains an instance, if available, of [SpinePublishing] extension
+ * applied to this project.
+ */
+internal val Project.localSpinePublishing: SpinePublishing?
+ get() = extensions.findByType()
+
+/**
+ * Obtains [SpinePublishing] extension from this [Project].
+ *
+ * If this [Project] doesn't have one, it returns [SpinePublishing]
+ * declared in the root project.
+ */
+internal val Project.spinePublishing: SpinePublishing
+ get() {
+ val local = localSpinePublishing
+ if (local != null) {
+ return local
+ }
+ val fromRoot = this.rootProject.extensions.findByType()
+ if (fromRoot != null) {
+ return fromRoot
+ }
+ error("`SpinePublishing` is not found in `${project.name}`.")
+ }
+
+/**
+ * Tells if this project has custom publishing.
+ *
+ * For a multi-module project this is checked by presence of this project
+ * in the list of [SpinePublishing.modulesWithCustomPublishing] of the root project.
+ *
+ * In a single-module project, the value of the [SpinePublishing.customPublishing]
+ * property is returned.
+ */
+internal val Project.hasCustomPublishing: Boolean
+ get() = rootProject.spinePublishing.modulesWithCustomPublishing.contains(name)
+ || spinePublishing.customPublishing
+
+private const val PUBLISH_TASK = "publish"
+
+/**
+ * Locates `publish` task in this [TaskContainer].
+ *
+ * This task publishes all defined publications to all defined repositories. To achieve that,
+ * the task depends on all `publish`*PubName*`PublicationTo`*RepoName*`Repository` tasks.
+ *
+ * Please note, task execution would not copy publications to the local Maven cache.
+ *
+ * @see
+ * Tasks | The Maven Publish Plugin
+ */
+internal val TaskContainer.publish: TaskProvider
+ get() = named(PUBLISH_TASK)
+
+/**
+ * Sets dependencies for `publish` task in this [Project].
+ *
+ * This method performs the following:
+ *
+ * 1. When this [Project] is not a root, makes `publish` task in a root project
+ * depend on a local `publish`.
+ * 2. Makes local `publish` task verify that credentials are present for each
+ * of destination repositories.
+ */
+internal fun Project.configurePublishTask(destinations: Set) {
+ attachCredentialsVerification(destinations)
+ bindToRootPublish()
+}
+
+private fun Project.attachCredentialsVerification(destinations: Set) {
+ val checkCredentials = tasks.registerCheckCredentialsTask(destinations)
+ val localPublish = tasks.publish
+ localPublish.configure { dependsOn(checkCredentials) }
+}
+
+private fun Project.bindToRootPublish() {
+ if (project == rootProject) {
+ return
+ }
+
+ val localPublish = tasks.publish
+ val rootPublish = rootProject.tasks.getOrCreatePublishTask()
+ rootPublish.configure { dependsOn(localPublish) }
+}
+
+/**
+ * Use this task accessor when it is not guaranteed that the task is present
+ * in this [TaskContainer].
+ */
+private fun TaskContainer.getOrCreatePublishTask(): TaskProvider =
+ if (names.contains(PUBLISH_TASK)) {
+ named(PUBLISH_TASK)
+ } else {
+ register(PUBLISH_TASK) {
+ group = SpineTaskGroup.name
+ description = "Aggregates `publish` tasks of all subprojects"
+ }
+ }
+
+@Suppress(
+ /* Several types of exceptions may be thrown,
+ and Kotlin does not have a multi-catch support yet. */
+ "TooGenericExceptionCaught"
+)
+private fun TaskContainer.registerCheckCredentialsTask(
+ destinations: Set,
+): TaskProvider {
+ val checkCredentials = "checkCredentials"
+ val taskDescription = "Checks credentials for the configured publishing destinations"
+ try {
+ // The result of this call is ignored intentionally.
+ //
+ // We expect this line to fail with the exception
+ // in case the task with this name is NOT registered.
+ //
+ // Otherwise, we need to replace the existing task
+ // to avoid checking the credentials
+ // for some previously asked `destinations`.
+ named(checkCredentials)
+ val toConfigure = replace(checkCredentials)
+ toConfigure.group = SpineTaskGroup.name
+ toConfigure.description = taskDescription
+ toConfigure.doLastCredentialsCheck(destinations)
+ return named(checkCredentials)
+ } catch (_: Exception) {
+ return register(checkCredentials) {
+ group = SpineTaskGroup.name
+ description = taskDescription
+ doLastCredentialsCheck(destinations)
+ }
+ }
+}
+
+private fun Task.doLastCredentialsCheck(destinations: Set) {
+ doLast {
+ if (logger.isDebugEnabled) {
+ val isSnapshot = project.version.toString().isSnapshot()
+ val destinationsStr = destinations.joinToString(", ") { it.target(isSnapshot) }
+ logger.debug(
+ "Project '${project.name}': checking the credentials for repos: $destinationsStr."
+ )
+ }
+ destinations.forEach { it.ensureCredentials(project) }
+ }
+}
+
+private fun Repository.ensureCredentials(project: Project) {
+ val credentials = credentials(project)
+ if (Objects.isNull(credentials)) {
+ throw InvalidUserDataException(
+ "No valid credentials for repository `${this}`. Please make sure " +
+ "to pass username/password or a valid `.properties` file."
+ )
+ }
+}
+
+/**
+ * Excludes Google `.proto` sources from all artifacts.
+ *
+ * Goes through all registered `Jar` tasks and filters out Google's files.
+ */
+@Suppress("unused")
+fun TaskContainer.excludeGoogleProtoFromArtifacts() {
+ withType().configureEach {
+ exclude { it.isGoogleProtoSource() }
+ }
+}
+
+/**
+ * Locates or creates `sourcesJar` task in this [Project].
+ *
+ * The output of this task is a `jar` archive. The archive contains sources from `main` source set.
+ * The task makes sure that sources from the directories below will be included
+ * in the resulting archive:
+ *
+ * - Kotlin
+ * - Java
+ * - Proto
+ *
+ * Java and Kotlin sources are default to `main` source set since it is created by `java` plugin.
+ * For Proto sources to be included – [special treatment][protoSources] is needed.
+ */
+fun Project.sourcesJar(): TaskProvider = tasks.getOrCreate("sourcesJar") {
+ group = SpineTaskGroup.name
+ description = "Assembles a JAR with Java, Kotlin, and Proto sources from the `main` source set"
+ dependOnGenerateProto()
+ archiveClassifier.set("sources")
+ // `allSource` also sees the generated `proto-resources` directory, which bundles
+ // copies of `.proto` files (this module's own and its dependencies') as runtime resources.
+ // This behavior starts from Protobuf Gradle Plugin 0.10.0, and it is not expected
+ // to be changed in the future.
+ // This is why we do not call `from(protoSources())` in this function any more.
+ from(sourceSets["main"].allSource)
+
+ // Even if there are duplicates in sources, we want only one.
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+
+ exclude("desc.ref", "*.desc") // Exclude descriptor files and the descriptor reference.
+}
+
+/**
+ * Locates or creates `protoJar` task in this [Project].
+ *
+ * The output of this task is a `jar` archive. The archive contains only
+ * [Proto sources][protoSources] from `main` source set.
+ */
+fun Project.protoJar(): TaskProvider = tasks.getOrCreate("protoJar") {
+ group = SpineTaskGroup.name
+ description = "Assembles a JAR with Proto sources from the `main` source set"
+ dependOnGenerateProto()
+ archiveClassifier.set("proto")
+ from(protoSources())
+}
+
+/**
+ * Locates or creates `testJar` task in this [Project].
+ *
+ * The output of this task is a `jar` archive. The archive contains compilation output
+ * of `test` source set.
+ */
+internal fun Project.testJar(): TaskProvider = tasks.getOrCreate("testJar") {
+ group = SpineTaskGroup.name
+ description = "Assembles a JAR with compiled output of the `test` source set"
+ archiveClassifier.set("test")
+ from(sourceSets["test"].output)
+}
+
+/**
+ * Locates or creates `javadocJar` task in this [Project].
+ *
+ * The output of this task is a `jar` archive. The archive contains Javadoc,
+ * generated upon Java sources from `main` source set. If Javadoc for Kotlin is also needed,
+ * apply the Dokka plugin. It tunes `javadoc` task to generate docs upon Kotlin sources as well.
+ */
+fun Project.javadocJar(): TaskProvider = tasks.getOrCreate("javadocJar") {
+ group = SpineTaskGroup.name
+ description = "Assembles a JAR with generated Javadoc"
+ archiveClassifier.set("javadoc")
+ val javadocFiles = layout.buildDirectory.dir("dokka/javadoc")
+ from(javadocFiles)
+ dependsOn("dokkaGeneratePublicationJavadoc")
+}
+
+internal fun TaskContainer.getOrCreate(name: String, init: Jar.() -> Unit): TaskProvider =
+ if (names.contains(name)) {
+ named(name)
+ } else {
+ register(name) {
+ init()
+ }
+ }
+
+/**
+ * Obtains as a set of [Jar] tasks, output of which is used as Maven artifacts.
+ *
+ * By default, only a jar with Java compilation output is included into publication. This method
+ * registers tasks that produce additional artifacts according to the values of [jarFlags].
+ *
+ * @return the list of the registered tasks.
+ */
+internal fun Project.artifacts(jarFlags: JarFlags): Set> {
+ val tasks = mutableSetOf>()
+
+ if (jarFlags.sourcesJar) {
+ tasks.add(sourcesJar())
+ }
+
+ tasks.add(javadocJar())
+ tasks.add(htmlDocsJar())
+
+ // We don't want to have an empty "proto.jar" when a project doesn't have any Proto files.
+ if (hasProto()) {
+ tasks.add(protoJar())
+ }
+
+ // Here, we don't have the corresponding `hasTests()` check, since this artifact is disabled
+ // by default. And turning it on means "We have tests and need them to be published."
+ if (jarFlags.publishTestJar) {
+ tasks.add(testJar())
+ }
+
+ return tasks
+}
+
+/**
+ * Adds the source code and documentation JARs to the publication.
+ */
+@Suppress("unused")
+fun MavenPublication.addSourceAndDocJars(project: Project) {
+ val tasks = mutableSetOf>()
+ tasks.add(project.sourcesJar())
+ tasks.add(project.javadocJar())
+ tasks.add(project.htmlDocsJar())
+ if (project.hasProto()) {
+ tasks.add(project.protoJar())
+ }
+ tasks.forEach {
+ artifact(it)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/TaskName.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingRepos.kt
similarity index 72%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/TaskName.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingRepos.kt
index 2823416da..eea6dc190 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/TaskName.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingRepos.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,17 +24,19 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.github.pages
+package io.spine.gradle.publish
-object TaskName {
+import io.spine.gradle.repo.Repository
- /**
- * The name of the task which updates the GitHub Pages.
- */
- const val updateGitHubPages = "updateGitHubPages"
+/**
+ * Repositories to which we may publish.
+ */
+object PublishingRepos {
+
+ val cloudArtifactRegistry = CloudArtifactRegistry.repository
/**
- * The name of the helper task to gather the generated Javadoc before updating GitHub Pages.
+ * Obtains a GitHub repository by the given name.
*/
- const val copyJavadoc = "copyJavadoc"
+ fun gitHub(repoName: String): Repository = GitHubPackages.repository(repoName)
}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/ShadowJarExts.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/ShadowJarExts.kt
new file mode 100644
index 000000000..0fa60fdc0
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/ShadowJarExts.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+@file:Suppress("ConstPropertyName")
+
+package io.spine.gradle.publish
+
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import org.gradle.api.file.DuplicatesStrategy
+
+/**
+ * Configures a `ShadowJar` task for Spine SDK publishing.
+ */
+@Suppress("unused")
+fun ShadowJar.setup() {
+ duplicatesStrategy = DuplicatesStrategy.INCLUDE // Required for service-file merging.
+ mergeServiceFiles()
+ append(DESC_REF)
+ deduplicateEntries()
+}
+
+/**
+ * The name of a descriptor set reference file.
+ */
+private const val DESC_REF = "desc.ref"
+
+/**
+ * Installs a first-copy-wins exclusion predicate for all JAR entries except those
+ * registered for merging, such as service files, descriptor set references, etc.
+ *
+ * Shadow's [org.gradle.api.file.DuplicatesStrategy.INCLUDE] must remain on the task so
+ * that every copy of a merged file reaches its
+ * [AppendingTransformer][com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer].
+ * All other entries — `.class` files, settings JSONs, Kotlin module descriptors,
+ * Maven metadata, etc. — are deduplicated here to suppress duplicate-entry warnings
+ * and keep the fat JAR size minimal.
+ */
+private fun ShadowJar.deduplicateEntries() {
+ val seenPaths = mutableSetOf()
+ doFirst { seenPaths.clear() }
+ eachFile {
+ val needsMerging = path.isServiceFile || path.isDescriptorSetReference
+ if (!needsMerging && !seenPaths.add(path)) {
+ exclude()
+ }
+ }
+}
+
+/**
+ * Returns `true` for file paths containing references to descriptor set files.
+ */
+private val String.isDescriptorSetReference: Boolean
+ get() = contains(DESC_REF)
+
+/**
+ * Tells if the path belongs to a service file.
+ */
+private val String.isServiceFile: Boolean
+ get() = contains("META-INF/services")
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/SpinePublishing.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/SpinePublishing.kt
new file mode 100644
index 000000000..bb412ed63
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/SpinePublishing.kt
@@ -0,0 +1,492 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+@file:Suppress("TooManyFunctions")
+
+package io.spine.gradle.publish
+
+import io.spine.dependency.local.Spine
+import io.spine.gradle.repo.Repository
+import java.util.Locale
+import org.gradle.api.Project
+import org.gradle.api.publish.maven.plugins.MavenPublishPlugin
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.create
+import org.gradle.kotlin.dsl.findByType
+
+/**
+ * Configures [SpinePublishing] extension.
+ *
+ * This extension sets up publishing of artifacts to Maven repositories.
+ *
+ * The extension can be configured for single- and multi-module projects.
+ *
+ * ## Using in a multi-module project
+ *
+ * When used with a multi-module project, the extension should be opened in a root project's
+ * build file. The published modules are specified explicitly by their names:
+ *
+ * ```kotlin
+ * spinePublishing {
+ * modules = setOf(
+ * "subprojectA",
+ * "subprojectB",
+ * )
+ * destinations = PublishingRepos.run { setOf(
+ * cloudArtifactRegistry,
+ * gitHub("") // The name of the GitHub repository of the project.
+ * )}
+ * }
+ * ```
+ *
+ * ### Filtering out test-only modules
+ *
+ * Sometimes a functional or an integration test requires a significant amount of
+ * configuration code that is better understood when isolated into a separate module.
+ * Conventionally, we use the `-tests` suffix for naming such modules.
+ *
+ * In order to avoid publishing of such a test-only module, we use the following extensions
+ * for the Gradle [Project] class: [productionModules], [productionModuleNames].
+ * So the above code for specifying the modules to publish could be rewritten as follows:
+ *
+ * ```kotlin
+ * spinePublishing {
+ * modules = productionModuleNames.toSet()
+ * }
+ * ```
+ * This code works for most of the projects.
+ *
+ * ### Arranging custom publishing for a module
+ * ```kotlin
+ *
+ * 1. Modify the list of standardly published modules in the root project like this:
+ *
+ * ```kotlin
+ * spinePublishing {
+ * modules = productionModuleNames
+ * .minus("my-custom-module")
+ * .toSet()
+ *
+ * modulesWithCustomPublishing = setOf(
+ * "my-custom-module"
+ * )
+ *
+ * // ...
+ * }
+ * ```
+ * 2. Arrange the custom publishing in the `my-custom-module` project.
+ *
+ * ## Using in a single-module project
+ *
+ * When used with a single-module project, the extension should be opened in a project's build file.
+ * Only destinations should be specified:
+ *
+ * ```kotlin
+ * spinePublishing {
+ * destinations = PublishingRepos.run { setOf(
+ * cloudArtifactRegistry,
+ * gitHub("")
+ * )}
+ * }
+ * ```
+ *
+ * ## Publishing modules
+ *
+ * It is worth mentioning that publishing of a module can be configured only from a single place.
+ * For example, declaring `subprojectA` as published in a root project and opening
+ * `spinePublishing` extension within `subprojectA` itself would lead to an exception.
+ *
+ * In Gradle, in order to publish something somewhere, one should create a publication. In each
+ * of published modules, the extension will create a [publication][StandardJavaPublicationHandler]
+ * named "mavenJava". All artifacts published by this extension belong to this publication.
+ *
+ * ## Published artifacts
+ *
+ * By default, along with the compilation output of the `main` source set, the extension publishes
+ * the following artifacts:
+ *
+ * 1. [sourcesJar] — sources from the `main` source set. Includes handcrafted and generated
+ * code in Java, Kotlin, and `.proto` files.
+ *
+ * 2. [protoJar] – only `.proto` sources from the `main` source set. It is published only if
+ * Proto files are actually present in the source set.
+ *
+ * 3. [javadocJar] — Javadoc, generated upon Java sources from the `main` source set.
+ * If Javadoc for Kotlin is also needed, apply the Dokka plugin.
+ * It tunes the `javadoc` task to generate docs upon Kotlin sources as well.
+ *
+ * 4. [htmlDocsJar] — documentation generated by Dokka for Kotlin and Java sources
+ * using the Kotlin API mode.
+ *
+ * Additionally, [testJar] artifact can be published. This artifact contains compilation output
+ * of the `test` source set. Use [SpinePublishing.testJar] to enable its publishing.
+ *
+ * @see [artifacts]
+ * @see SpinePublishing
+ */
+fun Project.spinePublishing(block: SpinePublishing.() -> Unit): SpinePublishing {
+ apply()
+ val extension = with(extensions) {
+ findByType() ?: create(SpinePublishing.extensionName, project)
+ }
+ extension.run {
+ block()
+ configured()
+ }
+ return extension
+}
+
+/**
+ * A Gradle extension for setting up publishing of modules of Spine SDK modules
+ * using `maven-publish` plugin.
+ *
+ * ### Implementation Note
+ *
+ * This extension is overloaded with responsibilities.
+ * It basically does what an extension AND a Gradle plugin would normally do.
+ *
+ * We [should introduce a plugin class](https://github.com/SpineEventEngine/config/issues/562)
+ * and move the code related to creating tasks or setting dependencies between them into the plugin.
+ *
+ * @param project The project in which the extension is opened. By default, this project will be
+ * published as long as a [set][modules] of modules to publish is not specified explicitly.
+ *
+ * @see spinePublishing
+ */
+open class SpinePublishing(private val project: Project) {
+
+ companion object {
+
+ /**
+ * The default prefix added before a module name when publishing artifacts.
+ */
+ const val DEFAULT_PREFIX = "spine-"
+
+ /**
+ * The reserved value that means that no prefix should be added
+ * to a tool module's artifact ID.
+ */
+ const val NONE_PREFIX = "NONE"
+
+ /**
+ * The name of the extension registered in a Gradle project.
+ */
+ public val extensionName: String = SpinePublishing::class.java.simpleName
+ .replaceFirstChar { it.lowercase(Locale.ROOT) }
+ }
+
+ private val testJar = TestJar()
+
+ /**
+ * Set of modules to be published.
+ *
+ * Both the module's name or path can be used.
+ *
+ * Use this property if the extension is configured from a root project's build file.
+ *
+ * If left empty, the [project], in which the extension is opened, will be published.
+ *
+ * Empty by default.
+ */
+ var modules: Set = emptySet()
+
+ /**
+ * Controls whether the [module][project] needs standard publications.
+ *
+ * Default value is `false`.
+ *
+ * In a single module [project], settings this property to `true` it tells
+ * that the project configures the publication in a specific way and
+ * [CustomPublicationHandler] should be used.
+ * Otherwise, the extension will configure the
+ * [standard publication][StandardJavaPublicationHandler].
+ *
+ * This property is an analogue of [modulesWithCustomPublishing] in
+ * [multi-module][Project.getSubprojects] projects,
+ * for which [spinePublishing] is configured individually.
+ *
+ * Setting of this property to `true` and having a non-empty [modules] property
+ * in the project to which the extension is applied will lead to [IllegalStateException].
+ *
+ * Settings this property to `true` in a subproject serves only the documentation purposes.
+ * This subproject still must be listed in the [modulesWithCustomPublishing] property in
+ * the extension of the [rootProject][Project.getRootProject], so that its publication
+ * can be configured in a specific way.
+ */
+ var customPublishing = false
+
+ /**
+ * Set of modules that have custom publications and do not need standard ones.
+ *
+ * Empty by default.
+ */
+ var modulesWithCustomPublishing: Set = emptySet()
+
+ /**
+ * Set of repositories, to which the resulting artifacts will be sent.
+ *
+ * Usually, Spine-related projects are published to one or more repositories,
+ * declared in [PublishingRepos]:
+ *
+ * ```kotlin
+ * destinations = PublishingRepos.run { setOf(
+ * cloudArtifactRegistry,
+ * gitHub("") // The name of the GitHub repository of the project.
+ * )}
+ * ```
+ *
+ * If the property is not initialized, the destinations will be taken from
+ * the parent project.
+ */
+ lateinit var destinations: Set
+
+ /**
+ * A prefix to be added before the name of each artifact, if it does not belong
+ * to the Maven group `"io.spine.tools"`.
+ *
+ * @see toolArtifactPrefix
+ */
+ var artifactPrefix: String = DEFAULT_PREFIX
+
+ /**
+ * A prefix to be added before a module name if it belongs to
+ * the Maven group `"io.spine.tools"`.
+ *
+ * Use `"NONE"` if you need no prefix before tool module names.
+ *
+ * @see artifactPrefix
+ */
+ var toolArtifactPrefix: String = ""
+
+ /**
+ * Allows enabling publishing of [testJar] artifact, containing compilation output
+ * of "test" source set.
+ *
+ * Here's an example of how to enable it for some of the published modules:
+ *
+ * ```
+ * spinePublishing {
+ * modules = setOf(
+ * "subprojectA",
+ * "subprojectB",
+ * )
+ * testJar {
+ * inclusions = setOf(
+ * "subprojectB",
+ * )
+ * }
+ * }
+ * ```
+ *
+ * For all modules, or when the extension is configured within a published module itself:
+ *
+ * ```
+ * spinePublishing {
+ * testJar {
+ * enabled = true
+ * }
+ * }
+ * ```
+ *
+ * The resulting artifact is available under the "test" classifier.
+ * For example, in Gradle 7+, one could depend on it like this:
+ *
+ * ```
+ * implementation("io.spine:spine-client:$version@test")
+ * ```
+ */
+ fun testJar(block: TestJar.() -> Unit) = testJar.run(block)
+
+ /**
+ * Called to notify the extension that its configuration is completed.
+ *
+ * On this stage the extension will validate the received configuration and set up
+ * `maven-publish` plugin for each published module.
+ */
+ internal fun configured() {
+ ensureTestJarInclusionsArePublished()
+ ensureModulesNotDuplicated()
+ ensureCustomPublishingNotMisused()
+
+ val projectsToPublish = projectsToPublish()
+ projectsToPublish.forEach { project ->
+ val jarFlags = JarFlags.create(project.name, testJar)
+ project.setUpPublishing(jarFlags)
+ }
+ }
+
+ /**
+ * Maps the names of published modules to [Project] instances.
+ *
+ * The method considers two options:
+ *
+ * 1. The [set][modules] of subprojects to publish is not empty. It means that the extension
+ * is opened from a root project.
+ * 2. The [set][modules] is empty. Then, the [project] in which the extension is opened
+ * will be published.
+ *
+ * @see modules
+ */
+ fun projectsToPublish(): Set {
+ if (project.subprojects.isEmpty()) {
+ return setOf(project)
+ }
+ return modules.union(modulesWithCustomPublishing)
+ .map { name -> project.project(name) }
+ .ifEmpty { setOf(project) }
+ .toSet()
+ }
+
+ /**
+ * Sets up `maven-publish` plugin for the given project.
+ *
+ * Firstly, an instance of [PublicationHandler] is created for the project depending
+ * on the nature of the publication process configured.
+ * Then, this the handler is scheduled to apply on [Project.afterEvaluate].
+ *
+ * General rule of thumb is to avoid using [Project.afterEvaluate] of this closure,
+ * as it configures a project when its configuration is considered completed.
+ * Which is quite counter-intuitive.
+ *
+ * We selected to use [Project.afterEvaluate] so that we can configure publishing of multiple
+ * modules from a root project. When we do this, we configure publishing for a module,
+ * a build file of which has not been even evaluated yet.
+ *
+ * The simplest example here is specifying of `version` and `group` for Maven coordinates.
+ * Let's suppose they are declared in a module's build file. It is a common practice.
+ * But publishing of the module is configured from a root project's build file.
+ * By the time when we need to specify them, we just don't know them.
+ * As the result, we have to use [Project.afterEvaluate] in order to guarantee that
+ * the module will be configured by the time we configure publishing for it.
+ */
+ private fun Project.setUpPublishing(jarFlags: JarFlags) {
+ val customPublishing = modulesWithCustomPublishing.contains(name) || customPublishing
+ val destinations = project.publishTo()
+ val handler = if (customPublishing) {
+ CustomPublicationHandler.serving(project, destinations)
+ } else {
+ StandardJavaPublicationHandler.serving(project, destinations, jarFlags)
+ }
+ afterEvaluate {
+ handler.apply()
+ }
+ }
+
+ /**
+ * Obtains the set of repositories for publishing.
+ *
+ * If there is a local instance of [io.spine.gradle.publish.SpinePublishing] extension,
+ * the [destinations] are obtained from this instance.
+ * Otherwise, the function attempts to obtain it from a [parent project][Project.getParent].
+ * If there is no a parent project, an empty set is returned.
+ *
+ * The normal execution should end up at the root project of a multi-module project
+ * if there are no custom destinations specified by the local extension.
+ */
+ private fun Project.publishTo(): Set {
+ val ext = localSpinePublishing
+ if (ext != null && ext::destinations.isInitialized) {
+ return destinations
+ }
+ return parent?.publishTo() ?: emptySet()
+ }
+
+ /**
+ * Obtains an artifact ID for the given project.
+ *
+ * @see artifactPrefix
+ * @see toolArtifactPrefix
+ */
+ fun artifactId(project: Project): String {
+ val result = if (project.isTool) {
+ check(!toolArtifactPrefix.isEmpty()) {
+ "Artifact prefix cannot be empty for tool modules. " +
+ "Please set the `toolArtifactPrefix` property in `spinePublishing`. " +
+ "Use `\"NONE\"` to have an empty prefix for tool modules."
+ }
+ val prefix =
+ if (toolArtifactPrefix == NONE_PREFIX) {
+ ""
+ } else {
+ toolArtifactPrefix
+ }
+ "$prefix${project.name}"
+ } else {
+ "$artifactPrefix${project.name}"
+ }
+ return result
+ }
+
+ private val Project.isTool: Boolean
+ get() = group == Spine.toolsGroup
+
+ /**
+ * Ensures that all modules, marked as included into [testJar] publishing,
+ * are actually published.
+ *
+ * It makes no sense to tell a module publish [testJar] artifact, if the module is not
+ * published at all.
+ */
+ private fun ensureTestJarInclusionsArePublished() {
+ val nonPublishedInclusions = testJar.inclusions.minus(modules)
+ if (nonPublishedInclusions.isNotEmpty()) {
+ error(
+ "One or more modules are marked as `included into test JAR publication`," +
+ " but they are not even published: $nonPublishedInclusions."
+ )
+ }
+ }
+
+ /**
+ * Ensures that publishing of a module is configured only from a single place.
+ *
+ * We allow configuration of publishing from two places - a root project and the module itself.
+ * Here we verify that publishing of a module is not configured in both places simultaneously.
+ */
+ private fun ensureModulesNotDuplicated() {
+ val rootProject = project.rootProject
+ if (rootProject == project) {
+ return
+ }
+
+ val rootExtension = with(rootProject.extensions) { findByType() }
+ rootExtension?.let { rootPublishing ->
+ val thisProject = setOf(project.name, project.path)
+ if (thisProject.minus(rootPublishing.modules).size != 2) {
+ error(
+ "Publishing of `$thisProject` module is already configured in a root project!"
+ )
+ }
+ }
+ }
+
+ private fun ensureCustomPublishingNotMisused() {
+ if (modules.isNotEmpty() && customPublishing) {
+ error("`customPublishing` property can be set only if `spinePublishing` extension " +
+ "is open in an individual module, so `modules` property should be empty.")
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/StandardJavaPublicationHandler.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/StandardJavaPublicationHandler.kt
new file mode 100644
index 000000000..17648f803
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/StandardJavaPublicationHandler.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.publish
+
+import io.spine.gradle.repo.Repository
+import org.gradle.api.Project
+import org.gradle.api.publish.maven.MavenPublication
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.api.tasks.bundling.Jar
+import org.gradle.kotlin.dsl.create
+
+/**
+ * A publication for a typical Java project.
+ *
+ * In Gradle, to publish something, one should create a publication.
+ * A publication has a name and consists of one or more artifacts plus information about
+ * those artifacts – the metadata.
+ *
+ * An instance of this class represents
+ * [MavenPublication][org.gradle.api.publish.maven.MavenPublication]
+ * named [`"mavenJava"`][PUBLICATION_NAME].
+ * It is generally accepted that a publication with this name contains a Java project
+ * published to one or more Maven repositories.
+ *
+ * By default, only a jar with the compilation output of `main` source set and its
+ * metadata files are published. Other artifacts are specified through the
+ * [constructor parameter][jarFlags].
+ * Please take a look on [specifyArtifacts] for additional info.
+ *
+ * @param jarFlags The flags for additional JARs published along with the compilation output.
+ * @param destinations Maven repositories to which the produced artifacts will be sent.
+ * @see
+ * The Maven Publish Plugin | Publications
+ * @see CustomPublicationHandler
+ */
+class StandardJavaPublicationHandler private constructor(
+ project: Project,
+ private val jarFlags: JarFlags,
+ destinations: Set,
+) : PublicationHandler(project, destinations) {
+
+ companion object : HandlerFactory() {
+
+ /**
+ * The name of the publication created by [StandardJavaPublicationHandler].
+ */
+ const val PUBLICATION_NAME = "mavenJava"
+
+ override fun create(
+ project: Project,
+ destinations: Set,
+ vararg params: Any
+ ): StandardJavaPublicationHandler {
+ return StandardJavaPublicationHandler(project, params[0] as JarFlags, destinations)
+ }
+ }
+
+ /**
+ * Creates a new `"mavenJava"` [MavenPublication][org.gradle.api.publish.maven.MavenPublication]
+ * in the [project] associated with this publication handler.
+ */
+ override fun handlePublications() {
+ val jars = project.artifacts(jarFlags)
+ val publications = project.publications
+ publications.create(PUBLICATION_NAME) {
+ copyProjectAttributes()
+ specifyArtifacts(jars)
+ }
+ }
+
+ /**
+ * Specifies which artifacts this [MavenPublication] will contain.
+ *
+ * A typical Maven publication contains:
+ *
+ * 1. Jar archives. For example, compilation output, sources, javadoc, etc.
+ * 2. Maven metadata file that has the ".pom" extension.
+ * 3. Gradle's metadata file that has the ".module" extension.
+ *
+ * Metadata files contain information about a publication itself, its artifacts, and their
+ * dependencies. Presence of ".pom" file is mandatory for publication to be consumed by
+ * `mvn` build tool itself or other build tools that understand Maven notation (Gradle, Ivy).
+ * The presence of ".module" is optional, but useful when a publication is consumed by Gradle.
+ *
+ * @see Maven – POM Reference
+ * @see
+ * Understanding Gradle Module Metadata
+ */
+ private fun MavenPublication.specifyArtifacts(jars: Set>) {
+
+ /*
+ "java" component provides a jar with compilation output of "main" source set.
+ It is NOT defined as another `Jar` task intentionally. Doing that will leave the
+ publication without correct ".pom" and ".module" metadata files generated.
+ */
+ val javaComponent = project.components.findByName("java")
+ javaComponent?.let {
+ from(it)
+ }
+
+ /*
+ Other artifacts are represented by `Jar` tasks. Those artifacts do not bring any other
+ metadata in comparison with `Component` (such as the `dependencies` notation).
+ */
+ jars.forEach {
+ artifact(it)
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Pmd.kt b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Credentials.kt
similarity index 79%
rename from buildSrc/src/main/kotlin/io/spine/internal/dependency/Pmd.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/repo/Credentials.kt
index badd77174..1624b389a 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Pmd.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Credentials.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,10 +24,12 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.dependency
+package io.spine.gradle.repo
-// https://pmd.github.io/
-@Suppress("unused") // Will be used when `config/gradle/pmd.gradle` migrates to Kotlin.
-object Pmd {
- const val version = "6.44.0"
-}
+/**
+ * Password credentials for a Maven repository.
+ */
+data class Credentials(
+ val username: String?,
+ val password: String?
+)
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/RepoSlug.kt b/buildSrc/src/main/kotlin/io/spine/gradle/repo/RepoSlug.kt
similarity index 84%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/RepoSlug.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/repo/RepoSlug.kt
index 48a493852..d4a6bfd2a 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/RepoSlug.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/repo/RepoSlug.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,13 +24,14 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle
+package io.spine.gradle.repo
import org.gradle.api.GradleException
/**
* A name of a repository.
*/
+@Suppress("unused")
class RepoSlug(val value: String) {
companion object {
@@ -44,12 +45,12 @@ class RepoSlug(val value: String) {
/**
* Reads `REPO_SLUG` environment variable and returns its value.
*
- * In case it is not set, a [GradleException] is thrown.
+ * In case it is not set, a [org.gradle.api.GradleException] is thrown.
*/
fun fromVar(): RepoSlug {
val envValue = System.getenv(environmentVariable)
if (envValue.isNullOrEmpty()) {
- throw GradleException("`REPO_SLUG` environment variable is not set.")
+ throw GradleException("`$environmentVariable` environment variable is not set.")
}
return RepoSlug(envValue)
}
@@ -61,6 +62,6 @@ class RepoSlug(val value: String) {
* Returns the GitHub URL to the project repository.
*/
fun gitHost(): String {
- return "git@github.com-publish:${value}.git"
+ return "git@github-publish:${value}.git"
}
}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt
new file mode 100644
index 000000000..8877dc615
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+@file:Suppress("TooManyFunctions") // Deprecated functions will be kept for a while.
+
+package io.spine.gradle.repo
+
+import io.spine.gradle.publish.PublishingRepos
+import java.net.URI
+import org.gradle.api.artifacts.dsl.RepositoryHandler
+import org.gradle.api.artifacts.repositories.ArtifactRepository
+import org.gradle.api.artifacts.repositories.MavenArtifactRepository
+import org.gradle.kotlin.dsl.maven
+
+/**
+ * Registers the standard set of Maven repositories.
+ *
+ * To be used in `buildscript` clauses when a fully-qualified call must be made.
+ */
+@Suppress("unused")
+@Deprecated(
+ message = "Please use `standardSpineSdkRepositories()`.",
+ replaceWith = ReplaceWith("standardSpineSdkRepositories()")
+)
+fun doApplyStandard(repositories: RepositoryHandler) = repositories.standardToSpineSdk()
+
+/**
+ * A scrambled version of PAT generated with the only "read:packages" scope.
+ *
+ * The scrambling around PAT is necessary because GitHub analyzes commits for the presence
+ * of tokens and invalidates them.
+ *
+ * @see
+ * How to make GitHub packages to the public
+ */
+private object Pat {
+ private const val shade = "_phg->8YlN->MFRA->gxIk->HVkm->eO6g->FqHJ->z8MS->H4zC->ZEPq"
+ private const val separator = "->"
+ private val chunks: Int = shade.split(separator).size - 1
+
+ fun credentials(): Credentials {
+ val pass = shade.replace(separator, "").splitAndReverse(chunks, "")
+ return Credentials("public", pass)
+ }
+
+ /**
+ * Splits this string to the chunks, reverses each chunk, and joins them
+ * back to a string using the [separator].
+ */
+ private fun String.splitAndReverse(numChunks: Int, separator: String): String {
+ check(length / numChunks >= 2) {
+ "The number of chunks is too big. Must be <= ${length / 2}."
+ }
+ val chunks = chunked(length / numChunks)
+ val reversedChunks = chunks.map { chunk -> chunk.reversed() }
+ return reversedChunks.joinToString(separator)
+ }
+}
+
+/**
+ * Adds a read-only view to all artifacts of the SpineEventEngine
+ * GitHub organization.
+ */
+fun RepositoryHandler.spineArtifacts(): MavenArtifactRepository = maven {
+ url = URI("https://maven.pkg.github.com/SpineEventEngine/*")
+ includeSpineOnly()
+ val pat = Pat.credentials()
+ credentials {
+ username = pat.username
+ password = pat.password
+ }
+}
+
+val RepositoryHandler.intellijReleases: MavenArtifactRepository
+ get() = maven("https://www.jetbrains.com/intellij-repository/releases") {
+ includeIntelliJPlatformOnly()
+ }
+
+val RepositoryHandler.intellijDependencies: MavenArtifactRepository
+ get() = maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") {
+ includeIntelliJPlatformOnly()
+ }
+
+/**
+ * Applies repositories commonly used by Spine Event Engine projects.
+ */
+fun RepositoryHandler.standardToSpineSdk() {
+ //
+ // General-purpose, highly available repositories come first. Gradle stops at
+ // the first repository that can serve an artifact, so keeping these ahead of
+ // the special-purpose ones means coordinates shared with them (such as
+ // `org.jetbrains:annotations`) resolve here and never reach a less reliable
+ // JetBrains mirror.
+ //
+ // `io.spine.*` modules are served only by the Spine repositories below, so
+ // they are excluded here. Otherwise Gradle would query Central / the Plugin
+ // Portal for every Spine module first, adding pointless lookups and making
+ // Spine resolution depend on the health of repositories that never host it.
+ //
+ mavenCentral { excludeSpine() }
+ gradlePluginPortal { excludeSpine() }
+
+ spineArtifacts()
+
+ @Suppress("DEPRECATION") // Still use `CloudRepo` for earlier versions.
+ val spineRepos = listOf(
+ Repos.spine,
+ Repos.spineSnapshots,
+ Repos.artifactRegistry,
+ Repos.artifactRegistrySnapshots
+ )
+
+ spineRepos
+ .map { URI(it) }
+ .forEach {
+ maven {
+ url = it
+ includeSpineOnly()
+ }
+ }
+
+ // IntelliJ Platform repositories. `intellijReleases` serves the platform
+ // artifacts (`com.jetbrains.intellij.*`); `intellijDependencies` serves the
+ // repackaged third-party dependencies (`org.jetbrains.intellij.deps.*` and
+ // JetBrains-internal builds). Each is restricted to the coordinates it serves
+ // (see `includeIntelliJPlatformOnly`), so a transient 5xx from one of them
+ // cannot break the resolution of unrelated artifacts.
+ intellijReleases
+ intellijDependencies
+
+ maven {
+ url = URI(Repos.sonatypeSnapshots)
+ // This repository only ever serves snapshots; restrict it so it is not
+ // queried (and cannot fail the build) for release artifacts.
+ mavenContent { snapshotsOnly() }
+ }
+
+ mavenLocal().includeSpineOnly()
+}
+
+@Deprecated(
+ message = "Please use `standardToSpineSdk() instead.",
+ replaceWith = ReplaceWith("standardToSpineSdk()")
+)
+fun RepositoryHandler.applyStandard() = this.standardToSpineSdk()
+
+/**
+ * Defines names of additional repositories commonly used in the Spine SDK projects.
+ *
+ * @see [applyStandard]
+ */
+@Suppress(
+ "DEPRECATION" /* Still need to use `CloudRepo` for older versions. */,
+ "ConstPropertyName" // https://bit.ly/kotlin-prop-names
+)
+private object Repos {
+ @Deprecated(message = "Please use `cloudArtifactRegistry.releases` instead.")
+ val spine = io.spine.gradle.publish.CloudRepo.published.target(snapshots = false)
+
+ @Deprecated(message = "Please use `artifactRegistry.snapshots` instead.")
+ val spineSnapshots = io.spine.gradle.publish.CloudRepo.published.target(snapshots = true)
+
+ val artifactRegistry = PublishingRepos.cloudArtifactRegistry.target(snapshots = false)
+ val artifactRegistrySnapshots = PublishingRepos.cloudArtifactRegistry.target(snapshots = true)
+
+ const val sonatypeSnapshots = "https://oss.sonatype.org/content/repositories/snapshots"
+}
+
+/**
+ * Narrows down the search for this repository to Spine-related artifact groups.
+ */
+private fun MavenArtifactRepository.includeSpineOnly() {
+ content {
+ includeGroupByRegex("io\\.spine.*")
+ }
+}
+
+/**
+ * Excludes Spine artifact groups from this repository.
+ *
+ * `io.spine.*` modules are published only to the Spine repositories (each scoped
+ * via [includeSpineOnly]). Excluding them from a general-purpose repository keeps
+ * Gradle from querying it — and depending on its health — for coordinates it
+ * never hosts.
+ */
+private fun ArtifactRepository.excludeSpine() {
+ content {
+ excludeGroupByRegex("io\\.spine.*")
+ }
+}
+
+/**
+ * Restricts a JetBrains/IntelliJ Platform repository to the coordinates it
+ * actually serves.
+ *
+ * These hosts periodically answer with HTTP 5xx. Once Gradle sees such an error,
+ * it disables the repository for the rest of the build and fails the resolution
+ * instead of falling back to another repository. Without this filter such a
+ * repository is queried for every artifact, so a single 502 on an unrelated POM
+ * (such as `com.fasterxml.jackson:jackson-parent`) would break the whole build.
+ */
+private fun MavenArtifactRepository.includeIntelliJPlatformOnly() {
+ content {
+ includeGroupByRegex("com\\.jetbrains.*")
+ includeGroupByRegex("org\\.jetbrains.*")
+ includeGroupByRegex("com\\.intellij.*")
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt
new file mode 100644
index 000000000..d345725b0
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.repo
+
+import java.io.File
+import java.util.Properties
+import org.gradle.api.Project
+
+/**
+ * A Maven repository.
+ *
+ * @param name The human-readable name that is also used in the publishing task names
+ * for identifying the target repository.
+ * The name must match the [regex].
+ * @param releases The URL for publishing release versions of artifacts.
+ * @param snapshots The URL for publishing [snapshot][io.spine.gradle.isSnapshot] versions.
+ * @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.
+ */
+data class Repository(
+ private val name: String,
+ private val releases: String,
+ private val snapshots: String,
+ private val credentialsFile: String? = null,
+ private val credentialValues: ((Project) -> Credentials?)? = null
+) {
+
+ companion object {
+ val regex = Regex("[A-Za-z0-9_\\-.]+")
+ }
+
+ init {
+ require(regex.matches(name)) {
+ "The repository name `$name` does not match the regex `$regex`."
+ }
+ }
+
+ /**
+ * Obtains the name of the repository.
+ *
+ * The name will be primarily used in the publishing tasks.
+ *
+ * @param snapshots If `true` this repository is used for publishing snapshots,
+ * and the suffix `-snapshots` will be added to the value of the [name] property.
+ * Otherwise, the function returns just [name].
+ */
+ fun name(snapshots: Boolean): String = name + if (snapshots) "-snapshots" else ""
+
+ /**
+ * Obtains the target URL of the repository for publishing.
+ */
+ fun target(snapshots: Boolean): String = if (snapshots) this.snapshots else releases
+
+ /**
+ * Tells if release and snapshot versions are published to the same destination
+ * of this repository.
+ */
+ fun hasOneTarget() = snapshots == releases
+
+ /**
+ * Obtains the publishing password credentials to this 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.
+ */
+ 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."
+ )
+ }
+
+ private fun credsFromFile(fileName: String, project: Project): Credentials? {
+ val file = project.rootProject.file(fileName)
+ if (file.exists().not()) {
+ return null
+ }
+
+ val log = project.logger
+ log.info("Using credentials from `$fileName`.")
+ val creds = file.parseCredentials()
+ log.info("Publishing build as `${creds.username}`.")
+ return creds
+ }
+
+ private fun File.parseCredentials(): Credentials {
+ val properties = Properties().apply { load(inputStream()) }
+ val username = properties.getProperty("user.name")
+ val password = properties.getProperty("user.password")
+ return Credentials(username, password)
+ }
+
+ override fun equals(other: Any?): Boolean = when {
+ this === other -> true
+ other !is Repository -> false
+ else -> name == other.name &&
+ releases == other.releases &&
+ snapshots == other.snapshots
+}
+
+ override fun hashCode(): Int {
+ var result = name.hashCode()
+ result = 31 * result + releases.hashCode()
+ result = 31 * result + snapshots.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return name
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt
new file mode 100644
index 000000000..e3bd5b98d
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/KoverConfig.kt
@@ -0,0 +1,390 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.report.coverage
+
+import io.spine.dependency.test.Jacoco
+import io.spine.dependency.test.Kover
+import io.spine.gradle.testing.TESTKIT_COVERAGE_DIR
+import java.io.File
+import kotlinx.kover.gradle.plugin.dsl.KoverProjectExtension
+import org.gradle.api.Project
+import org.gradle.api.file.SourceDirectorySet
+import org.gradle.api.plugins.JavaPluginExtension
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.SourceSet
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
+
+private const val GENERATED_MARKER: String = "generated"
+private const val KOTLIN_SOURCE_SET_EXT_NAME: String = "kotlin"
+private const val KOTLIN_MAIN_SOURCE_SET_SUFFIX: String = "Main"
+private const val JAVA_SOURCE_SUFFIX: String = ".java"
+private const val KOTLIN_SOURCE_SUFFIX: String = ".kt"
+private const val PROTO_KOTLIN_SUFFIX: String = ".proto.kt"
+private const val KOTLIN_FILE_CLASS_SUFFIX: String = "Kt"
+
+/**
+ * Configures Kover at the root of a multi-module Gradle project to aggregate
+ * coverage across subprojects and exclude classes that originate from
+ * `generated/` source directories.
+ *
+ * Apply once from the root build script, at top level:
+ * ```
+ * KoverConfig.applyTo(rootProject)
+ * ```
+ *
+ * Do **not** wrap this call in `gradle.projectsEvaluated { … }`. The Kover
+ * plugin registers its own `afterEvaluate` hooks at apply time; applying it
+ * after the root project has been evaluated fails with `Cannot run
+ * Project.afterEvaluate(Action) when the project is already evaluated`.
+ *
+ * Subproject wiring is deferred via `pluginManager.withPlugin(...)`:
+ * the per-subproject `useJacoco(...)`, aggregation dependency, and exclude
+ * filter are registered the moment a subproject applies the Kover plugin —
+ * either immediately (if the plugin is already applied) or later in the
+ * same configuration phase. Both branches run **before** Kover's own
+ * `afterEvaluate` finalization, so the engine pin and the aggregation
+ * dependency are visible when Kover builds its task graph.
+ *
+ * Generated-class FQN discovery is resolved lazily through a [Provider]
+ * passed to `classes(...)`. The directory walk happens at task-graph time
+ * (not at configuration time), so `protoc`-generated sources created by
+ * upstream tasks are picked up correctly on a clean build.
+ *
+ * The configuration:
+ *
+ * - Applies the Kover plugin to the root project.
+ * - Pins the coverage engine to the JaCoCo version declared in
+ * [io.spine.dependency.test.Jacoco] via `useJacoco(...)` on the root **and
+ * on every eligible subproject**. The `jvm-module` / `kmp-module` script
+ * plugins already pin the same version, so the per-subproject call is
+ * idempotent for those modules; it matters for subprojects that apply
+ * Kover directly without the convention plugin.
+ * - For every subproject that applies Kover, adds a `kover(project(...))`
+ * dependency so the subproject's coverage flows into the root rollup,
+ * and pushes the subproject's generated-class FQNs into its own
+ * `kover { reports { filters { excludes { classes(...) } } } }`.
+ * - Configures the root `koverXmlReport` task with `onCheck = true` and
+ * excludes the union of generated-class FQNs across all subprojects.
+ * - Feeds the JaCoCo execution data produced by Gradle TestKit worker JVMs
+ * (see [io.spine.gradle.testing.enableTestKitCoverage]) into the `total`
+ * reports as `additionalBinaryReports`, both per subproject and at the root.
+ * Without this, plugin code exercised out-of-process through `GradleRunner`
+ * — most notably `Plugin` implementations, which cannot be
+ * unit-tested in-process — is not credited to coverage.
+ *
+ * The worker data is merged as binary `.exec` rather than as a generated
+ * XML report on purpose. `additionalBinaryReports` is the only merge hook
+ * Kover offers (there is no XML-merge equivalent), and binary data merges at
+ * the probe level against each module's actual bytecode: a line hit both
+ * in-process and out-of-process is counted once. Summing pre-aggregated XML
+ * reports instead would double-count such lines.
+ *
+ * This is the Kover-based successor to the deprecated JaCoCo-based
+ * coverage aggregation pipeline. The behaviour mirrors what
+ * the former JaCoCo-based pipeline provided, but is wired through Kover
+ * (`koverXmlReport`) instead of vanilla `jacocoRootReport`.
+ */
+@Suppress("unused")
+class KoverConfig private constructor(
+ private val rootProject: Project,
+) {
+
+ companion object {
+
+ /**
+ * Configures Kover aggregation and generated-code exclusion at the
+ * root of a multi-module Gradle project.
+ *
+ * Must be called with the root project; throws an
+ * [IllegalArgumentException] if called with a non-root project, and
+ * an [IllegalStateException] if [project] has no subprojects —
+ * a single-module Gradle project does not need root aggregation,
+ * so apply the `jvm-module` / `kmp-module` script plugin (or the
+ * Kover plugin) directly to that module instead.
+ *
+ * Eligibility is determined per subproject: only subprojects that
+ * apply the Kover plugin (directly or through `jvm-module` /
+ * `kmp-module`) are wired into the rollup. Subprojects that apply
+ * Kover after `applyTo` returns are still picked up — wiring runs
+ * inside a `pluginManager.withPlugin(...)` callback that fires
+ * the moment the plugin is applied.
+ */
+ fun applyTo(project: Project) {
+ require(project == project.rootProject) {
+ "`KoverConfig.applyTo` must be called with the root project. " +
+ "Received ${project.path}."
+ }
+ check(project.subprojects.isNotEmpty()) {
+ "In a single-module Gradle project, `KoverConfig` is NOT needed. " +
+ "Apply the Kover plugin directly to the module instead."
+ }
+ project.pluginManager.apply(Kover.id)
+ KoverConfig(project).configure()
+ }
+ }
+
+ private fun configure() {
+ configureRoot()
+ rootProject.subprojects.forEach { sub ->
+ sub.pluginManager.withPlugin(Kover.id) {
+ addAggregationDependency(sub)
+ configureSubproject(sub)
+ }
+ }
+ }
+
+ private fun addAggregationDependency(sub: Project) {
+ rootProject.dependencies.add("kover", rootProject.project(sub.path))
+ }
+
+ /**
+ * Pins the coverage engine to the JaCoCo version declared in
+ * [io.spine.dependency.test.Jacoco] on [sub] and registers a lazy
+ * exclude filter that resolves [sub]'s generated-class FQNs at
+ * task-graph time, after upstream code-generation tasks have run.
+ *
+ * Calling `useJacoco(...)` is idempotent: the `jvm-module` and
+ * `kmp-module` script plugins already pin the same version; the call
+ * here matters for subprojects that apply Kover directly.
+ */
+ private fun configureSubproject(sub: Project) {
+ sub.extensions.configure(KoverProjectExtension::class.java) {
+ useJacoco(Jacoco.version)
+ reports {
+ total {
+ additionalBinaryReports.addAll(testKitExecFilesProvider(sub))
+ }
+ filters {
+ excludes {
+ classes(perSubprojectExcludePatternsProvider(sub))
+ }
+ }
+ }
+ }
+ }
+
+ private fun configureRoot() {
+ rootProject.extensions.configure(KoverProjectExtension::class.java) {
+ useJacoco(Jacoco.version)
+ reports {
+ total {
+ xml {
+ onCheck.set(true)
+ }
+ additionalBinaryReports.addAll(rootTestKitExecFilesProvider())
+ }
+ filters {
+ excludes {
+ classes(generatedExcludePatternsProvider())
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Lazy `Provider` of the union of generated-class FQN exclusion patterns
+ * across every subproject that applies the Kover plugin.
+ *
+ * Resolved at task-graph time; the per-subproject FQN walk runs **after**
+ * `protoc` (and other code-generation tasks) have populated each
+ * subproject's `generated/` directories on a clean build.
+ */
+ private fun generatedExcludePatternsProvider(): Provider> =
+ rootProject.provider {
+ rootProject.subprojects.asSequence()
+ .filter { it.pluginManager.hasPlugin(Kover.id) }
+ .flatMap { generatedClassFqns(it).asSequence() }
+ .toSortedSet()
+ .toExclusionPatterns()
+ }
+
+ /**
+ * Lazy `Provider` of the JaCoCo execution-data files produced by the Gradle
+ * TestKit workers of every subproject that applies the Kover plugin.
+ *
+ * These files credit out-of-process plugin execution (settings plugins and
+ * other code run through `GradleRunner`) to the root coverage rollup.
+ * Resolved at task-graph time, after the `test` tasks have written them.
+ *
+ * @see io.spine.gradle.testing.enableTestKitCoverage
+ */
+ private fun rootTestKitExecFilesProvider(): Provider> =
+ rootProject.provider {
+ rootProject.subprojects.asSequence()
+ .filter { it.pluginManager.hasPlugin(Kover.id) }
+ .flatMap { testKitExecFiles(it).asSequence() }
+ .toList()
+ }
+
+ /**
+ * Lazy `Provider` of the JaCoCo execution-data files produced by the Gradle
+ * TestKit workers of [sub]. See [rootTestKitExecFilesProvider] for timing notes.
+ */
+ private fun testKitExecFilesProvider(sub: Project): Provider> =
+ sub.provider { testKitExecFiles(sub) }
+
+ /**
+ * Lazy `Provider` of the generated-class FQN exclusion patterns
+ * for [sub]. See [generatedExcludePatternsProvider] for timing notes.
+ */
+ private fun perSubprojectExcludePatternsProvider(
+ sub: Project,
+ ): Provider> =
+ sub.provider {
+ generatedClassFqns(sub).toSortedSet().toExclusionPatterns()
+ }
+
+ /**
+ * Returns the fully-qualified names of all classes that originate from
+ * `generated/` source directories of the [project]'s production source sets.
+ *
+ * Java/Kotlin-JVM projects expose these dirs through the `main` source set.
+ * Kotlin Multiplatform projects expose them through source sets such as
+ * `commonMain` and `jvmMain`.
+ */
+ private fun generatedClassFqns(project: Project): List {
+ return generatedSrcDirs(project)
+ .asSequence()
+ .filter { it.exists() && it.isDirectory }
+ .flatMap { root ->
+ root.walk()
+ .filter { !it.isDirectory }
+ .flatMap { it.classNamesIn(root).asSequence() }
+ }
+ .distinct()
+ .toList()
+ }
+}
+
+/**
+ * Returns the JaCoCo execution-data (`.exec`) files written by the Gradle TestKit
+ * workers of the [project], or an empty list if the module produced none.
+ *
+ * The files reside under `build/`[TESTKIT_COVERAGE_DIR] and are created by the
+ * `plugin-testlib` test harness when a module opts in via
+ * [io.spine.gradle.testing.enableTestKitCoverage].
+ */
+private fun testKitExecFiles(project: Project): List {
+ val dir = project.layout.buildDirectory.dir(TESTKIT_COVERAGE_DIR).get().asFile
+ if (!dir.isDirectory) {
+ return emptyList()
+ }
+ return dir.listFiles { file -> file.isFile && file.extension == "exec" }
+ ?.sorted()
+ ?: emptyList()
+}
+
+private fun generatedSrcDirs(project: Project): Set {
+ val javaDirs = javaMainSourceSet(project)
+ ?.let(::generatedSrcDirs)
+ ?: emptySet()
+ val kotlinMultiplatformDirs = kotlinMultiplatformMainSourceSets(project)
+ .asSequence()
+ .flatMap { generatedSrcDirs(it).asSequence() }
+ .toSet()
+ return javaDirs + kotlinMultiplatformDirs
+}
+
+private fun javaMainSourceSet(project: Project): SourceSet? =
+ project.extensions.findByType(JavaPluginExtension::class.java)
+ ?.sourceSets
+ ?.findByName(SourceSet.MAIN_SOURCE_SET_NAME)
+
+private fun kotlinMultiplatformMainSourceSets(project: Project): List =
+ project.extensions.findByType(KotlinMultiplatformExtension::class.java)
+ ?.sourceSets
+ ?.filter { it.isMainSourceSet() }
+ ?: emptyList()
+
+private fun generatedSrcDirs(main: SourceSet): Set {
+ val javaDirs = main.allJava.srcDirs
+ val kotlinDirs =
+ (main.extensions.findByName(KOTLIN_SOURCE_SET_EXT_NAME) as? SourceDirectorySet)
+ ?.srcDirs
+ ?: emptySet()
+ return (javaDirs + kotlinDirs).filter { it.absolutePath.contains(GENERATED_MARKER) }
+ .toSet()
+}
+
+@OptIn(ExperimentalKotlinGradlePluginApi::class)
+private fun generatedSrcDirs(sourceSet: KotlinSourceSet): Set {
+ val kotlinDirs = sourceSet.kotlin.srcDirs
+ val generatedKotlinDirs = sourceSet.generatedKotlin.srcDirs
+ return (kotlinDirs + generatedKotlinDirs)
+ .filter { it.absolutePath.contains(GENERATED_MARKER) }
+ .toSet()
+}
+
+private fun KotlinSourceSet.isMainSourceSet(): Boolean =
+ name == KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME ||
+ name.endsWith(KOTLIN_MAIN_SOURCE_SET_SUFFIX)
+
+/**
+ * Derives one or more class FQNs from this source file's path relative
+ * to [root].
+ *
+ * - `.java` — one FQN.
+ * - `.kt` — the declared class plus the Kotlin file-class synthetic
+ * (`Kt`).
+ * - `.proto.kt` — `protoc-gen-kotlin` convention; the two-part suffix
+ * is stripped, otherwise treated as a `.kt` file.
+ * - any other extension — an empty list.
+ *
+ * Returns an empty list if this file is not under [root].
+ */
+internal fun File.classNamesIn(root: File): List {
+ if (!startsWith(root)) {
+ return emptyList()
+ }
+ val relative = toRelativeString(root)
+ return when {
+ relative.endsWith(PROTO_KOTLIN_SUFFIX) -> {
+ val base = relative.removeSuffix(PROTO_KOTLIN_SUFFIX).toFqn()
+ listOf(base, base + KOTLIN_FILE_CLASS_SUFFIX)
+ }
+ relative.endsWith(KOTLIN_SOURCE_SUFFIX) -> {
+ val base = relative.removeSuffix(KOTLIN_SOURCE_SUFFIX).toFqn()
+ listOf(base, base + KOTLIN_FILE_CLASS_SUFFIX)
+ }
+ relative.endsWith(JAVA_SOURCE_SUFFIX) ->
+ listOf(relative.removeSuffix(JAVA_SOURCE_SUFFIX).toFqn())
+ else -> emptyList()
+ }
+}
+
+/**
+ * Expands each fully-qualified class name into two Kover exclusion
+ * patterns: the class itself, and `$*` for any nested or anonymous
+ * classes the compiler emits alongside it.
+ */
+private fun Collection.toExclusionPatterns(): List =
+ flatMap { listOf(it, "$it\$*") }
+
+private fun String.toFqn(): String = replace(File.separatorChar, '.')
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/SiblingCoverage.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/SiblingCoverage.kt
new file mode 100644
index 000000000..1c9d1f54d
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/SiblingCoverage.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.report.coverage
+
+import java.io.File
+import kotlinx.kover.gradle.plugin.dsl.KoverProjectExtension
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.TaskCollection
+import org.gradle.api.tasks.testing.Test
+
+/**
+ * Credits the test coverage produced by the [contributor] module for the classes
+ * of this project to this project's own Kover report.
+ *
+ * Some modules' production classes are exercised only by the tests of a sibling
+ * module — for example, the language-neutral `psi` classes are tested through
+ * the Java-PSI fixtures that live in `psi-java`. Kover's per-module report sees
+ * only this module's own test execution data, so that cross-module coverage is
+ * otherwise missing from the per-module report (which is what Codecov consumes),
+ * even though the root aggregated report already accounts for it.
+ *
+ * This function adds the [contributor]'s JaCoCo execution data to this project's
+ * `total` report as additional binary reports. Only this project's classes are
+ * credited from them — coverage of unrelated classes in the same execution data
+ * is ignored, because a Kover report is scoped to the owning project's classes.
+ * The report tasks are wired to run after the contributor's JVM test tasks so the
+ * data is present when a report is generated.
+ *
+ * The contributor's JVM test tasks are discovered by type rather than by name, so
+ * the helper works regardless of the module convention: a `jvm-module` contributes
+ * through its `test` task, a `kmp-module` through `jvmTest`, and any additional
+ * JVM test tasks are picked up as well. Non-JVM Kotlin test tasks (`*Native`,
+ * `*Js`, …) are not of type [Test] and are correctly ignored — Kover instruments
+ * only JVM test tasks.
+ *
+ * Requires the Kover plugin to be applied to this project.
+ * A cross-project **task** dependency is used, not a project dependency,
+ * so it does not introduce a dependency cycle even when the [contributor]
+ * already depends on this project.
+ */
+fun Project.creditTestCoverageFrom(contributor: Project) {
+ val contributorTests = contributor.tasks.withType(Test::class.java)
+ extensions.configure(KoverProjectExtension::class.java) {
+ reports {
+ total {
+ additionalBinaryReports.addAll(contributor.execFilesOf(contributorTests))
+ }
+ }
+ }
+ tasks.matching { it.consumesCoverageBinaryReports() }.configureEach {
+ dependsOn(contributorTests)
+ }
+}
+
+/**
+ * Lazy `Provider` of the JaCoCo execution-data files produced by [testTasks]
+ * of this project.
+ *
+ * When the coverage engine is pinned to JaCoCo via `useJacoco(...)`, Kover writes
+ * one binary report per instrumented JVM test task at `build/`[BIN_REPORTS_DIR]
+ * `/.exec`, so the file name follows the task name. Resolved at
+ * task-graph time, after the contributor's test tasks have been registered.
+ */
+private fun Project.execFilesOf(testTasks: TaskCollection): Provider> {
+ val binReports = layout.buildDirectory.dir(BIN_REPORTS_DIR)
+ return provider {
+ testTasks.map { binReports.get().file("${it.name}.exec").asFile }
+ }
+}
+
+/**
+ * The directory under a module's `build/` where Kover writes the per-test-task
+ * binary execution-data files.
+ */
+private const val BIN_REPORTS_DIR: String = "kover/bin-reports"
+
+/**
+ * Tells whether this is a Kover task that reads the binary reports and therefore
+ * must run only after the [contributor's][creditTestCoverageFrom] test data exists.
+ *
+ * This matches both the report tasks (`koverXmlReport`, `koverHtmlReport`,
+ * `koverBinaryReport`) and the verification tasks (`koverVerify` and its
+ * cacheable companion `koverCachedVerify`) — the suffix test covers the
+ * `Cached*` variants Kover registers, which are the ones that actually consume
+ * the binary reports.
+ */
+private fun Task.consumesCoverageBinaryReports(): Boolean =
+ name.startsWith("kover") && (name.endsWith("Report") || name.endsWith("Verify"))
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Configuration.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Configuration.kt
similarity index 92%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Configuration.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/license/Configuration.kt
index 65fd036c2..f6e06fdd2 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Configuration.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Configuration.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.license
+package io.spine.gradle.report.license
import com.github.jk1.license.ConfigurationData
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/LicenseReporter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt
similarity index 59%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/LicenseReporter.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt
index be3e250dc..aa7e65f44 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/LicenseReporter.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,13 +24,15 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.license
+package io.spine.gradle.report.license
import com.github.jk1.license.LicenseReportExtension
import com.github.jk1.license.LicenseReportExtension.ALL
import com.github.jk1.license.LicenseReportPlugin
-import io.spine.internal.gradle.applyPlugin
-import io.spine.internal.gradle.findTask
+import io.spine.dependency.local.Spine
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.applyPlugin
+import io.spine.gradle.getTask
import java.io.File
import org.gradle.api.Project
import org.gradle.api.Task
@@ -42,7 +44,7 @@ import org.gradle.kotlin.dsl.the
*
* Transitive dependencies are included.
*
- * The output file is placed to the root folder of the root Gradle project.
+ * The output file is placed under `docs/dependencies` of the root Gradle project.
*
* Usage:
*
@@ -62,7 +64,7 @@ import org.gradle.kotlin.dsl.the
object LicenseReporter {
/**
- * The name of the Gradle task which generates the reports for a specific Gradle project.
+ * The name of the Gradle task that generates the reports for a specific Gradle project.
*/
private const val projectTaskName = "generateLicenseReport"
@@ -80,33 +82,54 @@ object LicenseReporter {
*/
fun generateReportIn(project: Project) {
project.applyPlugin(LicenseReportPlugin::class.java)
- val reportOutputDir = project.buildDir.resolve(Paths.relativePath)
+ val reportOutputDir = project.layout.buildDirectory.dir(Paths.relativePath).get().asFile
with(project.the()) {
outputDir = reportOutputDir.absolutePath
- excludeGroups = arrayOf("io.spine", "io.spine.tools", "io.spine.gcloud")
+ excludeGroups = arrayOf(
+ Spine.group,
+ "io.spine.gcloud",
+ Spine.toolsGroup,
+ "io.spine.validation"
+ )
configurations = ALL
renderers = arrayOf(MarkdownReportRenderer(Paths.outputFilename))
}
+
+ // The rendered report embeds the project's Maven coordinates — including its
+ // version — in the report header (see `Template.writeHeader`). The
+ // `generateLicenseReport` task is a `@CacheableTask` that keys its up-to-date check
+ // and build-cache entry on the resolved dependencies only, not on the project version.
+ // Without the version as an explicit input, a version-only change leaves the task
+ // `UP-TO-DATE` (or restorable from the build cache), so the report keeps the previous
+ // version while `pom.xml`, produced by an always-running task, is updated. Declaring
+ // the version as an input invalidates the cached output when it changes, so the report
+ // is regenerated. The value is read lazily so it reflects the version resolved at
+ // execution time, regardless of when `project.version` is assigned during configuration.
+ project.tasks.generateLicenseReport.configure {
+ inputs.property("projectVersion", project.provider { project.version.toString() })
+ }
}
/**
- * Tells to merge all per-project reports which were previously [generated][generateReportIn]
+ * Tells to merge all per-project reports that were previously [generated][generateReportIn]
* for each of the subprojects of the root Gradle project.
*
* The merge result is placed according to [Paths].
*
- * Registers a `mergeAllLicenseReports` which is specified to be executed after `build`.
+ * Registers a `mergeAllLicenseReports` that is specified to be executed after `build`.
*/
fun mergeAllReports(project: Project) {
val rootProject = project.rootProject
val mergeTask = rootProject.tasks.register(mergeTaskName) {
+ group = SpineTaskGroup.name
+ description = "Merges per-project license reports into a single repository-wide report"
val consolidationTask = this
- val assembleTask = project.findTask("assemble")
+ val assembleTask = project.getTask("assemble")
val sourceProjects: Iterable = sourceProjects(rootProject)
sourceProjects.forEach {
- val perProjectTask = it.findTask(projectTaskName)
+ val perProjectTask = it.getTask(projectTaskName)
consolidationTask.dependsOn(perProjectTask)
perProjectTask.dependsOn(assembleTask)
}
@@ -115,7 +138,7 @@ object LicenseReporter {
}
dependsOn(assembleTask)
}
- project.findTask("build")
+ project.getTask("build")
.finalizedBy(mergeTask)
}
@@ -139,18 +162,27 @@ object LicenseReporter {
/**
* Merges the license reports from all [sourceProjects] into a single file under
- * the [rootProject]'s root directory.
+ * the [rootProject]'s dependency report directory.
*/
private fun mergeReports(
sourceProjects: Iterable,
rootProject: Project
) {
- val paths = sourceProjects.map {
- "${it.buildDir}/${Paths.relativePath}/${Paths.outputFilename}"
- }
- println("Merging the license reports from the all projects.")
+ val paths = sourceProjects
+ .map {
+ val buildDir = it.layout.buildDirectory.asFile.get()
+ "$buildDir/${Paths.relativePath}/${Paths.outputFilename}"
+ }.filter {
+ val exists = File(it).exists()
+ if (!exists) {
+ rootProject.logger.debug("License report file not found: $it")
+ }
+ exists
+ }
+ println("Merging the license reports from all projects.")
val mergedContent = paths.joinToString("\n\n\n") { (File(it)).readText() }
- val output = File("${rootProject.rootDir}/${Paths.outputFilename}")
+ val output = Paths.outputFile(rootProject.rootDir, Paths.outputFilename)
+ output.parentFile.mkdirs()
output.writeText(mergedContent)
}
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/MarkdownReportRenderer.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/MarkdownReportRenderer.kt
similarity index 84%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/MarkdownReportRenderer.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/license/MarkdownReportRenderer.kt
index ebd62d5bb..b15821043 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/MarkdownReportRenderer.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/MarkdownReportRenderer.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,12 +24,12 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.license
+package io.spine.gradle.report.license
import com.github.jk1.license.LicenseReportExtension
import com.github.jk1.license.ProjectData
import com.github.jk1.license.render.ReportRenderer
-import io.spine.internal.markup.MarkdownDocument
+import io.spine.docs.MarkdownDocument
import java.io.File
import org.gradle.api.Project
@@ -54,9 +54,8 @@ internal class MarkdownReportRenderer(
}
private fun outputFile(project: Project): File {
- val config =
- project.extensions.findByName("licenseReport") as LicenseReportExtension
- return File(config.outputDir).resolve(filename)
+ val ext = project.extensions.findByName("licenseReport") as LicenseReportExtension
+ return File(ext.outputDir).resolve(filename)
}
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/ModuleDataExtensions.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ModuleDataExtensions.kt
similarity index 92%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/ModuleDataExtensions.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/license/ModuleDataExtensions.kt
index c219dec3f..e8518fd20 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/ModuleDataExtensions.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ModuleDataExtensions.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,10 +24,10 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.license
+package io.spine.gradle.report.license
import com.github.jk1.license.ModuleData
-import io.spine.internal.markup.MarkdownDocument
+import io.spine.docs.MarkdownDocument
import kotlin.reflect.KCallable
/**
@@ -49,7 +49,7 @@ internal fun MarkdownDocument.printSection(
}
/**
- * Prints the metadata of the module to the specified [Markdown document][out].
+ * Prints the module metadata to this [MarkdownDocument].
*/
private fun MarkdownDocument.printModule(module: ModuleData) {
ol()
@@ -91,9 +91,8 @@ private fun MarkdownDocument.print(
return this
}
-
/**
- * Prints the URL to the project which provides the dependency.
+ * Prints the URL to the project that provides the dependency.
*
* If the passed project URL is `null` or empty, it is not printed.
*/
@@ -105,7 +104,7 @@ private fun MarkdownDocument.printProjectUrl(projectUrl: String?, indent: Int) {
}
/**
- * Prints the links to the the source code licenses.
+ * Prints the links to the source code licenses.
*/
@Suppress("SameParameterValue" /* Indentation is consistent across the list. */)
private fun MarkdownDocument.printLicenses(licenses: Set, indent: Int) {
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Paths.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Paths.kt
similarity index 63%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Paths.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/license/Paths.kt
index cf1f46022..3c750b31a 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Paths.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Paths.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,26 +24,40 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.license
+package io.spine.gradle.report.license
+
+import java.io.File
/**
- * Filesystem paths used by [LicenseReporter].
+ * Filesystem paths used by [LicenseReporter] and
+ * [PomGenerator][io.spine.gradle.report.pom.PomGenerator].
*/
internal object Paths {
+ /**
+ * The directory in the root project to which dependency reports are written.
+ */
+ internal const val outputDirectory = "docs/dependencies"
+
/**
* The output filename of the license report.
*
- * The file with this name is placed to the root folder of the root Gradle project —
+ * The file with this name is placed under [outputDirectory] of the root Gradle project —
* as the result of the [LicenseReporter] work.
*
* Its contents describe the licensing information for each of the Java dependencies
- * which are referenced by Gradle projects in the repository.
+ * that are referenced by Gradle projects in the repository.
*/
- internal const val outputFilename = "license-report.md"
+ internal const val outputFilename = "dependencies.md"
/**
* The path to a directory, to which a per-project report is generated.
*/
internal const val relativePath = "reports/dependency-license/dependency"
+
+ /**
+ * Obtains a dependency report file under [outputDirectory] of the root project directory.
+ */
+ internal fun outputFile(rootDirectory: File, filename: String): File =
+ rootDirectory.resolve(outputDirectory).resolve(filename)
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/ProjectDependencies.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ProjectDependencies.kt
similarity index 93%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/ProjectDependencies.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/license/ProjectDependencies.kt
index 164df070a..d9e569cb1 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/ProjectDependencies.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ProjectDependencies.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,11 +24,11 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.license
+package io.spine.gradle.report.license
import com.github.jk1.license.ModuleData
import com.github.jk1.license.ProjectData
-import io.spine.internal.markup.MarkdownDocument
+import io.spine.docs.MarkdownDocument
/**
* Dependencies of some [Gradle project][ProjectData] classified by the Gradle configuration
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Tasks.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Tasks.kt
similarity index 91%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Tasks.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/license/Tasks.kt
index 4206f3d4a..05df91adb 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Tasks.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Tasks.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.license
+package io.spine.gradle.report.license
import org.gradle.api.Task
import org.gradle.api.tasks.TaskContainer
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Template.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Template.kt
similarity index 81%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Template.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/license/Template.kt
index f8f835eac..15bd24d82 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Template.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Template.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,11 +24,11 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.license
+package io.spine.gradle.report.license
-import io.spine.internal.gradle.artifactId
-import io.spine.internal.markup.MarkdownDocument
-import java.util.*
+import io.spine.docs.MarkdownDocument
+import io.spine.gradle.artifactId
+import java.util.Date
import org.gradle.api.Project
/**
@@ -40,32 +40,33 @@ internal class Template(
) {
private companion object {
+ @Suppress("ConstPropertyName")
private const val longBreak = "\n\n"
}
- internal fun writeHeader() {
+ internal fun writeHeader() = with(project) {
out.nl()
- .h1(
- "Dependencies of " +
- "`${project.group}:${project.artifactId}:${project.version}`"
- )
- .nl()
+ .h1("Dependencies of `$group:$artifactId:$version`")
+ .nl()
}
internal fun writeFooter() {
+ val currentTime = Date()
out.text(longBreak)
.text("The dependencies distributed under several licenses, ")
.text("are used according their commercial-use-friendly license.")
.text(longBreak)
.text("This report was generated on ")
- .bold("${Date()}")
+ .bold("$currentTime")
.text(" using ")
+ .nl()
.link(
"Gradle-License-Report plugin",
"https://github.com/jk1/Gradle-License-Report"
)
.text(" by Evgeny Naumenko, ")
.text("licensed under ")
+ .nl()
.link(
"Apache 2.0 License",
"https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE"
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/DependencyScope.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyScope.kt
similarity index 91%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/DependencyScope.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyScope.kt
index 6a11b7c12..1b4f47835 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/DependencyScope.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyScope.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.pom
+package io.spine.gradle.report.pom
/**
* A Maven dependency scope.
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/DependencyWriter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt
similarity index 64%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/DependencyWriter.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt
index 49326e95b..db2bf761a 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/DependencyWriter.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,13 +24,14 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.pom
+package io.spine.gradle.report.pom
import groovy.xml.MarkupBuilder
import java.io.Writer
import java.util.*
import kotlin.reflect.full.isSubclassOf
import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.Dependency
import org.gradle.api.internal.artifacts.dependencies.AbstractExternalModuleDependency
import org.gradle.kotlin.dsl.withGroovyBuilder
@@ -53,7 +54,9 @@ import org.gradle.kotlin.dsl.withGroovyBuilder
* ```
*
* When there are several versions of the same dependency, only the one with
- * the newest version is retained.
+ * the newest version is retained. If the retained version is used in several
+ * configurations, the highest-ranking Maven scope is reported, e.g. `compile`
+ * wins over `test`.
*
* @see PomGenerator
*/
@@ -64,7 +67,7 @@ private constructor(
internal companion object {
/**
- * Creates the `ProjectDependenciesAsXml` for the passed [project].
+ * Creates the `DependencyWriter` for the passed [project].
*/
fun of(project: Project): DependencyWriter {
return DependencyWriter(project.dependencies())
@@ -74,7 +77,7 @@ private constructor(
/**
* Writes the dependencies in their `pom.xml` format to the passed [out] writer.
*
- * Used writer will not be closed.
+ * The used writer will not be closed.
*/
fun writeXmlTo(out: Writer) {
val xml = MarkupBuilder(out)
@@ -85,7 +88,12 @@ private constructor(
"dependency" {
"groupId" { xml.text(dependency.group) }
"artifactId" { xml.text(dependency.name) }
- "version" { xml.text(dependency.version) }
+ // A BOM-managed dependency carries no explicit version.
+ // Omit the element rather than emit `null`,
+ // since `null` is not a valid Maven version.
+ dependency.version?.let { version ->
+ "version" { xml.text(version) }
+ }
if (scopedDep.hasDefinedScope()) {
"scope" { xml.text(scopedDep.scopeName()) }
}
@@ -120,19 +128,37 @@ fun Project.dependencies(): SortedSet {
private fun Project.depsFromAllConfigurations(): Set {
val result = mutableSetOf()
this.configurations.forEach { configuration ->
- if (configuration.isCanBeResolved) {
- // Force resolution of the configuration.
- configuration.resolvedConfiguration
- }
- configuration.dependencies.filter { it.isExternal() }
+ configuration.dependencies
+ .filter { it.isExternal() }
.forEach { dependency ->
- val moduleDependency = ModuleDependency(project, configuration, dependency)
+ val forcedVersion = configuration.forcedVersionOf(dependency)
+ val moduleDependency =
+ if (forcedVersion != null) {
+ ModuleDependency(project, configuration, dependency, forcedVersion)
+ } else {
+ ModuleDependency(project, configuration, dependency)
+ }
result.add(moduleDependency)
}
}
return result
}
+/**
+ * Searches for a forced version of given [dependency] in this [Configuration].
+ *
+ * Returns `null`, if it wasn't forced.
+ */
+private fun Configuration.forcedVersionOf(dependency: Dependency): String? {
+ val forcedModules = resolutionStrategy.forcedModules
+ val maybeForced = forcedModules.firstOrNull {
+ it.group == dependency.group
+ && it.name == dependency.name
+ && it.version != null
+ }
+ return maybeForced?.version
+}
+
/**
* Tells whether the dependency is an external module dependency.
*/
@@ -151,16 +177,29 @@ private fun Dependency.isExternal(): Boolean {
* But for our `pom.xml`, which has clearly representative character, a single version
* of a dependency is quite enough.
*
+ * Versions are compared by [VersionComparator] rather than as plain text, so `10.0.0`
+ * is recognized as newer than `9.2.0`, and `2.0.0-SNAPSHOT.100` — as newer
+ * than `2.0.0-SNAPSHOT.99`.
+ *
+ * When the newest version comes from several configurations, the occurrence with
+ * the highest-ranking Maven scope (as defined by [ScopedDependency.dependencyPriority])
+ * is retained. For example, a dependency declared via `api` in one module and via
+ * `testImplementation` in another is reported with the `compile` scope, so a production
+ * dependency is not misrepresented as a test-scoped one. Likewise, an artifact coming
+ * from `compileOnly` or `annotationProcessor` in one module and from a test
+ * configuration in another is reported as `provided`.
+ *
* The rejected duplicates are logged.
*/
private fun Project.deduplicate(dependencies: Set): List {
- val groups = dependencies.distinctBy { it.gav }
- .groupBy { it.run { "$group:$name" } }
+ val groups = dependencies.groupBy { it.run { "$group:$name" } }
- logDuplicates(groups)
+ logDuplicates(groups.mapValues { (_, deps) -> deps.distinctBy { it.gav } })
- val filtered = groups.map { group ->
- group.value.maxByOrNull { dep -> dep.version!! }!!
+ val filtered = groups.values.map { sameArtifact ->
+ val newest = sameArtifact.maxWith(compareBy(VersionComparator) { it.version ?: "" })
+ sameArtifact.filter { it.version == newest.version }
+ .minBy { it.scoped.dependencyPriority() }
}
return filtered
}
@@ -177,7 +216,7 @@ private fun Project.logDuplicate(dependency: String, versions: List {
companion object {
- private val COMPARATOR = compareBy { it.module }
+ private val COMPARATOR = compareBy { it.project }
.thenBy { it.configuration.name }
.thenBy { it.group }
.thenBy { it.name }
- .thenBy { it.version }
+ .thenBy { it.factualVersion }
}
+ override fun getVersion(): String? = factualVersion
+
/**
* A project dependency with its [scope][DependencyScope].
*
* Doesn't contain any info about an origin module and configuration.
*/
- val scoped = ScopedDependency.of(dependency, configuration)
+ val scoped = ScopedDependency.of(this, configuration)
/**
* GAV coordinates of this dependency.
*
* Gradle's [Dependency] is a mutable object. Its properties can change their
- * values with time. In parcticular, the version can be changed as more
+ * values with time. In particular, the version can be changed as more
* configurations are getting resolved. This is why this property is calculated.
*/
val gav: String
- get() = "$group:$name:$version"
+ get() = "$group:$name:$factualVersion"
override fun compareTo(other: ModuleDependency): Int = COMPARATOR.compare(this, other)
@@ -76,17 +79,17 @@ internal class ModuleDependency(
other as ModuleDependency
- if (module != other.module) return false
+ if (project != other.project) return false
if (configuration != other.configuration) return false
- if (dependency != other.dependency) return false
+ if (gav != other.gav) return false
return true
}
override fun hashCode(): Int {
- var result = module.hashCode()
+ var result = project.hashCode()
result = 31 * result + configuration.hashCode()
- result = 31 * result + dependency.hashCode()
+ result = 31 * result + gav.hashCode()
return result
}
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomFormatting.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomFormatting.kt
similarity index 93%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomFormatting.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomFormatting.kt
index 3fca911ce..15059ea0a 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomFormatting.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomFormatting.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,10 +24,11 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.pom
+package io.spine.gradle.report.pom
import java.io.StringWriter
import java.lang.System.lineSeparator
+import java.util.*
/**
* Helps to format the `pom.xml` file according to its expected XML structure.
@@ -74,6 +75,7 @@ internal object PomFormatting {
"structure per-subproject." +
NL
return String.format(
+ Locale.US,
"",
NL, description, NL
)
@@ -90,7 +92,7 @@ internal object PomFormatting {
* Writes the specified lines using the specified [destination], dividing them
* by platform-specific line separator.
*
- * The written lines are also padded with platform's line separator from both sides
+ * Each written line is followed by two platform-specific line separators.
*/
internal fun writeBlocks(destination: StringWriter, vararg lines: String) {
lines.iterator().forEach {
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomGenerator.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt
similarity index 67%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomGenerator.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt
index 8b9ab504e..9ecb36244 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomGenerator.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,11 +24,12 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.pom
+package io.spine.gradle.report.pom
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.report.license.Paths
import org.gradle.api.Project
import org.gradle.api.plugins.BasePlugin
-import org.gradle.kotlin.dsl.extra
/**
* Generates a `pom.xml` file that contains dependencies of the root project as
@@ -42,7 +43,7 @@ import org.gradle.kotlin.dsl.extra
* The generated `pom.xml` is not usable for Maven build tasks and is merely a
* description of project dependencies.
*
- * Configures the `build` task to generate the `pom.xml` file.
+ * Configures the `build` task to generate the `pom.xml` file under `docs/dependencies`.
*
* Note that the generated `pom.xml` includes the group ID, artifact ID and the version of the
* project this script was applied to. In case you want to override the default values, do so in
@@ -58,38 +59,47 @@ import org.gradle.kotlin.dsl.extra
*
* By default, those values are taken from the `project` object, which may or may not include
* them. If the project does not have these values, and they are not specified in the `ext`
- * block, the resulting `pom.xml` file is going to contain empty blocks, e.g. ``.
+ * block, the resulting `pom.xml` file is going to contain empty blocks,
+ * e.g., ``.
*/
@Suppress("unused")
object PomGenerator {
+ private const val pomFilename = "pom.xml"
+
/**
* Configures the generator for the passed [project].
*/
fun applyTo(project: Project) {
/**
- * In some cases, the `base` plugin, which is by default is added by e.g. `java`,
- * is not yet added. `base` plugin defines the `build` task. This generator needs it.
+ * In some cases, the `base` plugin, which by default is added by e.g. `java`,
+ * is not yet added.
+ *
+ * The `base` plugin defines the `build` task.
+ * This generator needs it.
*/
project.apply {
plugin(BasePlugin::class.java)
}
- val task = project.tasks.create("generatePom")
- task.doLast {
- val pomFile = project.projectDir.resolve("pom.xml")
- project.delete(pomFile)
+ val task = project.tasks.register("generatePom") {
+ group = SpineTaskGroup.name
+ description = "Generates a `pom.xml` file describing project dependencies"
+ doLast {
+ val pomFile = Paths.outputFile(project.rootDir, pomFilename)
+ pomFile.parentFile.mkdirs()
+
+ val projectData = project.metadata()
+ val writer = PomXmlWriter(projectData)
+ writer.writeTo(pomFile)
+ }
- val projectData = project.metadata()
- val writer = PomXmlWriter(projectData)
- writer.writeTo(pomFile)
+ val assembleTask = project.tasks.findByName("assemble")!!
+ dependsOn(assembleTask)
}
val buildTask = project.tasks.findByName("build")!!
buildTask.finalizedBy(task)
-
- val assembleTask = project.tasks.findByName("assemble")!!
- task.dependsOn(assembleTask)
}
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomXmlWriter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomXmlWriter.kt
similarity index 78%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomXmlWriter.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomXmlWriter.kt
index 3df34034f..a0b1ade77 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomXmlWriter.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomXmlWriter.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,10 +24,10 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.pom
+package io.spine.gradle.report.pom
-import io.spine.internal.gradle.report.pom.PomFormatting.writeBlocks
-import io.spine.internal.gradle.report.pom.PomFormatting.writeStart
+import io.spine.gradle.report.pom.PomFormatting.writeBlocks
+import io.spine.gradle.report.pom.PomFormatting.writeStart
import java.io.File
import java.io.FileWriter
import java.io.StringWriter
@@ -35,9 +35,9 @@ import java.io.StringWriter
/**
* Writes the dependencies of a Gradle project and its subprojects as a `pom.xml` file.
*
- * The resulting file is not usable for `maven` build tasks, but serves rather as a description
- * of the first-level dependencies for each project/subproject. Their transitive dependencies
- * are not included into the result.
+ * The resulting file is not usable for `maven` build tasks but serves as a description
+ * of the first-level dependencies for each project or subproject.
+ * Their transitive dependencies are not included in the result.
*/
internal class PomXmlWriter
internal constructor(
@@ -51,12 +51,10 @@ internal constructor(
* If a file with the specified location exists, its contents will be substituted
* with a new `pom.xml`.
*
- * @param file a file to write `pom.xml` contents to
+ * @param file a file to write `pom.xml` contents to.
*/
fun writeTo(file: File) {
- val fileWriter = FileWriter(file)
val out = StringWriter()
-
writeStart(out)
writeBlocks(
out,
@@ -67,8 +65,9 @@ internal constructor(
)
PomFormatting.writeEnd(out)
- fileWriter.write(out.toString())
- fileWriter.close()
+ FileWriter(file).use {
+ it.write(out.toString())
+ }
}
/**
@@ -83,4 +82,3 @@ internal constructor(
return destination.toString()
}
}
-
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/ProjectMetadata.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ProjectMetadata.kt
similarity index 96%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/ProjectMetadata.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ProjectMetadata.kt
index 40c84f291..ffb89a263 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/ProjectMetadata.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ProjectMetadata.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.pom
+package io.spine.gradle.report.pom
import groovy.xml.MarkupBuilder
import java.io.StringWriter
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/ScopedDependency.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ScopedDependency.kt
similarity index 69%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/ScopedDependency.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ScopedDependency.kt
index 868d4332b..c969ce500 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/ScopedDependency.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ScopedDependency.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,13 +24,14 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.pom
+package io.spine.gradle.report.pom
-import io.spine.internal.gradle.report.pom.DependencyScope.compile
-import io.spine.internal.gradle.report.pom.DependencyScope.provided
-import io.spine.internal.gradle.report.pom.DependencyScope.runtime
-import io.spine.internal.gradle.report.pom.DependencyScope.test
-import io.spine.internal.gradle.report.pom.DependencyScope.undefined
+import io.spine.gradle.report.pom.DependencyScope.compile
+import io.spine.gradle.report.pom.DependencyScope.provided
+import io.spine.gradle.report.pom.DependencyScope.runtime
+import io.spine.gradle.report.pom.DependencyScope.system
+import io.spine.gradle.report.pom.DependencyScope.test
+import io.spine.gradle.report.pom.DependencyScope.undefined
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.Dependency
@@ -94,29 +95,30 @@ private constructor(
*/
fun of(dependency: Dependency, configuration: Configuration): ScopedDependency {
val configurationName = configuration.name
-
- if (CONFIG_TO_SCOPE.containsKey(configurationName)) {
- val scope = CONFIG_TO_SCOPE[configurationName]
- return ScopedDependency(dependency, scope!!)
- }
- if (configurationName.toLowerCase().startsWith("test")) {
- return ScopedDependency(dependency, test)
+ val knownScope = CONFIG_TO_SCOPE[configurationName]
+ return when {
+ knownScope != null -> ScopedDependency(dependency, knownScope)
+ isTestsRelated(configurationName) -> ScopedDependency(dependency, test)
+ else -> ScopedDependency(dependency, undefined)
}
- return ScopedDependency(dependency, undefined)
}
+ private fun isTestsRelated(configurationName: String): Boolean =
+ configurationName.startsWith("test", ignoreCase = true)
+
/**
- * Performs comparison of {@code DependencyWithScope} instances according to these rules:
+ * Performs comparison of `ScopedDependency` instances according to these rules:
*
- * * Compares the scope of the dependency first. Dependency with lower scope priority
- * number goes first.
+ * * Compares the scope of the dependency first. Dependency with a lower scope priority
+ * number goes first.
*
- * * For dependencies with same scope, does the lexicographical group name comparison.
+ * * For dependencies with the **same scope** does the lexicographical group
+ * name comparison.
*
- * * For dependencies within the same group, does the lexicographical artifact
+ * * For dependencies within the **same group**, does the lexicographical artifact
* name comparison.
*
- * * For dependencies with the same artifact name, does the lexicographical artifact
+ * * For dependencies with the **same artifact name**, does the lexicographical artifact
* version comparison.
*/
private val COMPARATOR: Comparator =
@@ -138,7 +140,7 @@ private constructor(
return dependency
}
- /** Obtains the scope name of this dependency .*/
+ /** Obtains the scope name of this dependency. */
fun scopeName(): String {
return scope.name
}
@@ -147,14 +149,24 @@ private constructor(
* Obtains the layout priority of a scope.
*
* Layout priority determines what scopes come first in the generated `pom.xml` file.
- * Dependencies with a lower priority number go on top.
+ * Dependencies with a lower priority number go on top, following the conventional
+ * Maven scope order: `compile`, `provided`, `runtime`, `test`, and `system`.
+ * Dependencies with an undefined scope go last.
+ *
+ * The same ordering also drives the scope selection when the same dependency
+ * comes from several configurations: the occurrence with the lowest priority
+ * number is reported. So, a scope required by production code wins over `test`,
+ * and a known scope wins over an undefined one.
*/
+ @Suppress("MagicNumber") // Reason: the values encode the relative scope order.
internal fun dependencyPriority(): Int {
return when (scope) {
compile -> 0
- runtime -> 1
- test -> 2
- else -> 3
+ provided -> 1
+ runtime -> 2
+ test -> 3
+ system -> 4
+ undefined -> 5
}
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/SpineLicense.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/SpineLicense.kt
similarity index 93%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/SpineLicense.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/report/pom/SpineLicense.kt
index 7c95db274..114395e62 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/SpineLicense.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/SpineLicense.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.report.pom
+package io.spine.gradle.report.pom
import groovy.xml.MarkupBuilder
import java.io.StringWriter
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/VersionComparator.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/VersionComparator.kt
new file mode 100644
index 000000000..c6984bb65
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/VersionComparator.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.report.pom
+
+/**
+ * Compares dependency version strings by their meaning rather than lexicographically.
+ *
+ * Numeric segments are ordered as numbers, so `10.0.0` is newer than `9.2.0`, and
+ * `2.0.0-SNAPSHOT.100` is newer than `2.0.0-SNAPSHOT.99`. A plain `String` comparison
+ * would order both pairs the other way around.
+ *
+ * The rules follow Semantic Versioning where it applies:
+ *
+ * 1. A version consists of a release part and an optional qualifier, separated by
+ * the first `-`: for `2.0.0-SNAPSHOT.100` these are `2.0.0` and `SNAPSHOT.100`.
+ * 2. Both parts are compared segment by segment, as split by `.`, and also by `-`
+ * within a qualifier. Two numeric segments are compared as numbers, two textual
+ * ones as case-insensitive text, and a numeric segment is older than a textual one.
+ * 3. When one version runs out of segments, it is the older one: `1.0.1` is newer
+ * than `1.0`, and `1.0.0-RC.1` is newer than `1.0.0-RC`.
+ * 4. When the release parts are equal, a version without a qualifier is newer than
+ * a version with one: `2.0.0` is newer than `2.0.0-SNAPSHOT.100`.
+ *
+ * Unlike full Maven semantics, qualifiers carry no special meaning: `RC`, `SNAPSHOT`,
+ * and the like are ordered as plain text. This keeps the comparison simple and
+ * predictable for the report, where only the relative recency of the versions
+ * of the same artifact matters.
+ */
+internal object VersionComparator : Comparator {
+
+ override fun compare(left: String, right: String): Int {
+ val (leftRelease, leftQualifier) = left.parse()
+ val (rightRelease, rightQualifier) = right.parse()
+ val byRelease = compareSegments(leftRelease, rightRelease)
+ if (byRelease != 0) {
+ return byRelease
+ }
+ return when {
+ leftQualifier == null && rightQualifier == null -> 0
+ leftQualifier == null -> 1
+ rightQualifier == null -> -1
+ else -> compareSegments(leftQualifier, rightQualifier)
+ }
+ }
+
+ /**
+ * Splits this version into the segments of its release part and the segments
+ * of its qualifier, the latter being `null` when the version has no qualifier.
+ */
+ private fun String.parse(): Pair, List?> {
+ val release = substringBefore('-')
+ val qualifier = if ('-' in this) substringAfter('-') else null
+ return release.split('.') to qualifier?.split('.', '-')
+ }
+
+ private fun compareSegments(left: List, right: List): Int {
+ for (index in 0 until maxOf(left.size, right.size)) {
+ val bySegment = compareSegment(
+ left.getOrElse(index) { "" },
+ right.getOrElse(index) { "" }
+ )
+ if (bySegment != 0) {
+ return bySegment
+ }
+ }
+ return 0
+ }
+
+ /**
+ * Compares single segments, ordering an absent (empty) segment below any present
+ * one, a numeric segment below a textual one, numbers by their value, and text
+ * case-insensitively.
+ *
+ * Keeping the empty, numeric, and textual segments in distinct buckets makes
+ * the order transitive: comparing a numeric pair as numbers, but a mixed pair
+ * as text, would order `2` < `10` < `1a` < `2`.
+ */
+ private fun compareSegment(left: String, right: String): Int {
+ if (left.isEmpty() || right.isEmpty()) {
+ return left.length.compareTo(right.length)
+ }
+ val leftNumber = left.toLongOrNull()
+ val rightNumber = right.toLongOrNull()
+ return when {
+ leftNumber != null && rightNumber != null -> leftNumber.compareTo(rightNumber)
+ leftNumber != null -> -1
+ rightNumber != null -> 1
+ else -> left.compareTo(right, ignoreCase = true)
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/testing/Logging.kt b/buildSrc/src/main/kotlin/io/spine/gradle/testing/Logging.kt
similarity index 84%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/testing/Logging.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/testing/Logging.kt
index 00143e60a..75af960e2 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/testing/Logging.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/testing/Logging.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,13 +24,13 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.testing
+package io.spine.gradle.testing
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.testing.TestDescriptor
+import org.gradle.api.tasks.testing.TestListener
import org.gradle.api.tasks.testing.TestResult
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
-import org.gradle.kotlin.dsl.KotlinClosure2
/**
* Configures logging of this [Test] task.
@@ -70,19 +70,16 @@ fun Test.configureLogging() {
>> $skippedTestCount skipped
"""
- afterSuite(
-
- // `GroovyInteroperability` is employed as `afterSuite()` has no equivalent in Kotlin DSL.
- // See issue: https://github.com/gradle/gradle/issues/5431
-
- KotlinClosure2({ descriptor, result ->
+ val listener = object : TestListener {
+ override fun afterSuite(descriptor: TestDescriptor, result: TestResult) {
// If the descriptor has no parent, then it is the root test suite,
// i.e. it includes the info about all the run tests.
-
if (descriptor.parent == null) {
logger.lifecycle(result.summary())
}
- })
- )
+ }
+ }
+
+ addTestListener(listener)
}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/testing/Multiproject.kt b/buildSrc/src/main/kotlin/io/spine/gradle/testing/Multiproject.kt
similarity index 83%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/testing/Multiproject.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/testing/Multiproject.kt
index 25ccac942..1f5ed4963 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/testing/Multiproject.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/testing/Multiproject.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,10 +24,10 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.testing
+package io.spine.gradle.testing
+import io.spine.gradle.publish.testJar
import org.gradle.api.Project
-import io.spine.internal.gradle.publish.testJar
/**
* Exposes the test classes of this project as a new "testArtifacts" configuration.
@@ -57,15 +57,13 @@ import io.spine.internal.gradle.publish.testJar
*
* Don't forget that this exposure mechanism works only for projects that reside within the same
* multi-project build. In order to share the test classes with external projects, publish a
- * dedicated [testJar][io.spine.internal.gradle.publish.SpinePublishing.testJar] artifact.
+ * dedicated [testJar][io.spine.gradle.publish.SpinePublishing.testJar] artifact.
*/
@Suppress("unused")
fun Project.exposeTestConfiguration() {
- if (pluginManager.hasPlugin("java").not()) {
- throw IllegalStateException(
- "Can't expose the test configuration because `java` plugin has not been applied."
- )
+ check(pluginManager.hasPlugin("java")) {
+ "Can't expose the test configuration because `java` plugin has not been applied."
}
configurations.create("testArtifacts") {
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/testing/Tasks.kt b/buildSrc/src/main/kotlin/io/spine/gradle/testing/Tasks.kt
similarity index 69%
rename from buildSrc/src/main/kotlin/io/spine/internal/gradle/testing/Tasks.kt
rename to buildSrc/src/main/kotlin/io/spine/gradle/testing/Tasks.kt
index 841d4c7e5..41f07f09c 100644
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/testing/Tasks.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/testing/Tasks.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,11 +24,13 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.internal.gradle.testing
+package io.spine.gradle.testing
+import io.spine.gradle.SpineTaskGroup
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.register
+import org.gradle.kotlin.dsl.withType
/**
* Registers [slowTest][SlowTest] and [fastTest][FastTest] tasks in this [TaskContainer].
@@ -45,6 +47,15 @@ import org.gradle.kotlin.dsl.register
*/
@Suppress("unused")
fun TaskContainer.registerTestTasks() {
+ withType().configureEach {
+ filter {
+ // There could be cases with no matching tests.
+ // E.g., tests could be based on Kotest, which has custom task types and names.
+ isFailOnNoMatchingTests = false
+ includeTestsMatching("*Test")
+ includeTestsMatching("*Spec")
+ }
+ }
register("fastTest").let {
register("slowTest") {
shouldRunAfter(it)
@@ -56,18 +67,20 @@ fun TaskContainer.registerTestTasks() {
* Name of a tag for annotating a test class or method that is known to be slow and
* should not normally be run together with the main test suite.
*
- * @see [SlowTest](https://spine.io/base/reference/testlib/io/spine/testing/SlowTest.html)
- * @see [Tag](https://junit.org/junit5/docs/5.0.2/api/org/junit/jupiter/api/Tag.html)
+ * @see
+ * SlowTest
+ * @see
+ * Tag
*/
private const val SLOW_TAG = "slow"
/**
* Executes JUnit tests filtering out the ones tagged as `slow`.
*/
-private open class FastTest : Test() {
+private abstract class FastTest : Test() {
init {
description = "Executes all JUnit tests but the ones tagged as `slow`."
- group = "Verification"
+ group = SpineTaskGroup.name
this.useJUnitPlatform {
excludeTags(SLOW_TAG)
@@ -78,11 +91,12 @@ private open class FastTest : Test() {
/**
* Executes JUnit tests tagged as `slow`.
*/
-private open class SlowTest : Test() {
+private abstract class SlowTest : Test() {
init {
description = "Executes JUnit tests tagged as `slow`."
- group = "Verification"
-
+ group = SpineTaskGroup.name
+ // No slow tests -- no problem.
+ filter.isFailOnNoMatchingTests = false
this.useJUnitPlatform {
includeTags(SLOW_TAG)
}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt b/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt
new file mode 100644
index 000000000..9682596d8
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/testing/TestKitCoverage.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.gradle.testing
+
+import io.spine.dependency.test.Jacoco
+import java.util.concurrent.atomic.AtomicBoolean
+import org.gradle.api.Project
+import org.gradle.api.tasks.testing.Test
+import org.gradle.kotlin.dsl.withType
+
+/**
+ * Configures the `test` tasks of this project so that the JaCoCo agent is
+ * attached to the Gradle TestKit worker JVMs they spawn.
+ *
+ * `Plugin` implementations and other plugin code exercised through
+ * [`GradleRunner`][org.gradle.testkit.runner.GradleRunner] run in a separate
+ * worker JVM. Kover (and JaCoCo) instrument only the test JVM, so that
+ * out-of-process execution is otherwise not credited to coverage.
+ *
+ * This method:
+ *
+ * 1. Resolves the standalone JaCoCo agent JAR pinned to [Jacoco.version]
+ * through a dedicated [AGENT_CONFIGURATION] configuration.
+ * 2. Passes the agent JAR path and a per-module exec directory
+ * (`build/`[TESTKIT_COVERAGE_DIR]) to the test JVM as system properties.
+ * The `plugin-testlib` harness reads these and writes a `gradle.properties`
+ * into the worker's Gradle user home that adds `-javaagent:…` to the worker JVM.
+ * 3. Wipes the exec directory at most once per build invocation, from the
+ * `doFirst` of the first `Test` task that actually executes, so stale worker
+ * coverage from a previous run does not accumulate. Two failure modes are
+ * avoided deliberately:
+ * - Cleaning is **not** wired through a `dependsOn` clean task. Such a task
+ * would run even when the `Test` task is up-to-date or restored from
+ * cache, deleting the `.exec` files without regenerating them — a later
+ * `koverXmlReport`/`check` run would then drop all TestKit coverage. A
+ * `doFirst` action runs only when the task truly executes.
+ * - The wipe is guarded by a one-shot flag so that, when a module declares
+ * several TestKit `Test` tasks, only the first to run clears the
+ * directory. The workers append to a single per-module exec file, so the
+ * remaining tasks accumulate into it instead of erasing one another.
+ * 4. Marks the `Test` tasks non-cacheable. The worker `.exec` data is flushed
+ * out-of-process on worker-daemon shutdown, *after* the task action
+ * completes, so it cannot be declared as a task output and captured by the
+ * build cache. Were the task left cacheable, a cache hit would skip
+ * execution and restore no exec files, leaving Kover with no TestKit
+ * coverage. An up-to-date (non-cache) run is unaffected: the previous run's
+ * files remain on disk and the guarded `doFirst` never deletes them.
+ *
+ * The produced `.exec` files are merged into the Kover reports by
+ * [io.spine.gradle.report.coverage.KoverConfig]. The agent emits binary
+ * execution data rather than an XML report because that is its only file output,
+ * and because Kover merges binary data at the probe level — see `KoverConfig`
+ * for why binary, not XML.
+ *
+ * The method is idempotent and may be called once per module that runs
+ * TestKit-based tests.
+ */
+fun Project.enableTestKitCoverage() {
+ val agent = configurations.maybeCreate(AGENT_CONFIGURATION).apply {
+ isCanBeConsumed = false
+ isCanBeResolved = true
+ }
+ dependencies.add(agent.name, "org.jacoco:org.jacoco.agent:${Jacoco.version}:runtime")
+
+ val agentPath = agent.elements.map { it.single().asFile.absolutePath }
+ val execDir = layout.buildDirectory.dir(TESTKIT_COVERAGE_DIR)
+
+ // Wiped at most once per build invocation, by the first `Test` task that
+ // actually executes — see the KDoc above for why this is a guarded `doFirst`
+ // wipe rather than a `dependsOn` clean task.
+ val cleaned = AtomicBoolean(false)
+
+ tasks.withType().configureEach {
+ inputs.files(agent).withPropertyName(AGENT_CONFIGURATION)
+ outputs.cacheIf(
+ "TestKit worker coverage is produced out-of-process and cannot be a " +
+ "declared task output; a cache hit would drop it."
+ ) { false }
+ doFirst {
+ val dir = execDir.get().asFile
+ if (cleaned.compareAndSet(false, true)) {
+ dir.deleteRecursively()
+ }
+ dir.mkdirs()
+ systemProperty(AGENT_PROPERTY, agentPath.get())
+ systemProperty(EXEC_DIR_PROPERTY, dir.absolutePath)
+ }
+ }
+}
+
+/**
+ * The name of the directory under a module's `build` directory where the
+ * coverage of Gradle TestKit worker JVMs is collected.
+ *
+ * The directory holds JaCoCo execution-data (`.exec`) files — one per test
+ * project directory — written by the JaCoCo agent attached to the TestKit
+ * worker. `KoverConfig` picks these files up and feeds them into the Kover
+ * reports as additional binary reports.
+ *
+ * @see io.spine.gradle.report.coverage.KoverConfig
+ */
+internal const val TESTKIT_COVERAGE_DIR: String = "jacoco-testkit"
+
+/**
+ * The name of the system property carrying the absolute path to the JaCoCo
+ * agent JAR that the test harness attaches to TestKit worker JVMs.
+ *
+ * The value is read by `plugin-testlib` at test runtime.
+ *
+ * The constant is duplicated in `io.spine.tools.gradle.testing.TestKitCoverage`
+ * of the `plugin-testlib` module (which cannot depend on `buildSrc`). Keep the
+ * two values in sync.
+ */
+private const val AGENT_PROPERTY: String =
+ "io.spine.tools.gradle.testkit.coverage.agent"
+
+/**
+ * The name of the system property carrying the absolute path to the directory
+ * where TestKit workers write their JaCoCo execution data.
+ *
+ * The constant is duplicated in `io.spine.tools.gradle.testing.TestKitCoverage`
+ * of the `plugin-testlib` module. Keep the two values in sync.
+ */
+private const val EXEC_DIR_PROPERTY: String =
+ "io.spine.tools.gradle.testkit.coverage.execDir"
+
+/**
+ * The name of the dedicated, resolvable configuration that holds the standalone
+ * JaCoCo agent JAR (`org.jacoco:org.jacoco.agent::runtime`) attached to
+ * TestKit worker JVMs.
+ *
+ * The configuration is hidden and non-consumable; it exists only to resolve the
+ * agent JAR and to register it as an input of the `test` tasks.
+ */
+private const val AGENT_CONFIGURATION: String = "testKitJacocoAgent"
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/CommonsCollections.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CommonsCollections.kt
deleted file mode 100644
index 31b1b93b7..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/CommonsCollections.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.dependency
-
-/**
- * [Apache Commons Collections](https://commons.apache.org/proper/commons-collections/) is
- * a transitive dependency which we don't use directly. This object is used for forcing
- * the version.
- */
-object CommonsCollections {
- // https://commons.apache.org/proper/commons-collections/
- private const val version = "3.2.2"
- const val lib = "commons-collections:commons-collections:${version}"
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Dokka.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Dokka.kt
deleted file mode 100644
index b9f811d04..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Dokka.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.dependency
-
-// https://github.com/Kotlin/dokka
-@Suppress("unused")
-object Dokka {
- private const val group = "org.jetbrains.dokka"
-
- /**
- * When changing the version, also change the version used in the `buildSrc/build.gradle.kts`.
- */
- const val version = "1.6.20"
-
- object GradlePlugin {
- const val id = "org.jetbrains.dokka"
-
- /**
- * The version of this plugin is already specified in `buildSrc/build.gradle.kts` file.
- * Thus, when applying the plugin in project's build files, only the [id] should be used.
- */
- const val lib = "${group}:dokka-gradle-plugin:${version}"
- }
-
- object BasePlugin {
- const val lib = "${group}:dokka-base:${version}"
- }
-
- /**
- * To generate the documentation as seen from Java perspective use this plugin.
- *
- * @see
- * Dokka output formats
- */
- object KotlinAsJavaPlugin {
- const val lib = "${group}:kotlin-as-java-plugin:${version}"
- }
-
- /**
- * Custom Dokka plugins developed for Spine-specific needs like excluding by `@Internal`
- * annotation.
- *
- * @see
- * Custom Dokka Plugins
- */
- object SpineExtensions {
- private const val group = "io.spine.tools"
-
- const val version = "2.0.0-SNAPSHOT.3"
- const val lib = "${group}:spine-dokka-extensions:${version}"
- }
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/ErrorProne.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/ErrorProne.kt
deleted file mode 100644
index ca73fedcc..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/ErrorProne.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.dependency
-
-// https://errorprone.info/
-@Suppress("unused")
-object ErrorProne {
- // https://github.com/google/error-prone
- private const val version = "2.10.0"
- // https://github.com/tbroyer/gradle-errorprone-plugin/blob/v0.8/build.gradle.kts
- private const val javacPluginVersion = "9+181-r4173-1"
-
- val annotations = listOf(
- "com.google.errorprone:error_prone_annotations:${version}",
- "com.google.errorprone:error_prone_type_annotations:${version}"
- )
- const val core = "com.google.errorprone:error_prone_core:${version}"
- const val checkApi = "com.google.errorprone:error_prone_check_api:${version}"
- const val testHelpers = "com.google.errorprone:error_prone_test_helpers:${version}"
- const val javacPlugin = "com.google.errorprone:javac:${javacPluginVersion}"
-
- // https://github.com/tbroyer/gradle-errorprone-plugin/releases
- object GradlePlugin {
- const val id = "net.ltgt.errorprone"
- /**
- * The version of this plugin is already specified in `buildSrc/build.gradle.kts` file.
- * Thus, when applying the plugin in projects build files, only the [id] should be used.
- *
- * When the plugin is used as a library (e.g. in tools), its version and the library
- * artifacts are of importance.
- */
- const val version = "2.0.2"
- const val lib = "net.ltgt.gradle:gradle-errorprone-plugin:${version}"
- }
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/FindBugs.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/FindBugs.kt
deleted file mode 100644
index 52b05f758..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/FindBugs.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.dependency
-
-/**
- * The FindBugs project is dead since 2017. It has a successor called SpotBugs, but we don't use it.
- * We use ErrorProne for static analysis instead. The only reason for having this dependency is
- * the annotations for null-checking introduced by JSR-305. These annotations are troublesome,
- * but no alternatives are known for some of them so far. Please see
- * [this issue](https://github.com/SpineEventEngine/base/issues/108) for more details.
- */
-object FindBugs {
- private const val version = "3.0.2"
- const val annotations = "com.google.code.findbugs:jsr305:${version}"
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Grpc.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Grpc.kt
deleted file mode 100644
index c9d55667c..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Grpc.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.dependency
-
-// https://github.com/grpc/grpc-java
-@Suppress("unused")
-object Grpc {
- @Suppress("MemberVisibilityCanBePrivate")
- const val version = "1.45.1"
- const val api = "io.grpc:grpc-api:${version}"
- const val auth = "io.grpc:grpc-auth:${version}"
- const val core = "io.grpc:grpc-core:${version}"
- const val context = "io.grpc:grpc-context:${version}"
- const val stub = "io.grpc:grpc-stub:${version}"
- const val okHttp = "io.grpc:grpc-okhttp:${version}"
- const val protobuf = "io.grpc:grpc-protobuf:${version}"
- const val protobufLite = "io.grpc:grpc-protobuf-lite:${version}"
- const val protobufPlugin = "io.grpc:protoc-gen-grpc-java:${version}"
- const val netty = "io.grpc:grpc-netty:${version}"
- const val nettyShaded = "io.grpc:grpc-netty-shaded:${version}"
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/JUnit.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/JUnit.kt
deleted file mode 100644
index 0c436ce4c..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/JUnit.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.dependency
-
-// https://junit.org/junit5/
-@Suppress("unused")
-object JUnit {
- const val version = "5.8.2"
- private const val platformVersion = "1.8.2"
- private const val legacyVersion = "4.13.1"
-
- // https://github.com/apiguardian-team/apiguardian
- private const val apiGuardianVersion = "1.1.2"
- // https://github.com/junit-pioneer/junit-pioneer
- private const val pioneerVersion = "1.5.0"
-
- const val legacy = "junit:junit:${legacyVersion}"
- val api = listOf(
- "org.apiguardian:apiguardian-api:${apiGuardianVersion}",
- "org.junit.jupiter:junit-jupiter-api:${version}",
- "org.junit.jupiter:junit-jupiter-params:${version}"
- )
- const val bom = "org.junit:junit-bom:${version}"
- const val runner = "org.junit.jupiter:junit-jupiter-engine:${version}"
- const val pioneer = "org.junit-pioneer:junit-pioneer:${pioneerVersion}"
- const val platformCommons = "org.junit.platform:junit-platform-commons:${platformVersion}"
- const val platformLauncher = "org.junit.platform:junit-platform-launcher:${platformVersion}"
- const val params = "org.junit.jupiter:junit-jupiter-params:${version}"
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Jackson.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Jackson.kt
deleted file mode 100644
index 1c92848f2..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Jackson.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.dependency
-
-@Suppress("unused")
-object Jackson {
- const val version = "2.13.2"
- const val databindVersion = "2.13.2.2"
- // https://github.com/FasterXML/jackson-core
- const val core = "com.fasterxml.jackson.core:jackson-core:${version}"
- // https://github.com/FasterXML/jackson-databind
- const val databind = "com.fasterxml.jackson.core:jackson-databind:${databindVersion}"
- // https://github.com/FasterXML/jackson-dataformat-xml/releases
- const val dataformatXml = "com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${version}"
- // https://github.com/FasterXML/jackson-dataformats-text/releases
- const val dataformatYaml = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${version}"
- // https://github.com/FasterXML/jackson-module-kotlin/releases
- const val moduleKotlin = "com.fasterxml.jackson.module:jackson-module-kotlin:${version}"
- // https://github.com/FasterXML/jackson-bom
- const val bom = "com.fasterxml.jackson:jackson-bom:${version}"
- // https://github.com/FasterXML/jackson-annotations
- const val annotations = "com.fasterxml.jackson.core:jackson-annotations:${version}"
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Kotlin.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Kotlin.kt
deleted file mode 100644
index ecee68630..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Kotlin.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.dependency
-
-// https://github.com/JetBrains/kotlin
-// https://github.com/Kotlin
-object Kotlin {
- /**
- * When changing the version, also change the version used in the `buildSrc/build.gradle.kts`.
- */
- @Suppress("MemberVisibilityCanBePrivate") // used directly from outside
- const val version = "1.6.21"
- const val reflect = "org.jetbrains.kotlin:kotlin-reflect:${version}"
- const val stdLib = "org.jetbrains.kotlin:kotlin-stdlib:${version}"
- const val stdLibCommon = "org.jetbrains.kotlin:kotlin-stdlib-common:${version}"
- const val stdLibJdk8 = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${version}"
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/OsDetector.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/OsDetector.kt
deleted file mode 100644
index 7f4bb16b8..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/OsDetector.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.dependency
-
-object OsDetector {
- // https://github.com/google/osdetector-gradle-plugin
- const val version = "1.7.0"
- const val id = "com.google.osdetector"
- const val lib = "com.google.gradle:osdetector-gradle-plugin:${version}"
- const val classpath = lib
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/ThreeTen.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/ThreeTen.kt
deleted file mode 100644
index 9e420dea6..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/dependency/ThreeTen.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.dependency
-
-/**
- * A [JSR-310 replacement](https://github.com/ThreeTen/threetenbp), which is used by some
- * Google APIs. This object is used for forcing the version of this transitive dependency.
- */
-object ThreeTen {
- // https://github.com/ThreeTen/threetenbp
- private const val version = "1.5.1"
- const val lib = "org.threeten:threetenbp:${version}"
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/Repositories.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Repositories.kt
deleted file mode 100644
index db807672c..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/Repositories.kt
+++ /dev/null
@@ -1,242 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle
-
-import io.spine.internal.gradle.publish.CloudRepo
-import io.spine.internal.gradle.publish.PublishingRepos
-import io.spine.internal.gradle.publish.PublishingRepos.gitHub
-import java.io.File
-import java.net.URI
-import java.util.*
-import org.gradle.api.Project
-import org.gradle.api.artifacts.dsl.RepositoryHandler
-import org.gradle.api.artifacts.repositories.MavenArtifactRepository
-
-/**
- * A Maven repository.
- */
-data class Repository(
- val releases: String,
- val snapshots: String,
- private val credentialsFile: String? = null,
- private val credentialValues: ((Project) -> Credentials?)? = null,
- val name: String = "Maven repository `$releases`"
-) {
-
- /**
- * Obtains the publishing password credentials to this 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.
- */
- fun credentials(project: Project): Credentials? {
- if (credentialValues != null) {
- return credentialValues.invoke(project)
- }
- credentialsFile!!
- val log = project.logger
- log.info("Using credentials from `$credentialsFile`.")
- val file = project.rootProject.file(credentialsFile)
- if (!file.exists()) {
- return null
- }
- val creds = file.readCredentials()
- log.info("Publishing build as `${creds.username}`.")
- return creds
- }
-
- private fun File.readCredentials(): Credentials {
- val properties = Properties()
- properties.load(inputStream())
- val username = properties.getProperty("user.name")
- val password = properties.getProperty("user.password")
- return Credentials(username, password)
- }
-
- override fun toString(): String {
- return name
- }
-}
-
-/**
- * Password credentials for a Maven repository.
- */
-data class Credentials(
- val username: String?,
- val password: String?
-)
-
-/**
- * Defines names of additional repositories commonly used in the framework projects.
- *
- * @see [applyStandard]
- */
-@Suppress("unused")
-object Repos {
- @Deprecated(
- message = "Please use another repository.",
- replaceWith = ReplaceWith("artifactRegistry"),
- level = DeprecationLevel.ERROR
- )
- val oldSpine = PublishingRepos.mavenTeamDev.releases
-
- @Deprecated(
- message = "Please use another repository.",
- replaceWith = ReplaceWith("artifactRegistrySnapshots"),
- level = DeprecationLevel.ERROR
- )
- val oldSpineSnapshots = PublishingRepos.mavenTeamDev.snapshots
-
- val spine = CloudRepo.published.releases
- val spineSnapshots = CloudRepo.published.snapshots
-
- val artifactRegistry = PublishingRepos.cloudArtifactRegistry.releases
- val artifactRegistrySnapshots = PublishingRepos.cloudArtifactRegistry.snapshots
-
- @Deprecated(
- message = "Sonatype release repository redirects to the Maven Central",
- replaceWith = ReplaceWith("sonatypeSnapshots"),
- level = DeprecationLevel.ERROR
- )
- const val sonatypeReleases = "https://oss.sonatype.org/content/repositories/snapshots"
- const val sonatypeSnapshots = "https://oss.sonatype.org/content/repositories/snapshots"
-}
-
-/**
- * Registers the standard set of Maven repositories.
- *
- * To be used in `buildscript` clauses when a fully-qualified call must be made.
- */
-@Suppress("unused")
-fun doApplyStandard(repositories: RepositoryHandler) = repositories.applyStandard()
-
-/**
- * Registers the selected GitHub Packages repos as Maven repositories.
- *
- * To be used in `buildscript` clauses when a fully-qualified call must be made.
- *
- * @param repositories
- * the handler to accept registration of the GitHub Packages repository
- * @param shortRepositoryName
- * the short name of the GitHub repository (e.g. "core-java")
- * @param project
- * the project which is going to consume or publish artifacts from
- * the registered repository
- * @see applyGitHubPackages
- */
-@Suppress("unused")
-fun doApplyGitHubPackages(
- repositories: RepositoryHandler,
- shortRepositoryName: String,
- project: Project
-) = repositories.applyGitHubPackages(shortRepositoryName, project)
-
-/**
- * Applies the repositories hosted at GitHub Packages, to which Spine artifacts were published.
- *
- * This method should be used by those wishing to have Spine artifacts published
- * to GitHub Packages as dependencies.
- *
- * @param shortRepositoryName
- * the short name of the GitHub repository (e.g. "core-java")
- * @param project
- * the project which is going to consume or publish artifacts from
- * the registered repository
- */
-fun RepositoryHandler.applyGitHubPackages(shortRepositoryName: String, project: Project) {
- val repository = gitHub(shortRepositoryName)
- val credentials = repository.credentials(project)
-
- credentials?.let {
- spineMavenRepo(it, repository.releases)
- spineMavenRepo(it, repository.snapshots)
- }
-}
-
-/**
- * Applies repositories commonly used by Spine Event Engine projects.
- *
- * Does not include the repositories hosted at GitHub Packages.
- *
- * @see applyGitHubPackages
- */
-@Suppress("unused")
-fun RepositoryHandler.applyStandard() {
-
- gradlePluginPortal()
- mavenLocal()
-
- val spineRepos = listOf(
- Repos.spine,
- Repos.spineSnapshots,
- Repos.artifactRegistry,
- Repos.artifactRegistrySnapshots
- )
-
- spineRepos
- .map { URI(it) }
- .forEach {
- maven {
- url = it
- includeSpineOnly()
- }
- }
-
- mavenCentral()
- maven {
- url = URI(Repos.sonatypeSnapshots)
- }
-}
-
-/**
- * Registers the Maven repository with the passed [repoCredentials] for authorization.
- *
- * Only includes the Spine-related artifact groups.
- */
-private fun RepositoryHandler.spineMavenRepo(
- repoCredentials: Credentials,
- repoUrl: String
-) {
- maven {
- url = URI(repoUrl)
- includeSpineOnly()
- credentials {
- username = repoCredentials.username
- password = repoCredentials.password
- }
- }
-}
-
-/**
- * Narrows down the search for this repository to Spine-related artifact groups.
- */
-private fun MavenArtifactRepository.includeSpineOnly() {
- content {
- includeGroupByRegex("io\\.spine.*")
- }
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/Scripts.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Scripts.kt
deleted file mode 100644
index 7baf9454e..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/Scripts.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle
-
-import org.gradle.api.Project
-
-@Suppress("unused")
-object Scripts {
- @Suppress("MemberVisibilityCanBePrivate") // is used from Groovy-based scripts.
- const val commonPath = "/buildSrc/src/main/groovy/"
-
- fun testArtifacts(p: Project) = p.script("test-artifacts.gradle")
- fun testOutput(p: Project) = p.script("test-output.gradle")
- fun slowTests(p: Project) = p.script("slow-tests.gradle")
- fun jacoco(p: Project) = p.script("jacoco.gradle")
- fun publish(p: Project) = p.script("publish.gradle")
- fun publishProto(p: Project) = p.script("publish-proto.gradle")
- fun javacArgs(p: Project) = p.script("javac-args.gradle")
- fun jsBuildTasks(p: Project) = p.script("js/build-tasks.gradle")
- fun jsConfigureProto(p: Project) = p.script("js/configure-proto.gradle")
- fun npmPublishTasks(p: Project) = p.script("js/npm-publish-tasks.gradle")
- fun npmCli(p: Project) = p.script("js/npm-cli.gradle")
- fun updatePackageVersion(p: Project) = p.script("js/update-package-version.gradle")
- fun dartBuildTasks(p: Project) = p.script("dart/build-tasks.gradle")
- fun pubPublishTasks(p: Project) = p.script("dart/pub-publish-tasks.gradle")
-
- @Deprecated("Use `pmd-settings` script plugin instead")
- fun pmd(p: Project) = p.script("pmd.gradle")
-
- fun runBuild(p: Project) = p.script("run-build.gradle")
- fun licenseReportCommon(p: Project) = p.script("license-report-common.gradle")
- fun projectLicenseReport(p: Project) = p.script("license-report-project.gradle")
- fun repoLicenseReport(p: Project) = p.script("license-report-repo.gradle")
-
- private fun Project.script(name: String) = "${rootDir}$commonPath${name}"
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/VersionWriter.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/VersionWriter.kt
deleted file mode 100644
index bcec4a56b..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/VersionWriter.kt
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle
-
-import java.util.*
-import org.gradle.api.DefaultTask
-import org.gradle.api.Plugin
-import org.gradle.api.Project
-import org.gradle.api.file.DirectoryProperty
-import org.gradle.api.provider.MapProperty
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.OutputDirectory
-import org.gradle.api.tasks.TaskAction
-
-/**
- * A task that generates a dependency versions `.properties` file.
- */
-abstract class WriteVersions : DefaultTask() {
-
- /**
- * Versions to add to the file.
- *
- * The map key is a string in the format of `_`, and the value
- * is the version corresponding to those group ID and artifact name.
- *
- * @see WriteVersions.version
- */
- @get:Input
- abstract val versions: MapProperty
-
- /**
- * The directory that hosts the generated file.
- */
- @get:OutputDirectory
- abstract val versionsFileLocation: DirectoryProperty
-
- /**
- * Adds a dependency version to write into the file.
- *
- * The given dependency notation is a Gradle artifact string of format:
- * `"::"`.
- *
- * @see WriteVersions.versions
- * @see WriteVersions.includeOwnVersion
- */
- fun version(dependencyNotation: String) {
- val parts = dependencyNotation.split(":")
- check(parts.size == 3) { "Invalid dependency notation: `$dependencyNotation`." }
- versions.put("${parts[0]}_${parts[1]}", parts[2])
- }
-
- /**
- * Enables the versions file to include the version of the project that owns this task.
- *
- * @see WriteVersions.version
- * @see WriteVersions.versions
- */
- fun includeOwnVersion() {
- val groupId = project.group.toString()
- val artifactId = project.artifactId
- val version = project.version.toString()
- versions.put("${groupId}_${artifactId}", version)
- }
-
- /**
- * Creates a `.properties` file with versions, named after the value
- * of [Project.artifactId] property.
- *
- * The name of the file would be: `versions-.properties`.
- *
- * By default, value of [Project.artifactId] property is a project's name with "spine-" prefix.
- * For example, if a project's name is "tools", then the name of the file would be:
- * `versions-spine-tools.properties`.
- */
- @TaskAction
- private fun writeFile() {
- versions.finalizeValue()
- versionsFileLocation.finalizeValue()
-
- val values = versions.get()
- val properties = Properties()
- properties.putAll(values)
- val outputDir = versionsFileLocation.get().asFile
- outputDir.mkdirs()
- val fileName = resourceFileName()
- val file = outputDir.resolve(fileName)
- file.createNewFile()
- file.writer().use {
- properties.store(it, "Dependency versions supplied by the `$path` task.")
- }
- }
-
- private fun resourceFileName(): String {
- val artifactId = project.artifactId
- return "versions-${artifactId}.properties"
- }
-}
-
-/**
- * A plugin that enables storing dependency versions into a resource file.
- *
- * Dependency version may be used by Gradle plugins at runtime.
- *
- * The plugin adds one task — `writeVersions`, which generates a `.properties` file with some
- * dependency versions.
- *
- * The generated file will be available in classpath of the target project under the name:
- * `versions-.properties`, where `` is the name of the target
- * Gradle project.
- */
-@Suppress("unused")
-class VersionWriter : Plugin {
-
- override fun apply(target: Project): Unit = with (target.tasks) {
- val task = register("writeVersions", WriteVersions::class.java) {
- versionsFileLocation.convention(project.layout.buildDirectory.dir(name))
- includeOwnVersion()
- project.sourceSets
- .getByName("main")
- .resources
- .srcDir(versionsFileLocation)
- }
- getByName("processResources").dependsOn(task)
- }
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dokka/DokkaExtensions.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dokka/DokkaExtensions.kt
deleted file mode 100644
index 12a4262b1..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dokka/DokkaExtensions.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle.dokka
-
-import java.io.File
-import org.gradle.api.file.FileCollection
-import org.jetbrains.dokka.gradle.GradleDokkaSourceSetBuilder
-
-/**
- * Returns only Java source roots out of all present in the source set.
- *
- * It is a helper method for generating documentation by Dokka only for Java code.
- * It is helpful when both Java and Kotlin source files are present in a source set.
- * Dokka can properly generate documentation for either Kotlin or Java depending on
- * the configuration, but not both.
- */
-internal fun GradleDokkaSourceSetBuilder.onlyJavaSources(): FileCollection {
- return sourceRoots.filter(File::isJavaSourceDirectory)
-}
-
-private fun File.isJavaSourceDirectory(): Boolean {
- return isDirectory && name == "java"
-}
-
-/**
- * Returns only non-generated source roots out of all present in the source set.
- *
- * It is a helper method for generating documentation by Dokka only for non-generated code.
- * It helps to filter out source files generated by`Protoc`.
- */
-internal fun GradleDokkaSourceSetBuilder.onlyNonGeneratedSources(): FileCollection {
- return sourceRoots.filter(File::isNonGeneratedDirectory)
-}
-
-private fun File.isNonGeneratedDirectory(): Boolean {
- return isDirectory && !path.contains("/generated/")
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/Update.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/Update.kt
deleted file mode 100644
index 283405dd8..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/Update.kt
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle.github.pages
-
-import io.spine.internal.gradle.Cli
-import io.spine.internal.gradle.RepoSlug
-import java.io.File
-import java.nio.file.Path
-import org.gradle.api.GradleException
-import org.gradle.api.Project
-import org.gradle.api.Task
-import org.gradle.api.file.ConfigurableFileCollection
-import org.gradle.api.file.FileCollection
-import org.gradle.api.logging.Logger
-
-/**
- * Performs the update of GitHub pages.
- */
-fun Task.updateGhPages(project: Project) {
- val plugin = project.plugins.getPlugin(UpdateGitHubPages::class.java)
- val op = with(plugin) {
- Operation(project, rootFolder, checkoutTempFolder, javadocOutputPath, logger)
- }
- op.run()
-}
-
-private class Operation(
- private val project: Project,
- private val rootFolder: File,
- checkoutTempFolder: Path,
- private val javadocOutputPath: Path,
- private val logger: Logger
-) {
-
- private val ghRepoFolder: File = File("${checkoutTempFolder}/${Branch.ghPages}")
- private val docDirPostfix = "reference/$project.name"
- private val mostRecentDocDir = File("$ghRepoFolder/$docDirPostfix")
-
- fun run() {
- SshKey(rootFolder).register()
- checkoutDocs()
- val generatedDocs = replaceMostRecentDocs()
- copyIntoVersionDir(generatedDocs)
- addCommitAndPush()
- logger.debug("The GitHub Pages contents were successfully updated.")
- }
-
- /** Executes a command in the project [rootFolder]. */
- private fun execute(vararg command: String): String = Cli(rootFolder).execute(*command)
-
- /** Executes a command in the [ghRepoFolder] */
- private fun pagesExecute(vararg command: String): String = Cli(ghRepoFolder).execute(*command)
-
- private fun checkoutDocs() {
- val gitHost = RepoSlug.fromVar().gitHost()
-
- execute("git", "clone", gitHost, ghRepoFolder.absolutePath)
- pagesExecute("git", "checkout", Branch.ghPages)
- }
-
- private fun replaceMostRecentDocs(): ConfigurableFileCollection {
- logger.debug("Replacing the most recent docs in `$mostRecentDocDir`.")
- val generatedDocs = project.files(javadocOutputPath)
- copyDocs(generatedDocs, mostRecentDocDir)
- return generatedDocs
- }
-
- private fun copyIntoVersionDir(generatedDocs: ConfigurableFileCollection) {
- val versionedDocDir = File("$mostRecentDocDir/v/$project.version")
- logger.debug("Storing the new version of docs in the directory `$versionedDocDir`.")
- copyDocs(generatedDocs, versionedDocDir)
- }
-
- private fun addCommitAndPush() {
- pagesExecute("git", "add", docDirPostfix)
- configureCommitter()
- commitAndPush()
- }
-
- private fun copyDocs(source: FileCollection, destination: File) {
- destination.mkdir()
- project.copy {
- from(source)
- into(destination)
- }
- }
-
- /**
- * Configures Git to publish the changes under "UpdateGitHubPages Plugin" Git user name
- * and email stored in "FORMAL_GIT_HUB_PAGES_AUTHOR" env variable.
- */
- private fun configureCommitter() {
- pagesExecute("git", "config", "user.name", "\"UpdateGitHubPages Plugin\"")
- val authorEmail = AuthorEmail.fromVar().toString()
- pagesExecute("git", "config", "user.email", authorEmail)
- }
-
- private fun commitAndPush() {
- pagesExecute(
- "git",
- "commit",
- "--allow-empty",
- "--message=\"Update Javadoc for module ${project.name}" +
- " as for version ${project.version}\""
- )
- pagesExecute("git", "push")
- }
-}
-
-/**
- * Registers SSH key for further operations with GitHub Pages.
- */
-private class SshKey(private val rootFolder: File) {
-
- /**
- * Creates an SSH key with the credentials and registers it
- * by invoking the `register-ssh-key.sh` script.
- */
- fun register() {
- val gitHubAccessKey = gitHubKey()
- val sshConfigFile = sshConfigFile()
- val nl = System.lineSeparator()
- sshConfigFile.appendText(
- nl +
- "Host github.com-publish" + nl +
- "User git" + nl +
- "IdentityFile ${gitHubAccessKey.absolutePath}" + nl
- )
-
- execute(
- "${rootFolder.absolutePath}/config/scripts/register-ssh-key.sh",
- gitHubAccessKey.absolutePath
- )
- }
-
- /**
- * Locates `deploy_key_rsa` in the [rootFolder] and returns it as a [File].
- *
- * If it is not found, a [GradleException] is thrown.
- *
- * A CI instance comes with an RSA key. However, of course, the default key has no
- * privileges in Spine repositories. Thus, we add our own RSA key — `deploy_rsa_key`.
- * It must have `write` rights in the associated repository.
- * Also, we don't want that key to be used for anything else but GitHub Pages publishing.
- *
- * Thus, we configure the SSH agent to use the `deploy_rsa_key`
- * only for specific references, namely in `github.com-publish`.
- */
- private fun gitHubKey(): File {
- val gitHubAccessKey = File("${rootFolder.absolutePath}/deploy_key_rsa")
-
- if (!gitHubAccessKey.exists()) {
- throw GradleException(
- "File $gitHubAccessKey does not exist. It should be encrypted" +
- " in the repository and decrypted on CI."
- )
- }
- return gitHubAccessKey
- }
-
- private fun sshConfigFile(): File {
- val sshConfigFile = File("${System.getProperty("user.home")}/.ssh/config")
- if (!sshConfigFile.exists()) {
- val parentDir = sshConfigFile.canonicalFile.parentFile
- parentDir.mkdirs()
- sshConfigFile.createNewFile()
- }
- return sshConfigFile
- }
-
- /** Executes a command in the project [rootFolder]. */
- private fun execute(vararg command: String): String = Cli(rootFolder).execute(*command)
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/UpdateGitHubPages.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/UpdateGitHubPages.kt
deleted file mode 100644
index e3c1e6ce9..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/UpdateGitHubPages.kt
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle.github.pages
-
-import io.spine.internal.gradle.fs.LazyTempPath
-import io.spine.internal.gradle.github.pages.TaskName.copyJavadoc
-import io.spine.internal.gradle.github.pages.TaskName.updateGitHubPages
-import io.spine.internal.gradle.isSnapshot
-import io.spine.internal.gradle.javadoc.ExcludeInternalDoclet
-import io.spine.internal.gradle.javadoc.javadocTask
-import java.io.File
-import org.gradle.api.Plugin
-import org.gradle.api.Project
-import org.gradle.api.Task
-import org.gradle.api.tasks.Copy
-import org.gradle.api.tasks.TaskContainer
-import org.gradle.api.tasks.TaskProvider
-
-/**
- * Registers the `updateGitHubPages` task which performs the update of the GitHub Pages
- * with the Javadoc generated for a particular Gradle project. The generated documentation
- * is appended to the `spine.io` site via GitHub pages by pushing commits to the `gh-pages` branch.
- *
- * Please note that the update is only performed for the projects which are NOT snapshots.
- *
- * Users may supply [allowInternalJavadoc][UpdateGitHubPagesExtension.allowInternalJavadoc] option,
- * which if `true`, includes the documentation for types marked `@Internal`.
- * By default, this option is `false`.
- *
- * Usage:
- * ```
- * updateGitHubPages {
- *
- * // Include `@Internal`-annotated types.
- * allowInternalJavadoc.set(true)
- *
- * // Propagate the full path to the local folder of the repository root.
- * rootFolder.set(rootDir.absolutePath)
- * }
- * ```
- *
- * In order to work, the script needs a `deploy_key_rsa` private RSA key file in the repository
- * root. It is recommended to decrypt it in the repository and then decrypt it on CI upon
- * publication. Also, the script uses the `FORMAL_GIT_HUB_PAGES_AUTHOR` environment variable to
- * set the author email for the commits. The `gh-pages` branch itself should exist before the plugin
- * is run.
- *
- * NOTE: when changing the value of "FORMAL_GIT_HUB_PAGES_AUTHOR", one also must change
- * the SSH private (encrypted `deploy_key_rsa`) and the public ("GitHub Pages publisher (Travis CI)"
- * on GitHub) keys.
- *
- * Another requirement is an environment variable `REPO_SLUG`, which is set by the CI environment,
- * such as `Publish` GitHub Actions workflow. It points to the repository for which the update
- * is executed. E.g.:
- *
- * ```
- * REPO_SLUG: SpineEventEngine/base
- * ```
- *
- * @see UpdateGitHubPagesExtension for the extension which is used to configure this plugin
- */
-class UpdateGitHubPages : Plugin {
-
- /**
- * Root folder of the repository, to which this `Project` belongs.
- */
- internal lateinit var rootFolder: File
-
- /**
- * The external inputs to include into the publishing.
- *
- * The inputs are evaluated according to [Copy.from] specification.
- */
- private lateinit var includedInputs: Set
-
- /**
- * Path to the temp folder used to gather the Javadoc output
- * before submitting it to the GitHub Pages update.
- */
- internal val javadocOutputPath = LazyTempPath("javadoc")
-
- /**
- * Path to the temp folder used checkout the original GitHub Pages branch.
- */
- internal val checkoutTempFolder = LazyTempPath("repoTemp")
-
- /**
- * Applies the plugin to the specified [project].
- *
- * If the project version says it is a snapshot, the plugin registers a no-op task.
- *
- * Even in such a case, the extension object is still created in the given project, to allow
- * customization of the parameters in its build script, for later usage when the project
- * version changes to non-snapshot.
- */
- override fun apply(project: Project) {
- val extension = UpdateGitHubPagesExtension.createIn(project)
- project.afterEvaluate {
- val projectVersion = project.version.toString()
- if (projectVersion.isSnapshot()) {
- registerNoOpTask()
- } else {
- registerTasks(extension)
- }
- }
- }
-
- private fun Project.registerTasks(extension: UpdateGitHubPagesExtension) {
- val allowInternalJavadoc = extension.allowInternalJavadoc()
- rootFolder = extension.rootFolder()
- includedInputs = extension.includedInputs()
- if (!allowInternalJavadoc) {
- val doclet = ExcludeInternalDoclet(extension.excludeInternalDocletVersion)
- doclet.registerTaskIn(this)
- }
- tasks.registerCopyJavadoc(allowInternalJavadoc)
- val updatePagesTask = tasks.registerUpdateTask()
- updatePagesTask.configure {
- dependsOn(copyJavadoc)
- }
- }
-
- private fun TaskContainer.registerCopyJavadoc(allowInternalJavadoc: Boolean) {
- val inputs = composeInputs(allowInternalJavadoc)
- register(copyJavadoc, Copy::class.java) {
- doLast {
- from(*inputs.toTypedArray())
- into(javadocOutputPath)
- }
- }
- }
-
- private fun TaskContainer.composeInputs(allowInternalJavadoc: Boolean): MutableList {
- val inputs = mutableListOf()
- if (allowInternalJavadoc) {
- inputs.add(javadocTask())
- } else {
- inputs.add(javadocTask(ExcludeInternalDoclet.taskName))
- }
- inputs.addAll(includedInputs)
- return inputs
- }
-
- private fun TaskContainer.registerUpdateTask(): TaskProvider {
- return register(updateGitHubPages) {
- doLast {
- try {
- updateGhPages(project)
- } finally {
- cleanup()
- }
- }
- }
- }
-
- private fun cleanup() {
- val folders = listOf(checkoutTempFolder, javadocOutputPath)
- folders.forEach {
- it.toFile().deleteRecursively()
- }
- }
-}
-
-/**
- * Registers `updateGitHubPages` task which performs no actual update, but prints the message
- * telling the update is skipped, since the project is in its `SNAPSHOT` version.
- */
-private fun Project.registerNoOpTask() {
- tasks.register(updateGitHubPages) {
- doLast {
- val project = this@registerNoOpTask
- println(
- "GitHub Pages update will be skipped since this project is a snapshot: " +
- "`${project.name}-${project.version}`."
- )
- }
- }
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/TaskContainerExtensions.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/TaskContainerExtensions.kt
deleted file mode 100644
index f80d53ee1..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/TaskContainerExtensions.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle.javadoc
-
-import org.gradle.api.tasks.TaskContainer
-import org.gradle.api.tasks.javadoc.Javadoc
-
-/**
- * Finds a [Javadoc] Gradle task by the passed name.
- */
-fun TaskContainer.javadocTask(named: String) = this.getByName(named) as Javadoc
-
-/**
- * Finds a default [Javadoc] Gradle task.
- */
-fun TaskContainer.javadocTask() = this.getByName("javadoc") as Javadoc
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/protobuf/ProtoTaskExtensions.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/protobuf/ProtoTaskExtensions.kt
deleted file mode 100644
index 79c2a1640..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/protobuf/ProtoTaskExtensions.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle.protobuf
-
-import com.google.protobuf.gradle.GenerateProtoTask
-import java.io.File
-import org.gradle.configurationcache.extensions.capitalized
-import org.gradle.kotlin.dsl.get
-
-/**
- * Configures protobuf code generation task.
- *
- * The task configuration consists of the following steps.
- *
- * 1. Generation of descriptor set file is turned op for each source set.
- * These files are placed under the `build/descriptors` directory.
- *
- * 2. At the final steps of the code generation, the code belonging to
- * the `com.google` package is removed.
- *
- * 3. Make `processResource` tasks depend on corresponding `generateProto` tasks.
- * If the source set of the configured task isn't `main`, appropriate infix for
- * the task names is used.
- *
- * The usage of this extension in a module build file would be:
- * ```
- * val generatedDir by extra("$projectDir/generated")
- * protobuf {
- * generateProtoTasks {
- * for (task in all()) {
- * task.setup(generatedDir)
- * }
- * }
- * }
- * ```
- * Using the same code under `subprojects` in a root build file does not seem work because
- * test descriptor set files are not copied to resources. Performing this configuration from
- * subprojects solves the issue.
- */
-@Suppress("unused")
-fun GenerateProtoTask.setup(generatedDir: String) {
-
- /**
- * Generate descriptor set files.
- */
- val ssn = sourceSet.name
- generateDescriptorSet = true
- with(descriptorSetOptions) {
- path = "${project.buildDir}/descriptors/${ssn}/known_types_${ssn}.desc"
- includeImports = true
- includeSourceInfo = true
- }
-
- /**
- * Remove the code generated for Google Protobuf library types.
- *
- * Java code for the `com.google` package was generated because we wanted
- * to have descriptors for all the types, including those from Google Protobuf library.
- * We want all the descriptors so that they are included into the resources used by
- * the `io.spine.type.KnownTypes` class.
- *
- * Now, as we have the descriptors _and_ excessive Java code, we delete it to avoid
- * classes that duplicate those coming from Protobuf library JARs.
- */
- doLast {
- val comPackage = File("${generatedDir}/${ssn}/java/com")
- val googlePackage = comPackage.resolve("google")
-
- project.delete(googlePackage)
-
- // We don't need an empty `com` package.
- if (comPackage.exists() && comPackage.list()?.isEmpty() == true) {
- project.delete(comPackage)
- }
- }
-
- /**
- * Make the tasks `processResources` depend on `generateProto` tasks explicitly so that:
- * 1) descriptor set files get into resources, avoiding the racing conditions
- * during the build.
- * 2) we don't have the warning "Execution optimizations have been disabled..." issued
- * by Gradle during the build because Protobuf Gradle Plugin does not set
- * dependencies between `generateProto` and `processResources` tasks.
- */
- val processResources = processResourceTaskName(ssn)
- project.tasks[processResources].dependsOn(this)
-}
-
-/**
- * Obtains the name of the task `processResource` task for the given source set name.
- */
-fun processResourceTaskName(sourceSetName: String): String {
- val infix = if (sourceSetName == "main") "" else sourceSetName.capitalized()
- return "process${infix}Resources"
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/Artifacts.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/Artifacts.kt
deleted file mode 100644
index e6860cdd8..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/Artifacts.kt
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle.publish
-
-import io.spine.internal.gradle.sourceSets
-import org.gradle.api.Project
-import org.gradle.api.file.FileTreeElement
-import org.gradle.api.tasks.TaskContainer
-import org.gradle.api.tasks.TaskProvider
-import org.gradle.api.tasks.bundling.Jar
-import org.gradle.kotlin.dsl.get
-import org.gradle.kotlin.dsl.named
-import org.gradle.kotlin.dsl.register
-import org.gradle.kotlin.dsl.withType
-
-/**
- * Excludes Google `.proto` sources from all artifacts.
- *
- * Goes through all registered `Jar` tasks and filters out Google's files.
- */
-@Suppress("unused")
-fun TaskContainer.excludeGoogleProtoFromArtifacts() {
- withType().configureEach {
- exclude { it.isGoogleProtoSource() }
- }
-}
-
-/**
- * Checks if the given file belongs to the Google `.proto` sources.
- */
-private fun FileTreeElement.isGoogleProtoSource(): Boolean {
- val pathSegments = relativePath.segments
- return pathSegments.isNotEmpty() && pathSegments[0].equals("google")
-}
-
-/**
- * Locates or creates `sourcesJar` task in this [Project].
- *
- * The output of this task is a `jar` archive. The archive contains sources from `main` source set.
- * The task makes sure that sources from the directories below will be included into
- * a resulted archive:
- *
- * - Kotlin
- * - Java
- * - Proto
- *
- * Java and Kotlin sources are default to `main` source set since it is created by `java` plugin.
- * For Proto sources to be included – [special treatment][protoSources] is needed.
- */
-internal fun Project.sourcesJar() = tasks.getOrCreate("sourcesJar") {
- archiveClassifier.set("sources")
- from(sourceSets["main"].allSource) // Puts Java and Kotlin sources.
- from(protoSources()) // Puts Proto sources.
-}
-
-/**
- * Locates or creates `protoJar` task in this [Project].
- *
- * The output of this task is a `jar` archive. The archive contains only
- * [Proto sources][protoSources] from `main` source set.
- */
-internal fun Project.protoJar() = tasks.getOrCreate("protoJar") {
- archiveClassifier.set("proto")
- from(protoSources())
-}
-
-/**
- * Locates or creates `testJar` task in this [Project].
- *
- * The output of this task is a `jar` archive. The archive contains compilation output
- * of `test` source set.
- */
-internal fun Project.testJar() = tasks.getOrCreate("testJar") {
- archiveClassifier.set("test")
- from(sourceSets["test"].output)
-}
-
-/**
- * Locates or creates `javadocJar` task in this [Project].
- *
- * The output of this task is a `jar` archive. The archive contains Javadoc,
- * generated upon Java sources from `main` source set. If javadoc for Kotlin is also needed,
- * apply Dokka plugin. It tunes `javadoc` task to generate docs upon Kotlin sources as well.
- */
-internal fun Project.javadocJar() = tasks.getOrCreate("javadocJar") {
- archiveClassifier.set("javadoc")
- from(files("$buildDir/docs/javadoc"))
- dependsOn("javadoc")
-}
-
-/**
- * Locates or creates `dokkaJar` task in this [Project].
- *
- * The output of this task is a `jar` archive. The archive contains the Dokka output, generated upon
- * Java sources from `main` source set. Requires Dokka to be configured in the target project by
- * applying `dokka-for-java` plugin.
- */
-internal fun Project.dokkaJar() = tasks.getOrCreate("dokkaJar") {
- archiveClassifier.set("dokka")
- from(files("$buildDir/docs/dokka"))
- dependsOn("dokkaHtml")
-}
-
-private fun TaskContainer.getOrCreate(name: String, init: Jar.() -> Unit): TaskProvider =
- if (names.contains(name)) {
- named(name)
- } else {
- register(name) {
- init()
- }
- }
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/IncrementGuard.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/IncrementGuard.kt
deleted file mode 100644
index 0423ee088..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/IncrementGuard.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-@file:Suppress("unused")
-
-package io.spine.internal.gradle.publish
-
-import org.gradle.api.Plugin
-import org.gradle.api.Project
-
-/**
- * Gradle plugin which adds a [CheckVersionIncrement] task.
- *
- * The task is called `checkVersionIncrement` inserted before the `check` task.
- */
-class IncrementGuard : Plugin {
-
- companion object {
- const val taskName = "checkVersionIncrement"
- }
-
- /**
- * Adds the [CheckVersionIncrement] task to the project.
- *
- * Only adds the check if the project is built on Travis CI and the job is a pull request.
- *
- * The task only runs on non-master branches on GitHub Actions. This is done
- * to prevent unexpected CI fails when re-building `master` multiple times, creating git
- * tags, and in other cases that go outside of the "usual" development cycle.
- */
- override fun apply(target: Project) {
- val tasks = target.tasks
- tasks.register(taskName, CheckVersionIncrement::class.java) {
- repository = CloudRepo.published
- tasks.getByName("check").dependsOn(this)
-
- shouldRunAfter("test")
- if (!shouldCheckVersion()) {
- logger.info(
- "The build does not represent a GitHub Actions feature branch job, " +
- "the `checkVersionIncrement` task is disabled."
- )
- this.enabled = false
- }
- }
- }
-
- /**
- * Returns `true` if the current build is a GitHub Actions build which represents a push
- * to a feature branch.
- *
- * Returns `false` if the associated reference is not a branch (e.g. a tag) or if it has
- * the name which ends with `master`. So, on branches such as `master` and `2.x-jdk8-master`
- * this method would return `false`.
- *
- * @see
- * List of default environment variables provided for GitHub Actions builds
- */
- private fun shouldCheckVersion(): Boolean {
- val eventName = System.getenv("GITHUB_EVENT_NAME")
- if ("push" != eventName) {
- return false
- }
- val reference = System.getenv("GITHUB_REF") ?: return false
- val matches = Regex("refs/heads/(.+)").matchEntire(reference) ?: return false
- val branch = matches.groupValues[1]
- return !branch.endsWith("master")
- }
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/MavenJavaPublication.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/MavenJavaPublication.kt
deleted file mode 100644
index cb5fa2f0d..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/MavenJavaPublication.kt
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle.publish
-
-import io.spine.internal.gradle.Repository
-import io.spine.internal.gradle.isSnapshot
-import org.gradle.api.Project
-import org.gradle.api.artifacts.dsl.RepositoryHandler
-import org.gradle.api.publish.PublishingExtension
-import org.gradle.api.publish.maven.MavenPublication
-import org.gradle.api.tasks.TaskProvider
-import org.gradle.api.tasks.bundling.Jar
-import org.gradle.kotlin.dsl.create
-import org.gradle.kotlin.dsl.get
-import org.gradle.kotlin.dsl.getByType
-
-/**
- * A publication for a typical Java project.
- *
- * In Gradle, in order to publish something somewhere one should create a publication.
- * A publication has a name and consists of one or more artifacts plus information about
- * those artifacts – the metadata.
- *
- * An instance of this class represents [MavenPublication] named "mavenJava". It is generally
- * accepted that a publication with this name contains a Java project published to one or
- * more Maven repositories.
- *
- * By default, only a jar with the compilation output of `main` source set and its
- * metadata files are published. Other artifacts are specified through the
- * [constructor parameter][jars]. Please, take a look on [specifyArtifacts] for additional info.
- *
- * See: [Maven Publish Plugin | Publications](https://docs.gradle.org/current/userguide/publishing_maven.html#publishing_maven:publications)
- *
- * @param artifactId a name that a project is known by.
- * @param jars list of artifacts to be published along with the compilation output.
- * @param destinations Maven repositories to which the produced artifacts will be sent.
- */
-internal class MavenJavaPublication(
- private val artifactId: String,
- private val jars: Set>,
- private val destinations: Set,
-) {
-
- /**
- * Registers this publication in the given project.
- *
- * The only prerequisite for the project is to have `maven-publish` plugin applied.
- */
- fun registerIn(project: Project) {
- createPublication(project)
- registerDestinations(project)
- }
-
- /**
- * Creates a new "mavenJava" [MavenPublication] in the given project.
- */
- private fun createPublication(project: Project) {
- val gradlePublishing = project.extensions.getByType()
- val gradlePublications = gradlePublishing.publications
- gradlePublications.create("mavenJava") {
- specifyMavenCoordinates(project)
- specifyArtifacts(project)
- }
- }
-
- private fun MavenPublication.specifyMavenCoordinates(project: Project) {
- groupId = project.group.toString()
- artifactId = this@MavenJavaPublication.artifactId
- version = project.version.toString()
- }
-
- /**
- * Specifies which artifacts this [MavenPublication] will contain.
- *
- * A typical Maven publication contains:
- *
- * 1. Jar archives. For example: compilation output, sources, javadoc, etc.
- * 2. Maven metadata file that has ".pom" extension.
- * 3. Gradle metadata file that has ".module" extension.
- *
- * Metadata files contain information about a publication itself, its artifacts and their
- * dependencies. Presence of ".pom" file is mandatory for publication to be consumed by
- * `mvn` build tool itself or other build tools that understand Maven notation (Gradle, Ivy).
- * Presence of ".module" is optional, but useful when a publication is consumed by Gradle.
- *
- * See: [Maven – POM Reference](https://maven.apache.org/pom.html)
- * [Understanding Gradle Module Metadata](https://docs.gradle.org/current/userguide/publishing_gradle_module_metadata.html)
- */
- private fun MavenPublication.specifyArtifacts(project: Project) {
-
- // "java" component provides a jar with compilation output of "main" source set.
- // It is NOT defined as another `Jar` task intentionally. Doing that will leave the
- // publication without correct ".pom" and ".module" metadata files generated.
- from(project.components["java"])
-
- // Other artifacts are represented by `Jar` tasks. Those artifacts don't bring any other
- // metadata in comparison with `Component` (such as dependencies notation).
- jars.forEach {
- artifact(it)
- }
- }
-
- /**
- * Goes through the [destinations] and registers each as a repository for publishing
- * in the given Gradle project.
- */
- private fun registerDestinations(project: Project) {
- val gradlePublishing = project.extensions.getByType()
- val isSnapshot = project.version.toString().isSnapshot()
- val gradleRepositories = gradlePublishing.repositories
- destinations.forEach { destination ->
- gradleRepositories.register(destination, isSnapshot, project)
- }
- }
-
- private fun RepositoryHandler.register(
- repository: Repository,
- isSnapshot: Boolean,
- project: Project
- ) {
- val target = if (isSnapshot) repository.snapshots else repository.releases
- val credentials = repository.credentials(project.rootProject)
- maven {
- url = project.uri(target)
- credentials {
- username = credentials?.username
- password = credentials?.password
- }
- }
- }
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/PublishingConfig.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/PublishingConfig.kt
deleted file mode 100644
index f2f252e0d..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/PublishingConfig.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle.publish
-
-import io.spine.internal.gradle.Repository
-import org.gradle.api.Project
-import org.gradle.api.tasks.TaskProvider
-import org.gradle.api.tasks.bundling.Jar
-import org.gradle.kotlin.dsl.apply
-
-/**
- * Information, required to set up publishing of a project using `maven-publish` plugin.
- *
- * @param artifactId a name that a project is known by.
- * @param destinations set of repositories, to which the resulting artifacts will be sent.
- * @param includeProtoJar tells whether [protoJar] artifact should be published.
- * @param includeTestJar tells whether [testJar] artifact should be published.
- * @param includeDokkaJar tells whether [dokkaJar] artifact should be published.
- */
-internal class PublishingConfig(
- val artifactId: String,
- val destinations: Set,
- val includeProtoJar: Boolean = true,
- val includeTestJar: Boolean = false,
- val includeDokkaJar: Boolean = false
-)
-
-/**
- * Applies this configuration to the given project.
- *
- * This method does the following:
- *
- * 1. Applies `maven-publish` plugin to the project.
- * 2. Registers [MavenJavaPublication] in Gradle's [PublicationContainer][org.gradle.api.publish.PublicationContainer].
- * 4. Configures "publish" task.
- *
- * The actual list of resulted artifacts is determined by [registerArtifacts].
- */
-internal fun PublishingConfig.apply(project: Project) = with(project) {
- apply(plugin = "maven-publish")
- createPublication(project)
- configurePublishTask(destinations)
-}
-
-private fun PublishingConfig.createPublication(project: Project) {
- val artifacts = project.registerArtifacts(includeProtoJar, includeTestJar, includeDokkaJar)
- val publication = MavenJavaPublication(
- artifactId = artifactId,
- jars = artifacts,
- destinations = destinations
- )
- publication.registerIn(project)
-}
-
-/**
- * Registers [Jar] tasks, output of which is used as Maven artifacts.
- *
- * By default, only a jar with java compilation output is included into publication. This method
- * registers tasks which produce additional artifacts.
- *
- * The list of additional artifacts to be registered:
- *
- * 1. [sourcesJar] – Java, Kotlin and Proto source files.
- * 2. [protoJar] – only Proto source files.
- * 3. [javadocJar] – documentation, generated upon Java files.
- * 4. [testJar] – compilation output of "test" source set.
- * 5. [dokkaJar] - documentation generated by Dokka.
- *
- * Registration of [protoJar], [testJar] and [dokkaJar] is optional. It can be controlled by the
- * method's parameters.
- *
- * @return the list of the registered tasks.
- */
-private fun Project.registerArtifacts(
- includeProtoJar: Boolean = true,
- includeTestJar: Boolean = false,
- includeDokkaJar: Boolean = false
-): Set> {
-
- val artifacts = mutableSetOf(
- sourcesJar(),
- javadocJar(),
- )
-
- // We don't want to have an empty "proto.jar" when a project doesn't have any Proto files.
- if (hasProto() && includeProtoJar) {
- artifacts.add(protoJar())
- }
-
- // Here, we don't have the corresponding `hasTests()` check, since this artifact is disabled
- // by default. And turning it on means "We have tests and need them to be published."
- if (includeTestJar) {
- artifacts.add(testJar())
- }
-
- if(includeDokkaJar) {
- artifacts.add(dokkaJar())
- }
-
- return artifacts
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/SpinePublishing.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/SpinePublishing.kt
deleted file mode 100644
index 1b10f71e7..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/SpinePublishing.kt
+++ /dev/null
@@ -1,409 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle.publish
-
-import io.spine.internal.gradle.Repository
-import org.gradle.api.Project
-import org.gradle.kotlin.dsl.create
-import org.gradle.kotlin.dsl.findByType
-
-/**
- * Configures [SpinePublishing] extension.
- *
- * This extension sets up publishing of artifacts to Maven repositories.
- *
- * The extension can be configured for single- and multi-module projects.
- *
- * When used with a multi-module project, the extension should be opened in a root project's
- * build file. The published modules are specified explicitly by their names:
- *
- * ```
- * spinePublishing {
- * modules = setOf(
- * "subprojectA",
- * "subprojectB",
- * )
- * destinations = setOf(
- * PublishingRepos.cloudRepo,
- * PublishingRepos.cloudArtifactRegistry,
- * )
- * }
- * ```
- *
- * When used with a single-module project, the extension should be opened in a project's build file.
- * Only destinations should be specified:
- *
- * ```
- * spinePublishing {
- * destinations = setOf(
- * PublishingRepos.cloudRepo,
- * PublishingRepos.cloudArtifactRegistry,
- * )
- * }
- * ```
- *
- * It is worth to mention, that publishing of a module can be configured only from a single place.
- * For example, declaring `subprojectA` as published in a root project and opening
- * `spinePublishing` extension within `subprojectA` itself would lead to an exception.
- *
- * In Gradle, in order to publish something somewhere one should create a publication. In each
- * of published modules, the extension will create a [publication][MavenJavaPublication]
- * named "mavenJava". All artifacts, published by this extension belong to this publication.
- *
- * By default, along with the compilation output of "main" source set, the extension publishes
- * the following artifacts:
- *
- * 1. [sourcesJar] – sources from "main" source set. Includes "hand-made" Java,
- * Kotlin and Proto files. In order to include the generated code into this artifact, a module
- * should specify those files as a part of "main" source set.
- *
- * Here's an example of how to do that:
- *
- * ```
- * sourceSets {
- * val generatedDir by extra("$projectDir/generated")
- * val generatedSpineDir by extra("$generatedDir/main/java")
- * main {
- * java.srcDir(generatedSpineDir)
- * }
- * }
- * ```
- * 2. [protoJar] – only Proto sources from "main" source set. It's published only if
- * Proto files are actually present in the source set. Publication of this artifact is optional
- * and can be disabled via [SpinePublishing.protoJar].
- * 3. [javadocJar] - javadoc, generated upon Java sources from "main" source set.
- * If javadoc for Kotlin is also needed, apply Dokka plugin. It tunes `javadoc` task to generate
- * docs upon Kotlin sources as well.
- *
- * Additionally, [testJar] artifact can be published. This artifact contains compilation output
- * of "test" source set. Use [SpinePublishing.testJar] to enable its publishing.
- *
- * @see [registerArtifacts]
- */
-fun Project.spinePublishing(configuration: SpinePublishing.() -> Unit) {
- val name = SpinePublishing::class.java.simpleName
- val extension = with(extensions) { findByType() ?: create(name, project) }
- extension.run {
- configuration()
- configured()
- }
-}
-
-/**
- * A Gradle extension for setting up publishing of spine modules using `maven-publish` plugin.
- *
- * @param project a project in which the extension is opened. By default, this project will be
- * published as long as a [set][modules] of modules to publish is not specified explicitly.
- *
- * @see spinePublishing
- */
-open class SpinePublishing(private val project: Project) {
-
- private val protoJar = ProtoJar()
- private val testJar = TestJar()
- private val dokkaJar = DokkaJar()
-
- /**
- * Set of modules to be published.
- *
- * Both module's name or path can be used.
- *
- * Use this property if the extension is configured from a root project's build file.
- *
- * If left empty, the [project], in which the extension is opened, will be published.
- *
- * Empty by default.
- */
- var modules: Set = emptySet()
-
- /**
- * Set of repositories, to which the resulting artifacts will be sent.
- *
- * Usually, Spine-related projects are published to one or more repositories,
- * declared in [PublishingRepos]:
- *
- * ```
- * destinations = setOf(
- * PublishingRepos.cloudRepo,
- * PublishingRepos.cloudArtifactRegistry,
- * PublishingRepos.gitHub("base"),
- * )
- * ```
- *
- * Empty by default.
- */
- var destinations: Set = emptySet()
-
- /**
- * A prefix to be added before the name of each artifact.
- *
- * Default value is "spine-".
- */
- var artifactPrefix: String = "spine-"
-
- /**
- * Allows disabling publishing of [protoJar] artifact, containing all Proto sources
- * from `sourceSets.main.proto`.
- *
- * Here's an example of how to disable it for some of published modules:
- *
- * ```
- * spinePublishing {
- * modules = setOf(
- * "subprojectA",
- * "subprojectB",
- * )
- * protoJar {
- * exclusions = setOf(
- * "subprojectB",
- * )
- * }
- * }
- * ```
- *
- * For all modules, or when the extension is configured within a published module itself:
- *
- * ```
- * spinePublishing {
- * protoJar {
- * disabled = true
- * }
- * }
- * ```
- *
- * The resulting artifact is available under "proto" classifier. I.e., in Gradle 7+, one could
- * depend on it like this:
- *
- * ```
- * implementation("io.spine:spine-client:$version@proto")
- * ```
- */
- fun protoJar(configuration: ProtoJar.() -> Unit) = protoJar.run(configuration)
-
- /**
- * Allows enabling publishing of [testJar] artifact, containing compilation output
- * of "test" source set.
- *
- * Here's an example of how to enable it for some of published modules:
- *
- * ```
- * spinePublishing {
- * modules = setOf(
- * "subprojectA",
- * "subprojectB",
- * )
- * testJar {
- * inclusions = setOf(
- * "subprojectB",
- * )
- * }
- * }
- * ```
- *
- * For all modules, or when the extension is configured within a published module itself:
- *
- * ```
- * spinePublishing {
- * testJar {
- * enabled = true
- * }
- * }
- * ```
- *
- * The resulting artifact is available under "test" classifier. I.e., in Gradle 7+, one could
- * depend on it like this:
- *
- * ```
- * implementation("io.spine:spine-client:$version@test")
- * ```
- */
- fun testJar(configuration: TestJar.() -> Unit) = testJar.run(configuration)
-
-
- /**
- * Configures publishing of [dokkaJar] artifact, containing Dokka-generated documentation. By
- * default, publishing of the artifact is disabled.
- *
- * Remember that the Dokka Gradle plugin should be applied to publish this artifact as it is
- * produced by the `dokkaHtml` task. It can be done by using the
- * [io.spine.internal.dependency.Dokka] dependency object or by applying the
- * `buildSrc/src/main/kotlin/dokka-for-java` script plugin for Java projects.
- *
- * Here's an example of how to use this option:
- *
- * ```
- * spinePublishing {
- * dokkaJar {
- * enabled = true
- * }
- * }
- * ```
- *
- * The resulting artifact is available under "dokka" classifier.
- */
- fun dokkaJar(configuration: DokkaJar.() -> Unit) = dokkaJar.run(configuration)
-
- /**
- * Called to notify the extension that its configuration is completed.
- *
- * On this stage the extension will validate the received configuration and set up
- * `maven-publish` plugin for each published module.
- */
- internal fun configured() {
-
- ensureProtoJarExclusionsArePublished()
- ensureTestJarInclusionsArePublished()
- ensuresModulesNotDuplicated()
-
- val protoJarExclusions = protoJar.exclusions
- val testJarInclusions = testJar.inclusions
- val publishedProjects = publishedProjects()
-
- publishedProjects.forEach { project ->
- val name = project.name
- val includeProtoJar = (protoJarExclusions.contains(name) || protoJar.disabled).not()
- val includeTestJar = (testJarInclusions.contains(name) || testJar.enabled)
- setUpPublishing(project, includeProtoJar, includeTestJar, dokkaJar.enabled)
- }
- }
-
- /**
- * Maps the names of published modules to [Project] instances.
- *
- * The method considers two options:
- *
- * 1. The [set][modules] of subprojects to publish is not empty. It means that the extension
- * is opened from a root project.
- * 2. The [set][modules] is empty. Then, the [project] in which the extension is opened
- * will be published.
- *
- * @see modules
- */
- private fun publishedProjects() = modules.map { name -> project.project(name) }
- .ifEmpty { setOf(project) }
-
- /**
- * Sets up `maven-publish` plugin for the given project.
- *
- * Firstly, an instance of [PublishingConfig] is assembled for the project. Then, this
- * config is applied.
- *
- * This method utilizes `project.afterEvaluate` closure. General rule of thumb is to avoid using
- * of this closure, as it configures a project when its configuration is considered completed.
- * Which is quite counter-intuitive.
- *
- * The root cause why it is used here is a possibility to configure publishing of multiple
- * modules from a root project. When this possibility is employed, in fact, we configure
- * publishing for a module, build file of which has not been even evaluated by that time.
- * That leads to an unexpected behavior.
- *
- * The simplest example here is specifying of `version` and `group` for Maven coordinates.
- * Let's suppose, they are declared in a module's build file. It is a common practice.
- * But publishing of the module is configured from a root project's build file. By the time,
- * when we need to specify them, we just don't know them. As a result, we have to use
- * `project.afterEvaluate` in order to guarantee that a module will be configured by the time
- * we configure publishing for it.
- */
- private fun setUpPublishing(
- project: Project,
- includeProtoJar: Boolean,
- includeTestJar: Boolean,
- includeDokkaJar: Boolean
- ) {
- val artifactId = artifactId(project)
- val publishingConfig = PublishingConfig(
- artifactId,
- destinations,
- includeProtoJar,
- includeTestJar,
- includeDokkaJar
- )
- project.afterEvaluate {
- publishingConfig.apply(project)
- }
- }
-
- /**
- * Obtains an artifact ID for the given project.
- *
- * It consists of a project's name and [prefix][artifactPrefix]:
- * ``.
- */
- internal fun artifactId(project: Project): String = "$artifactPrefix${project.name}"
-
- /**
- * Ensures that all modules, marked as excluded from [protoJar] publishing,
- * are actually published.
- *
- * It makes no sense to tell a module don't publish [protoJar] artifact, if the module is not
- * published at all.
- */
- private fun ensureProtoJarExclusionsArePublished() {
- val nonPublishedExclusions = protoJar.exclusions.minus(modules)
- if (nonPublishedExclusions.isNotEmpty()) {
- throw IllegalStateException("One or more modules are marked as `excluded from proto " +
- "JAR publication`, but they are not even published: $nonPublishedExclusions")
- }
- }
-
- /**
- * Ensures that all modules, marked as included into [testJar] publishing,
- * are actually published.
- *
- * It makes no sense to tell a module publish [testJar] artifact, if the module is not
- * published at all.
- */
- private fun ensureTestJarInclusionsArePublished() {
- val nonPublishedInclusions = testJar.inclusions.minus(modules)
- if (nonPublishedInclusions.isNotEmpty()) {
- throw IllegalStateException("One or more modules are marked as `included into test " +
- "JAR publication`, but they are not even published: $nonPublishedInclusions")
- }
- }
-
- /**
- * Ensures that publishing of a module is configured only from a single place.
- *
- * We allow configuration of publishing from two places - a root project and module itself.
- * Here we verify that publishing of a module is not configured in both places simultaneously.
- */
- private fun ensuresModulesNotDuplicated() {
- val rootProject = project.rootProject
- if (rootProject == project) {
- return
- }
-
- val rootExtension = with(rootProject.extensions) { findByType() }
- rootExtension?.let { rootPublishing ->
- val thisProject = setOf(project.name, project.path)
- if (thisProject.minus(rootPublishing.modules).size != 2) {
- throw IllegalStateException("Publishing of `$thisProject` module is already " +
- "configured in a root project!")
- }
- }
- }
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/Tasks.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/Tasks.kt
deleted file mode 100644
index b132638cd..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/Tasks.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle.publish
-
-import io.spine.internal.gradle.Repository
-import java.util.*
-import org.gradle.api.InvalidUserDataException
-import org.gradle.api.Project
-import org.gradle.api.Task
-import org.gradle.api.tasks.TaskContainer
-import org.gradle.api.tasks.TaskProvider
-
-private const val PUBLISH = "publish"
-
-/**
- * Locates `publish` task in this [TaskContainer].
- *
- * This task publishes all defined publications to all defined repositories. To achieve that,
- * the task depends on all `publish`*PubName*`PublicationTo`*RepoName*`Repository` tasks.
- *
- * Please note, task execution would not copy publications to the local Maven cache.
- *
- * @see
- * Tasks | Maven Publish Plugin
- */
-internal val TaskContainer.publish: TaskProvider
- get() = named(PUBLISH)
-
-/**
- * Sets dependencies for `publish` task in this [Project].
- *
- * This method performs the following:
- *
- * 1. When this [Project] is not a root, makes `publish` task in a root project
- * depend on a local `publish`.
- * 2. Makes local `publish` task verify that credentials are present for each
- * of destination repositories.
- */
-internal fun Project.configurePublishTask(destinations: Set) {
- attachCredentialsVerification(destinations)
- bindToRootPublish()
-}
-
-private fun Project.attachCredentialsVerification(destinations: Set) {
- val checkCredentials = tasks.registerCheckCredentialsTask(destinations)
- val localPublish = tasks.publish
- localPublish.configure { dependsOn(checkCredentials) }
-}
-
-private fun Project.bindToRootPublish() {
- if (project == rootProject) {
- return
- }
-
- val localPublish = tasks.publish
- val rootPublish = rootProject.tasks.getOrCreatePublishTask()
- rootPublish.configure { dependsOn(localPublish) }
-}
-
-/**
- * Use this task accessor when it is not guaranteed that the task is present
- * in this [TaskContainer].
- */
-private fun TaskContainer.getOrCreatePublishTask() =
- if (names.contains(PUBLISH)) {
- named(PUBLISH)
- } else {
- register(PUBLISH)
- }
-
-private fun TaskContainer.registerCheckCredentialsTask(destinations: Set) =
- register("checkCredentials") {
- doLast {
- destinations.forEach { it.ensureCredentials(project) }
- }
- }
-
-private fun Repository.ensureCredentials(project: Project) {
- val credentials = credentials(project)
- if (Objects.isNull(credentials)) {
- throw InvalidUserDataException(
- "No valid credentials for repository `${this}`. Please make sure " +
- "to pass username/password or a valid `.properties` file."
- )
- }
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/CodebaseFilter.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/CodebaseFilter.kt
deleted file mode 100644
index 89e291292..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/CodebaseFilter.kt
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle.report.coverage
-
-import com.google.errorprone.annotations.CanIgnoreReturnValue
-import io.spine.internal.gradle.report.coverage.FileFilter.generatedOnly
-import java.io.File
-import kotlin.streams.toList
-import org.gradle.api.Project
-import org.gradle.api.file.ConfigurableFileTree
-import org.gradle.api.file.FileTree
-import org.gradle.api.tasks.SourceSetOutput
-
-/**
- * Serves to distinguish the `.java` and `.class` files built on top of the Protobuf definitions
- * from the human-created production code.
- *
- * Works on top of the passed [source][srcDirs] and [output][outputDirs] directories, by analyzing
- * the source file names and finding the corresponding compiler output.
- */
-internal class CodebaseFilter(
- private val project: Project,
- private val srcDirs: Set,
- private val outputDirs: Set
-) {
-
- /**
- * Returns the file tree containing the compiled `.class` files which were produced
- * from the human-written production code.
- *
- * Such filtering excludes the output obtained from the generated sources.
- */
- internal fun humanProducedCompiledFiles(): List {
- log("Source dirs for the code coverage calculation:")
- this.srcDirs.forEach {
- log(" - $it")
- }
-
- val generatedClassNames = generatedClassNames()
- val humanProducedTree = outputDirs
- .stream()
- .flatMap { it.classesDirs.files.stream() }
- .map { srcFile ->
- log("Filtering out the generated classes for ${srcFile}.")
- project.fileTree(srcFile).without(generatedClassNames)
- }.toList()
- return humanProducedTree
- }
-
- private fun generatedClassNames(): List {
- val generatedSourceFiles = generatedOnly(srcDirs)
- val generatedNames = mutableListOf()
- generatedSourceFiles
- .filter { it.exists() && it.isDirectory }
- .forEach { folder ->
- folder.walk()
- .filter { !it.isDirectory }
- .forEach { file ->
- file.parseName(
- File::asJavaClassName,
- File::asGrpcClassName,
- File::asSpineClassName
- )?.let { clsName ->
- generatedNames.add(clsName)
- }
- }
- }
- return generatedNames
- }
-
- private fun log(message: String) {
- project.logger.info(message)
- }
-}
-
-/**
- * Excludes the elements which [Java compiled file names][File.asJavaCompiledClassName]
- * are present among the passed [names].
- *
- * Returns the same instance of `ConfigurableFileTree`, for call chaining.
- */
-@CanIgnoreReturnValue
-private fun ConfigurableFileTree.without(names: List): ConfigurableFileTree {
- this.exclude { element ->
- val className = element.file.asJavaCompiledClassName()
- names.contains(className)
- }
- return this
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileExtensions.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileExtensions.kt
deleted file mode 100644
index 4a4279793..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileExtensions.kt
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle.report.coverage
-
-import io.spine.internal.gradle.report.coverage.FileExtension.COMPILED_CLASS
-import io.spine.internal.gradle.report.coverage.FileExtension.JAVA_SOURCE
-import io.spine.internal.gradle.report.coverage.PathMarker.ANONYMOUS_CLASS
-import io.spine.internal.gradle.report.coverage.PathMarker.GENERATED
-import io.spine.internal.gradle.report.coverage.PathMarker.GRPC_SRC_FOLDER
-import io.spine.internal.gradle.report.coverage.PathMarker.JAVA_OUTPUT_FOLDER
-import io.spine.internal.gradle.report.coverage.PathMarker.JAVA_SRC_FOLDER
-import io.spine.internal.gradle.report.coverage.PathMarker.SPINE_JAVA_SRC_FOLDER
-import java.io.File
-
-/**
- * This file contains extension methods and properties for `java.io.File`.
- */
-
-/**
- * Parses the name of a class from the absolute path of this file.
- *
- * Treats the fragment between the [precedingMarker] and [extension] as the value to look for.
- * In case the fragment is located and it contains `/` symbols, they are treated
- * as Java package delimiters and are replaced by `.` symbols before returning the value.
- *
- * If the absolute path of this file has either no [precedingMarker] or no [extension],
- * returns `null`.
- */
-internal fun File.parseClassName(
- precedingMarker: PathMarker,
- extension: FileExtension
-): String? {
- val index = this.absolutePath.lastIndexOf(precedingMarker.infix)
- return if (index > 0) {
- var inFolder = this.absolutePath.substring(index + precedingMarker.length)
- if (inFolder.endsWith(extension.value)) {
- inFolder = inFolder.substring(0, inFolder.length - extension.length)
- inFolder.replace('/', '.')
- } else {
- null
- }
- } else {
- null
- }
-}
-
-/**
- * Attempts to parse the file name with either of the specified [parsers],
- * in their respective order.
- *
- * Returns the first non-`null` parsed value.
- *
- * If none of the parsers returns non-`null` value, returns `null`.
- */
-internal fun File.parseName(vararg parsers: (file: File) -> String?): String? {
- for (parser in parsers) {
- val className = parser.invoke(this)
- if (className != null) {
- return className
- }
- }
- return null
-}
-
-/**
- * Attempts to parse the Java fully-qualified class name from the absolute path of this file,
- * treating it as a path to a human-produced `.java` file.
- */
-internal fun File.asJavaClassName(): String? =
- this.parseClassName(JAVA_SRC_FOLDER, JAVA_SOURCE)
-
-/**
- * Attempts to parse the Java fully-qualified class name from the absolute path of this file,
- * treating it as a path to a compiled `.class` file.
- *
- * If the `.class` file corresponds to the anonymous class, only the name of the parent
- * class is returned.
- */
-internal fun File.asJavaCompiledClassName(): String? {
- var className = this.parseClassName(JAVA_OUTPUT_FOLDER, COMPILED_CLASS)
- if (className != null && className.contains(ANONYMOUS_CLASS.infix)) {
- className = className.split(ANONYMOUS_CLASS.infix)[0]
- }
- return className
-}
-
-/**
- * Attempts to parse the Java fully-qualified class name from the absolute path of this file,
- * treating it as a path to a gRPC-generated `.java` file.
- */
-internal fun File.asGrpcClassName(): String? =
- this.parseClassName(GRPC_SRC_FOLDER, JAVA_SOURCE)
-
-/**
- * Attempts to parse the Java fully-qualified class name from the absolute path of this file,
- * treating it as a path to a Spine-generated `.java` file.
- */
-internal fun File.asSpineClassName(): String? =
- this.parseClassName(SPINE_JAVA_SRC_FOLDER, JAVA_SOURCE)
-
-/**
- * Tells whether this file is a part of the generated sources, and not produced by a human.
- */
-internal val File.isGenerated
- get() = this.absolutePath.contains(GENERATED.infix)
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/JacocoConfig.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/JacocoConfig.kt
deleted file mode 100644
index 5e7fff476..000000000
--- a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/JacocoConfig.kt
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.internal.gradle.report.coverage
-
-import io.spine.internal.gradle.applyPlugin
-import io.spine.internal.gradle.findTask
-import io.spine.internal.gradle.report.coverage.TaskName.check
-import io.spine.internal.gradle.report.coverage.TaskName.copyReports
-import io.spine.internal.gradle.report.coverage.TaskName.jacocoRootReport
-import io.spine.internal.gradle.report.coverage.TaskName.jacocoTestReport
-import io.spine.internal.gradle.sourceSets
-import java.io.File
-import java.util.*
-import org.gradle.api.Project
-import org.gradle.api.Task
-import org.gradle.api.file.ConfigurableFileCollection
-import org.gradle.api.plugins.BasePlugin
-import org.gradle.api.tasks.Copy
-import org.gradle.api.tasks.SourceSetContainer
-import org.gradle.api.tasks.SourceSetOutput
-import org.gradle.api.tasks.TaskContainer
-import org.gradle.api.tasks.TaskProvider
-import org.gradle.kotlin.dsl.get
-import org.gradle.testing.jacoco.plugins.JacocoPlugin
-import org.gradle.testing.jacoco.tasks.JacocoReport
-
-/**
- * Configures JaCoCo plugin to produce `jacocoRootReport` task which accumulates
- * the test coverage results from all subprojects in a multi-project Gradle build.
- *
- * Users must apply `jacoco` plugin to all the subprojects, for which the report aggregation
- * is required.
- *
- * In a single-module Gradle project, this utility is NOT needed. Just a plain `jacoco` plugin
- * applied to the project is sufficient.
- *
- * Therefore, tn case this utility is applied to a single-module Gradle project,
- * an `IllegalStateException` is thrown.
- */
-@Suppress("unused")
-class JacocoConfig(
- private val rootProject: Project,
- private val reportsDir: File,
- private val projects: Iterable
-) {
-
- companion object {
-
- /**
- * A folder under the `buildDir` of the [rootProject] to which the reports will
- * be copied when aggregating the coverage reports.
- *
- * If it does not exist, it will be created.
- */
- private const val reportsDirSuffix = "subreports/jacoco/"
-
- /**
- * Applies the JaCoCo plugin to the Gradle project.
- *
- * If the passed project has no subprojects, an `IllegalStateException` is thrown,
- * telling that this utility should NOT be used.
- *
- * Registers `jacocoRootReport` task which aggregates all coverage reports
- * from the subprojects.
- */
- fun applyTo(project: Project) {
- project.applyPlugin(BasePlugin::class.java)
- project.afterEvaluate {
- val javaProjects: Iterable = eligibleProjects(project)
- val reportsDir = project.rootProject.buildDir.resolve(reportsDirSuffix)
- JacocoConfig(project.rootProject, reportsDir, javaProjects)
- .configure()
- }
- }
-
- /**
- * For a multi-module Gradle project, returns those subprojects of the passed [project]
- * which have JaCoCo plugin applied.
- *
- * Throws an exception in case this project has no subprojects.
- */
- private fun eligibleProjects(project: Project): Iterable {
- val projects: Iterable =
- if (project.subprojects.isNotEmpty()) {
- project.subprojects.filter {
- it.pluginManager.hasPlugin(JacocoPlugin.PLUGIN_EXTENSION_NAME)
- }
- } else {
- throw IllegalStateException(
- "In a single-module Gradle project, `JacocoConfig` is NOT needed." +
- " Please apply `jacoco` plugin instead."
- )
- }
- return projects
- }
- }
-
- private fun configure() {
- val tasks = rootProject.tasks
- val copyReports = registerCopy(tasks)
- val rootReport = registerRootReport(tasks, copyReports)
- rootProject
- .findTask(check.name)
- .dependsOn(rootReport)
- }
-
- private fun registerRootReport(
- tasks: TaskContainer,
- copyReports: TaskProvider?
- ): TaskProvider {
- val allSourceSets = Projects(projects).sourceSets()
- val mainJavaSrcDirs = allSourceSets.mainJavaSrcDirs()
- val humanProducedSourceFolders = FileFilter.producedByHuman(mainJavaSrcDirs)
-
- val filter = CodebaseFilter(rootProject, mainJavaSrcDirs, allSourceSets.mainOutputs())
- val humanProducedCompiledFiles = filter.humanProducedCompiledFiles()
-
- val rootReport = tasks.register(jacocoRootReport.name, JacocoReport::class.java) {
- dependsOn(copyReports)
-
- additionalSourceDirs.from(humanProducedSourceFolders)
- sourceDirectories.from(humanProducedSourceFolders)
- executionData.from(rootProject.fileTree(reportsDir))
-
- classDirectories.from(humanProducedCompiledFiles)
- additionalClassDirs.from(humanProducedCompiledFiles)
-
- reports {
- html.required.set(true)
- xml.required.set(true)
- csv.required.set(false)
- }
- onlyIf { true }
- }
- return rootReport
- }
-
- private fun registerCopy(tasks: TaskContainer): TaskProvider {
- val everyExecData = mutableListOf()
- projects.forEach { project ->
- val jacocoTestReport = project.findTask(jacocoTestReport.name)
- val executionData = jacocoTestReport.executionData
- everyExecData.add(executionData)
- }
-
- val originalLocation = rootProject.files(everyExecData)
-
- val copyReports = tasks.register(copyReports.name, Copy::class.java) {
- from(originalLocation)
- into(reportsDir)
- rename {
- "${UUID.randomUUID()}.exec"
- }
- dependsOn(projects.map { it.findTask(jacocoTestReport.name) })
- }
- return copyReports
- }
-}
-
-/**
- * Extensions for working with groups of Gradle `Project`s.
- */
-private class Projects(
- private val projects: Iterable
-) {
-
- /**
- * Returns all source sets for this group of projects.
- */
- fun sourceSets(): SourceSets {
- val sets = projects.asSequence().map { it.sourceSets }.toList()
- return SourceSets(sets)
- }
-}
-
-/**
- * Extensions for working with several of Gradle `SourceSetContainer`s.
- */
-private class SourceSets(
- private val sourceSets: Iterable
-) {
-
- /**
- * Returns all Java source folders corresponding to the `main` source set type.
- */
- fun mainJavaSrcDirs(): Set {
- return sourceSets
- .asSequence()
- .flatMap { it["main"].allJava.srcDirs }
- .toSet()
- }
-
- /**
- * Returns all source set outputs corresponding to the `main` source set type.
- */
- fun mainOutputs(): Set {
- return sourceSets
- .asSequence()
- .map { it["main"].output }
- .toSet()
- }
-}
diff --git a/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts b/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts
new file mode 100644
index 000000000..7334ef97f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import java.io.File
+import org.gradle.kotlin.dsl.getValue
+import org.gradle.kotlin.dsl.getting
+import org.gradle.kotlin.dsl.jacoco
+import org.gradle.testing.jacoco.tasks.JacocoReport
+
+// DEPRECATED: this script plugin distributes vanilla JaCoCo.
+// New code should apply `kmp-module`, which configures Kover via
+// `useJacoco(version = Jacoco.version)` and writes JaCoCo-format XML at
+// `build/reports/kover/report.xml`. (Same task and path as Kotlin-JVM —
+// `kmp-module` configures only Kover's `total` report, so no
+// `koverXmlReport` task is generated.) The `raise-coverage` skill
+// migrates existing consumers automatically. Kept so older consumer repos
+// continue to build; will be removed in a future release.
+// See: .agents/skills/raise-coverage/references/migrate-to-kover.md
+
+plugins {
+ jacoco
+}
+
+logger.warn(
+ "'jacoco-kmm-jvm' is deprecated; use 'kmp-module' which applies Kover. " +
+ "See .agents/skills/raise-coverage/references/migrate-to-kover.md."
+)
+
+/**
+ * Configures [JacocoReport] task to run in a Kotlin KMM project for `commonMain` and `jvmMain`
+ * source sets.
+ *
+ * This script plugin must be applied using the following construct at the end of
+ * a `build.gradle.kts` file of a module:
+ *
+ * ```kotlin
+ * apply(plugin="jacoco-kmm-jvm")
+ * ```
+ * Please do not apply this script plugin in the `plugins {}` block because `jacocoTestReport`
+ * task is not yet available at this stage.
+ */
+@Suppress("unused")
+private val about = ""
+
+/**
+ * Configure the Jacoco task with custom input a KMM project
+ * to which this convention plugin is applied.
+ */
+@Suppress("unused")
+val jacocoTestReport: JacocoReport by tasks.getting(JacocoReport::class) {
+ val buildDir = project.layout.buildDirectory.get().asFile.absolutePath
+ val classFiles = File("${buildDir}/classes/kotlin/jvm/")
+ .walkBottomUp()
+ .toSet()
+ classDirectories.setFrom(classFiles)
+
+ val coverageSourceDirs = arrayOf(
+ "src/commonMain",
+ "src/jvmMain"
+ )
+ sourceDirectories.setFrom(files(coverageSourceDirs))
+
+ executionData.setFrom(files("${buildDir}/jacoco/jvmTest.exec"))
+}
diff --git a/buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts b/buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts
new file mode 100644
index 000000000..185c9cdfd
--- /dev/null
+++ b/buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import io.spine.gradle.buildDirectory
+
+// DEPRECATED: this script plugin distributes vanilla JaCoCo.
+// New code should apply `jvm-module`, which configures Kover via
+// `useJacoco(version = Jacoco.version)` and writes JaCoCo-format XML at
+// `build/reports/kover/report.xml`. The `raise-coverage` skill migrates
+// existing consumers automatically. Kept so older consumer repos continue to
+// build; will be removed in a future release.
+// See: .agents/skills/raise-coverage/references/migrate-to-kover.md
+
+plugins {
+ jacoco
+}
+
+logger.warn(
+ "'jacoco-kotlin-jvm' is deprecated; use 'jvm-module' which applies Kover. " +
+ "See .agents/skills/raise-coverage/references/migrate-to-kover.md."
+)
+
+/**
+ * Configures [JacocoReport] task to run in a Kotlin Multiplatform project for
+ * `commonMain` and `jvmMain` source sets.
+ *
+ * This script plugin must be applied using the following construct at the end of
+ * a `build.gradle.kts` file of a module:
+ *
+ * ```kotlin
+ * apply(plugin="jacoco-kotlin-jvm")
+ * ```
+ * Please do not apply this script plugin in the `plugins {}` block because `jacocoTestReport`
+ * task is not yet available at this stage.
+ */
+@Suppress("unused")
+private val about = ""
+
+/**
+ * Configure Jacoco task with custom input from this Kotlin Multiplatform project.
+ */
+@Suppress("unused")
+val jacocoTestReport: JacocoReport by tasks.getting(JacocoReport::class) {
+
+ val classFiles = File("$buildDirectory/classes/kotlin/jvm/")
+ .walkBottomUp()
+ .toSet()
+ classDirectories.setFrom(classFiles)
+
+ val coverageSourceDirs = arrayOf(
+ "src/commonMain",
+ "src/jvmMain"
+ )
+ sourceDirectories.setFrom(files(coverageSourceDirs))
+
+ executionData.setFrom(files("$buildDirectory/jacoco/jvmTest.exec"))
+}
diff --git a/buildSrc/src/main/kotlin/jvm-module.gradle.kts b/buildSrc/src/main/kotlin/jvm-module.gradle.kts
new file mode 100644
index 000000000..a7b311309
--- /dev/null
+++ b/buildSrc/src/main/kotlin/jvm-module.gradle.kts
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import io.spine.dependency.boms.BomsPlugin
+import io.spine.dependency.build.CheckerFramework
+import io.spine.dependency.build.Dokka
+import io.spine.dependency.build.ErrorProne
+import io.spine.dependency.build.JSpecify
+import io.spine.dependency.lib.Guava
+import io.spine.dependency.lib.Jackson
+import io.spine.dependency.lib.Kotlin
+import io.spine.dependency.lib.Protobuf
+import io.spine.dependency.local.Reflect
+import io.spine.dependency.test.Jacoco
+import io.spine.gradle.SpineTaskGroup
+import io.spine.gradle.checkstyle.CheckStyleConfig
+import io.spine.gradle.github.pages.updateGitHubPages
+import io.spine.gradle.javac.configureErrorProne
+import io.spine.gradle.javac.configureJavac
+import io.spine.gradle.javadoc.JavadocConfig
+import io.spine.gradle.kotlin.setFreeCompilerArgs
+import io.spine.gradle.report.license.LicenseReporter
+
+plugins {
+ `java-library`
+ id("net.ltgt.errorprone")
+ id("pmd-settings")
+ id("project-report")
+ kotlin("jvm")
+ id("detekt-code-analysis")
+ id("dokka-setup")
+ id("org.jetbrains.kotlinx.kover")
+ id("module-testing")
+}
+apply()
+LicenseReporter.generateReportIn(project)
+JavadocConfig.applyTo(project)
+CheckStyleConfig.applyTo(project)
+
+project.run {
+ configureJava()
+ configureKotlin()
+ addDependencies()
+ forceConfigurations()
+
+ val generatedDir = "$projectDir/generated"
+ setTaskDependencies(generatedDir)
+
+ configureGitHubPages()
+}
+
+typealias Module = Project
+
+fun Module.configureJava() {
+ java {
+ sourceCompatibility = BuildSettings.javaVersionCompat
+ targetCompatibility = BuildSettings.javaVersionCompat
+ }
+
+ tasks {
+ withType().configureEach {
+ configureJavac()
+ configureErrorProne()
+ }
+ }
+}
+
+fun Module.configureKotlin() {
+ kotlin {
+ explicitApi()
+ compilerOptions {
+ jvmTarget.set(BuildSettings.jvmTarget)
+ setFreeCompilerArgs()
+ }
+ }
+
+ kover {
+ useJacoco(version = Jacoco.version)
+ reports {
+ total {
+ xml {
+ onCheck = true
+ }
+ }
+ }
+ }
+}
+
+/**
+ * These dependencies are applied to all subprojects and do not have to
+ * be included explicitly.
+ *
+ * We expose production code dependencies as API because they are used
+ * by the framework parts that depend on `base`.
+ */
+fun Module.addDependencies() = dependencies {
+ errorprone(ErrorProne.core)
+
+ Protobuf.libs.forEach { api(it) }
+ api(Guava.lib)
+
+ compileOnlyApi(CheckerFramework.annotations)
+ api(JSpecify.annotations)
+ ErrorProne.annotations.forEach { compileOnlyApi(it) }
+}
+
+fun Module.forceConfigurations() {
+ with(configurations) {
+ forceVersions()
+ excludeProtobufLite()
+ all {
+ resolutionStrategy {
+ val cfg = this@all
+ val rs = this@resolutionStrategy
+ Jackson.forceArtifacts(project, cfg, rs)
+ Jackson.DataFormat.forceArtifacts(project, cfg, rs)
+ force(
+ Jackson.annotations,
+ Kotlin.bom,
+ Dokka.BasePlugin.lib,
+ Reflect.lib,
+ )
+ }
+ }
+ }
+}
+
+fun Module.setTaskDependencies(generatedDir: String) {
+ tasks {
+ val cleanGenerated by registering(Delete::class) {
+ group = SpineTaskGroup.name
+ description = "Deletes the directory with generated sources"
+ delete(generatedDir)
+ }
+ clean.configure {
+ dependsOn(cleanGenerated)
+ }
+
+ project.afterEvaluate {
+ val publish = tasks.findByName("publish")
+ publish?.dependsOn("${project.path}:updateGitHubPages")
+ }
+ }
+ afterEvaluate {
+ configureTaskDependencies()
+ }
+}
+
+fun Module.configureGitHubPages() {
+ updateGitHubPages {
+ rootFolder.set(rootDir)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/kmp-module.gradle.kts b/buildSrc/src/main/kotlin/kmp-module.gradle.kts
new file mode 100644
index 000000000..a2e6d82e5
--- /dev/null
+++ b/buildSrc/src/main/kotlin/kmp-module.gradle.kts
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2026, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import io.spine.dependency.boms.BomsPlugin
+import io.spine.dependency.lib.Jackson
+import io.spine.dependency.lib.Kotlin
+import io.spine.dependency.local.Reflect
+import io.spine.dependency.local.TestLib
+import io.spine.dependency.test.JUnit
+import io.spine.dependency.test.Jacoco
+import io.spine.dependency.test.Kotest
+import io.spine.gradle.checkstyle.CheckStyleConfig
+import io.spine.gradle.javac.configureJavac
+import io.spine.gradle.kotlin.setFreeCompilerArgs
+import io.spine.gradle.publish.IncrementGuard
+import io.spine.gradle.report.license.LicenseReporter
+
+/**
+ * Configures this [Project] as a Kotlin Multiplatform module.
+ *
+ * By its nature, this script plugin is similar to `jvm-module`. It performs
+ * the basic module configuration.
+ *
+ * `jvm-module` is based on a mix of Java and Kotlin Gradle plugins. It allows
+ * usage of Kotlin and Java in a single module that is built for JVM.
+ * Whereas `kmp-module` is based on a Kotlin Multiplatform plugin. This plugin
+ * supports different compilation targets within a single module: JVM, IOS,
+ * Desktop, JS, etc. Also, it allows having some common sources in Kotlin
+ * that can be shared with target-specific code. They are located in
+ * `commonMain` and `commonTest` source sets. Each concrete target implicitly
+ * depends on them.
+ *
+ * As for now, this script configures only JVM target, but other targets
+ * will be added further.
+ *
+ * ### JVM target
+ *
+ * Sources for this target are placed in `jvmMain` and `jvmTest` directories.
+ * Java is allowed to be used in `jvm` sources, but Kotlin is a preference.
+ * Use Java only as a fall-back option where Kotlin is insufficient.
+ * Due to this, Java linters are not even configured by `kmp-module`.
+ *
+ * @see Kotlin Multiplatform docs
+ */
+@Suppress("unused")
+val about = ""
+
+plugins {
+ kotlin("multiplatform")
+ id("detekt-code-analysis")
+ id("org.jetbrains.kotlinx.kover")
+ `project-report`
+}
+apply()
+apply()
+
+project.forceConfigurations()
+
+fun Project.forceConfigurations() {
+ with(configurations) {
+ forceVersions()
+ all {
+ resolutionStrategy {
+ val cfg = this@all
+ val rs = this@resolutionStrategy
+ Jackson.forceArtifacts(project, cfg, rs)
+ Jackson.DataFormat.forceArtifacts(project, cfg, rs)
+ force(
+ Kotlin.bom,
+ Reflect.lib
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Configures Kotlin Multiplatform plugin.
+ *
+ * Please note, this extension DOES NOT configure Kotlin for JVM.
+ * It configures KMP, in which Kotlin for JVM is only one of
+ * possible targets.
+ */
+@Suppress("UNUSED_VARIABLE") // Avoid warnings for source set vars.
+kotlin {
+ // Enables explicit API mode for any Kotlin sources within the module.
+ explicitApi()
+
+ compilerOptions {
+ setFreeCompilerArgs()
+ }
+
+ // Enables and configures JVM target.
+ jvm {
+ compilerOptions {
+ jvmTarget.set(BuildSettings.jvmTarget)
+ }
+ }
+
+ // Dependencies are specified per-target.
+ // Please note, common sources are implicitly available in all targets.
+ @Suppress("unused") // source set `val`s are used implicitly.
+ sourceSets {
+ val commonTest by getting {
+ dependencies {
+ implementation(kotlin("test-common"))
+ implementation(kotlin("test-annotations-common"))
+ implementation(Kotest.assertions)
+ implementation(Kotest.frameworkEngine)
+ }
+ }
+ val jvmTest by getting {
+ dependencies {
+ implementation(dependencies.enforcedPlatform(JUnit.bom))
+ implementation(TestLib.lib)
+ implementation(JUnit.Jupiter.engine)
+ implementation(Kotest.runnerJUnit5Jvm)
+ }
+ }
+ }
+}
+
+java {
+ sourceCompatibility = BuildSettings.javaVersionCompat
+ targetCompatibility = BuildSettings.javaVersionCompat
+}
+
+/**
+ * Performs the standard task's configuration.
+ *
+ * Here's no difference with `jvm-module`, which does the same.
+ *
+ * Kotlin here is configured for both common and JVM-specific sources.
+ * Java is for JVM only.
+ *
+ * Also, Kotlin and Java share the same test executor (JUnit), so tests
+ * configuration is for both.
+ */
+tasks {
+ withType().configureEach {
+ configureJavac()
+ }
+}
+
+/**
+ * Overrides the default location of Kotlin sources.
+ *
+ * The default configuration of Detekt assumes presence of Kotlin sources
+ * in `src/main/kotlin`, which is not the case for KMP.
+ */
+detekt {
+ source.setFrom(
+ "src/commonMain",
+ "src/jvmMain"
+ )
+}
+
+kover {
+ useJacoco(version = Jacoco.version)
+ reports {
+ total {
+ xml {
+ onCheck = true
+ }
+ }
+ }
+}
+
+LicenseReporter.generateReportIn(project)
+CheckStyleConfig.applyTo(project)
diff --git a/buildSrc/src/main/kotlin/kmp-publish.gradle.kts b/buildSrc/src/main/kotlin/kmp-publish.gradle.kts
new file mode 100644
index 000000000..e4c3aa1be
--- /dev/null
+++ b/buildSrc/src/main/kotlin/kmp-publish.gradle.kts
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import org.gradle.api.publish.maven.MavenPublication
+import org.gradle.kotlin.dsl.`maven-publish`
+import org.gradle.kotlin.dsl.named
+
+/**
+ * Configures publications for `kmp-module`.
+ *
+ * As for now, [spinePublishing][io.spine.gradle.publish.spinePublishing]
+ * doesn't support Kotlin Multiplatform modules. So, their publications are
+ * configured by this script plugin. Other publishing-related configuration
+ * is still performed by the extension.
+ *
+ * To publish a KMP module, one still needs to open and configure
+ * `spinePublishing` extension. Make sure `spinePublishing.customPublishing`
+ * property is set to `true`, and this script plugin is applied.
+ *
+ * For example:
+ *
+ * ```
+ * plugins {
+ * `kmp-module`
+ * `kmp-publish`
+ * }
+ *
+ * spinePublishing {
+ * destinations = setOf(...)
+ * customPublishing = true
+ * }
+ * ```
+ */
+@Suppress("unused")
+val about = ""
+
+plugins {
+ `maven-publish`
+ id("dokka-setup")
+}
+
+publishing.publications {
+ named("kotlinMultiplatform") {
+ // Although, the "common artifact" can't be used independently
+ // of target artifacts, it is published with documentation.
+ artifact(project.htmlDocsJar())
+ }
+ named("jvm") {
+ // Includes Kotlin (JVM + common) and Java documentation.
+ artifact(project.htmlDocsJar())
+ }
+}
diff --git a/buildSrc/src/main/kotlin/module-testing.gradle.kts b/buildSrc/src/main/kotlin/module-testing.gradle.kts
new file mode 100644
index 000000000..ee3fe6105
--- /dev/null
+++ b/buildSrc/src/main/kotlin/module-testing.gradle.kts
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import io.spine.dependency.lib.Guava
+import io.spine.dependency.local.TestLib
+import io.spine.dependency.test.JUnit
+import io.spine.dependency.test.JUnit.Jupiter
+import io.spine.dependency.test.Kotest
+import io.spine.dependency.test.Truth
+import io.spine.gradle.testing.configureLogging
+import io.spine.gradle.testing.registerTestTasks
+
+/**
+ * This convention plugin applies test dependencies and configures test-related tasks.
+ *
+ * The version of the [JUnit] platform must be applied via the [BomsPlugin][io.spine.dependency.boms.BomsPlugin]:
+ *
+ * ```kotlin
+ * apply()
+ * ```
+ */
+@Suppress("unused")
+private val about = ""
+
+plugins {
+ `java-library`
+}
+
+project.run {
+ setupTests()
+ forceTestDependencies()
+}
+
+dependencies {
+ forceJunitPlatform()
+
+ testImplementation(Jupiter.api)
+ testImplementation(Jupiter.params)
+ testImplementation(JUnit.pioneer)
+
+ testImplementation(Guava.testLib)
+
+ testImplementation(TestLib.lib)
+ testImplementation(Kotest.assertions)
+
+ testRuntimeOnly(Jupiter.engine)
+}
+
+/**
+ * Forces the version of [JUnit] platform and its dependencies via [JUnit.bom].
+ */
+private fun DependencyHandlerScope.forceJunitPlatform() {
+ testImplementation(enforcedPlatform(JUnit.bom))
+}
+
+typealias Module = Project
+
+/**
+ * Configure this module to run JUnit-based tests.
+ */
+fun Module.setupTests() {
+ tasks {
+ registerTestTasks()
+ test.configure {
+ useJUnitPlatform {
+ includeEngines("junit-jupiter")
+ }
+ configureLogging()
+ }
+ }
+}
+
+/**
+ * Forces the versions of task dependencies that are used _in addition_ to
+ * the forced JUnit platform.
+ */
+@Suppress(
+ /* We're OK with incubating API for configurations. It does not seem to change recently. */
+ "UnstableApiUsage"
+)
+fun Module.forceTestDependencies() {
+ configurations {
+ all {
+ resolutionStrategy {
+ forceTestDependencies()
+ }
+ }
+ }
+}
+
+private fun ResolutionStrategy.forceTestDependencies() {
+ force(
+ Guava.testLib,
+ Truth.libs,
+ Kotest.assertions,
+ )
+}
diff --git a/buildSrc/src/main/kotlin/module.gradle.kts b/buildSrc/src/main/kotlin/module.gradle.kts
new file mode 100644
index 000000000..b24b5b2ac
--- /dev/null
+++ b/buildSrc/src/main/kotlin/module.gradle.kts
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// This is a template file for an actual script which should be
+// defined by a project to which `config` is applied.
+//
+// The reason for having this file is that it is referenced as
+// a plugin in `uber-jar-module.gradle.kts`
diff --git a/buildSrc/src/main/kotlin/pmd-settings.gradle.kts b/buildSrc/src/main/kotlin/pmd-settings.gradle.kts
index 03e69a67e..938a76478 100644
--- a/buildSrc/src/main/kotlin/pmd-settings.gradle.kts
+++ b/buildSrc/src/main/kotlin/pmd-settings.gradle.kts
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-import io.spine.internal.dependency.Pmd
+import io.spine.dependency.build.Pmd
plugins {
pmd
@@ -42,7 +42,7 @@ pmd {
ruleSets = listOf()
// Load PMD settings.
- val pmdSettings = file("$rootDir/config/quality/pmd.xml")
+ val pmdSettings = file("$rootDir/buildSrc/quality/pmd.xml")
val textResource: TextResource = resources.text.fromFile(pmdSettings)
ruleSetConfig = textResource
diff --git a/buildSrc/src/main/kotlin/test-module.gradle.kts b/buildSrc/src/main/kotlin/test-module.gradle.kts
new file mode 100644
index 000000000..d64de094e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/test-module.gradle.kts
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2024, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import io.spine.dependency.local.Base
+import io.spine.dependency.local.Validation
+import io.spine.gradle.report.license.LicenseReporter
+
+plugins {
+ java
+ `java-test-fixtures`
+ id("module-testing")
+}
+LicenseReporter.generateReportIn(project)
+
+dependencies {
+ arrayOf(
+ Base.lib,
+ Validation.runtime
+ ).forEach {
+ testFixturesImplementation(it)?.because(
+ """
+ We do not apply CoreJvm Compiler Gradle plugin which adds
+ the `implementation` dependency on Validation runtime automatically
+ (see `Project.configureValidation()` function in `CompilerConfigPlugin.kt`).
+
+ In a test module we use vanilla `protoc` (via ProtoTap) and then run codegen
+ using the Spine Compiler `Pipeline` and the plugins of the module under the test.
+
+ Because of this we need to add the dependencies above explicitly for the
+ generated code of test fixtures to compile.
+ """.trimIndent()
+ )
+ }
+}
diff --git a/buildSrc/src/main/kotlin/uber-jar-module.gradle.kts b/buildSrc/src/main/kotlin/uber-jar-module.gradle.kts
new file mode 100644
index 000000000..0cace2ebe
--- /dev/null
+++ b/buildSrc/src/main/kotlin/uber-jar-module.gradle.kts
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+@file:Suppress("UnstableApiUsage") // `configurations` block.
+
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import io.spine.gradle.publish.IncrementGuard
+import io.spine.gradle.publish.SpinePublishing
+import io.spine.gradle.publish.setup
+import io.spine.gradle.publish.spinePublishing
+import io.spine.gradle.report.license.LicenseReporter
+
+plugins {
+ id("module")
+ `maven-publish`
+ id("com.gradleup.shadow")
+ id("write-manifest")
+ `project-report`
+ idea
+}
+apply