|
| 1 | +import io.gitlab.arturbosch.detekt.Detekt |
| 2 | +import org.springframework.boot.gradle.tasks.bundling.BootJar |
| 3 | +import java.net.URI |
| 4 | +import java.time.Duration |
| 5 | + |
| 6 | +group = "com.swisscom.health.des.cdr.client.service" |
| 7 | + |
| 8 | +val outputDir: Provider<Directory> = layout.buildDirectory.dir(".") |
| 9 | + |
| 10 | +plugins { |
| 11 | + alias(libs.plugins.dockerCompose) |
| 12 | + alias(libs.plugins.springBoot) |
| 13 | + alias(libs.plugins.springDependencyManagement) |
| 14 | + alias(libs.plugins.detekt) |
| 15 | + jacoco |
| 16 | + application |
| 17 | + kotlin("jvm").version(libs.versions.kotlin.lang) |
| 18 | + kotlin("plugin.spring").version(libs.versions.kotlin.lang) |
| 19 | + // https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.kotlin.configuration-properties |
| 20 | + // KAPT is end of life, but KSP is not supported yet: https://github.com/spring-projects/spring-boot/issues/28046 |
| 21 | + kotlin("kapt").version(libs.versions.kotlin.lang) |
| 22 | + `maven-publish` |
| 23 | + idea |
| 24 | +} |
| 25 | + |
| 26 | +idea { |
| 27 | + module { |
| 28 | + isDownloadJavadoc = true |
| 29 | + isDownloadSources = true |
| 30 | + } |
| 31 | +} |
| 32 | + |
| 33 | +tasks.bootRun { |
| 34 | + environment["JDK_JAVA_OPTIONS"] = |
| 35 | + listOfNotNull( |
| 36 | + environment["JDK_JAVA_OPTIONS"], |
| 37 | + "-Djavax.net.ssl.trustStore=src/main/resources/caddy_truststore.p12", |
| 38 | + "-Djavax.net.ssl.trustStorePassword=changeit", |
| 39 | + "-Djdk.net.hosts.file=src/main/resources/msal4j_hosts" |
| 40 | + ).joinToString(" ") |
| 41 | +} |
| 42 | + |
| 43 | +application { |
| 44 | + mainClass = "com.swisscom.health.des.cdr.client.CdrClientApplicationKt" |
| 45 | +} |
| 46 | + |
| 47 | +gradle.taskGraph.whenReady( |
| 48 | + closureOf<TaskExecutionGraph> { |
| 49 | + println("Tasks to be executed: ${this.allTasks}") |
| 50 | + application { |
| 51 | + applicationDefaultJvmArgs = |
| 52 | + if (gradle.taskGraph.hasTask(":bootRun")) { |
| 53 | + // when running the client locally via the `bootRun` target |
| 54 | + listOf("-Dspring.profiles.active=client,dev") |
| 55 | + } else { |
| 56 | + // gets picked up by application plugin when building the distribution as the |
| 57 | + // value of `DEFAULT_JVM_OPTS` in the generated start script |
| 58 | + listOf("-Dspring.profiles.active=client,customer") |
| 59 | + } |
| 60 | + } |
| 61 | + } |
| 62 | +) |
| 63 | + |
| 64 | +project.afterEvaluate { |
| 65 | + // https://github.com/detekt/detekt/issues/6198#issuecomment-2265183695 |
| 66 | + configurations.matching { it.name == "detekt" }.all { |
| 67 | + resolutionStrategy.eachDependency { |
| 68 | + if (requested.group == "org.jetbrains.kotlin") { |
| 69 | + useVersion(io.gitlab.arturbosch.detekt.getSupportedKotlinVersion()) |
| 70 | + } |
| 71 | + } |
| 72 | + } |
| 73 | +} |
| 74 | + |
| 75 | +dependencies { |
| 76 | + implementation(platform(libs.spring.boot.dependencies)) |
| 77 | + implementation(platform(libs.spring.cloud.dependencies)) |
| 78 | + implementation(libs.kache) |
| 79 | + implementation(libs.msal4j) |
| 80 | + implementation(libs.okhttp) |
| 81 | + implementation(libs.kfswatch) |
| 82 | + implementation(libs.kotlin.logging) |
| 83 | + implementation(libs.micrometer.tracing) |
| 84 | + implementation(libs.micrometer.tracing.bridge.otel) |
| 85 | + implementation(libs.logstash.encoder) |
| 86 | + implementation(libs.kotlin.reflect) |
| 87 | + implementation(libs.kotlin.stdlib) |
| 88 | + implementation(libs.kotlinx.coroutines.core) |
| 89 | + implementation(libs.kotlinx.coroutines.reactor) // to enable @Scheduled on Kotlin suspending functions |
| 90 | + implementation(libs.spring.boot.starter.actuator) |
| 91 | + implementation(libs.spring.boot.starter.web) |
| 92 | + implementation(libs.spring.retry) |
| 93 | + implementation(libs.spring.cloud.context) |
| 94 | + implementation(libs.jackson.dataformat.yaml) |
| 95 | + |
| 96 | + // Note: At the time of writing the configuration processor seems to be broken; might be related to the upgrade to Kotlin 2.x |
| 97 | + // Enable annotation processing via menu File | Settings | Build, Execution, Deployment |
| 98 | + // | Compiler | Annotation Processors | Enable annotation processing |
| 99 | + kapt("org.springframework.boot:spring-boot-configuration-processor") |
| 100 | + |
| 101 | + testImplementation(libs.jacocoCore) |
| 102 | + testImplementation(libs.spring.boot.starter.test) |
| 103 | + testImplementation(libs.kotlinx.coroutines.test) |
| 104 | + testImplementation(libs.mock.webserver) { |
| 105 | + // Unfortunately we cannot exclude JUnit 4 as MockWebServer implements interfaces from that version |
| 106 | +// exclude(group = "junit", module = "junit") |
| 107 | + } |
| 108 | + testImplementation(libs.mockk) { |
| 109 | + exclude(group = "junit", module = "junit") |
| 110 | + } |
| 111 | + testImplementation(libs.junit.jupiter) |
| 112 | + testImplementation(libs.micrometer.tracing.test) |
| 113 | + testImplementation(libs.spring.mockk) |
| 114 | + testImplementation(libs.awaitility) |
| 115 | + |
| 116 | +} |
| 117 | + |
| 118 | +kapt { |
| 119 | + correctErrorTypes = true |
| 120 | + mapDiagnosticLocations = true |
| 121 | + includeCompileClasspath = false |
| 122 | +} |
| 123 | + |
| 124 | +springBoot { |
| 125 | + buildInfo() |
| 126 | +} |
| 127 | + |
| 128 | +kotlin { |
| 129 | + jvmToolchain (libs.versions.jdk.get().toInt()) |
| 130 | + compilerOptions { |
| 131 | + freeCompilerArgs = listOf("-Xjsr305=strict") |
| 132 | + } |
| 133 | +} |
| 134 | + |
| 135 | +tasks.test { |
| 136 | + useJUnitPlatform { |
| 137 | + excludeTags(Constants.INTEGRATION_TEST_TAG) |
| 138 | + } |
| 139 | +} |
| 140 | + |
| 141 | +val jacocoTestCoverageVerification = tasks.named<JacocoCoverageVerification>("jacocoTestCoverageVerification") { |
| 142 | + violationRules { |
| 143 | + /** |
| 144 | + * Ensure tests cover at least 75% of the LoC. |
| 145 | + */ |
| 146 | + rule { |
| 147 | + classDirectories.setFrom(files(classDirectories.files.map { |
| 148 | + fileTree(it) { |
| 149 | + setExcludes( |
| 150 | + // CdrApiClient is tested with integration tests; need to find out how to merge the jacoco reports of the two test types |
| 151 | + listOf( |
| 152 | + "**/com/swisscom/health/des/cdr/client/handler/CdrApiClient*" |
| 153 | + ) |
| 154 | + ) |
| 155 | + } |
| 156 | + })) |
| 157 | + limit { |
| 158 | + minimum = "0.70".toBigDecimal() |
| 159 | + } |
| 160 | + } |
| 161 | + } |
| 162 | +} |
| 163 | + |
| 164 | +val jacocoTestReport = tasks.named<JacocoReport>("jacocoTestReport") { |
| 165 | + finalizedBy(jacocoTestCoverageVerification) // Verify after generating the report. |
| 166 | + group = "Reporting" |
| 167 | + |
| 168 | + reports { |
| 169 | + xml.required.set(true) |
| 170 | + xml.outputLocation.set(File("${outputDir.get().asFile.absolutePath}/reports/jacoco.xml")) |
| 171 | + csv.required.set(false) |
| 172 | + html.required.set(true) |
| 173 | + html.outputLocation.set(File("${outputDir.get().asFile.absolutePath}/reports/coverage")) |
| 174 | + } |
| 175 | +} |
| 176 | + |
| 177 | +tasks.withType<Test> { |
| 178 | + // show log output produced by tests in console |
| 179 | + testLogging.showStandardStreams = false |
| 180 | + |
| 181 | + useJUnitPlatform { |
| 182 | + includeEngines("junit-jupiter") |
| 183 | + } |
| 184 | + finalizedBy(jacocoTestReport) |
| 185 | + |
| 186 | + jvmArgs( |
| 187 | + // tests_hosts is used to redirect msal4j, which insists on talking to the Mothership, to our docker compose setup |
| 188 | + "-Djdk.net.hosts.file=${layout.projectDirectory.file("src/test/resources/test_hosts").asFile.absolutePath}" |
| 189 | + ) |
| 190 | +} |
| 191 | + |
| 192 | +jacoco { |
| 193 | + toolVersion = libs.versions.jacoco.get() |
| 194 | +} |
| 195 | + |
| 196 | +tasks.named<BootJar>("bootJar") { |
| 197 | + manifest { |
| 198 | + // Required so application name and version get rendered in the banner.txt; see |
| 199 | + // https://stackoverflow.com/questions/34519759/application-version-does-not-show-up-in-spring-boot-banner-txt |
| 200 | + attributes("Implementation-Title" to rootProject.name) |
| 201 | + attributes("Implementation-Version" to archiveVersion) |
| 202 | + } |
| 203 | +} |
| 204 | + |
| 205 | +// https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.properties-and-configuration.expand-properties.gradle |
| 206 | +// and https://stackoverflow.com/questions/40096007/how-to-configure-the-processresources-task-in-a-gradle-kotlin-build |
| 207 | +tasks.processResources { |
| 208 | + filesMatching("**/application.yaml") { |
| 209 | + expand(project.properties) |
| 210 | + } |
| 211 | +} |
| 212 | + |
| 213 | +tasks.withType<Detekt> { |
| 214 | + reports { |
| 215 | + xml { |
| 216 | + required.set(true) |
| 217 | + outputLocation.set(File("${outputDir.get().asFile.absolutePath}/reports/detekt.xml")) |
| 218 | + } |
| 219 | + html.required.set(false) |
| 220 | + sarif.required.set(false) |
| 221 | + txt.required.set(false) |
| 222 | + } |
| 223 | +} |
| 224 | + |
| 225 | +/** |
| 226 | + * Detekt |
| 227 | + * See https://docs.sonarqube.org/latest/analysis/scan/sonarscanner-for-gradle/ |
| 228 | + */ |
| 229 | +detekt { |
| 230 | + buildUponDefaultConfig = false // preconfigure defaults |
| 231 | + allRules = true |
| 232 | + parallel = true |
| 233 | + config.setFrom(files("$rootDir/config/detekt.yml")) // Global Detekt rule set. |
| 234 | +} |
| 235 | + |
| 236 | +tasks.register("publishVersion") { |
| 237 | + group = "publishing" |
| 238 | + description = "Publishes boot jar" |
| 239 | + dependsOn(tasks.withType<PublishToMavenRepository>().matching { |
| 240 | + it.repository == publishing.repositories["GitHubPackages"] && it.publication == publishing.publications["bootJava"] |
| 241 | + }) |
| 242 | +} |
| 243 | + |
| 244 | + |
| 245 | +publishing { |
| 246 | + publications { |
| 247 | + create<MavenPublication>("bootJava") { |
| 248 | + artifact(tasks.named("bootJar")) |
| 249 | + } |
| 250 | + } |
| 251 | + repositories { |
| 252 | + maven { |
| 253 | + name = "GitHubPackages" |
| 254 | + url = URI("https://maven.pkg.github.com/swisscom/cdr-client") |
| 255 | + credentials { |
| 256 | + username = System.getenv("GITHUB_ACTOR") |
| 257 | + password = System.getenv("GITHUB_TOKEN") |
| 258 | + } |
| 259 | + } |
| 260 | + } |
| 261 | +} |
| 262 | + |
| 263 | +/*********************** |
| 264 | + * Integration Testing * |
| 265 | + ***********************/ |
| 266 | +object Constants { |
| 267 | + const val TASK_GROUP_VERIFICATION = "verification" |
| 268 | + const val INTEGRATION_TEST_TAG = "integration-test" |
| 269 | +} |
| 270 | + |
| 271 | +tasks.register<Test>("integrationTest") { |
| 272 | + group = Constants.TASK_GROUP_VERIFICATION |
| 273 | + useJUnitPlatform { |
| 274 | + includeTags(Constants.INTEGRATION_TEST_TAG) |
| 275 | + } |
| 276 | + environment["JDK_JAVA_OPTIONS"] = |
| 277 | + listOfNotNull( |
| 278 | + environment["JDK_JAVA_OPTIONS"], |
| 279 | + "-Djavax.net.ssl.trustStore=src/main/resources/caddy_truststore.p12", |
| 280 | + "-Djavax.net.ssl.trustStorePassword=changeit", |
| 281 | + "-Djdk.net.hosts.file=src/main/resources/msal4j_hosts" |
| 282 | + ).joinToString(" ") |
| 283 | + shouldRunAfter(tasks.test) |
| 284 | + // Ensure latest images get pulled |
| 285 | + dependsOn(tasks.composePull) |
| 286 | +} |
| 287 | + |
| 288 | +dockerCompose { |
| 289 | + dockerComposeWorkingDirectory.set(rootProject.file("docker-compose")) |
| 290 | + dockerComposeStopTimeout.set(Duration.ofSeconds(5)) // time before docker-compose sends SIGTERM to the running containers after the composeDown task has been started |
| 291 | + ignorePullFailure.set(true) |
| 292 | + isRequiredBy(tasks.getByName("integrationTest")) |
| 293 | +} |
| 294 | +/*************************** |
| 295 | + * END Integration Testing * |
| 296 | + ***************************/ |
| 297 | + |
| 298 | +tasks.named<Jar>("jar") { |
| 299 | + enabled = false |
| 300 | +} |
| 301 | + |
| 302 | +val packagePrepare = "jpackage-prepare" |
| 303 | + |
| 304 | +tasks.register<Delete>("clearPackagePrepare") { |
| 305 | + delete(file("${outputDir.get().asFile.absolutePath}/$packagePrepare")) |
| 306 | +} |
| 307 | + |
| 308 | +tasks.register<Exec>("jpackageAppPrepareDebian") { |
| 309 | + dependsOn("clearPackagePrepare") |
| 310 | + executable = "jpackage" |
| 311 | + args( |
| 312 | + "--type", "app-image", |
| 313 | + "--name", project.name, |
| 314 | + "--input", "${outputDir.get().asFile.absolutePath}/libs", |
| 315 | + "--main-jar", "${project.name}-${project.version}.jar", |
| 316 | + "--app-version", project.version.toString(), |
| 317 | + "--vendor", "Swisscom (Schweiz) AG", |
| 318 | + "--copyright", "Copyright 2025, All rights reserved", |
| 319 | + "--icon", "resources/icon.png", |
| 320 | + "--dest", "${outputDir.get().asFile.absolutePath}/$packagePrepare", |
| 321 | + "--java-options", "-Dfile.encoding=UTF-8", |
| 322 | + "--java-options", "-Dspring.profiles.active=customer", |
| 323 | + ) |
| 324 | + doLast{ |
| 325 | + copy { |
| 326 | + from("."){ |
| 327 | + include("LICENSE") |
| 328 | + } |
| 329 | + into("${outputDir.get().asFile.absolutePath}/$packagePrepare/${project.name}/lib/app") |
| 330 | + } |
| 331 | + } |
| 332 | +} |
| 333 | + |
| 334 | +tasks.register<Exec>("jpackageAppFinishDebian") { |
| 335 | + dependsOn("jpackageAppPrepareDebian") |
| 336 | + executable = "jpackage" |
| 337 | + args( |
| 338 | + "--app-image", "${outputDir.get().asFile.absolutePath}/$packagePrepare/${project.name}", |
| 339 | + "--dest", "${outputDir.get().asFile.absolutePath}/jpackage", |
| 340 | + "--license-file", "${outputDir.get().asFile.absolutePath}/$packagePrepare/${project.name}/lib/app/LICENSE", |
| 341 | + "--copyright", "Copyright 2025, All rights reserved", |
| 342 | + "--app-version", project.version.toString(), |
| 343 | + "--verbose" |
| 344 | + ) |
| 345 | +} |
| 346 | + |
| 347 | +tasks.register<Exec>("jpackageAppPrepareWindows") { |
| 348 | + dependsOn("clearPackagePrepare") |
| 349 | + executable = "jpackage" |
| 350 | + args( |
| 351 | + "--type", "app-image", |
| 352 | + "--name", project.name, |
| 353 | + "--input", "${outputDir.get().asFile.absolutePath}/libs", |
| 354 | + "--main-jar", "${project.name}-${project.version}.jar", |
| 355 | + "--app-version", project.version.toString(), |
| 356 | + "--vendor", "Swisscom (Schweiz) AG", |
| 357 | + "--copyright", "Copyright 2025, All rights reserved", |
| 358 | + "--icon", "resources/windows/icon.ico", |
| 359 | + "--win-console", |
| 360 | + "--dest", "${outputDir.get().asFile.absolutePath}/$packagePrepare", |
| 361 | + "--java-options", "-Dfile.encoding=UTF-8", |
| 362 | + "--java-options", "-Dspring.profiles.active=customer", |
| 363 | + ) |
| 364 | + doLast{ |
| 365 | + copy { |
| 366 | + from("resources/windows") { |
| 367 | + include("cdrClient.exe") |
| 368 | + include("cdrClientw.exe") |
| 369 | + include("icon.ico") |
| 370 | + include("stop.bat") |
| 371 | + } |
| 372 | + from("."){ |
| 373 | + include("LICENSE") |
| 374 | + } |
| 375 | + into("${outputDir.get().asFile.absolutePath}/$packagePrepare/${project.name}/lib/app") |
| 376 | + } |
| 377 | + } |
| 378 | +} |
| 379 | + |
| 380 | +tasks.register<Exec>("jpackageAppFinishWindows") { |
| 381 | + dependsOn("jpackageAppPrepareWindows") |
| 382 | + executable = "jpackage" |
| 383 | + args( |
| 384 | + "--app-image", "${outputDir.get().asFile.absolutePath}/$packagePrepare/${project.name}", |
| 385 | + "--win-dir-chooser", |
| 386 | + "--license-file", "${outputDir.get().asFile.absolutePath}/$packagePrepare/${project.name}/lib/app/LICENSE", |
| 387 | + "--copyright", "Copyright 2025, All rights reserved", |
| 388 | + "--dest", "${outputDir.get().asFile.absolutePath}/jpackage", |
| 389 | + "--app-version", project.version.toString(), |
| 390 | + "--verbose" |
| 391 | + ) |
| 392 | +} |
0 commit comments