Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/memory/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ See [README.md](README.md) for the format and routing rules.
## Project (durable context & rationale)

- [which-fixer applied](which-fixer-applied.md) — bulk sweep done; skill now runs in incremental mode
- [integration tests stale compiler after version bump](project/integration-tests-stale-compiler-after-version-bump.md) — after a version bump, clean-build `integrationTest` or it may launch the old compiler

## Reference (external systems)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
name: integration-tests-stale-compiler-after-version-bump
description: After a version bump, an incremental `integrationTest` can silently launch the OLD compiler — clean-build it.
metadata:
type: project
since: 2026-06-30
---

After bumping `compilerVersion` in `version.gradle.kts`, an **incremental**
`./gradlew integrationTest` can silently exercise the **previous** compiler
version, masking or falsely reproducing bugs.

The Compiler Gradle plugin records its own version in a generated resource
(`version.txt` / `META-INF/io.spine/…compiler-gradle-plugin.meta`), and
`io.spine.tools.compiler.gradle.plugin.Plugin.version` reads it to decide which
`compiler-cli-all:<version>` the consumer launches (`LaunchSpineCompiler`). That
resource is **not reliably regenerated** on an incremental build when only the
version changes, so a freshly published `…-N` plugin can still embed `…-(N-1)`
and launch the older CLI from Maven Local — even though the `…-N` `compiler-api`
/ `compiler-cli-all` jars themselves contain the new bytecode.

**Why:** the version-bump step changes `version.gradle.kts` but the metadata
task's up-to-date check (and the build cache) may not treat that as an input, so
the stale resource survives. CI never hits this because it builds clean.

**How to apply:** after a version bump, verify integration with a clean build —
`./gradlew clean integrationTest --no-build-cache` — not an incremental one. To
diagnose which compiler a consumer actually launches, run
`cd tests && ./gradlew :consumer:dependencies --configuration spineCompilerRawArtifact`
and confirm the resolved `compiler-cli-all` version matches `version.gradle.kts`.
To confirm a published jar has the expected code, `javap -l -p` the bundled
`io/spine/tools/compiler/render/*.class` and compare line numbers. The compiler
runs as the `compiler-cli-all` fat jar via the CLI, not the plain api jar.
2 changes: 1 addition & 1 deletion .github/workflows/build-on-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
# there anyway).
- name: Upload code coverage report
if: steps.codecov.outputs.available == 'true' || github.event_name == 'push'
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
Expand Down
19 changes: 18 additions & 1 deletion api/src/main/kotlin/io/spine/tools/compiler/render/Renderer.kt
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -44,9 +44,26 @@ protected constructor(language: L) : Member<L>(language) {

/**
* Performs required changes to the given source set.
*
* The renderer processes only the files written in its [language].
* If [sources] contains files but none of them are in this language,
* the set is assumed to belong to another language and is left untouched.
Comment thread
Copilot marked this conversation as resolved.
*
* An empty source set is still passed to [render] because a renderer may
* create new files from scratch rather than modify the existing ones.
*/
public fun renderSources(sources: SourceFileSet) {
val relevantFiles = sources.subsetWhere { language.matches(it.relativePath) }

// A non-empty source set with no files in this renderer's `language`
// belongs to another language. Skip it so that, for example, a Java
// renderer does not attempt to create or locate `.java` files under
// a directory that holds generated Kotlin code.
val belongsToAnotherLanguage = relevantFiles.isEmpty && !sources.isEmpty
if (belongsToAnotherLanguage) {
return
}

relevantFiles.prepareForQueries(this)
render(relevantFiles)
sources.mergeBack(relevantFiles)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -31,12 +31,18 @@ import io.spine.tools.compiler.backend.CodeGenerationContext
import io.spine.tools.compiler.context.CodegenContext
import io.spine.tools.compiler.type.TypeSystem
import io.spine.tools.code.AnyLanguage
import io.spine.tools.code.Java
import java.nio.file.Path
import kotlin.io.path.createParentDirectories
import kotlin.io.path.div
import kotlin.io.path.writeText
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir

@DisplayName("`Renderer` should")
internal class RendererSpec {
Expand Down Expand Up @@ -86,6 +92,48 @@ internal class RendererSpec {
}
}
}

@Test
fun `render a source set written in its language`(
@TempDir input: Path,
@TempDir output: Path
) {
val javaRenderer = RecordingJavaRenderer()
val sourceSet = sourceSetWith("corp/acme/Hello.java", input = input, output = output)
javaRenderer.renderSources(sourceSet)
javaRenderer.invoked shouldBe true
}

@Test
fun `skip a source set written in another language`(
@TempDir input: Path,
@TempDir output: Path
) {
val javaRenderer = RecordingJavaRenderer()
val sourceSet = sourceSetWith("corp/acme/Hello.kt", input = input, output = output)
javaRenderer.renderSources(sourceSet)
javaRenderer.invoked shouldBe false
}

@Test
fun `render an empty source set so that new files can be created`(
@TempDir input: Path,
@TempDir output: Path
) {
val javaRenderer = RecordingJavaRenderer()
javaRenderer.renderSources(SourceFileSet.create(input, output))
javaRenderer.invoked shouldBe true
}
}

