` remote-tracking branch.
+ *
+ * Returns `null` when the file does not exist at the base — a newly introduced version file.
+ * Throws a [GradleException] when the base ref cannot be resolved: the Version Guard workflow
+ * is responsible for fetching it, and failing closed surfaces that misconfiguration instead
+ * of silently passing the check.
+ */
+ fun contentInBase(rootDir: File, baseRef: String): String? {
+ val result = gitShow(rootDir, "origin/$baseRef:$NAME")
+ if (result.exitCode == 0) {
+ return result.stdout
+ }
+ // `git show` reports a missing path with these phrasings; everything else
+ // (e.g. an unresolvable ref) is a configuration error we must not swallow.
+ val missingPath = result.stderr.contains("does not exist") ||
+ result.stderr.contains("exists on disk, but not in")
+ if (missingPath) {
+ return null
+ }
+ throw GradleException(
+ "Unable to read `$NAME` from base `origin/$baseRef` " +
+ "(git exit code ${result.exitCode}): ${result.stderr.trim()}.\n" +
+ "Ensure the Version Guard workflow fetches the base branch before this check."
+ )
+ }
+}
+
+/**
+ * The outcome of a `git` invocation: its [exitCode] and the captured [stdout] and [stderr].
+ *
+ * @property exitCode The process exit code; `0` on success.
+ * @property stdout The captured standard output stream.
+ * @property stderr The captured standard error stream.
+ */
+private data class GitResult(val exitCode: Int, val stdout: String, val stderr: String)
+
+private fun gitShow(rootDir: File, spec: String): GitResult {
+ // Redirect to files rather than reading the process pipes sequentially: draining
+ // stdout fully before stderr can deadlock if a stream fills its pipe buffer.
+ val outFile = File.createTempFile("git-show", ".out")
+ val errFile = File.createTempFile("git-show", ".err")
+ try {
+ val exitCode = ProcessBuilder("git", "show", spec)
+ .directory(rootDir)
+ .redirectOutput(outFile)
+ .redirectError(errFile)
+ .start()
+ .waitFor()
+ return GitResult(exitCode, outFile.readText(), errFile.readText())
+ } finally {
+ outFile.delete()
+ errFile.delete()
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/kotlin/KotlinConfig.kt b/buildSrc/src/main/kotlin/io/spine/gradle/kotlin/KotlinConfig.kt
index 92b7a38743..0e5099a77f 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/kotlin/KotlinConfig.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/kotlin/KotlinConfig.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2025, 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.
@@ -55,20 +55,26 @@ fun KotlinJvmProjectExtension.applyJvmToolchain(version: String) =
*/
@Suppress("unused")
fun KotlinCommonCompilerOptions.setFreeCompilerArgs() {
+ val optIns = mutableListOf(
+ "kotlin.contracts.ExperimentalContracts",
+ "kotlin.ExperimentalUnsignedTypes",
+ "kotlin.ExperimentalStdlibApi",
+ "kotlin.experimental.ExperimentalTypeInference",
+ )
if (this is KotlinJvmCompilerOptions) {
jvmDefault.set(JvmDefaultMode.NO_COMPATIBILITY)
+ // `kotlin.io.path` ships only in the JVM standard library, so for common
+ // and Native compilations this opt-in marker is unresolved and the compiler
+ // warns about it. Scope it to JVM compilations; multiplatform common and
+ // Native code cannot use the API anyway.
+ optIns.add("kotlin.io.path.ExperimentalPathApi")
}
freeCompilerArgs.addAll(
listOf(
"-Xskip-prerelease-check",
"-Xexpect-actual-classes",
"-Xcontext-parameters",
- "-opt-in=" +
- "kotlin.contracts.ExperimentalContracts," +
- "kotlin.io.path.ExperimentalPathApi," +
- "kotlin.ExperimentalUnsignedTypes," +
- "kotlin.ExperimentalStdlibApi," +
- "kotlin.experimental.ExperimentalTypeInference",
+ "-opt-in=" + optIns.joinToString(separator = ","),
)
)
}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt
index 4c215f14bd..16897ffd91 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt
@@ -26,12 +26,10 @@
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.gradle.VersionComparator
+import io.spine.gradle.VersionGradleFile
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
import org.gradle.api.Project
@@ -39,8 +37,20 @@ import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
/**
- * A task that verifies that the current version of the library has not been published to the given
- * Maven repository yet.
+ * A task that verifies the project version is fit to be published.
+ *
+ * Two independent checks run:
+ *
+ * 1. [checkIncrementedAgainstBase] — inside the dedicated `Version Guard` workflow, the
+ * project [version] must be strictly greater than the version declared by
+ * `version.gradle.kts` on the PR's base branch. This is deterministic and
+ * network-independent: it catches a behavior-changing PR that forgot to bump, and two
+ * parallel PRs that bumped to the same value, regardless of what is (or is not yet)
+ * published.
+ * 2. [checkNotPublished] — the [version] must not already exist in the target Maven
+ * repository, so a publication cannot overwrite an immutable artifact.
+ *
+ * The two checks are complementary; neither subsumes the other.
*/
open class CheckVersionIncrement : DefaultTask() {
@@ -57,7 +67,111 @@ open class CheckVersionIncrement : DefaultTask() {
val version: String = project.version as String
@TaskAction
- fun fetchAndCheck() {
+ fun checkVersion() {
+ checkIncrementedAgainstBase()
+ checkNotPublished()
+ }
+
+ /**
+ * Verifies that the project [version] is strictly greater than the version declared by
+ * `version.gradle.kts` on the pull request's base branch.
+ *
+ * The comparison reads the base branch tip with `git show origin/:version.gradle.kts`,
+ * so it runs **only inside the dedicated `Version Guard` workflow** — the one context that
+ * fetches the base ref and signals it via the `VERSION_GUARD` environment variable (see
+ * [IncrementGuard.shouldCompareToBase]). Every other build skips it: a shallow CI checkout
+ * (e.g. the Ubuntu/Windows builds, which pull this task in via `publishToMavenLocal`) has
+ * no base ref to read, and local publishes are not pull requests. Those rely on
+ * [checkNotPublished] instead.
+ *
+ * Within the `Version Guard` workflow, failure modes are deliberately asymmetric:
+ * - base ref unresolvable — **fail closed** (a workflow misconfiguration must not pass
+ * silently);
+ * - `version.gradle.kts` absent on base — treated as a newly introduced file (**pass**);
+ * - the publishing-version property cannot be identified — **skip** with a warning,
+ * leaving [checkNotPublished] as the remaining guard, rather than blocking every PR in
+ * a repository whose `version.gradle.kts` uses an unrecognized shape.
+ */
+ private fun checkIncrementedAgainstBase() {
+ val baseRef = System.getenv("GITHUB_BASE_REF")
+ if (!IncrementGuard.shouldCompareToBase(underVersionGuard(), baseRef)) {
+ logger.info(
+ "Skipping the base-branch increment comparison: it runs only inside the " +
+ "`Version Guard` workflow, which fetches the base branch. " +
+ "`checkNotPublished` remains the active guard here."
+ )
+ return
+ }
+ val baseVersion = baseVersionToCompare(
+ checkNotNull(baseRef) { "`shouldCompareToBase` guarantees a non-blank base ref." }
+ )
+ if (baseVersion != null && VersionComparator.compare(version, baseVersion) <= 0) {
+ throw GradleException(
+ """
+ The project version `$version` is not greater than the base branch version
+ `$baseVersion` (base `$baseRef`).
+
+ A pull request that merges into `$baseRef` must increment the version in
+ `${VersionGradleFile.NAME}`. Publishing runs on every push to the base branch,
+ so a non-incremented version would collide with the already-published artifact.
+
+ Bump the version (e.g. run `/bump-version`) and push again.
+
+ To disable this check, run Gradle with `-x $name`.
+ """.trimIndent()
+ )
+ }
+ }
+
+ /**
+ * Tells whether the build runs inside the dedicated `Version Guard` workflow.
+ *
+ * That workflow fetches the base branch before invoking this task and signals it by
+ * setting the `VERSION_GUARD` environment variable to `true`. The variable is the
+ * authoritative marker that `origin/` is present, so the base-branch comparison
+ * may run; see [IncrementGuard.shouldCompareToBase].
+ */
+ private fun underVersionGuard(): Boolean =
+ "true".equals(System.getenv("VERSION_GUARD"))
+
+ /**
+ * Resolves the base-branch publishing version to compare [version] against, or `null`
+ * when the comparison does not apply.
+ *
+ * Returns `null` (skipping the check) when the publishing-version property cannot be
+ * identified in the working-tree `version.gradle.kts`, or when the base branch has no
+ * comparable value (the file is absent or newly introduced). Throws via
+ * [VersionGradleFile.contentInBase] when the base ref itself cannot be resolved.
+ */
+ private fun baseVersionToCompare(baseRef: String): String? {
+ val headContent = VersionGradleFile.contentUnder(project.rootDir)
+ val key = headContent?.let { VersionGradleFile.keyForValue(it, version) }
+ if (key == null) {
+ logger.warn(
+ "Could not identify the publishing-version property matching `$version` in " +
+ "`${VersionGradleFile.NAME}`; skipping the base-branch increment check."
+ )
+ return null
+ }
+ val baseContent = VersionGradleFile.contentInBase(project.rootDir, baseRef)
+ val baseVersion = baseContent?.let { VersionGradleFile.valueForKey(it, key) }
+ if (baseVersion == null) {
+ logger.info(
+ "No comparable `$key` in `${VersionGradleFile.NAME}` on base `$baseRef` " +
+ "(absent or newly introduced); skipping the base-branch increment check."
+ )
+ }
+ return baseVersion
+ }
+
+ /**
+ * Verifies that the current [version] has not been published to the target Maven
+ * repository yet.
+ *
+ * Both the `releases` and `snapshots` repositories are checked; artifacts in either
+ * may not be overwritten.
+ */
+ private fun checkNotPublished() {
val artifact = "${project.artifactPath()}/${MavenMetadata.FILE_NAME}"
val snapshots = repository.target(snapshots = true)
checkInRepo(snapshots, artifact)
@@ -76,7 +190,7 @@ open class CheckVersionIncrement : DefaultTask() {
"""
The version `$version` is already published to the Maven repository `$repoUrl`.
Try incrementing the library version.
- All available versions are: ${versions?.joinToString(separator = ", ")}.
+ All available versions are: ${versions.joinToString(separator = ", ")}.
To disable this check, run Gradle with `-x $name`.
""".trimIndent()
@@ -114,34 +228,3 @@ open class CheckVersionIncrement : DefaultTask() {
return result
}
}
-
-private data class MavenMetadata(var versioning: Versioning = Versioning()) {
-
- companion object {
-
- const val FILE_NAME = "maven-metadata.xml"
-
- private val mapper = XmlMapper()
-
- init {
- mapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
- }
-
- /**
- * Fetches the metadata for the repository and parses the document.
- *
- * If the document could not be found, assumes that the module was never
- * released and thus has no metadata.
- */
- fun fetchAndParse(url: URL): MavenMetadata? {
- return try {
- val metadata = mapper.readValue(url, MavenMetadata::class.java)
- metadata
- } catch (_: FileNotFoundException) {
- null
- }
- }
- }
-}
-
-private data class Versioning(var versions: List = listOf())
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt
index 24d6d0e2d6..ddda57e793 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/IncrementGuard.kt
@@ -28,14 +28,21 @@
package io.spine.gradle.publish
+import io.spine.gradle.Build
import io.spine.gradle.SpineTaskGroup
import org.gradle.api.Plugin
import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.publish.maven.tasks.PublishToMavenLocal
/**
- * Gradle plugin that adds a [CheckVersionIncrement] task.
+ * Gradle plugin that adds a [CheckVersionIncrement] task verifying that the
+ * project version was incremented before its artifacts are published.
*
- * The task is called `checkVersionIncrement` inserted before the `check` task.
+ * The task — named `checkVersionIncrement` — is run directly by the `Version Guard`
+ * CI workflow and before any `publishToMavenLocal` task. It is deliberately kept out
+ * of the `check` lifecycle, and actually executes only when the verification is
+ * meaningful; see [apply].
*/
class IncrementGuard : Plugin {
@@ -57,40 +64,114 @@ class IncrementGuard : Plugin {
}
return baseBranch.endsWith("master") || baseBranch.endsWith("main")
}
+
+ /**
+ * Tells whether the [CheckVersionIncrement] action must actually run for
+ * the current build.
+ *
+ * The increment is verified in two situations:
+ * 1. [ciPullRequest] — a CI pull request that must check the version
+ * (see [shouldCheckVersion]); or
+ * 2. a local build (not [onCi]) that is going to publish to Maven Local
+ * ([localPublish]).
+ *
+ * CI pushes and tag builds that publish to Maven Local — e.g. to feed
+ * integration tests — deliberately skip the check, so that re-publishing
+ * an already released version does not fail them.
+ */
+ internal fun mustVerify(
+ ciPullRequest: Boolean,
+ onCi: Boolean,
+ localPublish: Boolean,
+ ): Boolean = ciPullRequest || (!onCi && localPublish)
+
+ /**
+ * Tells whether [CheckVersionIncrement] should compare the project version against
+ * the base branch.
+ *
+ * The comparison reads `origin/:version.gradle.kts`, so it needs the base
+ * branch to have been fetched. Only the dedicated `Version Guard` workflow fetches it
+ * and sets the `VERSION_GUARD` environment variable, which [CheckVersionIncrement]
+ * passes here as the [underVersionGuard] flag; the workflow runs for pull requests, so
+ * a non-blank [baseRef] is required as well.
+ *
+ * Every other CI build (e.g. the Ubuntu and Windows builds) pulls the check into the
+ * task graph through `publishToMavenLocal` but runs a shallow checkout without the
+ * base ref. Such builds report `underVersionGuard = false`, so the comparison is
+ * skipped and `checkNotPublished` stays the guard — otherwise the comparison would
+ * fail closed on `origin/` and break every pull request.
+ */
+ internal fun shouldCompareToBase(underVersionGuard: Boolean, baseRef: String?): Boolean =
+ underVersionGuard && !baseRef.isNullOrBlank()
+
+ /**
+ * Tells whether [tasks] contains a Maven Local publishing task that
+ * belongs to the given [project].
+ *
+ * The scan is limited to [project]'s own publications so that, in a
+ * multi-project build, a sibling module's `publishToMavenLocal` does not
+ * trigger this module's check — which would verify an unrelated version.
+ */
+ internal fun localPublishPlanned(tasks: Iterable, project: Project): Boolean =
+ tasks.any { it is PublishToMavenLocal && it.project == project }
}
/**
- * Adds the [CheckVersionIncrement] task to the project.
+ * Adds the [CheckVersionIncrement] task to the [target] project and makes every
+ * `publishToMavenLocal` task depend on it, so that a local publish — used by
+ * integration tests that consume artifacts from `~/.m2` — cannot overwrite an
+ * already published version.
+ *
+ * The CI pull-request increment check is driven separately by the `Version Guard`
+ * workflow (`increment-guard.yml`), which invokes `checkVersionIncrement` by name after
+ * fetching the base branch and setting `VERSION_GUARD` — the signal that gates
+ * [CheckVersionIncrement]'s strict base-branch comparison (see [shouldCompareToBase]).
+ * The task is intentionally not wired into the `check` lifecycle: the version check
+ * belongs to the publishing path, not to generic `check` runs. A build that still pulls
+ * the task in through `publishToMavenLocal` (e.g. the Ubuntu/Windows CI builds, which
+ * publish locally to feed integration tests) stays green, because the base comparison —
+ * which reads `origin/` and would otherwise fail closed on a shallow checkout — is
+ * skipped outside the `Version Guard` workflow.
*
- * 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.
+ * The task is always created and wired, but its action runs only when:
+ * 1. the build is a GitHub Actions pull request targeting a default
+ * (`master` or `main`) or a release-line (e.g. `2.x-jdk8-master`) branch; or
+ * 2. the build runs locally (outside CI) and is going to publish artifacts
+ * to Maven Local.
*
* 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.
+ * direct pushes, and tag builds do not run the check. In particular, the
+ * Maven Local guard is restricted to local builds: re-building `master`,
+ * creating git tags, and other CI jobs that publish locally (e.g. to feed
+ * integration tests) keep succeeding even though their version is already
+ * published. Ordinary local builds that do not publish stay free from the
+ * network-bound version check as well.
*/
override fun apply(target: Project) {
val tasks = target.tasks
- tasks.register(taskName, CheckVersionIncrement::class.java) {
+ val checkVersion = 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
+ onlyIf {
+ mustVerify(shouldCheckVersion(), Build.ci, it.publishesToMavenLocal())
}
}
+
+ // The CI pull-request increment check is run by the `Version Guard` workflow,
+ // which calls `checkVersionIncrement` directly after fetching the base branch.
+ // It is intentionally not a dependency of `check`: that would run it in every
+ // `./gradlew build` (e.g. the Ubuntu/Windows CI builds), where `origin/`
+ // is not fetched and the fail-closed base comparison would break the build.
+
+ // Verify before publishing to Maven Local: integration tests in this and
+ // sibling projects consume the freshly published artifacts from `~/.m2`, so a
+ // non-incremented version would let them pick up a stale artifact.
+ tasks.withType(PublishToMavenLocal::class.java).configureEach {
+ dependsOn(checkVersion)
+ }
}
/**
@@ -110,3 +191,20 @@ class IncrementGuard : Plugin {
return shouldCheckVersion(event, baseBranch)
}
}
+
+/**
+ * Tells whether the current build is going to publish this task's project to
+ * Maven Local.
+ *
+ * Integration tests in this and sibling projects consume freshly built artifacts
+ * from `~/.m2`. Publishing them under a version that already exists would let those
+ * tests pick up a stale artifact, so the version increment must be verified before
+ * any local publication runs.
+ *
+ * Only this task's own project is considered: a sibling module's local publish in
+ * the same invocation must not trigger this module's check. The predicate is
+ * evaluated lazily as a task `onlyIf` spec, by which point the execution
+ * [task graph][org.gradle.api.execution.TaskExecutionGraph] is fully populated.
+ */
+private fun Task.publishesToMavenLocal(): Boolean =
+ IncrementGuard.localPublishPlanned(project.gradle.taskGraph.allTasks, project)
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/MavenMetadata.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/MavenMetadata.kt
new file mode 100644
index 0000000000..3d4906d293
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/MavenMetadata.kt
@@ -0,0 +1,84 @@
+/*
+ * 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 com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
+import com.fasterxml.jackson.dataformat.xml.XmlMapper
+import java.io.FileNotFoundException
+import java.net.URL
+
+/**
+ * A minimal model of a Maven `maven-metadata.xml` document, exposing the published
+ * versions of an artifact.
+ *
+ * Instances are produced by [XmlMapper] from a registry response; only the ``
+ * element is mapped, and unknown elements are ignored.
+ *
+ * @property versioning The `` element holding the list of published versions.
+ * It is `var` with a default value purely to support deserialization: `buildSrc` uses a
+ * plain [XmlMapper] without the Kotlin module, so Jackson instantiates this class through
+ * the synthesized no-arg constructor and then assigns the property through its setter. The
+ * mutability is required by Jackson, not used by our own code; a `val` would silently
+ * leave the version list empty.
+ */
+internal data class MavenMetadata(var versioning: Versioning = Versioning()) {
+
+ companion object {
+
+ const val FILE_NAME = "maven-metadata.xml"
+
+ private val mapper = XmlMapper()
+
+ init {
+ mapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
+ }
+
+ /**
+ * Fetches the metadata for the repository and parses the document.
+ *
+ * If the document could not be found, assumes that the module was never
+ * released and thus has no metadata.
+ */
+ fun fetchAndParse(url: URL): MavenMetadata? {
+ return try {
+ val metadata = mapper.readValue(url, MavenMetadata::class.java)
+ metadata
+ } catch (_: FileNotFoundException) {
+ null
+ }
+ }
+ }
+}
+
+/**
+ * The `` element of a `maven-metadata.xml` document, listing the published versions.
+ *
+ * @property versions The published version strings. It is `var` for the same reason as
+ * [MavenMetadata.versioning]: Jackson assigns it through the setter during deserialization
+ * (`buildSrc` has no Kotlin module), so a `val` would leave it empty.
+ */
+internal data class Versioning(var versions: List = listOf())
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt
index 800f2a38f8..8877dc615e 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt
@@ -99,11 +99,6 @@ val RepositoryHandler.intellijReleases: MavenArtifactRepository
includeIntelliJPlatformOnly()
}
-val RepositoryHandler.jetBrainsCacheRedirector: MavenArtifactRepository
- get() = maven("https://cache-redirector.jetbrains.com/intellij-dependencies") {
- includeIntelliJPlatformOnly()
- }
-
val RepositoryHandler.intellijDependencies: MavenArtifactRepository
get() = maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") {
includeIntelliJPlatformOnly()
@@ -118,7 +113,7 @@ fun RepositoryHandler.standardToSpineSdk() {
// 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
- // mirror like `cache-redirector.jetbrains.com`.
+ // 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
@@ -147,11 +142,13 @@ fun RepositoryHandler.standardToSpineSdk() {
}
}
- // IntelliJ Platform repositories. Each is restricted to the IntelliJ
- // coordinates it serves (see `includeIntelliJPlatformOnly`), so a transient
- // 5xx from one of them cannot break the resolution of unrelated artifacts.
+ // 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
- jetBrainsCacheRedirector
intellijDependencies
maven {
@@ -219,12 +216,11 @@ private fun ArtifactRepository.excludeSpine() {
* Restricts a JetBrains/IntelliJ Platform repository to the coordinates it
* actually serves.
*
- * These hosts — `cache-redirector.jetbrains.com` in particular — 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 the redirector is
- * queried for every artifact, so a single 502 on an unrelated POM (such as
- * `com.fasterxml.jackson:jackson-parent`) breaks the whole build.
+ * 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 {
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt
index db2bf761ae..64e46ef75b 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt
+++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt
@@ -27,6 +27,7 @@
package io.spine.gradle.report.pom
import groovy.xml.MarkupBuilder
+import io.spine.gradle.VersionComparator
import java.io.Writer
import java.util.*
import kotlin.reflect.full.isSubclassOf
diff --git a/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts b/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts
deleted file mode 100644
index 7334ef97f0..0000000000
--- a/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 185c9cdfdd..0000000000
--- a/buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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
index a7b3113092..8e777c6aca 100644
--- a/buildSrc/src/main/kotlin/jvm-module.gradle.kts
+++ b/buildSrc/src/main/kotlin/jvm-module.gradle.kts
@@ -29,6 +29,7 @@ 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.isDokka
import io.spine.dependency.lib.Guava
import io.spine.dependency.lib.Jackson
import io.spine.dependency.lib.Kotlin
@@ -132,6 +133,9 @@ fun Module.forceConfigurations() {
forceVersions()
excludeProtobufLite()
all {
+ if (isDokka) {
+ return@all
+ }
resolutionStrategy {
val cfg = this@all
val rs = this@resolutionStrategy
diff --git a/buildSrc/src/main/kotlin/kmp-module.gradle.kts b/buildSrc/src/main/kotlin/kmp-module.gradle.kts
index a2e6d82e58..636a84d22f 100644
--- a/buildSrc/src/main/kotlin/kmp-module.gradle.kts
+++ b/buildSrc/src/main/kotlin/kmp-module.gradle.kts
@@ -25,6 +25,7 @@
*/
import io.spine.dependency.boms.BomsPlugin
+import io.spine.dependency.isDokka
import io.spine.dependency.lib.Jackson
import io.spine.dependency.lib.Kotlin
import io.spine.dependency.local.Reflect
@@ -83,6 +84,9 @@ fun Project.forceConfigurations() {
with(configurations) {
forceVersions()
all {
+ if (isDokka) {
+ return@all
+ }
resolutionStrategy {
val cfg = this@all
val rs = this@resolutionStrategy
diff --git a/buildSrc/src/main/kotlin/module.gradle.kts b/buildSrc/src/main/kotlin/module.gradle.kts
index e0280fba4b..bc077f0644 100644
--- a/buildSrc/src/main/kotlin/module.gradle.kts
+++ b/buildSrc/src/main/kotlin/module.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright 2025, 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.
@@ -141,7 +141,7 @@ fun Module.forceConfigurations() {
fun Module.setTaskDependencies(generatedDir: String) {
tasks {
- val cleanGenerated by registering(Delete::class) {
+ val cleanGenerated = register("cleanGenerated") {
delete(generatedDir)
}
clean.configure {
diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/VersionComparatorSpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/VersionComparatorSpec.kt
new file mode 100644
index 0000000000..c643645baa
--- /dev/null
+++ b/buildSrc/src/test/kotlin/io/spine/gradle/VersionComparatorSpec.kt
@@ -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.
+ */
+
+package io.spine.gradle
+
+import io.kotest.matchers.ints.shouldBeGreaterThan
+import io.kotest.matchers.ints.shouldBeLessThan
+import io.kotest.matchers.shouldBe
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+
+@DisplayName("`VersionComparator` should")
+internal class VersionComparatorSpec {
+
+ /**
+ * Asserts that [newer] compares above [older], checking both directions.
+ */
+ private fun assertNewer(newer: String, older: String) {
+ VersionComparator.compare(newer, older) shouldBeGreaterThan 0
+ VersionComparator.compare(older, newer) shouldBeLessThan 0
+ }
+
+ @Test
+ fun `compare numeric segments as numbers`() {
+ assertNewer("10.0.0", "9.2.0")
+ assertNewer("2.10.0", "2.9.1")
+ assertNewer("1.0.10", "1.0.9")
+ }
+
+ @Test
+ fun `compare numeric qualifier segments as numbers`() {
+ assertNewer("2.0.0-SNAPSHOT.100", "2.0.0-SNAPSHOT.99")
+ assertNewer("2.0.0-SNAPSHOT.100", "2.0.0-SNAPSHOT.070")
+ }
+
+ @Test
+ fun `treat a release as newer than its pre-release`() {
+ assertNewer("2.0.0", "2.0.0-SNAPSHOT.100")
+ assertNewer("1.0.0", "1.0.0-RC.2")
+ }
+
+ @Test
+ fun `treat a longer version as newer when the common segments are equal`() {
+ assertNewer("1.0.1", "1.0")
+ assertNewer("1.0.0-RC.1", "1.0.0-RC")
+ }
+
+ @Test
+ fun `ignore the case of textual segments`() {
+ assertNewer("1.0.0-snapshot.10", "1.0.0-SNAPSHOT.2")
+ VersionComparator.compare("1.0.0-RC", "1.0.0-rc") shouldBe 0
+ }
+
+ @Test
+ fun `order a numeric segment before a textual one`() {
+ assertNewer("1.0.0-alpha", "1.0.0-1")
+ }
+
+ @Test
+ fun `treat equal versions as equal`() {
+ VersionComparator.compare("2.0.0-SNAPSHOT.070", "2.0.0-SNAPSHOT.070") shouldBe 0
+ VersionComparator.compare("31.1-jre", "31.1-jre") shouldBe 0
+ }
+}
diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/VersionGradleFileSpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/VersionGradleFileSpec.kt
new file mode 100644
index 0000000000..e76febe211
--- /dev/null
+++ b/buildSrc/src/test/kotlin/io/spine/gradle/VersionGradleFileSpec.kt
@@ -0,0 +1,86 @@
+/*
+ * 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
+
+import io.kotest.matchers.shouldBe
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+
+@DisplayName("`VersionGradleFile` should read the publishing version")
+internal class VersionGradleFileSpec {
+
+ @Test
+ fun `declared as a literal`() {
+ val content = """
+ val versionToPublish: String by extra("2.0.0-SNAPSHOT.182")
+ """.trimIndent()
+
+ VersionGradleFile.keyForValue(content, "2.0.0-SNAPSHOT.182") shouldBe "versionToPublish"
+ VersionGradleFile.valueForKey(content, "versionToPublish") shouldBe "2.0.0-SNAPSHOT.182"
+ }
+
+ @Test
+ fun `declared as an alias to another 'extra'`() {
+ val content = """
+ val compilerVersion: String by extra("2.0.0-SNAPSHOT.043")
+ val versionToPublish by extra(compilerVersion)
+ """.trimIndent()
+
+ VersionGradleFile.valueForKey(content, "versionToPublish") shouldBe "2.0.0-SNAPSHOT.043"
+ VersionGradleFile.valueForKey(content, "compilerVersion") shouldBe "2.0.0-SNAPSHOT.043"
+ }
+
+ @Test
+ fun `declared as an alias to a plain 'val'`() {
+ val content = """
+ val base = "2.0.0-SNAPSHOT.043"
+ val versionToPublish by extra(base)
+ """.trimIndent()
+
+ VersionGradleFile.valueForKey(content, "versionToPublish") shouldBe "2.0.0-SNAPSHOT.043"
+ }
+
+ @Test
+ fun `identified by the resolved project version, not a hard-coded name`() {
+ val content = """
+ val kotlinVersion: String by extra("2.1.0")
+ val versionToPublish: String by extra("2.0.0-SNAPSHOT.182")
+ """.trimIndent()
+
+ VersionGradleFile.keyForValue(content, "2.0.0-SNAPSHOT.182") shouldBe "versionToPublish"
+ }
+
+ @Test
+ fun `absent when no property matches`() {
+ val content = """
+ val versionToPublish: String by extra("2.0.0-SNAPSHOT.182")
+ """.trimIndent()
+
+ VersionGradleFile.keyForValue(content, "9.9.9") shouldBe null
+ VersionGradleFile.valueForKey(content, "missing") shouldBe null
+ }
+}
diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt b/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt
index 5633c30d4d..7b317c94e9 100644
--- a/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt
+++ b/buildSrc/src/test/kotlin/io/spine/gradle/publish/IncrementGuardTest.kt
@@ -26,8 +26,17 @@
package io.spine.gradle.publish
+import io.kotest.matchers.collections.shouldContain
+import io.kotest.matchers.collections.shouldNotContain
import io.kotest.matchers.shouldBe
+import io.spine.gradle.publish.IncrementGuard.Companion.localPublishPlanned
+import io.spine.gradle.publish.IncrementGuard.Companion.mustVerify
import io.spine.gradle.publish.IncrementGuard.Companion.shouldCheckVersion
+import io.spine.gradle.publish.IncrementGuard.Companion.shouldCompareToBase
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.publish.maven.tasks.PublishToMavenLocal
+import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
@@ -76,4 +85,138 @@ class IncrementGuardTest {
shouldCheckVersion(null, null) shouldBe false
}
}
+
+ @Nested
+ inner class `actually run the check` {
+
+ @Test
+ fun `on a CI pull request to a protected branch`() {
+ mustVerify(ciPullRequest = true, onCi = true, localPublish = false) shouldBe true
+ }
+
+ @Test
+ fun `on a local build that publishes to Maven Local`() {
+ mustVerify(ciPullRequest = false, onCi = false, localPublish = true) shouldBe true
+ }
+ }
+
+ @Nested
+ inner class `skip the check` {
+
+ @Test
+ fun `on a local build that does not publish`() {
+ mustVerify(ciPullRequest = false, onCi = false, localPublish = false) shouldBe false
+ }
+
+ @Test
+ fun `on a CI build that publishes to Maven Local outside a protected-branch PR`() {
+ // E.g. a push to `master` or a tag build running integration tests: the
+ // version is already published, so re-verifying it would fail the build.
+ mustVerify(ciPullRequest = false, onCi = true, localPublish = true) shouldBe false
+ }
+ }
+
+ @Nested
+ inner class `compare against the base branch` {
+
+ @Test
+ fun `inside the Version Guard workflow with a base branch`() {
+ shouldCompareToBase(underVersionGuard = true, baseRef = "master") shouldBe true
+ shouldCompareToBase(underVersionGuard = true, baseRef = "2.x-jdk8-master") shouldBe true
+ }
+ }
+
+ @Nested
+ inner class `not compare against the base branch` {
+
+ @Test
+ fun `outside the Version Guard workflow`() {
+ // The Ubuntu/Windows CI builds pull the task in via `publishToMavenLocal`,
+ // but they never fetch the base ref, so `VERSION_GUARD` is unset.
+ shouldCompareToBase(underVersionGuard = false, baseRef = "master") shouldBe false
+ }
+
+ @Test
+ fun `when no base branch is present`() {
+ shouldCompareToBase(underVersionGuard = true, baseRef = null) shouldBe false
+ shouldCompareToBase(underVersionGuard = true, baseRef = "") shouldBe false
+ }
+ }
+
+ @Nested
+ inner class `detect a Maven Local publish` {
+
+ @Test
+ fun `for the task's own project`() {
+ val project = guardedProject()
+ val publish = project.tasks
+ .register("publishFooPublicationToMavenLocal", PublishToMavenLocal::class.java)
+ .get()
+
+ localPublishPlanned(listOf(publish), project) shouldBe true
+ }
+
+ @Test
+ fun `but not when only a sibling project publishes`() {
+ val root = ProjectBuilder.builder().build()
+ val lib = ProjectBuilder.builder().withParent(root).withName("lib").build()
+ val app = ProjectBuilder.builder().withParent(root).withName("app").build()
+ app.pluginManager.apply("maven-publish")
+ val appPublish = app.tasks
+ .register("publishFooPublicationToMavenLocal", PublishToMavenLocal::class.java)
+ .get()
+
+ localPublishPlanned(listOf(appPublish), lib) shouldBe false
+ }
+ }
+
+ @Nested
+ inner class `make 'checkVersionIncrement' a dependency of` {
+
+ @Test
+ fun `every Maven Local publishing task`() {
+ val project = guardedProject()
+ val localPublish = project.tasks.register(
+ "publishFooPublicationToMavenLocal",
+ PublishToMavenLocal::class.java
+ ).get()
+
+ localPublish.dependencyNames() shouldContain IncrementGuard.taskName
+ }
+ }
+
+ @Nested
+ inner class `keep 'checkVersionIncrement' out of` {
+
+ @Test
+ fun `the 'check' lifecycle task`() {
+ // The CI check runs via the `Version Guard` workflow, which fetches the
+ // base branch first. Wiring it into `check` would run it in every
+ // `./gradlew build`, where `origin/` is absent and the fail-closed
+ // base comparison would break the build.
+ val project = guardedProject()
+ val check = project.tasks.getByName("check")
+
+ check.dependencyNames() shouldNotContain IncrementGuard.taskName
+ }
+ }
}
+
+/**
+ * Creates a project with the `base` plugin (for the `check` task), the
+ * `maven-publish` plugin (for [PublishToMavenLocal] tasks), and [IncrementGuard]
+ * applied.
+ */
+private fun guardedProject(): Project {
+ val project = ProjectBuilder.builder().build()
+ project.pluginManager.apply("base")
+ project.pluginManager.apply("maven-publish")
+ project.pluginManager.apply(IncrementGuard::class.java)
+ return project
+}
+
+/**
+ * Obtains the names of the tasks this task directly depends on.
+ */
+private fun Task.dependencyNames(): Set =
+ taskDependencies.getDependencies(this).map { it.name }.toSet()
diff --git a/buildSrc/src/test/kotlin/io/spine/gradle/publish/MavenMetadataSpec.kt b/buildSrc/src/test/kotlin/io/spine/gradle/publish/MavenMetadataSpec.kt
new file mode 100644
index 0000000000..39f8a510fe
--- /dev/null
+++ b/buildSrc/src/test/kotlin/io/spine/gradle/publish/MavenMetadataSpec.kt
@@ -0,0 +1,53 @@
+/*
+ * 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 com.fasterxml.jackson.dataformat.xml.XmlMapper
+import io.kotest.matchers.collections.shouldContainExactly
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+
+@DisplayName("`MavenMetadata` should")
+internal class MavenMetadataSpec {
+
+ /**
+ * Round-trips through the same [XmlMapper] used in production, asserting the version list
+ * survives. This guards the `var` properties of [MavenMetadata] and [Versioning]: a `val`
+ * (or `internal`-mangled setter) would leave the list empty after deserialization, silently
+ * disabling the "already published" check.
+ */
+ @Test
+ fun `survive a Jackson round-trip, keeping its versions`() {
+ val versions = listOf("2.0.0-SNAPSHOT.79", "2.0.0-SNAPSHOT.80", "2.0.0-SNAPSHOT.81")
+ val mapper = XmlMapper()
+
+ val xml = mapper.writeValueAsString(MavenMetadata(Versioning(versions)))
+ val parsed = mapper.readValue(xml, MavenMetadata::class.java)
+
+ parsed.versioning.versions shouldContainExactly versions
+ }
+}
diff --git a/config b/config
index d93220ab6d..3f13608b3b 160000
--- a/config
+++ b/config
@@ -1 +1 @@
-Subproject commit d93220ab6d1e3bc97c333894596bac8b9bc9e898
+Subproject commit 3f13608b3bb1a8bc736fbb748b95ec3dc11843a2
diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md
index fd604e743e..91bc369b57 100644
--- a/docs/dependencies/dependencies.md
+++ b/docs/dependencies/dependencies.md
@@ -1,6 +1,6 @@
-# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.421`
+# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.422`
## Runtime
1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.1.0.
@@ -90,6 +90,10 @@
* **Project URL:** [https://github.com/google/gson](https://github.com/google/gson)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : com.google.code.gson. **Name** : gson. **Version** : 2.8.9.
+ * **Project URL:** [https://github.com/google/gson/gson](https://github.com/google/gson/gson)
+ * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : com.google.errorprone. **Name** : error_prone_annotation. **Version** : 2.36.0.
* **Project URL:** [https://errorprone.info/error_prone_annotation](https://errorprone.info/error_prone_annotation)
* **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -98,6 +102,10 @@
* **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations)
* **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.47.0.
+ * **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations)
+ * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : com.google.errorprone. **Name** : error_prone_check_api. **Version** : 2.36.0.
* **Project URL:** [https://errorprone.info/error_prone_check_api](https://errorprone.info/error_prone_check_api)
* **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -425,6 +433,10 @@
* **Project URL:** [https://jcommander.org](https://jcommander.org)
* **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 23.0.0.
+ * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations)
+ * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.1.0.
* **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations)
* **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -637,11 +649,11 @@
* **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
-1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.7.1.
+1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.8.0.
* **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
-1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.7.1.
+1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.8.0.
* **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -760,14 +772,14 @@
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
+This report was generated on **Mon Jun 29 20:03:32 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine:spine-base:2.0.0-SNAPSHOT.421`
+# Dependencies of `io.spine:spine-base:2.0.0-SNAPSHOT.422`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -839,7 +851,7 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
* **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
-1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0.
+1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0.
* **Project URL:** [https://github.com/FasterXML/jackson-bom](https://github.com/FasterXML/jackson-bom)
* **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
* **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -905,6 +917,10 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://github.com/google/gson](https://github.com/google/gson)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : com.google.code.gson. **Name** : gson. **Version** : 2.8.9.
+ * **Project URL:** [https://github.com/google/gson/gson](https://github.com/google/gson/gson)
+ * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing-api. **Version** : 2.3.9.
* **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp)
* **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -917,6 +933,10 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations)
* **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.47.0.
+ * **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations)
+ * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : com.google.errorprone. **Name** : error_prone_check_api. **Version** : 2.36.0.
* **Project URL:** [https://errorprone.info/error_prone_check_api](https://errorprone.info/error_prone_check_api)
* **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -1212,13 +1232,13 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
1. **Group** : org.apache.httpcomponents.core5. **Name** : httpcore5-h2. **Version** : 5.1.3.
* **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
-1. **Group** : org.apache.logging.log4j. **Name** : log4j-api. **Version** : 2.20.0.
- * **Project URL:** [https://www.apache.org/](https://www.apache.org/)
- * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : org.apache.logging.log4j. **Name** : log4j-api. **Version** : 2.26.0.
+ * **Project URL:** [https://logging.apache.org/log4j/2.x/](https://logging.apache.org/log4j/2.x/)
+ * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
-1. **Group** : org.apache.logging.log4j. **Name** : log4j-core. **Version** : 2.20.0.
- * **Project URL:** [https://www.apache.org/](https://www.apache.org/)
- * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : org.apache.logging.log4j. **Name** : log4j-core. **Version** : 2.26.0.
+ * **Project URL:** [https://logging.apache.org/log4j/2.x/](https://logging.apache.org/log4j/2.x/)
+ * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
1. **Group** : org.apiguardian. **Name** : apiguardian-api. **Version** : 1.1.2.
* **Project URL:** [https://github.com/apiguardian-team/apiguardian](https://github.com/apiguardian-team/apiguardian)
@@ -1269,6 +1289,10 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://jcommander.org](https://jcommander.org)
* **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 23.0.0.
+ * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations)
+ * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.1.0.
* **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations)
* **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -1425,6 +1449,10 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/)
* **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 1.8.20.
+ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/)
+ * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.0.21.
* **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/)
* **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -1433,6 +1461,10 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/)
* **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 1.8.20.
+ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/)
+ * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.0.21.
* **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/)
* **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -1457,10 +1489,18 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-bom. **Version** : 1.7.3.
+ * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)
+ * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-core. **Version** : 1.10.2.
* **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-core. **Version** : 1.7.3.
+ * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)
+ * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-core-jvm. **Version** : 1.10.2.
* **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -1469,6 +1509,10 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)
* **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-core-jvm. **Version** : 1.7.3.
+ * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)
+ * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-jdk8. **Version** : 1.10.2.
* **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -1481,11 +1525,11 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
-1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.7.1.
+1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.8.0.
* **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
-1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.7.1.
+1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.8.0.
* **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -1604,14 +1648,14 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
+This report was generated on **Mon Jun 29 20:03:32 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine:spine-environment:2.0.0-SNAPSHOT.421`
+# Dependencies of `io.spine:spine-environment:2.0.0-SNAPSHOT.422`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -1681,11 +1725,11 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
-1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.7.1.
+1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.8.0.
* **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
-1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.7.1.
+1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.8.0.
* **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -1760,6 +1804,10 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://github.com/google/gson](https://github.com/google/gson)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : com.google.code.gson. **Name** : gson. **Version** : 2.8.9.
+ * **Project URL:** [https://github.com/google/gson/gson](https://github.com/google/gson/gson)
+ * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : com.google.errorprone. **Name** : error_prone_annotation. **Version** : 2.36.0.
* **Project URL:** [https://errorprone.info/error_prone_annotation](https://errorprone.info/error_prone_annotation)
* **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -1768,6 +1816,10 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations)
* **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.47.0.
+ * **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations)
+ * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : com.google.errorprone. **Name** : error_prone_check_api. **Version** : 2.36.0.
* **Project URL:** [https://errorprone.info/error_prone_check_api](https://errorprone.info/error_prone_check_api)
* **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -2095,6 +2147,10 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://jcommander.org](https://jcommander.org)
* **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 23.0.0.
+ * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations)
+ * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.1.0.
* **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations)
* **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -2307,11 +2363,11 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
-1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.7.1.
+1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.8.0.
* **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
-1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.7.1.
+1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.8.0.
* **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -2430,14 +2486,14 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
+This report was generated on **Mon Jun 29 20:03:32 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine:spine-format:2.0.0-SNAPSHOT.421`
+# Dependencies of `io.spine:spine-format:2.0.0-SNAPSHOT.422`
## Runtime
1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0.
@@ -2662,6 +2718,10 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://github.com/google/gson](https://github.com/google/gson)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : com.google.code.gson. **Name** : gson. **Version** : 2.8.9.
+ * **Project URL:** [https://github.com/google/gson/gson](https://github.com/google/gson/gson)
+ * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : com.google.errorprone. **Name** : error_prone_annotation. **Version** : 2.36.0.
* **Project URL:** [https://errorprone.info/error_prone_annotation](https://errorprone.info/error_prone_annotation)
* **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -2670,6 +2730,10 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations)
* **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.47.0.
+ * **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations)
+ * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : com.google.errorprone. **Name** : error_prone_check_api. **Version** : 2.36.0.
* **Project URL:** [https://errorprone.info/error_prone_check_api](https://errorprone.info/error_prone_check_api)
* **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -2997,6 +3061,10 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://jcommander.org](https://jcommander.org)
* **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 23.0.0.
+ * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations)
+ * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
+
1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.1.0.
* **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations)
* **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -3209,11 +3277,11 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
* **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
-1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.7.1.
+1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.8.0.
* **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
-1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.7.1.
+1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.8.0.
* **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)
* **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)
@@ -3336,6 +3404,6 @@ This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Jun 23 00:11:07 WEST 2026** using
+This report was generated on **Mon Jun 29 20:03:32 WEST 2026** using
[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under
[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
\ No newline at end of file
diff --git a/docs/dependencies/pom.xml b/docs/dependencies/pom.xml
index 8179771bf9..47208a1141 100644
--- a/docs/dependencies/pom.xml
+++ b/docs/dependencies/pom.xml
@@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject.
-->
io.spine
base-libraries
-2.0.0-SNAPSHOT.421
+2.0.0-SNAPSHOT.422
2015
@@ -81,7 +81,7 @@ all modules and does not describe the project structure per-subproject.
io.spine
spine-logging
- 2.0.0-SNAPSHOT.417
+ 2.0.0-SNAPSHOT.419
compile
@@ -164,7 +164,7 @@ all modules and does not describe the project structure per-subproject.
io.spine
spine-logging-smoke-test
- 2.0.0-SNAPSHOT.417
+ 2.0.0-SNAPSHOT.419
test
@@ -176,7 +176,7 @@ all modules and does not describe the project structure per-subproject.
io.spine.tools
logging-testlib
- 2.0.0-SNAPSHOT.417
+ 2.0.0-SNAPSHOT.419
test
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index df6a6ad763..a9db11550c 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
diff --git a/gradlew b/gradlew
index b9bb139f79..249efbb032 100755
--- a/gradlew
+++ b/gradlew
@@ -20,7 +20,7 @@
##############################################################################
#
-# Gradle start up script for POSIX generated by Gradle.
+# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
-# ksh Gradle
+# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
diff --git a/gradlew.bat b/gradlew.bat
index 24c62d56f2..a51ec4f588 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -19,7 +19,7 @@
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
-@rem Gradle startup script for Windows
+@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2
-@rem Execute Gradle
+@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
diff --git a/init-submodules b/init-submodules
index d2914ac6b9..2ae143d8ea 100755
--- a/init-submodules
+++ b/init-submodules
@@ -2,8 +2,8 @@
################################################################################
#
-# Materialize the submodules a fresh working tree is missing, so agent assets
-# resolve.
+# Materialize the *config-managed* submodules a fresh working tree is missing, so
+# agent assets resolve.
#
# `git worktree add` — and some shallow CI / cloud checkouts — populate only the
# superproject's own tracked files; registered submodules are left UNinitialized.
@@ -17,12 +17,28 @@
# (distributed by `config`), so `git worktree add` always checks it out — it can
# therefore bring `config` itself into existence.
#
-# It initializes ONLY submodules that are not yet checked out (those
-# `git submodule status` marks with a leading `-`), at the commit the branch
-# pins. Submodules already present are left exactly as they are, so a tree that
-# floated `config` / `.agents/shared` to a branch tip via `./config/pull` is
-# never silently rewound to the pin. That makes the script idempotent and safe to
-# run on every session start.
+# It initializes ONLY submodules that are BOTH:
+#
+# * not yet checked out — those `git submodule status` marks with a leading `-`,
+# at the commit the branch pins; and
+#
+# * config-managed — `config` itself (the bootstrap target `pull` lives inside,
+# which carries no tracked `branch` in a consumer's `.gitmodules`), plus every
+# submodule that declares a tracked `branch` in `.gitmodules`. This is exactly
+# the rule `./config/pull` uses to decide what it floats, so the two scripts
+# can never disagree about what is shared.
+#
+# Consumer-owned submodules (a Hugo theme, a vendored library, documentation
+# examples, ...) declare no tracked branch and are deliberately left untouched.
+# Because a `SessionStart` hook runs this script automatically on every session,
+# initializing them would mean trying to clone — or failing on credentials for —
+# a submodule this project does not manage, on every single start. They are
+# skipped (noted on stderr).
+#
+# Submodules already present are left exactly as they are, so a tree that floated
+# `config` / `.agents/shared` to a branch tip via `./config/pull` is never
+# silently rewound to the pin. That makes the script idempotent and safe to run on
+# every session start.
#
# It does NOT float submodules to their branch tips — run `./config/pull`
# afterwards for that. Unlike `pull`, it depends on no pre-existing `config`
@@ -39,14 +55,45 @@ cd "$root" || exit 0
# Nothing to do in a repo without submodules.
[ -f .gitmodules ] || exit 0
+# The set of config-managed submodule paths: `config` itself (handled specially —
+# it carries no tracked branch, exactly as in `./config/pull`), plus every
+# submodule declaring a tracked `branch` in `.gitmodules`. Mirrors `pull`'s rule.
+config_managed_paths() {
+ printf '%s\n' 'config'
+ git config -f .gitmodules --get-regexp '^submodule\..*\.branch$' 2>/dev/null \
+ | while read -r key _branch; do
+ name=${key#submodule.}; name=${name%.branch}
+ git config -f .gitmodules --get "submodule.$name.path" 2>/dev/null
+ done
+}
+
+managed=$(config_managed_paths | sort -u)
+
# `git submodule status` prefixes each uninitialized submodule with `-`; an
-# initialized one starts with a space (at the pinned commit) or `+` (ahead of
-# it). Act only on the `-` lines, taking the path from the second field.
+# initialized one starts with a space (at the pinned commit) or `+` (ahead of it).
+# Act only on the `-` lines, taking the path from the second field, and only when
+# that path is config-managed.
git submodule status 2>/dev/null | awk '$1 ~ /^-/ { print $2 }' | while read -r path; do
[ -n "$path" ] || continue
- echo "init-submodules: initializing '$path'"
- git submodule update --init --recursive -- "$path" \
- || echo "init-submodules: WARNING — could not initialize '$path' (offline?)." >&2
+ if printf '%s\n' "$managed" | grep -qxF -- "$path"; then
+ echo "init-submodules: initializing '$path'"
+ git submodule update --init --recursive -- "$path" \
+ || echo "init-submodules: WARNING — could not initialize '$path' (offline?)." >&2
+ else
+ echo "init-submodules: skipping consumer-owned '$path' (not config-managed)." >&2
+ fi
done
+# Route Git hooks to the shared hooks directory so the secret-scan `pre-commit`
+# hook is active even in a brand-new worktree, before `./config/pull` runs. The
+# path floats with the `.agents/shared` submodule; until that submodule is
+# initialized the hook simply does not fire (Git skips a missing hook). Set only
+# when unset or already ours — never override a repo's own `core.hooksPath`.
+desired_hooks=".agents/scripts/git-hooks"
+current_hooks=$(git config --local --get core.hooksPath 2>/dev/null || true)
+if [ -z "$current_hooks" ] || [ "$current_hooks" = "$desired_hooks" ]; then
+ git config --local core.hooksPath "$desired_hooks" \
+ && echo "init-submodules: Git hooks routed to '$desired_hooks' (secret-scan pre-commit active)."
+fi
+
exit 0
diff --git a/version.gradle.kts b/version.gradle.kts
index 3254c54509..42a0dc25b4 100644
--- a/version.gradle.kts
+++ b/version.gradle.kts
@@ -24,4 +24,4 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-val versionToPublish: String by extra("2.0.0-SNAPSHOT.421")
+val versionToPublish: String by extra("2.0.0-SNAPSHOT.422")