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:
+90
-12
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user