refactor: 빌드 로직 개선, gradle 파일 경량화
This commit is contained in:
@@ -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')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user