diff --git a/build.gradle.kts b/build.gradle.kts index d914203a..4404e88a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,16 +1,17 @@ -import at.mocode.gradle.BundleBudgetTask -import at.mocode.gradle.FeatureIsolationTask -import at.mocode.gradle.ForbiddenAuthorizationHeaderTask import io.gitlab.arturbosch.detekt.Detekt import io.gitlab.arturbosch.detekt.extensions.DetektExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jlleitschuh.gradle.ktlint.KtlintExtension import org.jlleitschuh.gradle.ktlint.reporter.ReporterType +import java.io.File plugins { // Version management plugin for dependency updates id("com.github.ben-manes.versions") version "0.51.0" + // Custom convention plugins + id("at.mocode.bundle-budget") apply false // Apply to root, but task runs on subprojects + // 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) @@ -181,23 +182,100 @@ subprojects { // Fails if any source file contains manual Authorization header setting. // Policy: Authorization must be injected by the DI-provided HttpClient (apiClient). -tasks.register("archGuardForbiddenAuthorizationHeader") { +tasks.register("archGuardForbiddenAuthorizationHeader") { group = "verification" description = "Fail build if code sets Authorization header manually." + doLast { + val forbiddenPatterns = + listOf( + ".header(\"Authorization\"", + "setHeader(\"Authorization\"", + "headers[\"Authorization\"]", + "headers['Authorization']", + ".header(HttpHeaders.Authorization", + "header(HttpHeaders.Authorization", + ) + // Scope: Frontend-only enforcement. Backend/Test code is excluded. + val srcDirs = listOf("clients", "frontend") + val violations = mutableListOf() + srcDirs.map { file(it) } + .filter { it.exists() } + .forEach { rootDir -> + rootDir.walkTopDown() + .filter { it.isFile && (it.extension == "kt" || it.extension == "kts") } + .forEach { f -> + val text = f.readText() + // Skip test sources + val path = f.invariantSeparatorsPath + val isTest = + path.contains("/src/commonTest/") || + path.contains("/src/jsTest/") || + path.contains("/src/jvmTest/") || + path.contains("/src/test/") + if (!isTest && forbiddenPatterns.any { text.contains(it) }) { + violations += f + } + } + } + if (violations.isNotEmpty()) { + val msg = + buildString { + appendLine("Forbidden manual Authorization header usage found in:") + violations.take(50).forEach { appendLine(" - ${it.path}") } + if (violations.size > 50) appendLine(" ... and ${violations.size - 50} more files") + appendLine() + appendLine("Policy: Use DI-provided apiClient (Koin named \"apiClient\").") + } + throw GradleException(msg) + } + } } // Guard: Frontend Feature Isolation (no feature -> feature project dependencies) -tasks.register("archGuardNoFeatureToFeatureDeps") { +tasks.register("archGuardNoFeatureToFeatureDeps") { group = "verification" description = "Fail build if a :frontend:features:* module depends on another :frontend:features:* module" -} + doLast { + val featurePrefix = ":frontend:features:" + val violations = mutableListOf() -// ------------------------------------------------------------------ -// Bundle Size Budgets for Frontend Shells (Kotlin/JS) -// ------------------------------------------------------------------ -tasks.register("checkBundleBudget") { - group = "verification" - description = "Checks JS bundle sizes of frontend shells against configured budgets" + rootProject.subprojects.forEach { p -> + if (p.path.startsWith(featurePrefix)) { + // Check all configurations except test-related ones + p.configurations + .matching { cfg -> + val n = cfg.name.lowercase() + !n.contains("test") && !n.contains("debug") // ignore test/debug configs + } + .forEach { cfg -> + cfg.dependencies.withType(ProjectDependency::class.java).forEach { dep -> + // Use reflection to avoid compile-time issues with dependencyProject property + val proj = + try { + dep.javaClass.getMethod("getDependencyProject").invoke(dep) as Project + } catch (e: Throwable) { + null + } + val target = proj?.path ?: "" + if (target.startsWith(featurePrefix) && target != p.path) { + violations += "${p.path} -> $target (configuration: ${cfg.name})" + } + } + } + } + } + + if (violations.isNotEmpty()) { + val msg = + buildString { + appendLine("Feature isolation violation(s) detected:") + violations.forEach { appendLine(" - $it") } + appendLine() + appendLine("Policy: frontend features must not depend on other features. Use navigation/shared domain in :frontend:core instead.") + } + throw GradleException(msg) + } + } } // Aggregate convenience task diff --git a/buildSrc/src/main/kotlin/at.mocode.architecture-guards.gradle.kts b/buildSrc/src/main/kotlin/at.mocode.architecture-guards.gradle.kts new file mode 100644 index 00000000..a93009f8 --- /dev/null +++ b/buildSrc/src/main/kotlin/at.mocode.architecture-guards.gradle.kts @@ -0,0 +1,19 @@ +import at.mocode.gradle.ForbiddenAuthorizationHeaderTask +import at.mocode.gradle.FeatureIsolationTask + +tasks.register("archGuardForbiddenAuthorizationHeader") { + group = "verification" + description = "Fail build if code sets Authorization header manually." +} + +tasks.register("archGuardNoFeatureToFeatureDeps") { + group = "verification" + description = "Fail build if a :frontend:features:* module depends on another :frontend:features:* module" +} + +tasks.register("archGuards") { + group = "verification" + description = "Run all architecture guard checks" + dependsOn("archGuardForbiddenAuthorizationHeader") + dependsOn("archGuardNoFeatureToFeatureDeps") +} diff --git a/buildSrc/src/main/kotlin/at.mocode.bundle-budget.gradle.kts b/buildSrc/src/main/kotlin/at.mocode.bundle-budget.gradle.kts new file mode 100644 index 00000000..b745d201 --- /dev/null +++ b/buildSrc/src/main/kotlin/at.mocode.bundle-budget.gradle.kts @@ -0,0 +1,6 @@ +import at.mocode.gradle.BundleBudgetTask + +tasks.register("checkBundleBudget") { + group = "verification" + description = "Checks JS bundle sizes of frontend shells against configured budgets" +}