Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3b29c7f
[BOOK-94] refactor: root - 프로젝트 root 이름 변경
move-hoon Jul 6, 2025
c49bb9a
[BOOK-94] chore: SonarCloud와 JaCoCo를 활용한 코드 품질 및 테스트 커버리지 관리 기능 추가
move-hoon Jul 6, 2025
34bfabd
[BOOK-94] refactor: CI 워크플로우에 fullCheck 태스크 적용
move-hoon Jul 6, 2025
41a17e4
[BOOK-94] fix: executionData 설정 방식을 각 하위 모듈의 build/jacoco/test.exec 파…
move-hoon Jul 6, 2025
252293f
[BOOK-94] refactor: 코드레빗 리뷰 반영
move-hoon Jul 6, 2025
774ef36
[BOOK-94] chore: (임시) info -> debug로 빌드 실패 원인 파악
move-hoon Jul 6, 2025
77a7b26
[BOOK-94] chore: debug -> info로 변경
move-hoon Jul 6, 2025
09284b2
[BOOK-94] refactor: classesDirs를 사용하여 JaCoCo 분석 대상 경로 명시
move-hoon Jul 6, 2025
9c412a9
[BOOK-94] fix: 중복 인덱싱 방지 로직 추가
move-hoon Jul 6, 2025
bf4fe3d
[BOOK-94] fix: 테스트하지 않는 코드 패턴을 명시하여 중복 인덱싱 방지
move-hoon Jul 6, 2025
ed47f16
[BOOK-94] fix: classes/kotlin/main로 경로 명확히 지정
move-hoon Jul 6, 2025
328cbcb
[BOOK-94] fix: 자동 탐지와 수동 설정 충돌 해결
move-hoon Jul 6, 2025
d6d0bda
[BOOK-94] chore: 주석 제거
move-hoon Jul 6, 2025
a15892a
[BOOK-94] chore: 코드레빗 리뷰 반영
move-hoon Jul 6, 2025
e57c642
[BOOK-94] chore: 코드레빗 리뷰 반영
move-hoon Jul 6, 2025
4b9ea80
[BOOK-94] fix: 패턴 수정
move-hoon Jul 6, 2025
cd1c928
[BOOK-94] refactor: 코드레빗 리뷰 반영
move-hoon Jul 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .github/workflows/ci-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ concurrency:
jobs:
build-validation:
runs-on: ubuntu-24.04
timeout-minutes: 10
timeout-minutes: 15

steps:
- name: Checkout code
Expand All @@ -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
175 changes: 162 additions & 13 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -19,29 +20,41 @@ 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 {
languageVersion.set(JavaLanguageVersion.of(Versions.JAVA_VERSION.toInt()))
}
}

dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:2025.0.0")
}
}

tasks.withType<Test> {
useJUnitPlatform()
}

plugins.withId(Plugins.Kotlin.ALLOPEN) {
extensions.configure<org.jetbrains.kotlin.allopen.gradle.AllOpenExtension> {
annotation("jakarta.persistence.Entity")
Expand All @@ -53,14 +66,150 @@ subprojects {
// Configure Kotlin compiler options
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
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<Test> {
useJUnitPlatform()
finalizedBy("jacocoTestReport")

testLogging {
events("passed", "skipped", "failed")
showStandardStreams = false
}
}

// 각 서브모듈의 JaCoCo 테스트 리포트 설정
tasks.withType<JacocoReport> {
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<Jar> { enabled = true }
withType<BootJar> { enabled = false }
}

// 루트 프로젝트 JaCoCo 통합 리포트 설정
tasks.register<JacocoReport>("jacocoRootReport") {
description = "Generates an aggregate report from all subprojects"
group = "reporting"

dependsOn(subprojects.map { it.tasks.named("test") })

sourceDirectories.setFrom(subprojects.map { it.the<SourceSetContainer>()["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")
}
})
Comment on lines +118 to +135
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

통합 리포트에도 .exec 존재 여부 필터 필요

jacocoRootReportexecutionData.from(...) 에서도 .exec 파일이 없을 때 경고가 출력됩니다. 아래처럼 필터를 추가해 안전하게 수집하세요.

-    executionData.from(subprojects.map { subproject ->
-        subproject.fileTree(subproject.layout.buildDirectory.dir("jacoco")) {
-            include("**/*.exec")
-        }
-    })
+    executionData.from(subprojects.map { sp ->
+        sp.fileTree("${sp.buildDir}/jacoco") {
+            include("**/*.exec")
+        }.filter { it.exists() }
+    })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 루트 프로젝트 JaCoCo 통합 리포트 설정
tasks.register<JacocoReport>("jacocoRootReport") {
description = "Generates an aggregate report from all subprojects"
group = "reporting"
dependsOn(subprojects.map { it.tasks.named("test") })
sourceDirectories.setFrom(subprojects.map { it.the<SourceSetContainer>()["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")
}
})
// 루트 프로젝트 JaCoCo 통합 리포트 설정
tasks.register<JacocoReport>("jacocoRootReport") {
description = "Generates an aggregate report from all subprojects"
group = "reporting"
dependsOn(subprojects.map { it.tasks.named("test") })
sourceDirectories.setFrom(subprojects.map { it.the<SourceSetContainer>()["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 { sp ->
sp.fileTree("${sp.buildDir}/jacoco") {
include("**/*.exec")
}.filter { it.exists() }
})
}
🤖 Prompt for AI Agents
In build.gradle.kts around lines 118 to 135, the jacocoRootReport task's
executionData.from(...) collects .exec files without checking if they exist,
causing warnings when no .exec files are present. Modify the code to filter out
empty file collections by adding a check to include only non-empty file trees,
ensuring safe and warning-free aggregation of execution data.


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")
}
2 changes: 2 additions & 0 deletions buildSrc/src/main/kotlin/Plugins.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
rootProject.name = "multi-module-test"
rootProject.name = "reed"

include(
"admin",
Expand Down