refactor(build): consolidate architecture guard tasks into reusable custom Gradle scripts

Replaced task implementations for `ForbiddenAuthorizationHeaderTask`, `FeatureIsolationTask`, and `BundleBudgetTask` with dedicated buildSrc scripts for improved clarity and maintainability. Updated task registrations accordingly in `build.gradle.kts`.
This commit is contained in:
2026-01-03 14:38:22 +01:00
parent 4f79c3c755
commit a828198377
3 changed files with 115 additions and 12 deletions
+90 -12
View File
@@ -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<ForbiddenAuthorizationHeaderTask>("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<File>()
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<FeatureIsolationTask>("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<String>()
// ------------------------------------------------------------------
// Bundle Size Budgets for Frontend Shells (Kotlin/JS)
// ------------------------------------------------------------------
tasks.register<BundleBudgetTask>("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
@@ -0,0 +1,19 @@
import at.mocode.gradle.ForbiddenAuthorizationHeaderTask
import at.mocode.gradle.FeatureIsolationTask
tasks.register<ForbiddenAuthorizationHeaderTask>("archGuardForbiddenAuthorizationHeader") {
group = "verification"
description = "Fail build if code sets Authorization header manually."
}
tasks.register<FeatureIsolationTask>("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")
}
@@ -0,0 +1,6 @@
import at.mocode.gradle.BundleBudgetTask
tasks.register<BundleBudgetTask>("checkBundleBudget") {
group = "verification"
description = "Checks JS bundle sizes of frontend shells against configured budgets"
}