diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b5e40f60..d3a4572d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,10 @@ on: permissions: contents: read +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: changes: name: Detect Changes @@ -97,7 +101,7 @@ jobs: - name: Run tests if: needs.changes.outputs.backend == 'true' - run: ./gradlew verifyAll --no-daemon --stacktrace + run: ./gradlew verifyAll --configuration-cache --no-daemon --stacktrace - name: Skip backend tests if: needs.changes.outputs.backend != 'true' diff --git a/build.gradle.kts b/build.gradle.kts index 1a51664d4..44334f6c5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,7 @@ +import java.io.File import java.util.Base64 import org.gradle.api.component.AdhocComponentWithVariants +import org.gradle.api.tasks.PathSensitivity plugins { alias(libs.plugins.spring.boot) apply false @@ -361,52 +363,63 @@ val allowedProductionProjectDependencies = mapOf( ), ) +val moduleDependencyViolations = objects.listProperty() +val bootSourceRoot = project(":muyun-boot").file("src/main/java") + tasks.register("verifyModuleBoundaries") { description = "Verifies production Gradle dependency direction and Boot host boundaries." group = LifecycleBasePlugin.VERIFICATION_GROUP + inputs.property("dependencyViolations", moduleDependencyViolations) + inputs.files(fileTree(bootSourceRoot) { include("**/*.java") }) + .withPathSensitivity(PathSensitivity.RELATIVE) + inputs.property("bootSourceRoot", bootSourceRoot.absolutePath) doLast { - val violations = mutableListOf() - subprojects.forEach { sourceProject -> - productionDependencyConfigurations.forEach { configurationName -> - sourceProject.configurations.findByName(configurationName) - ?.dependencies - ?.withType(org.gradle.api.artifacts.ProjectDependency::class.java) - ?.forEach { dependency: org.gradle.api.artifacts.ProjectDependency -> - val targetPath = dependency.path - if (targetPath !in allowedProductionProjectDependencies.getValue(sourceProject.path)) { - violations += ("${sourceProject.path} must not depend on $targetPath " - + "through production configuration $configurationName") - } - if (sourceProject.path in coreModulePaths && targetPath in deliveryModulePaths) { - violations += "${sourceProject.path} must not depend on delivery module $targetPath" - } - if (sourceProject.path == ":muyun-web-adapter" - && (targetPath in webDeliveryModulePaths || targetPath == ":muyun-boot")) { - violations += ":muyun-web-adapter must not depend on $targetPath" - } - if (sourceProject.path in webDeliveryModulePaths && targetPath == ":muyun-boot") { - violations += "${sourceProject.path} must not depend on application host :muyun-boot" - } - } - } - } - + @Suppress("UNCHECKED_CAST") + val violations = (inputs.properties.getValue("dependencyViolations") as List).toMutableList() + val sourceRoot = File(inputs.properties.getValue("bootSourceRoot") as String) val forbiddenBootStereotypes = Regex("@(RestController|Controller|Service|Repository)\\b") - fileTree(project(":muyun-boot").file("src/main/java")) { - include("**/*.java") - }.files.forEach { source -> + inputs.files.files.forEach { source -> if (forbiddenBootStereotypes.containsMatchIn(source.readText())) { - violations += ":muyun-boot must not declare delivery or domain stereotype: ${source.relativeTo(rootDir)}" + violations += ":muyun-boot must not declare delivery or domain stereotype: " + + source.relativeTo(sourceRoot) } } - check(violations.isEmpty()) { "Module boundary violations:\n${violations.joinToString("\n") { " - $it" }}" } } } +gradle.projectsEvaluated { + val violations = mutableListOf() + subprojects.forEach { sourceProject -> + productionDependencyConfigurations.forEach { configurationName -> + sourceProject.configurations.findByName(configurationName) + ?.dependencies + ?.withType(org.gradle.api.artifacts.ProjectDependency::class.java) + ?.forEach { dependency: org.gradle.api.artifacts.ProjectDependency -> + val targetPath = dependency.path + if (targetPath !in allowedProductionProjectDependencies.getValue(sourceProject.path)) { + violations += ("${sourceProject.path} must not depend on $targetPath " + + "through production configuration $configurationName") + } + if (sourceProject.path in coreModulePaths && targetPath in deliveryModulePaths) { + violations += "${sourceProject.path} must not depend on delivery module $targetPath" + } + if (sourceProject.path == ":muyun-web-adapter" + && (targetPath in webDeliveryModulePaths || targetPath == ":muyun-boot")) { + violations += ":muyun-web-adapter must not depend on $targetPath" + } + if (sourceProject.path in webDeliveryModulePaths && targetPath == ":muyun-boot") { + violations += "${sourceProject.path} must not depend on application host :muyun-boot" + } + } + } + } + moduleDependencyViolations.set(violations) +} + integrationTestTasks.forEach { integrationTest -> integrationTest.configure { mustRunAfter(unitTestTasks)