- Added `nativeDistributions` for Linux (.deb), Windows (.msi), and macOS (.dmg) in `build.gradle.kts` with platform-specific settings, embedded JRE, and JVM-args. - Implemented centralized semantic versioning via `version.properties` as the single source of truth, applying it across all builds. - Introduced CI/CD release workflow (`.gitea/workflows/release.yml`) for auto-tagging, artifact builds, and release summaries. - Created `CHANGELOG.md` following Keep-a-Changelog format for tracking changes. - Documented icon requirements and packaging steps in `ICONS_PLACEHOLDER.md`. - Updated DevOps roadmap to reflect completed Sprint C-1 and C-2 tasks. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
445 lines
18 KiB
Plaintext
445 lines
18 KiB
Plaintext
import groovy.json.JsonSlurper
|
||
import io.gitlab.arturbosch.detekt.Detekt
|
||
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
|
||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||
import org.jlleitschuh.gradle.ktlint.KtlintExtension
|
||
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
|
||
import java.io.ByteArrayOutputStream
|
||
import java.util.zip.GZIPOutputStream
|
||
|
||
plugins {
|
||
// Version management plugin for dependency updates
|
||
alias(libs.plugins.benManesVersions)
|
||
|
||
// Kotlin plugins declared here with 'apply false' to centralize version management
|
||
// This prevents "plugin loaded multiple times" errors in Gradle 9.2.1+
|
||
// Subprojects apply these plugins via version catalog: alias(libs.plugins.kotlinJvm)
|
||
alias(libs.plugins.kotlinJvm) apply false
|
||
// KMP plugin applied as 'apply false' to avoid root project conflict
|
||
alias(libs.plugins.kotlinMultiplatform) apply false
|
||
alias(libs.plugins.kotlinSerialization) apply false
|
||
alias(libs.plugins.kotlinSpring) apply false
|
||
alias(libs.plugins.kotlinJpa) apply false
|
||
alias(libs.plugins.composeMultiplatform) apply false
|
||
alias(libs.plugins.composeCompiler) apply false
|
||
alias(libs.plugins.spring.boot) apply false
|
||
alias(libs.plugins.spring.dependencyManagement) apply false
|
||
|
||
// Dokka plugin applied at root to create multi-module collector tasks
|
||
alias(libs.plugins.dokka)
|
||
|
||
// Static analysis (enabled at root and inherited by subprojects)
|
||
alias(libs.plugins.detekt)
|
||
alias(libs.plugins.ktlint)
|
||
}
|
||
|
||
// ##################################################################
|
||
// ### ALLPROJECTS CONFIGURATION ###
|
||
// ##################################################################
|
||
|
||
// ---------------------------------------------------------------
|
||
// Zentrale Versionierung — liest version.properties (SemVer)
|
||
// ---------------------------------------------------------------
|
||
val versionProps =
|
||
java.util.Properties().also { props ->
|
||
rootProject.file("version.properties").inputStream().use { props.load(it) }
|
||
}
|
||
val vMajor = versionProps.getProperty("VERSION_MAJOR", "1")
|
||
val vMinor = versionProps.getProperty("VERSION_MINOR", "0")
|
||
val vPatch = versionProps.getProperty("VERSION_PATCH", "0")
|
||
val vQualifier = versionProps.getProperty("VERSION_QUALIFIER", "").trim()
|
||
val semVer = if (vQualifier.isBlank()) "$vMajor.$vMinor.$vPatch" else "$vMajor.$vMinor.$vPatch-$vQualifier"
|
||
|
||
allprojects {
|
||
group = "at.mocode"
|
||
version = semVer
|
||
}
|
||
|
||
subprojects {
|
||
// FINALE KORREKTUR: Konsistente JVM-Target-Konfiguration für Java und Kotlin
|
||
// basierend auf der Tech-Stack-Referenz.
|
||
plugins.withType<org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper> {
|
||
extensions.configure<org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension> {
|
||
jvmToolchain {
|
||
languageVersion.set(JavaLanguageVersion.of(25))
|
||
}
|
||
}
|
||
}
|
||
|
||
tasks.withType<KotlinCompile>().configureEach {
|
||
compilerOptions {
|
||
jvmTarget.set(JvmTarget.JVM_25)
|
||
freeCompilerArgs.addAll(
|
||
"-opt-in=kotlin.RequiresOptIn",
|
||
"-Xannotation-default-target=param-property",
|
||
"-Xexpect-actual-classes",
|
||
)
|
||
}
|
||
}
|
||
|
||
tasks.withType<Test>().configureEach {
|
||
useJUnitPlatform {
|
||
excludeTags("perf")
|
||
}
|
||
// Configure CDS in auto-mode to prevent bootstrap classpath warnings
|
||
jvmArgs("-Xshare:auto", "-Djdk.instrument.traceUsage=false")
|
||
// Suppress Netty sun.misc.Unsafe warnings (transitiv via Spring Boot / Kafka)
|
||
jvmArgs("--add-opens=java.base/java.nio=ALL-UNNAMED")
|
||
// Suppress ByteBuddy/Mockito dynamic agent loading warnings (Java 21+)
|
||
jvmArgs("-XX:+EnableDynamicAgentLoading")
|
||
// Increase test JVM memory with a stable configuration
|
||
minHeapSize = "512m"
|
||
maxHeapSize = "2g"
|
||
// Parallel test execution for better performance
|
||
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Frontend/JS build noise reduction
|
||
// ---------------------------------------------------------------------------
|
||
// (B) Avoid noisy "will be copied ... overwriting" logs for Kotlin/JS *CompileSync tasks.
|
||
// The Kotlin JS plugin wires multiple resource sourcesets into the same destination.
|
||
// We keep the first occurrence and exclude duplicates.
|
||
tasks.withType<Copy>().configureEach {
|
||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||
}
|
||
tasks.withType<Sync>().configureEach {
|
||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||
}
|
||
|
||
// (A) Source map configuration is handled via `gradle.properties` (global Kotlin/JS settings)
|
||
// to avoid compiler-flag incompatibilities across toolchains.
|
||
|
||
// (B) JS test executable compilation/sync is currently very noisy (duplicate resource copying from jsMain + jsTest).
|
||
// We disable JS/WASM JS test executables in CI/build to keep output warning-free.
|
||
tasks.matching {
|
||
val n = it.name
|
||
n.contains("jsTest", ignoreCase = true) ||
|
||
n.contains("compileTestDevelopmentExecutableKotlinJs") ||
|
||
n.contains("compileTestDevelopmentExecutableKotlinWasmJs") ||
|
||
n.contains("TestDevelopmentExecutableCompileSync")
|
||
}.configureEach {
|
||
enabled = false
|
||
}
|
||
|
||
// Dedicated performance test task per JVM subproject
|
||
plugins.withId("java") {
|
||
val javaExt = extensions.getByType<JavaPluginExtension>()
|
||
// Ensure a full JDK toolchain with compiler is available (Gradle will auto-download if missing)
|
||
javaExt.toolchain.languageVersion.set(JavaLanguageVersion.of(25))
|
||
|
||
tasks.register<Test>("perfTest") {
|
||
description = "Runs tests tagged with 'perf'"
|
||
group = "verification"
|
||
// Use the regular test source set outputs
|
||
testClassesDirs = javaExt.sourceSets.getByName("test").output.classesDirs
|
||
classpath = javaExt.sourceSets.getByName("test").runtimeClasspath
|
||
useJUnitPlatform {
|
||
includeTags("perf")
|
||
}
|
||
shouldRunAfter("test")
|
||
// Keep the same JVM settings for consistency
|
||
jvmArgs("-Xshare:auto", "-Djdk.instrument.traceUsage=false")
|
||
jvmArgs("--add-opens=java.base/java.nio=ALL-UNNAMED")
|
||
jvmArgs("-XX:+EnableDynamicAgentLoading")
|
||
maxHeapSize = "2g"
|
||
dependsOn("testClasses")
|
||
}
|
||
}
|
||
|
||
// Suppress Node.js deprecation warnings (e.g., DEP0040 punycode) during Kotlin/JS npm/yarn tasks
|
||
// Applies to all Exec-based tasks (covers Yarn/NPM invocations used by Kotlin JS plugin)
|
||
tasks.withType<Exec>().configureEach {
|
||
// Merge existing NODE_OPTIONS with --no-deprecation
|
||
val current = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS")
|
||
val merged = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation"
|
||
environment("NODE_OPTIONS", merged)
|
||
// Also set the legacy switch to silence warnings entirely
|
||
environment("NODE_NO_WARNINGS", "1")
|
||
// Set a Chrome binary path to avoid snap permission issues
|
||
environment("CHROME_BIN", "/usr/bin/google-chrome-stable")
|
||
environment("CHROMIUM_BIN", "/usr/bin/chromium")
|
||
environment("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium")
|
||
}
|
||
|
||
// ------------------------------
|
||
// Detekt & Ktlint default setup
|
||
// ------------------------------
|
||
plugins.withId("io.gitlab.arturbosch.detekt") {
|
||
extensions.configure(DetektExtension::class.java) {
|
||
buildUponDefaultConfig = true
|
||
allRules = false
|
||
autoCorrect = false
|
||
config.setFrom(files(rootProject.file("config/detekt/detekt.yml")))
|
||
basePath = rootDir.absolutePath
|
||
}
|
||
tasks.withType<Detekt>().configureEach {
|
||
jvmTarget = "25"
|
||
reports {
|
||
xml.required.set(false)
|
||
txt.required.set(false)
|
||
sarif.required.set(false)
|
||
html.required.set(true)
|
||
}
|
||
}
|
||
}
|
||
|
||
plugins.withId("org.jlleitschuh.gradle.ktlint") {
|
||
extensions.configure(KtlintExtension::class.java) {
|
||
android.set(false)
|
||
outputToConsole.set(true)
|
||
ignoreFailures.set(false)
|
||
reporters {
|
||
reporter(ReporterType.CHECKSTYLE)
|
||
reporter(ReporterType.PLAIN)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Bundle Size Budgets for Frontend Shells (Kotlin/JS)
|
||
// ------------------------------------------------------------------
|
||
// ✅ FIX: Klasse auf Top-Level verschieben
|
||
data class Budget(val rawBytes: Long, val gzipBytes: Long)
|
||
|
||
tasks.register("checkBundleBudget") {
|
||
group = "verification"
|
||
description = "Checks JS bundle sizes of frontend shells against configured budgets"
|
||
doLast {
|
||
val budgetsFile = file("config/bundles/budgets.json")
|
||
if (!budgetsFile.exists()) {
|
||
throw GradleException("Budgets file not found: ${budgetsFile.path}")
|
||
}
|
||
|
||
// Load budgets JSON as simple Map<String, Budget>
|
||
val text = budgetsFile.readText()
|
||
|
||
@Suppress("UNCHECKED_CAST")
|
||
val parsed =
|
||
JsonSlurper().parseText(text) as Map<String, Map<String, Any?>>
|
||
val budgets =
|
||
parsed.mapValues { (_, v) ->
|
||
val raw = (v["rawBytes"] as Number).toLong()
|
||
val gz = (v["gzipBytes"] as Number).toLong()
|
||
Budget(raw, gz)
|
||
}
|
||
|
||
fun gzipSize(bytes: ByteArray): Long {
|
||
val baos = ByteArrayOutputStream()
|
||
GZIPOutputStream(baos).use { it.write(bytes) }
|
||
return baos.toByteArray().size.toLong()
|
||
}
|
||
|
||
val errors = mutableListOf<String>()
|
||
val report = StringBuilder()
|
||
report.appendLine("Bundle Budget Report (per shell)")
|
||
|
||
// Consider modules under :frontend:shells: as shells
|
||
val shellPrefix = ":frontend:shells:"
|
||
val shells = rootProject.subprojects.filter { it.path.startsWith(shellPrefix) }
|
||
if (shells.isEmpty()) {
|
||
report.appendLine("No frontend shells found under $shellPrefix")
|
||
}
|
||
|
||
shells.forEach { shell ->
|
||
val key = shell.path.trimStart(':').replace(':', '/') // or use a colon form for budgets keys below
|
||
val colonKey = shell.path.trimStart(':').replace('/', ':').trim() // ensure ":a:b:c"
|
||
// Budgets are keyed by a Gradle path with colons but without leading colon in config for readability
|
||
val budgetKeyCandidates =
|
||
listOf(
|
||
// e.g., frontend:shells:meldestelle-portal
|
||
shell.path.removePrefix(":"),
|
||
colonKey.removePrefix(":"),
|
||
shell.name,
|
||
)
|
||
|
||
val budgetEntry = budgetKeyCandidates.firstNotNullOfOrNull { budgets[it] }
|
||
if (budgetEntry == null) {
|
||
report.appendLine("- ${shell.path}: No budget configured (skipping)")
|
||
return@forEach
|
||
}
|
||
|
||
// Locate distributions directory
|
||
val distDir = shell.layout.buildDirectory.dir("distributions").get().asFile
|
||
if (!distDir.exists()) {
|
||
report.appendLine("- ${shell.path}: distributions dir not found (expected build/distributions) – did you build the JS bundle?")
|
||
return@forEach
|
||
}
|
||
|
||
// Collect JS files under distributions (avoid .map and .txt)
|
||
val jsFiles =
|
||
distDir.walkTopDown().filter { it.isFile && it.extension == "js" && !it.name.endsWith(".map") }.toList()
|
||
if (jsFiles.isEmpty()) {
|
||
report.appendLine("- ${shell.path}: no JS artifacts found in ${distDir.path}")
|
||
return@forEach
|
||
}
|
||
|
||
var totalRaw = 0L
|
||
var totalGzip = 0L
|
||
val topFiles = mutableListOf<Pair<String, Long>>()
|
||
jsFiles.forEach { f ->
|
||
val bytes = f.readBytes()
|
||
val raw = bytes.size.toLong()
|
||
val gz = gzipSize(bytes)
|
||
totalRaw += raw
|
||
totalGzip += gz
|
||
topFiles += f.name to raw
|
||
}
|
||
val top = topFiles.sortedByDescending { it.second }.take(5)
|
||
|
||
report.appendLine("- ${shell.path}:")
|
||
report.appendLine(" raw: $totalRaw bytes (budget ${budgetEntry.rawBytes})")
|
||
report.appendLine(" gzip: $totalGzip bytes (budget ${budgetEntry.gzipBytes})")
|
||
report.appendLine(" top files:")
|
||
top.forEach { (n, s) -> report.appendLine(" - $n: $s bytes") }
|
||
|
||
if (totalRaw > budgetEntry.rawBytes || totalGzip > budgetEntry.gzipBytes) {
|
||
errors += "${shell.path}: raw=$totalRaw/${budgetEntry.rawBytes}, gzip=$totalGzip/${budgetEntry.gzipBytes}"
|
||
}
|
||
}
|
||
|
||
val outDir = layout.buildDirectory.dir("reports/bundles").get().asFile
|
||
outDir.mkdirs()
|
||
file(outDir.resolve("bundle-budgets.txt")).writeText(report.toString())
|
||
|
||
if (errors.isNotEmpty()) {
|
||
val msg =
|
||
buildString {
|
||
appendLine("Bundle budget violations:")
|
||
errors.forEach { appendLine(" - $it") }
|
||
appendLine()
|
||
appendLine("See report: ${outDir.resolve("bundle-budgets.txt").path}")
|
||
}
|
||
throw GradleException(msg)
|
||
} else {
|
||
println(report.toString())
|
||
println("Bundle budgets OK. Report saved to ${outDir.resolve("bundle-budgets.txt").path}")
|
||
}
|
||
}
|
||
}
|
||
|
||
// Composite verification task including static analyzers if present
|
||
tasks.register("staticAnalysis") {
|
||
group = "verification"
|
||
description = "Run static analysis (detekt, ktlint) and architecture guards"
|
||
// Plugins provide these tasks; only 'depend on' if tasks exist
|
||
dependsOn(
|
||
tasks.matching { it.name == "detekt" },
|
||
tasks.matching { it.name == "ktlintCheck" },
|
||
// ARCHITECTURE-TESTS: Replaced old archGuards with the new test module
|
||
project(":platform:architecture-tests").tasks.named("test"),
|
||
)
|
||
}
|
||
|
||
// ##################################################################
|
||
// ### DOKKA (Multi-Module) ###
|
||
// ##################################################################
|
||
|
||
// Apply Dokka (V2) automatically to Kotlin subprojects
|
||
subprojects {
|
||
plugins.withId("org.jetbrains.kotlin.jvm") { apply(plugin = "org.jetbrains.dokka") }
|
||
plugins.withId("org.jetbrains.kotlin.multiplatform") { apply(plugin = "org.jetbrains.dokka") }
|
||
}
|
||
|
||
// Aggregate tasks to build multi-module docs in Markdown (GFM) and HTML
|
||
// Unified V2 aggregator: builds docs via `dokkaGenerate` in subprojects and aggregates outputs
|
||
val dokkaAll =
|
||
tasks.register("dokkaAll") {
|
||
group = "documentation"
|
||
description = "Builds Dokka (V2) for all modules and aggregates outputs under build/dokka/all"
|
||
// Trigger Dokka generation in all subprojects that have the Dokka plugin
|
||
dependsOn(
|
||
subprojects
|
||
.filter { it.plugins.hasPlugin("org.jetbrains.dokka") }
|
||
.map { "${it.path}:dokkaGenerate" },
|
||
)
|
||
doLast {
|
||
val dest = layout.buildDirectory.dir("dokka/all").get().asFile
|
||
if (dest.exists()) dest.deleteRecursively()
|
||
dest.mkdirs()
|
||
|
||
val modules = mutableListOf<Pair<String, String>>()
|
||
|
||
subprojects.filter { it.plugins.hasPlugin("org.jetbrains.dokka") }.forEach { p ->
|
||
// Dokka V2 writes into build/dokka/html
|
||
val outHtml = p.layout.buildDirectory.dir("dokka/html").get().asFile
|
||
if (outHtml.exists()) {
|
||
val modulePath = p.path.trimStart(':').replace(':', '/')
|
||
val targetDir = File(dest, modulePath)
|
||
outHtml.copyRecursively(targetDir, overwrite = true)
|
||
modules.add(p.name to modulePath)
|
||
}
|
||
}
|
||
|
||
// Generate a simple index.html to navigate the modules
|
||
val indexFile = File(dest, "index.html")
|
||
val links =
|
||
modules.sortedBy { it.first }.joinToString("\n") { (name, path) ->
|
||
"""<li><a href="$path/index.html">$name</a> <span style="color:gray; font-size:0.8em">($path)</span></li>"""
|
||
}
|
||
|
||
indexFile.writeText(
|
||
"""
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Meldestelle Documentation</title>
|
||
<style>
|
||
body { font-family: system-ui, -apple-system, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.5; }
|
||
h1 { border-bottom: 1px solid #eee; padding-bottom: 0.5rem; }
|
||
ul { list-style-type: none; padding: 0; }
|
||
li { margin: 0.5rem 0; padding: 0.5rem; background: #f9f9f9; border-radius: 4px; }
|
||
li:hover { background: #f0f0f0; }
|
||
a { text-decoration: none; color: #0066cc; font-weight: 500; }
|
||
a:hover { text-decoration: underline; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>Meldestelle Project Documentation</h1>
|
||
<p>Generated on ${java.time.LocalDateTime.now()}</p>
|
||
<ul>
|
||
$links
|
||
</ul>
|
||
</body>
|
||
</html>
|
||
""".trimIndent(),
|
||
)
|
||
|
||
println("[DOKKA] Aggregated Dokka V2 outputs into ${dest.absolutePath}")
|
||
println("[DOKKA] Open ${dest.resolve("index.html").absolutePath} in your browser")
|
||
}
|
||
}
|
||
|
||
// ##################################################################
|
||
// ### DOKU-AGGREGATOR ###
|
||
// ##################################################################
|
||
|
||
// Leichter Aggregator im Root-Projekt, ruft die eigentlichen Tasks im :docs Subprojekt auf
|
||
tasks.register("docs") {
|
||
description = "Aggregates documentation tasks from :docs"
|
||
group = "documentation"
|
||
dependsOn(":docs:generateAllDocs")
|
||
}
|
||
|
||
// Wrapper-Konfiguration
|
||
// Apply Node warning suppression on root project Exec tasks as well
|
||
// Ensures aggregated Kotlin/JS tasks created at root (e.g., kotlinNpmInstall) inherit the env
|
||
tasks.withType<Exec>().configureEach {
|
||
val current = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS")
|
||
val merged = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation"
|
||
environment("NODE_OPTIONS", merged)
|
||
environment("NODE_NO_WARNINGS", "1")
|
||
// Set a Chrome binary path to avoid snap permission issues
|
||
environment("CHROME_BIN", "/usr/bin/google-chrome-stable")
|
||
environment("CHROMIUM_BIN", "/usr/bin/chromium")
|
||
environment("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium")
|
||
}
|
||
|
||
tasks.wrapper {
|
||
gradleVersion = "9.3.1"
|
||
distributionType = Wrapper.DistributionType.BIN
|
||
}
|