diff --git a/.idea/misc.xml b/.idea/misc.xml
index 31f80aec..5358548e 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-
@@ -41,4 +40,4 @@
-
+
\ No newline at end of file
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 227e6d26..2cd94daf 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -192,6 +192,14 @@ dependencies {
).forEach {
implementation(it)
}
+
+ testImplementation(platform("org.junit:junit-bom:5.11.4"))
+ testImplementation("org.junit.jupiter:junit-jupiter")
+ testRuntimeOnly("org.junit.platform:junit-platform-launcher")
+}
+
+tasks.test {
+ useJUnitPlatform()
}
dependOnBuildSrcJar()
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt
index 0b545b1e..344e8a13 100644
--- a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt
@@ -46,12 +46,12 @@ object CoreJvmCompiler {
/**
* The version used to in the build classpath.
*/
- const val dogfoodingVersion = "2.0.0-SNAPSHOT.055"
+ const val dogfoodingVersion = "2.0.0-SNAPSHOT.058"
/**
* The version to be used for integration tests.
*/
- const val version = "2.0.0-SNAPSHOT.055"
+ const val version = "2.0.0-SNAPSHOT.058"
/**
* The ID of the Gradle plugin.
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 00000000..e384aa75
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/docs/UpdatePluginVersion.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.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)
+ }
+ }
+
+ private fun updatePluginVersion(file: File, id: String, version: String) {
+ val content = file.readText()
+ // Regex to match: id("plugin-id") version "version-number"
+ val regex = """id\("$id"\)\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/test/kotlin/io/spine/gradle/docs/UpdatePluginVersionTest.kt b/buildSrc/src/test/kotlin/io/spine/gradle/docs/UpdatePluginVersionTest.kt
new file mode 100644
index 00000000..43bad6d9
--- /dev/null
+++ b/buildSrc/src/test/kotlin/io/spine/gradle/docs/UpdatePluginVersionTest.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.testfixtures.ProjectBuilder
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+
+class UpdatePluginVersionTest {
+
+ @TempDir
+ lateinit var tempDir: File
+
+ private lateinit var buildFile: File
+
+ @BeforeEach
+ fun setUp() {
+ val subDir = File(tempDir, "subproject")
+ subDir.mkdir()
+ buildFile = File(subDir, "build.gradle.kts")
+ buildFile.writeText("""
+ plugins {
+ id("io.spine.validation") version "1.0.0"
+ id("other-plugin") version "0.1.0"
+ }
+ """.trimIndent())
+ }
+
+ @Test
+ fun `update plugin version in build file`() {
+ val project = ProjectBuilder.builder().build()
+ val task = project.tasks.register("updatePluginVersion", UpdatePluginVersion::class.java) {
+ directory.set(tempDir)
+ version.set("2.0.0-TEST")
+ pluginId.set("io.spine.validation")
+ }
+ task.get().update()
+
+ val updatedContent = buildFile.readText()
+ assertTrue(updatedContent.contains("""id("io.spine.validation") version "2.0.0-TEST""""))
+ assertTrue(updatedContent.contains("""id("other-plugin") version "0.1.0""""))
+ }
+
+ @Test
+ fun `update 'kotlin' plugin version when 'kotlinVersion' is set`() {
+ // Overwrite with a file that uses kotlin("jvm") syntax
+ buildFile.writeText(
+ """
+ plugins {
+ kotlin("jvm") version "1.9.10"
+ id("io.spine.validation") version "1.0.0"
+ }
+ """.trimIndent()
+ )
+
+ val project = ProjectBuilder.builder().build()
+ val task = project.tasks.register("updatePluginVersion", UpdatePluginVersion::class.java) {
+ directory.set(tempDir)
+ version.set("2.0.0-TEST")
+ pluginId.set("io.spine.validation")
+ kotlinVersion.set("2.2.21")
+ }
+ task.get().update()
+
+ val updatedContent = buildFile.readText()
+ assertTrue(updatedContent.contains("""kotlin("jvm") version "2.2.21"""))
+ assertTrue(updatedContent.contains("""id("io.spine.validation") version "2.0.0-TEST"""))
+ }
+
+ @Test
+ fun `handle multiple spaces between id and version`() {
+ buildFile.writeText("""
+ plugins {
+ id("io.spine.validation") version "1.0.0"
+ }
+ """.trimIndent())
+
+ val project = ProjectBuilder.builder().build()
+ val task = project.tasks.register("updatePluginVersion", UpdatePluginVersion::class.java) {
+ directory.set(tempDir)
+ version.set("2.0.0-TEST")
+ pluginId.set("io.spine.validation")
+ }
+
+ task.get().update()
+
+ val updatedContent = buildFile.readText()
+ assertTrue(updatedContent.contains("""id("io.spine.validation") version "2.0.0-TEST""""))
+ }
+}