add ADRs for gradle and spring boot version and version smoke-tests
This commit is contained in:
@@ -0,0 +1,113 @@
|
|||||||
|
# Gradle Version
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- proposed: 2026-06-09
|
||||||
|
- accepted: -
|
||||||
|
- rejected: -
|
||||||
|
- superseded: -
|
||||||
|
|
||||||
|
**Decision [proposed]:** Gradle 8.14 — minimum required by Spring Boot 4.0; Gradle 9 deferred until openapi-generator and ktlint-gradle confirm compatibility.
|
||||||
|
|
||||||
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
GitTally is a greenfield Kotlin/Spring Boot project.
|
||||||
|
A Gradle version must be chosen for the initial setup.
|
||||||
|
|
||||||
|
[Gradle 9](https://docs.gradle.org/9.3.0/release-notes.html) (currently 9.5.1) is now stable and available.
|
||||||
|
The question is whether it is the right choice at project start, or whether staying on Gradle 8.x is safer.
|
||||||
|
|
||||||
|
### Technical Background
|
||||||
|
|
||||||
|
**Configuration Cache** is a Gradle feature that serializes the result of the *configuration phase*
|
||||||
|
(evaluating all build scripts, resolving plugins, building the task graph) so that subsequent
|
||||||
|
builds can skip it entirely.
|
||||||
|
In large projects the configuration phase alone can take tens of seconds;
|
||||||
|
caching it makes every incremental build substantially faster.
|
||||||
|
In Gradle 8.x Configuration Cache is opt-in.
|
||||||
|
In Gradle 9 it is stable and **mandatory**: any plugin that reads mutable environment state or
|
||||||
|
modifies the project model after configuration will fail hard rather than emit a warning.
|
||||||
|
This enforcement fixed longstanding correctness issues across the ecosystem,
|
||||||
|
but it also means any plugin that has *not* been updated will break the build outright.
|
||||||
|
|
||||||
|
The Gradle version choice is not isolated to the build tool itself.
|
||||||
|
It has a direct dependency chain:
|
||||||
|
|
||||||
|
- [Gradle 9](https://docs.gradle.org/current/userguide/compatibility.html) is officially
|
||||||
|
supported by Spring Boot 4.0 only.
|
||||||
|
Spring Boot 4.0 also requires Gradle 8.14 as a minimum for the 8.x line.
|
||||||
|
- [Spring Boot 4.0](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Release-Notes)
|
||||||
|
requires Spring Framework 7 and Jakarta EE 11 — a generation step up from Spring Boot 3.x
|
||||||
|
(Spring Framework 6, Jakarta EE 10).
|
||||||
|
- Plugin ecosystem readiness varies: the
|
||||||
|
[openapi-generator Gradle plugin](https://github.com/OpenAPITools/openapi-generator/issues/22084)
|
||||||
|
has a known Gradle 9 issue in its generated build files, and
|
||||||
|
[ktlint-gradle 12.1.1](https://github.com/JLLeitschuh/ktlint-gradle) Gradle 9 support
|
||||||
|
needs verification.
|
||||||
|
- The [`io.spring.dependency-management` plugin](https://github.com/spring-gradle-plugins/dependency-management-plugin)
|
||||||
|
used to resolve the Spring BOM has known friction with Gradle 9
|
||||||
|
and would need to be replaced by a native `platform()` import.
|
||||||
|
|
||||||
|
The Spring Boot version decision (ADR 0003) concluded that Spring Boot 4.0 is required.
|
||||||
|
Spring Boot 4.0 mandates Gradle 8.14 as minimum, meaning a wrapper upgrade from the
|
||||||
|
current 8.8 is necessary regardless of whether Gradle 9 is chosen.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
* Gradle 8.14 (minimum for Spring Boot 4.0)
|
||||||
|
* Gradle 9.x
|
||||||
|
|
||||||
|
### [Gradle 8.x](https://docs.gradle.org/8.8/release-notes.html)
|
||||||
|
|
||||||
|
The currently configured series (wrapper at 8.8, to be bumped to 8.14+ for Spring Boot 4.0).
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
- Plugin compatibility is well understood for ktlint-gradle and openapi-generator-gradle-plugin.
|
||||||
|
- Configuration Cache is opt-in — teams can enable it incrementally and diagnose failures
|
||||||
|
without blocking the build.
|
||||||
|
- Lower risk for adopting spec-first OpenAPI:
|
||||||
|
the [openapi-generator-gradle-plugin](https://github.com/OpenAPITools/openapi-generator/issues/22084)
|
||||||
|
has a known Gradle 9 incompatibility in generated build files.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
- Not on the current Gradle major line; the Configuration Cache benefits are not enforced.
|
||||||
|
- An eventual migration to Gradle 9 will still be needed.
|
||||||
|
|
||||||
|
### [Gradle 9.x](https://docs.gradle.org/9.3.0/release-notes.html)
|
||||||
|
|
||||||
|
The current major release (9.5.1 as of May 2026), officially supported alongside Spring Boot 4.0.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
- Aligns with the latest Spring Boot generation in a fully supported pairing.
|
||||||
|
- Configuration Cache is mandatory: faster incremental builds and build-script correctness
|
||||||
|
is enforced across the whole plugin chain — problems surface immediately rather than silently.
|
||||||
|
- `io.spring.dependency-management` plugin is replaced by cleaner native `platform()` BOM import.
|
||||||
|
- Greenfield is the easiest moment to adopt it; no migration debt later.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
- [openapi-generator-gradle-plugin](https://github.com/OpenAPITools/openapi-generator/issues/22084)
|
||||||
|
has a known Gradle 9 issue in generated `build.gradle` files; impact on the spec-first
|
||||||
|
code generation task specifically needs verification.
|
||||||
|
- [ktlint-gradle 12.1.1](https://github.com/JLLeitschuh/ktlint-gradle) Gradle 9 support
|
||||||
|
needs verification.
|
||||||
|
- Configuration Cache mandatory means any currently uncached plugin will break the build
|
||||||
|
until updated, complicating initial setup.
|
||||||
|
|
||||||
|
## Decision Outcome
|
||||||
|
|
||||||
|
**Gradle 8.14** (latest stable 8.x) is chosen over Gradle 9 for the initial setup.
|
||||||
|
|
||||||
|
The main reasons:
|
||||||
|
|
||||||
|
- Spring Boot 4.0 requires Gradle 8.14 as minimum; upgrading from 8.8 is necessary in any case.
|
||||||
|
- Gradle 9 is technically compatible with Spring Boot 4.0, but the mandatory Configuration Cache
|
||||||
|
creates unacceptable risk until the openapi-generator-gradle-plugin Gradle 9 compatibility
|
||||||
|
is confirmed (see [issue #22084](https://github.com/OpenAPITools/openapi-generator/issues/22084)).
|
||||||
|
- ktlint-gradle 12.1.1 Gradle 9 support is unverified; a formatter breaking the build on day
|
||||||
|
one is a poor start.
|
||||||
|
|
||||||
|
Upgrade trigger: revisit Gradle 9 once openapi-generator and ktlint-gradle confirm support.
|
||||||
|
The upgrade itself will be low-risk at that point since the Spring Boot 4.0 step is already done.
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# Spring Boot Version
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- proposed: 2026-06-09
|
||||||
|
- accepted: -
|
||||||
|
- rejected: -
|
||||||
|
- superseded: -
|
||||||
|
|
||||||
|
**Decision [proposed]:** Spring Boot 4.0 — only actively-supported version; springmockk workaround via @TestConfiguration until a compatible release is published.
|
||||||
|
|
||||||
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
GitTally is a greenfield Kotlin/Spring Boot project.
|
||||||
|
A Spring Boot version must be chosen for the initial setup.
|
||||||
|
|
||||||
|
The choice is constrained by the support lifecycle: as of June 2026,
|
||||||
|
almost the entire Spring Boot 3.x release train is already end-of-life.
|
||||||
|
|
||||||
|
### Technical Background
|
||||||
|
|
||||||
|
[Spring Boot support lifecycle](https://endoflife.date/spring-boot)
|
||||||
|
([HeroDevs April 2026 overview](https://www.herodevs.com/blog-posts/spring-boot-versions-eol-dates-and-latest-releases-april-2026))
|
||||||
|
as of June 2026:
|
||||||
|
|
||||||
|
| Version | EOL date | Status |
|
||||||
|
|---------|----------------|---------------------------------|
|
||||||
|
| 3.5.x | June 30, 2026 | Expires this month |
|
||||||
|
| 4.0.x | December 2026 | **Active** (current: 4.0.6) |
|
||||||
|
|
||||||
|
Spring Boot 3.5.x is the final 3.x release; there will be no 3.6.
|
||||||
|
The Spring project ended the 3.x train at 3.5 and continues in the 4.x line
|
||||||
|
([Spring Boot milestones](https://github.com/spring-projects/spring-boot/milestones)).
|
||||||
|
Spring Boot 4.0 moves to Spring Framework 7 and Jakarta EE 11.
|
||||||
|
|
||||||
|
The key ecosystem dependencies and their Spring Boot 4.0 compatibility status:
|
||||||
|
|
||||||
|
| Dependency | Role | Spring Boot 4.0 status |
|
||||||
|
|---|---|---|
|
||||||
|
| [picocli-spring-boot-starter](https://picocli.info/) | CLI wiring | Untested; last documented target is Spring Boot 3.1. The integration is minimal (one `IFactory` bean), so it likely works in practice. |
|
||||||
|
| [springmockk](https://github.com/Ninja-Squad/springmockk) | `@MockkBean` in tests | **Incompatible** — confirmed Spring Boot 3.x only. |
|
||||||
|
| [kotest-extensions-spring](https://github.com/kotest/kotest-extensions-spring) | Kotest + `@SpringBootTest` | Compatibility unconfirmed. |
|
||||||
|
| [wiremock-spring-boot](https://wiremock.org/docs/spring-boot/) | HTTP stubbing in tests | **Compatible** — Spring Boot 4.0 explicitly supported since January 2026. |
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
* Spring Boot 3.5.x (latest 3.x)
|
||||||
|
* Spring Boot 4.0.x
|
||||||
|
|
||||||
|
### Spring Boot 3.5.x
|
||||||
|
|
||||||
|
The last actively-supported 3.x release.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
- Full compatibility with all current dependencies
|
||||||
|
(picocli-spring-boot-starter, springmockk, kotest-extensions-spring).
|
||||||
|
- Well-known ecosystem; documentation and community resources are mature.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
- EOL June 30, 2026 — expires this month.
|
||||||
|
Starting a project on a version ending within days is indefensible.
|
||||||
|
- Upgrading to Spring Boot 4.0 immediately after project start incurs migration cost
|
||||||
|
at the worst possible time (early, before the codebase is stable).
|
||||||
|
|
||||||
|
### [Spring Boot 4.0](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Release-Notes)
|
||||||
|
|
||||||
|
The current and only actively-supported major release.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
- The only version receiving security patches and updates beyond June 2026.
|
||||||
|
- Spring Framework 7 and Jakarta EE 11: active, long-term foundation.
|
||||||
|
- Aligns with Gradle 9 when that upgrade is eventually made (ADR 0002).
|
||||||
|
- Wiremock Spring Boot integration fully supports it.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
|
||||||
|
- **springmockk is incompatible.** The `@MockkBean` / `@SpykBean` annotations are not available.
|
||||||
|
This matters in particular for `@WebMvcTest` (controller error-behaviour) and
|
||||||
|
`@DataJpaTest`-adjacent tests where collaborators need to be mocked in the Spring context.
|
||||||
|
The workaround is to register MockK mocks as Spring beans via `@TestConfiguration`,
|
||||||
|
which preserves full MockK syntax (`every { }`, `verify { }`) at the cost of one
|
||||||
|
inner configuration class per test file and an explicit `clearMocks()` call in `beforeEach`:
|
||||||
|
```kotlin
|
||||||
|
@WebMvcTest(SomeController::class)
|
||||||
|
@Import(SomeControllerTest.Mocks::class)
|
||||||
|
class SomeControllerTest : FunSpec() {
|
||||||
|
@TestConfiguration
|
||||||
|
class Mocks {
|
||||||
|
@Bean fun someService(): SomeService = mockk()
|
||||||
|
}
|
||||||
|
@Autowired lateinit var someService: SomeService
|
||||||
|
init {
|
||||||
|
beforeEach { clearMocks(someService) }
|
||||||
|
// full MockK syntax available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
This is more verbose than `@MockkBean` but avoids mixing Mockito and MockK.
|
||||||
|
Revisit once springmockk publishes a Spring Boot 4.0-compatible release.
|
||||||
|
- **picocli-spring-boot-starter is untested against Spring Boot 4.0.**
|
||||||
|
The risk is low (the auto-configuration is a single `IFactory` bean with no framework-version-specific API),
|
||||||
|
but it cannot be assumed safe until verified by a running context test.
|
||||||
|
- **kotest-extensions-spring** compatibility needs verification before the first
|
||||||
|
`@SpringBootTest` test is written.
|
||||||
|
- Relatively new GA; fewer community examples for the 4.0 generation.
|
||||||
|
|
||||||
|
## Decision Outcome
|
||||||
|
|
||||||
|
**Spring Boot 4.0** is chosen.
|
||||||
|
|
||||||
|
The main reasons:
|
||||||
|
|
||||||
|
- Spring Boot 3.5.x expires this month; starting on it is not a viable option.
|
||||||
|
- Spring Boot 4.0 is the only release with active support through the foreseeable project lifetime.
|
||||||
|
- The ecosystem blockers are manageable:
|
||||||
|
- picocli-spring-boot-starter risk is low and will be confirmed by the existing
|
||||||
|
context-wiring smoke test (`ApplicationContextTest`).
|
||||||
|
- springmockk is a test-only dependency; the short-term workaround
|
||||||
|
(`@TestConfiguration` with `mockk()` beans, `clearMocks()` in `beforeEach`)
|
||||||
|
preserves full MockK syntax in `@WebMvcTest` and `@DataJpaTest` tests
|
||||||
|
without falling back to Mockito.
|
||||||
|
- kotest-extensions-spring compatibility must be verified before the
|
||||||
|
first `@SpringBootTest` test is written.
|
||||||
|
|
||||||
|
Open items before the code is upgraded:
|
||||||
|
1. Bump Spring Boot to 4.0.6 and verify `ApplicationContextTest` still passes.
|
||||||
|
2. Confirm picocli-spring-boot-starter works under the new context.
|
||||||
|
3. Confirm kotest-extensions-spring works with Spring Boot 4.0.
|
||||||
|
4. Replace `@MockkBean` usages with the `@TestConfiguration` + `mockk()` pattern
|
||||||
|
until springmockk publishes a compatible release.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package de.hoennig.gittally.framework
|
||||||
|
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
|
||||||
|
interface Greeter {
|
||||||
|
fun greet(name: String): String
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockKSmokeTest : FunSpec({
|
||||||
|
|
||||||
|
test("mockk stubs and verifies") {
|
||||||
|
val greeter = mockk<Greeter>()
|
||||||
|
every { greeter.greet("world") } returns "hello, world"
|
||||||
|
|
||||||
|
greeter.greet("world") shouldBe "hello, world"
|
||||||
|
|
||||||
|
verify(exactly = 1) { greeter.greet("world") }
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package de.hoennig.gittally.framework
|
||||||
|
|
||||||
|
import com.ninjasquad.springmockk.MockkBean
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.clearMocks
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Import
|
||||||
|
|
||||||
|
interface MessageService {
|
||||||
|
fun getMessage(): String
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Workaround for springmockk incompatibility: register MockK mock via @TestConfiguration. */
|
||||||
|
@SpringBootTest
|
||||||
|
@Import(SpringMockKSmokeTest.Mocks::class)
|
||||||
|
class SpringMockKSmokeTest : FunSpec() {
|
||||||
|
|
||||||
|
@TestConfiguration
|
||||||
|
class Mocks {
|
||||||
|
@Bean
|
||||||
|
fun messageService(): MessageService = mockk()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
lateinit var messageService: MessageService
|
||||||
|
|
||||||
|
init {
|
||||||
|
beforeEach { clearMocks(messageService) }
|
||||||
|
|
||||||
|
test("MockK mock registered as Spring bean can be stubbed and verified") {
|
||||||
|
every { messageService.getMessage() } returns "hello from mock"
|
||||||
|
|
||||||
|
messageService.getMessage() shouldBe "hello from mock"
|
||||||
|
|
||||||
|
verify(exactly = 1) { messageService.getMessage() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verifies whether @MockkBean works with the current Spring Boot version (springmockk 4.0.2). */
|
||||||
|
@SpringBootTest
|
||||||
|
class MockkBeanSmokeTest : FunSpec() {
|
||||||
|
|
||||||
|
@MockkBean
|
||||||
|
lateinit var messageService: MessageService
|
||||||
|
|
||||||
|
init {
|
||||||
|
beforeEach { clearMocks(messageService) }
|
||||||
|
|
||||||
|
test("@MockkBean stubs and verifies a Spring-managed MockK mock") {
|
||||||
|
every { messageService.getMessage() } returns "hello from @MockkBean"
|
||||||
|
|
||||||
|
messageService.getMessage() shouldBe "hello from @MockkBean"
|
||||||
|
|
||||||
|
verify(exactly = 1) { messageService.getMessage() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package de.hoennig.gittally.framework
|
||||||
|
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import org.testcontainers.containers.GenericContainer
|
||||||
|
import org.testcontainers.utility.DockerImageName
|
||||||
|
|
||||||
|
class TestcontainersSmokeTest : FunSpec({
|
||||||
|
|
||||||
|
test("Testcontainers starts a container") {
|
||||||
|
val container = GenericContainer(DockerImageName.parse("alpine:3"))
|
||||||
|
.withCommand("sh", "-c", "sleep 30")
|
||||||
|
container.start()
|
||||||
|
container.isRunning shouldBe true
|
||||||
|
container.stop()
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package de.hoennig.gittally.framework
|
||||||
|
|
||||||
|
import com.github.tomakehurst.wiremock.WireMockServer
|
||||||
|
import com.github.tomakehurst.wiremock.client.WireMock.aResponse
|
||||||
|
import com.github.tomakehurst.wiremock.client.WireMock.get
|
||||||
|
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.options
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.http.HttpClient
|
||||||
|
import java.net.http.HttpRequest
|
||||||
|
import java.net.http.HttpResponse
|
||||||
|
|
||||||
|
class WireMockSmokeTest : FunSpec({
|
||||||
|
|
||||||
|
val server = WireMockServer(options().dynamicPort())
|
||||||
|
|
||||||
|
beforeSpec { server.start() }
|
||||||
|
afterSpec { server.stop() }
|
||||||
|
|
||||||
|
test("WireMock stubs an HTTP endpoint") {
|
||||||
|
server.stubFor(get("/ping").willReturn(aResponse().withStatus(200).withBody("pong")))
|
||||||
|
|
||||||
|
val response = HttpClient.newHttpClient().send(
|
||||||
|
HttpRequest.newBuilder().uri(URI.create("http://localhost:${server.port()}/ping")).build(),
|
||||||
|
HttpResponse.BodyHandlers.ofString(),
|
||||||
|
)
|
||||||
|
|
||||||
|
response.statusCode() shouldBe 200
|
||||||
|
response.body() shouldBe "pong"
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user