/**
* Creates a source set with a single file under the given [input] root,
* placing the processed file into the [output] root.
*/
private fun sourceSetWith(relativePath: String, input: Path, output: Path): SourceFileSet {
val file = (input / relativePath).apply { createParentDirectories() }
file.writeText("// A stub file for testing.")
return SourceFileSet.create(input, output)
}

private class StubRenderer : Renderer<AnyLanguage>(AnyLanguage) {
Expand All @@ -97,3 +145,16 @@ private class StubRenderer : Renderer<AnyLanguage>(AnyLanguage) {

override fun render(sources: SourceFileSet) = Unit
}

/**
* A [Java] renderer that records whether its [render] method was invoked.
*/
private class RecordingJavaRenderer : Renderer<Java>(Java) {

var invoked: Boolean = false
private set

override fun render(sources: SourceFileSet) {
invoked = true
}
}
6 changes: 5 additions & 1 deletion buildSrc/src/main/kotlin/DependencyResolution.kt
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -30,6 +30,7 @@ import io.spine.dependency.build.Dokka
import io.spine.dependency.build.ErrorProne
import io.spine.dependency.build.FindBugs
import io.spine.dependency.build.JSpecify
import io.spine.dependency.isDokka
import io.spine.dependency.lib.Asm
import io.spine.dependency.lib.AutoCommon
import io.spine.dependency.lib.AutoService
Expand Down Expand Up @@ -73,6 +74,9 @@ fun doForceVersions(configurations: ConfigurationContainer) {
*/
fun NamedDomainObjectContainer<Configuration>.forceVersions() {
all {
if (isDokka) {
return@all
}
resolutionStrategy {
failOnVersionConflict()
cacheChangingModulesFor(0, "seconds")
Expand Down
11 changes: 10 additions & 1 deletion buildSrc/src/main/kotlin/dokka-setup.gradle.kts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -41,6 +41,15 @@ tasks.withType<DokkaBaseTask>().configureEach {
}
}

// The Dokka Javadoc format does not support Kotlin Multiplatform source sets, so its
// publication task fails for KMP modules ("No source set found for <module>/jvmMain").
// KMP modules publish HTML documentation, so skip the Javadoc publication for them.
plugins.withId("org.jetbrains.kotlin.multiplatform") {
tasks.matching { it.name == "dokkaGeneratePublicationJavadoc" }.configureEach {
enabled = false
}
}

afterEvaluate {
dokka {
configureForKotlin(
Expand Down
11 changes: 11 additions & 0 deletions buildSrc/src/main/kotlin/io/spine/dependency/Dependency.kt
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ abstract class DependencyWithBom : Dependency() {
fun Configuration.diagSuffix(project: Project): String =
"the configuration `$name` in the project: `${project.path}`."

/**
* Tells if this configuration belongs to Dokka's own generator/plugin classpath.
*
* Dokka resolves these `dokka*` configurations using dependency versions pinned by
* Dokka itself (for example, Jackson or Kotlin), which legitimately differ from the
* project's. Forcing the project's versions onto them breaks `dokkaGenerate`, so such
* configurations must be excluded from the project's version forcing.
*/
val Configuration.isDokka: Boolean
get() = name.startsWith("dokka")

private fun ResolutionStrategy.forceWithLogging(
project: Project,
configuration: Configuration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ package io.spine.dependency.boms
import io.gitlab.arturbosch.detekt.getSupportedKotlinVersion
import io.spine.dependency.DependencyWithBom
import io.spine.dependency.diagSuffix
import io.spine.dependency.isDokka
import io.spine.dependency.kotlinx.Coroutines
import io.spine.dependency.lib.Kotlin
import io.spine.dependency.test.JUnit
Expand Down Expand Up @@ -88,7 +89,7 @@ class BomsPlugin : Plugin<Project> {
applyBoms(project, Boms.core + Boms.testing)
}

matching { !supportsBom(it.name) }.all {
matching { !supportsBom(it.name) && !it.isDokka }.all {
resolutionStrategy.eachDependency {
if (requested.group == Kotlin.group) {
val kotlinVersion = Kotlin.runtimeVersion
Expand Down Expand Up @@ -170,7 +171,7 @@ private fun supportsBom(name: String) =
private fun Project.forceArtifacts() =
configurations.all {
resolutionStrategy {
if (!isDetekt) {
if (!isDetekt && !isDokka) {
val rs = this@resolutionStrategy
val project = this@forceArtifacts
val cfg = this@all
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ object Compiler : Dependency() {
* The version of the Compiler dependencies.
*/
override val version: String
private const val fallbackVersion = "2.0.0-SNAPSHOT.055"
private const val fallbackVersion = "2.0.0-SNAPSHOT.057"

/**
* The distinct version of the Compiler used by other build tools.
Expand All @@ -81,7 +81,7 @@ object Compiler : Dependency() {
* transitive dependencies, this is the version used to build the project itself.
*/
val dogfoodingVersion: String
private const val fallbackDfVersion = "2.0.0-SNAPSHOT.055"
private const val fallbackDfVersion = "2.0.0-SNAPSHOT.057"

/**
* The artifact for the Compiler Gradle plugin.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ package io.spine.dependency.local
*/
@Suppress("ConstPropertyName", "unused")
object Logging {
const val version = "2.0.0-SNAPSHOT.417"
const val version = "2.0.0-SNAPSHOT.419"
const val group = Spine.group

const val loggingArtifact = "spine-logging"
Expand Down
40 changes: 40 additions & 0 deletions buildSrc/src/main/kotlin/io/spine/dependency/storage/H2.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
* disclaimer.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package io.spine.dependency.storage

/**
* The H2 Database Engine — a fast, in-memory/embedded SQL database used for exercising
* the JDBC storage in tests.
*
* @see <a href="https://github.com/h2database/h2database">H2 Database Engine at GitHub</a>
* @see <a href="https://h2database.com/">H2 Database Engine site</a>
*/
@Suppress("unused", "ConstPropertyName")
object H2 {
private const val version = "2.4.240"
const val lib = "com.h2database:h2:$version"
}
40 changes: 40 additions & 0 deletions buildSrc/src/main/kotlin/io/spine/dependency/storage/Hikari.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
* disclaimer.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package io.spine.dependency.storage

/**
* HikariCP — a fast, lightweight JDBC connection pool.
*
* The JDBC storage uses it to pool database connections.
*
* @see <a href="https://github.com/brettwooldridge/HikariCP">HikariCP at GitHub</a>
*/
@Suppress("unused", "ConstPropertyName")
object Hikari {
private const val version = "7.1.0"
const val lib = "com.zaxxer:HikariCP:$version"
}
Loading
Loading