diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index f4c8e832..8558dd2d 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -17,7 +17,7 @@ concurrency: jobs: build-validation: runs-on: ubuntu-24.04 - timeout-minutes: 10 + timeout-minutes: 15 steps: - name: Checkout code @@ -44,8 +44,18 @@ jobs: with: gradle-home-cache-cleanup: true + - name: Cache SonarCloud packages + uses: actions/cache@v4 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Run Gradle check (fast validation) - run: ./gradlew check --parallel --build-cache + - name: Run full check and SonarCloud analysis + env: + GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: ./gradlew fullCheck --parallel --build-cache --info --stacktrace diff --git a/build.gradle.kts b/build.gradle.kts index e27db331..e6fe519d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,8 @@ plugins { kotlin(Plugins.Kotlin.Short.SPRING) version Versions.KOTLIN kotlin(Plugins.Kotlin.Short.JPA) version Versions.KOTLIN id(Plugins.DETEKT) version Versions.DETEKT - id(Plugins.KOVER) version Versions.KOVER + id(Plugins.JACOCO) + id(Plugins.SONAR_QUBE) version Versions.SONAR_QUBE } allprojects { @@ -19,12 +20,34 @@ allprojects { } } +// 테스트하지 않는 코드 패턴 (JaCoCo + SonarQube 커버리지 + CPD 공통) +val testExclusionPatterns = listOf( + "**/*Application*", + "**/config/**", + "**/*Config*", + "**/exception/**", + "**/*Exception*", + "**/*ErrorCode*", + "**/dto/**", + "**/*Request*", + "**/*Response*", + "**/*Entity*", + "**/annotation/**", + "**/generated/**" +) + +// SonarQube 전체 분석 제외 패턴 (분석 자체가 의미 없는 파일들) +val sonarGlobalExclusions = listOf( + "**/build/**", +) + subprojects { apply(plugin = Plugins.SPRING_BOOT) apply(plugin = Plugins.SPRING_DEPENDENCY_MANAGEMENT) apply(plugin = Plugins.Kotlin.SPRING) apply(plugin = Plugins.Kotlin.JPA) apply(plugin = Plugins.Kotlin.JVM) + apply(plugin = Plugins.JACOCO) java { toolchain { @@ -32,16 +55,6 @@ subprojects { } } - dependencyManagement { - imports { - mavenBom("org.springframework.cloud:spring-cloud-dependencies:2025.0.0") - } - } - - tasks.withType { - useJUnitPlatform() - } - plugins.withId(Plugins.Kotlin.ALLOPEN) { extensions.configure { annotation("jakarta.persistence.Entity") @@ -53,14 +66,150 @@ subprojects { // Configure Kotlin compiler options tasks.withType { kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") + freeCompilerArgs += listOf( + "-Xjsr305=strict", + "-Xconsistent-data-class-copy-visibility" + ) jvmTarget = Versions.JAVA_VERSION - freeCompilerArgs += "-Xconsistent-data-class-copy-visibility" } } } +// 루트 프로젝트에서 모든 JaCoCo 설정 관리 +configure(subprojects) { + jacoco { + toolVersion = Versions.JACOCO + } + + tasks.withType { + useJUnitPlatform() + finalizedBy("jacocoTestReport") + + testLogging { + events("passed", "skipped", "failed") + showStandardStreams = false + } + } + + // 각 서브모듈의 JaCoCo 테스트 리포트 설정 + tasks.withType { + dependsOn("test") + reports { + xml.required.set(true) + csv.required.set(false) + html.required.set(true) + } + + classDirectories.setFrom(fileTree(layout.buildDirectory.dir("classes/kotlin/main")) { + exclude(testExclusionPatterns) + }) + + executionData.setFrom(fileTree(layout.buildDirectory) { + include("jacoco/*.exec") + }) + } +} + tasks { withType { enabled = true } withType { enabled = false } } + +// 루트 프로젝트 JaCoCo 통합 리포트 설정 +tasks.register("jacocoRootReport") { + description = "Generates an aggregate report from all subprojects" + group = "reporting" + + dependsOn(subprojects.map { it.tasks.named("test") }) + + sourceDirectories.setFrom(subprojects.map { it.the()["main"].allSource.srcDirs }) + classDirectories.setFrom(subprojects.map { subproject -> + subproject.fileTree(subproject.layout.buildDirectory.get().asFile.resolve("classes/kotlin/main")) { + exclude(testExclusionPatterns) + } + }) + executionData.from(subprojects.map { subproject -> + subproject.fileTree(subproject.layout.buildDirectory.dir("jacoco")) { + include("**/*.exec") + } + }) + + reports { + xml.required.set(true) + csv.required.set(false) + html.required.set(true) + } +} + +// SonarQube 설정을 루트에서 모든 서브모듈에 대해 설정 +sonar { + properties { + property("sonar.projectKey", "YAPP-Github_26th-App-Team-1-BE") + property("sonar.organization", "yapp-github") + property("sonar.host.url", "https://sonarcloud.io") + property( + "sonar.coverage.jacoco.xmlReportPaths", + "${layout.buildDirectory.get()}/reports/jacoco/jacocoRootReport/jacocoRootReport.xml" + ) + property("sonar.kotlin.coveragePlugin", Plugins.JACOCO) + property("sonar.kotlin.version", Versions.KOTLIN) + property("sonar.exclusions", sonarGlobalExclusions.joinToString(",")) + property("sonar.cpd.exclusions", testExclusionPatterns.joinToString(",")) + property("sonar.coverage.exclusions", testExclusionPatterns.joinToString(",")) + } +} + +// SonarQube 태스크가 통합 JaCoCo 리포트에 의존하도록 설정 +tasks.named("sonar") { + dependsOn("jacocoRootReport") +} + +/** + * CI용 - 전체 품질 검증 파이프라인을 실행합니다. (테스트, 커버리지, SonarQube 분석) + * GitHub Actions에서 이 태스크 하나만 호출합니다. + * 사용 예: ./gradlew fullCheck + */ +tasks.register("fullCheck") { + description = "Runs all tests, generates reports, and performs SonarQube analysis" + group = "Verification" + dependsOn("testAll", "jacocoTestReportAll") + finalizedBy("sonar") +} + +/** + * 로컬용 - SonarQube 분석 없이 빠르게 테스트 커버리지만 확인합니다. + * 사용 예: ./gradlew checkCoverage + */ +tasks.register("checkCoverage") { + description = "Runs tests and generates coverage reports without SonarQube analysis" + group = "Verification" + dependsOn("testAll", "jacocoTestReportAll") +} + +/** + * 로컬용 - 빌드 과정에서 생성된 모든 리포트를 삭제합니다. + * 사용 예: ./gradlew cleanReports + */ +tasks.register("cleanReports") { + description = "Cleans all generated reports" + group = "Cleanup" + doLast { + subprojects.forEach { subproject -> + delete(subproject.layout.buildDirectory.dir("reports")) + } + delete(layout.buildDirectory.dir("reports")) + } +} + +tasks.register("testAll") { + description = "Runs tests in all subprojects" + group = "Verification" + dependsOn(subprojects.map { it.tasks.named("test") }) +} + +tasks.register("jacocoTestReportAll") { + description = "Generates JaCoCo test reports for all subprojects and creates aggregate report" + group = "Verification" + dependsOn(subprojects.map { it.tasks.named("jacocoTestReport") }) + finalizedBy("jacocoRootReport") +} diff --git a/buildSrc/src/main/kotlin/Plugins.kt b/buildSrc/src/main/kotlin/Plugins.kt index ae24afee..3651cb0e 100644 --- a/buildSrc/src/main/kotlin/Plugins.kt +++ b/buildSrc/src/main/kotlin/Plugins.kt @@ -3,6 +3,8 @@ object Plugins { const val SPRING_DEPENDENCY_MANAGEMENT = "io.spring.dependency-management" const val DETEKT = "io.gitlab.arturbosch.detekt" const val KOVER = "org.jetbrains.kotlinx.kover" + const val JACOCO = "jacoco" + const val SONAR_QUBE = "org.sonarqube" object Kotlin { const val ALLOPEN = "org.jetbrains.kotlin.plugin.allopen" diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 1423d785..7361fca5 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -3,6 +3,7 @@ object Versions { const val SPRING_DEPENDENCY_MANAGEMENT = "1.1.7" const val KOTLIN = "1.9.25" const val DETEKT = "1.23.1" - const val KOVER = "0.9.1" + const val SONAR_QUBE = "6.2.0.5505" + const val JACOCO = "0.8.13" const val JAVA_VERSION = "21" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 32191340..c7847254 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ -rootProject.name = "multi-module-test" +rootProject.name = "reed" include( "admin",