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.Detekt
|
||||||
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
|
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
import org.jlleitschuh.gradle.ktlint.KtlintExtension
|
import org.jlleitschuh.gradle.ktlint.KtlintExtension
|
||||||
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
|
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
// Version management plugin for dependency updates
|
// Version management plugin for dependency updates
|
||||||
id("com.github.ben-manes.versions") version "0.51.0"
|
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
|
// Kotlin plugins declared here with 'apply false' to centralize version management
|
||||||
// This prevents "plugin loaded multiple times" errors in Gradle 9.2.1+
|
// This prevents "plugin loaded multiple times" errors in Gradle 9.2.1+
|
||||||
// Subprojects apply these plugins via version catalog: alias(libs.plugins.kotlinJvm)
|
// 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.
|
// Fails if any source file contains manual Authorization header setting.
|
||||||
// Policy: Authorization must be injected by the DI-provided HttpClient (apiClient).
|
// Policy: Authorization must be injected by the DI-provided HttpClient (apiClient).
|
||||||
tasks.register<ForbiddenAuthorizationHeaderTask>("archGuardForbiddenAuthorizationHeader") {
|
tasks.register("archGuardForbiddenAuthorizationHeader") {
|
||||||
group = "verification"
|
group = "verification"
|
||||||
description = "Fail build if code sets Authorization header manually."
|
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)
|
// Guard: Frontend Feature Isolation (no feature -> feature project dependencies)
|
||||||
tasks.register<FeatureIsolationTask>("archGuardNoFeatureToFeatureDeps") {
|
tasks.register("archGuardNoFeatureToFeatureDeps") {
|
||||||
group = "verification"
|
group = "verification"
|
||||||
description = "Fail build if a :frontend:features:* module depends on another :frontend:features:* module"
|
description = "Fail build if a :frontend:features:* module depends on another :frontend:features:* module"
|
||||||
}
|
doLast {
|
||||||
|
val featurePrefix = ":frontend:features:"
|
||||||
|
val violations = mutableListOf<String>()
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
rootProject.subprojects.forEach { p ->
|
||||||
// Bundle Size Budgets for Frontend Shells (Kotlin/JS)
|
if (p.path.startsWith(featurePrefix)) {
|
||||||
// ------------------------------------------------------------------
|
// Check all configurations except test-related ones
|
||||||
tasks.register<BundleBudgetTask>("checkBundleBudget") {
|
p.configurations
|
||||||
group = "verification"
|
.matching { cfg ->
|
||||||
description = "Checks JS bundle sizes of frontend shells against configured budgets"
|
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
|
// 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"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user