refactor: 빌드 로직 개선, gradle 파일 경량화

This commit is contained in:
donghyeon-ka
2026-09-18 15:22:30 +09:00
parent ace8aaaef6
commit 60b6a319e7
397 changed files with 19320 additions and 8406 deletions
+10 -206
View File
@@ -3,6 +3,8 @@ plugins {
id 'ca.spring-config'
id 'java-test-fixtures'
id 'ca.jpa-evidence'
id 'ca.jpa-test-lanes'
id 'ca.auxiliary-source-set'
}
// Shared test code as a Gradle test-fixtures variant — ADR-BUILD-001.
@@ -20,7 +22,7 @@ plugins {
// The testkit is its own source set rather than part of `test` because more than one lane consumes
// it and because a source set whose dependencies are declared only on the test configurations gives
// the design's "no production module depends on the testkit" guarantee without a new Gradle project.
strictTestLanes {
auxiliarySourceSets {
sourceSet('postgresqlIntegrationTest') {
// 'testFixtures' as well as 'main': the integration lane consumed the testkit through the
// convention's `consumedBy 'test', 'postgresqlIntegrationTest'`, and java-test-fixtures only
@@ -101,143 +103,10 @@ dependencies {
jpaPlatformPerformanceTestRuntimeOnly 'org.postgresql:postgresql'
}
// The fourteen no-skip PostgreSQL readiness lanes, declared rather than assembled.
//
// They were fourteen calls to a local `tasks.register(..., Test)` factory that re-spelled the five
// lines `ca.strict-test-lane` owns. The convention adds what the factory could not: naming the test
// through `requires(...)` turns on `failOnNoMatchingTests` AND the post-run check that the named
// selector actually executed, so a renamed readiness class fails its lane instead of leaving it
// with nothing to run.
String jpaPostgreSqlEvidenceImage = providers.gradleProperty('jpaPostgreSqlEvidenceImage')
.getOrElse('postgres:16-alpine')
String readinessPackage = 'dev.caskeleton.adapter.outbound.persistence.readiness'
Map<String, String> postgresqlReadinessLanes = [
postgresqlLifecycleIntegrationTest : 'PostgreSqlLifecycleIntegrationTest',
postgresqlSecurityBaselineIntegrationTest : 'PostgreSqlSecurityBaselineIntegrationTest',
postgresqlMigrationIntegrationTest : 'PostgreSqlMigrationIntegrationTest',
postgresqlTransactionIntegrationTest : 'PostgreSqlTransactionIntegrationTest',
postgresqlAggregateIntegrationTest : 'PostgreSqlAggregateIntegrationTest',
postgresqlQueryIntegrationTest : 'PostgreSqlQueryIntegrationTest',
postgresqlIdempotencyIntegrationTest : 'PostgreSqlIdempotencyIntegrationTest',
postgresqlOutboxStorageIntegrationTest : 'PostgreSqlOutboxStorageIntegrationTest',
postgresqlOutboxPollingIntegrationTest : 'PostgreSqlOutboxPollingIntegrationTest',
postgresqlInboxIntegrationTest : 'PostgreSqlInboxIntegrationTest',
postgresqlFileserverMigrationIntegrationTest : 'PostgreSqlFileserverMigrationIntegrationTest',
postgresqlFileserverMetadataIntegrationTest : 'PostgreSqlFileserverMetadataStoreIntegrationTest',
postgresqlFileserverReclamationIntegrationTest : 'PostgreSqlFileserverReclamationIntegrationTest',
// The notification stream is opt-in and lives outside the default Flyway location, so "is it
// applied and promoted" is a real deployment question with a real wrong answer. This lane
// asks it against a real server; the entity-scan half is a unit test.
postgresqlNotificationSchemaActivationIntegrationTest :
'PostgreSqlNotificationSchemaActivationIntegrationTest'
]
postgresqlReadinessLanes.each { String taskName, String simpleName ->
String testClass = "${readinessPackage}.${simpleName}"
strictTestLanes.lane(taskName) {
sourceSet = 'postgresqlIntegrationTest'
description = "Runs the no-skip real PostgreSQL readiness scenario ${testClass}."
requires(testClass)
customize = { test ->
test.jvmArgs(
'-Duser.timezone=UTC',
"-Djpa.evidence.postgresql.image=${jpaPostgreSqlEvidenceImage}")
}
}
}
// The rule verifyJpaSecurityFixtures was reaching for is now a selector.
//
// It read PostgreSqlSecurityBaselineIntegrationTest.java as text and failed when three strings were
// absent from it — which three strings sitting in a comment would have satisfied, and which said
// nothing about whether the scenario ran. Naming the method on the lane means the convention fails
// when the runtime-role denial scenario is renamed or stops executing.
strictTestLanes.lanes.named('postgresqlSecurityBaselineIntegrationTest').configure { lane ->
lane.requires("${readinessPackage}.PostgreSqlSecurityBaselineIntegrationTest" +
'.runtimeRoleCannotCreateInApplicationSchema')
}
// The task itself stays, and its name is not negotiable: config/jpa/readiness-cards.yaml lists it as
// a support task of the `jpa-security-baseline` card, and the ca.jpa-evidence qualification plugin resolves every
// listed path through `tasks.findByName` and fails the build when one is missing. What changed is
// what it checks. Grepping the fixture's source text for 'runtimeRoleCannotCreateInApplicationSchema',
// 'assertDockerAvailable' and '42501' passed on three strings in a comment and proved nothing about
// execution; the lane's `requires(...)` above is the thing that now enforces the scenario, so this
// task verifies that the enforcement is declared rather than re-deriving it from source text.
tasks.register('verifyJpaSecurityFixtures') {
group = 'verification'
description = 'Verifies the no-skip PostgreSQL security lane still names the runtime-role namespace denial scenario.'
String laneName = 'postgresqlSecurityBaselineIntegrationTest'
String requiredSelector = "${readinessPackage}.PostgreSqlSecurityBaselineIntegrationTest" +
'.runtimeRoleCannotCreateInApplicationSchema'
// A live reference to the lane spec's own list, captured at configuration time. Reading it in
// `doLast` therefore sees the final declaration without touching `Task.project` at execution
// time, which Gradle 9 deprecates and the `--warning-mode=fail` gates reject.
List<String> declaredSelectors = strictTestLanes.lanes.getByName(laneName).requiredTests
doLast {
if (!declaredSelectors.contains(requiredSelector)) {
throw new GradleException(
"verifyJpaSecurityFixtures: strict test lane '${laneName}' no longer requires " +
"'${requiredSelector}'. Without that selector the lane can run the " +
'security baseline class with the runtime-role namespace denial scenario ' +
"renamed or deleted and still report success. It requires ${declaredSelectors}.")
}
logger.lifecycle(
"verifyJpaSecurityFixtures: OK — '${laneName}' requires the runtime-role namespace denial scenario.")
}
}
// verifyJpaSqlConstructionSafety keeps its name for the same registry reason, and gives up the half
// of its job that a real tool already does.
//
// It used to also match `(createNativeQuery|queryForObject|update)\s*\([^;]*"\s*\+` against Java
// source text: a regex that matches any method named `update`, and that stops at the first `;`
// inside a string literal, so it over- and under-reported at once. Concatenated SQL is covered
// repo-wide and inter-procedurally by FindSecBugs, which the root build puts on every leaf
// (`spotbugsPlugins libs.findsecbugs.plugin`) with SpotBugs' `ignoreFailures` left at its blocking
// default and no SQL_INJECTION / SQL_NONCONSTANT exclusion in config/spotbugs/exclude.xml. A
// bytecode dataflow check with no package restriction is strictly better than that regex, so the
// regex is gone rather than duplicated.
//
// What no tool covers is the PostgreSQL-specific rule: a `set_config` value must be bound, never
// interpolated, because that value carries the tenant id and the search_path. That check stays, and
// two things about it changed. It no longer parses Java — it matches the SQL token `set_config('`
// and asks whether the same line binds a parameter. And it scans the whole main source root: it was
// pinned to `.../persistence/postgresql`, which is why it never saw the two real call sites in
// experimental/rls/RlsTenantSessionBinder.java and
// experimental/schema/SchemaMultiTenantConnectionProvider.java.
tasks.register('verifyJpaSqlConstructionSafety') {
group = 'verification'
description = 'Rejects non-parameterized PostgreSQL set_config values anywhere in this leaf.'
File mainSource = file('src/main/java')
inputs.dir(mainSource)
doLast {
List<String> violations = []
mainSource.eachFileRecurse { File source ->
if (!source.name.endsWith('.java')) {
return
}
source.readLines().eachWithIndex { String line, int index ->
String trimmed = line.trim()
// Javadoc and line comments mention set_config to explain why it is used; a comment
// is not a call site, and treating one as a violation is how a correct build turns
// red for a sentence.
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) {
return
}
if (line.contains("set_config('") && !line.contains('?')) {
violations << "${source}:${index + 1}: set_config value is not parameterized"
}
}
}
if (!violations.isEmpty()) {
throw new GradleException(
"verifyJpaSqlConstructionSafety: ${violations.size()} violation(s):\n " +
violations.join('\n '))
}
logger.lifecycle(
"verifyJpaSqlConstructionSafety: OK — every set_config value in ${mainSource} binds a parameter.")
}
}
// PostgreSQL readiness, tagged platform, and pool-contract lanes are owned by
// `ca.jpa-test-lanes`. Their metadata is represented as Java records, so task names, selectors,
// tags, descriptions and runtime property wiring are compiled rather than assembled from Groovy Maps.
// The security lane includes the runtime-role namespace-denial method selector in that typed model.
tasks.named('postgresqlSecurityBaselineIntegrationTest') {
// Cross-leaf task edge: see the handoff. verifyCleanArchitectureDependencies inspects
@@ -245,73 +114,8 @@ tasks.named('postgresqlSecurityBaselineIntegrationTest') {
dependsOn project(':adapter:inbound:web').tasks.named('jpaPersistenceRedactionContractTest')
}
// JPA platform lanes (design §40-§41). Each maps one of the plan's JVM test suites onto this
// leaf's existing Docker-backed source set; the mapping is recorded in
// docs/jpa/repository-adaptation.md §3.
//
// Every lane fails closed. `failOnNoDiscoveredTests` matters more here than usual: a selected lane
// that discovers nothing reports success, and a contract suite that silently stopped running is
// indistinguishable from one that passes.
Map<String, List<String>> jpaPlatformLanes = [
jpaPlatformContractTest : ['jpa-contract',
'Runs the JPA platform contract suite against real PostgreSQL (design §40).'],
jpaPlatformMigrationTest : ['jpa-migration',
'Runs the Flyway upgrade snapshot scenarios (design §31).'],
jpaPlatformFailureTest : ['jpa-failure',
'Reproduces deadlock, serialization, and commit-ambiguity failures (design §39).'],
jpaPlatformQueryPlanTest : ['jpa-queryplan',
'Asserts query plan structure and planner estimate error (design §33).'],
jpaPlatformSecurityTest : ['jpa-security',
'Verifies runtime role privileges and search_path safety (design §36).']
]
jpaPlatformLanes.each { String taskName, List<String> spec ->
strictTestLanes.lane(taskName) {
sourceSet = 'postgresqlIntegrationTest'
tag = spec[0]
description = spec[1]
customize = { test ->
test.jvmArgs('-Duser.timezone=UTC')
// The Stable matrix selection. An unknown or empty value is an error in
// PostgreSqlVersion.parseSelection rather than an empty run.
test.systemProperty 'jpa.matrix.versions',
(project.findProperty('jpa.matrix.versions') ?: '16').toString()
}
}
}
// The pool behaviour contract. Named for what it does.
//
// It was `jpaPlatformPerformanceTest`, described as certifying pool and REQUIRES_NEW pressure, and
// gated behind a boolean that defaulted to off everywhere it appeared — in this file, and in the
// nightly workflow that set it explicitly to off. So the release gate depended on a lane whose only
// threshold assertion was that thresholds were not being asserted, and "certified" described a run
// in which no latency or throughput bound was ever compared to anything. The property is gone; its
// name is deliberately not repeated here, because a name in a comment is the next thing somebody
// tries to set.
//
// What the lane genuinely verifies is a behaviour contract: a REQUIRES_NEW depth of one needs two
// connections per concurrent thread, a saturated pool reports its pending count, and a caller waits
// rather than proceeding without a connection. Those are true on any machine, so they need no flag
// — and this name does not promise a number nobody measured. A real performance gate needs a
// dedicated runner, warmup and sample counts, and recorded thresholds; when that exists it belongs
// in a lane of its own rather than behind a boolean on this one.
strictTestLanes {
lane('jpaPlatformPoolContractTest') {
sourceSet = 'jpaPlatformPerformanceTest'
description = 'Verifies Hikari pool and REQUIRES_NEW connection behaviour (design §38).'
customize = { test -> test.jvmArgs('-Duser.timezone=UTC') }
}
}
// The JPA release gate (design §41). Aggregates every lane whose absence would let one of the
// documented gates in docs/jpa/support-matrix.md pass unverified.
tasks.register('jpaPlatformReleaseGate') {
group = 'verification'
description = 'Runs every JPA platform lane required for a release (design §41).'
dependsOn tasks.named('test')
jpaPlatformLanes.keySet().each { String laneName -> dependsOn tasks.named(laneName) }
dependsOn tasks.named('jpaPlatformPoolContractTest')
}
// Which JPA lanes block a release is declared by .github/workflows/jpa-release.yml.
// This leaf declares the lanes and how each runs; it does not own release orchestration.
// The unit lane reads three files that are not Java sources: the release registry and its two
// renderings. Without declaring them, Gradle calls the lane up-to-date after a registry demotion or
@@ -361,7 +165,7 @@ apiSurface {
// The JPA readiness registry describes this platform's lanes and resolves their task paths, so it
// runs with this leaf's `check` rather than with all 62. The task itself is registered by
// the ca.jpa-qualification plugin from build-qualification, which the root applies.
// the ca.jpa-qualification plugin from build-tools, which the root applies.
tasks.named('check') {
dependsOn rootProject.tasks.named('verifyJpaReadinessRegistry')
}