refactor(build,ci): CI를 단계별로 재편하고 감사 잔여 114건 정리

빌드·CI 레이어 전수 감사(133건) 중 Track A 에서 처리한 E등급 19건을 뺀
나머지를 마무리한다. 한 커밋인 이유는 서로 얽혀 있기 때문이다 — 워크플로가
`checkstyleMain` 을 부르려면 루트가 먼저 Checkstyle 을 붙여야 하고, 모듈 43개가
convention 을 적용하려면 build-logic 이 먼저 그 플러그인을 등록해야 한다.
영역별로 쪼개면 중간 커밋이 빌드되지 않는다.

## CI 단계 분리 (워크플로 29파일 3,360줄 → 19파일 2,692줄, 잡 69 → 64)

모듈이 아니라 단계로 자른다. 기존 28개는 `web-*` `jpa-*` 처럼 모듈로 갈려
있어서 같은 일이 파일마다 중복됐다.

- stage 1 `ci-quality-gates.yml` + `pr-adapters.yml`(신규, 잡 단위 경로 필터) — pull_request
- stage 2 `integration-main.yml`(신규) — push:main + 03:00. 문서 게이트는 여기에 둔다
- stage 3 `release.yml`(신규, 릴리스 워크플로 7개 중 5개 흡수) — push: tags v*

setup 블록 59회 복붙 → `.github/actions/setup-gradle-java` 1개(잡당 13줄 → 5줄).
잡 8개 삭제, 각각 대체 잡을 확인했다. `verifyCleanArchitectureDependencies` 실행
횟수가 태그당 9 → 6, PR당 8 → 4 로 줄었다.

## 컨테이너 릴리스 신설

이미지를 만드는 것이 아무것도 없었다. Dockerfile 은 있었지만
build-push-action / bootBuildImage / jib 사용처가 0건이고, `*-release.yml` 8개는
테스트 후 아티팩트만 올렸다 — 이름만 릴리스였다.

Boot 레이어드 추출 + thin-JAR 엔트리포인트로 Dockerfile 을 고치고 릴리스
워크플로에 이미지 빌드·푸시·SBOM·스캔을 넣었다. 로컬 빌드로 검증했다:
레지스트리 content 241MB, 기동 3.7초, uid 1000, 헬스체크 200.
코드만 바뀐 릴리스는 7.68MB 만 재푸시한다(이전이라면 156MB).
CI 는 배포하지 않는다 — 매니페스트와 ArgoCD 는 별도 repo 로 간다.

## 게이트 정리

- gate-matrix 의 개수 고정 해제: `EXPECTED_GATE_COUNT=49` 와 하드코딩된 49개 id
  목록을 지우고 불변식으로 대체(필드·enum, 워크플로/잡 실재, id 중복,
  `release_blocking: true` 는 실제로 release-gate 의 needs 여야 함).
  행을 추가하려면 테스트부터 고쳐야 하던 구조를 풀었다. 커버리지 8/28 → 28/28
- 문서 게이트 4개를 `check` 에서 떼어 `verifyDocumentationContracts` 로 묶고
  stage 2 에 배치. 어겨도 런타임은 멀쩡하므로 개발을 막지 않는다
- `verifyOneTypePerFile`(정규식 Java 파싱, 126파일 미탐) → Checkstyle
  `OneTopLevelClass` + `OuterTypeFilename`. main 위반 0건, test 의 fixture 29건은
  정책을 넓히지 않고 suppressions 에 사유와 함께 명시 제외
- leaf 하나의 `check` 가 끌고 오던 저장소 전역 게이트 18개를 재배치.
  `:domain-core:check` 가 13 태스크 11초로 끝난다
- convention 플러그인 2개 신설(`ca.platform-module`, `ca.grpc-platform-module`),
  플랫폼 모듈 43개에 적용. 손수 짠 Test 태스크 17개를 `strictTestLanes` 로 전환
  (태스크 이름 전부 보존 — CI 가 이름으로 부른다)
- `ca.api-surface` 의 정규식 Java 파서를 javac parse-only 로 교체
  (기존 베이스라인 3개와 바이트 동일 확인)
- 죽은 태스크 5개 삭제, `src/gradle` 1,713 → 1,440줄, 모듈 build.gradle
  3,072 → 2,977줄

## 검사가 검사를 못 하고 있던 것들

- 11개 계약 테스트가 gitignore 된 `src/.env` 를 요구했다. `.gitignore` 자신이
  "examples beside it are the tracked contract, never a real one" 이라고 적어둔
  규칙과 어긋난다. 깨끗한 체크아웃에는 그 파일이 없으므로 CI 에서 돌 수 없었다.
  추적되는 `.env.example` 로 돌린다
- **`.env.local.example` 이 5432 를 가리키는데 compose 는 5433 을 게시한다.**
  이 파일을 복사해 시작하는 신규 개발자는 DB 연결에 실패한다. 이걸 잡으라고
  만든 테스트가 추적 안 되는 파일을 읽어서, 이미 설정이 끝난 머신에서만 돌고
  정작 처음 받는 사람에겐 아무 검사도 안 하고 있었다. 포트를 고치고 테스트를
  추적 파일로 돌렸다
- `MongoModuleBoundaryTest` 의 `DO_NOT_INCLUDE_JARS` 때문에 임포트가 0개가 되어
  규칙 10개가 "failed to check any classes" 로 실패 중이었다. 이 레인에서는
  모듈 자기 클래스가 jar 로 올라온다. `importPackages(ROOT)` 가 이미 서드파티를
  거르므로 옵션은 불필요했다
- `ReleaseManifestTaskExistenceTest` 가 build 파일 텍스트에서 `tasks.register(`
  만 찾아, convention 의 `lane('...')` 로 바뀐 태스크를 미등록으로 오판했다
- `ProfileSeparationContractTest` 는 런처가 주입하는 `src/.env` 가 맞는 대상이라
  그대로 두되, 파일이 없으면 명시적으로 skip 한다 — "안 돌았다" 가 "통과했다"
  로 읽히지 않게

## 검증 (전부 깨끗한 체크아웃에서, 커밋 전에 실행)

`verify-gradle-wrapper.sh` PASS · `verify-gate-matrix.sh` OK(drift 0) ·
워크플로 YAML 전수 파싱 OK · actionlint 지적 0 · `gradlew help` ·
`verifyCleanArchitectureDependencies` · `build-logic test` ·
`:app-bootstrap:test` **1001 tests 실패 0 스킵 5** · `:domain-core:check` ·
`verifyDocumentationContracts`.

## 남은 문제

- 첫 `v*` 태그는 이미지 취약점 스캔에서 실패한다(CRITICAL/HIGH 9건:
  ubuntu 베이스 2, tomcat-embed-core 3, amqp-client 3, httpcore5 2).
  억제를 넣지 않았다 — 릴리스 1회차를 초록으로 만들려고 임계값을 내리면
  게이트가 장식이 된다. 의존성·베이스 갱신이 선행돼야 한다
- `fileserver-v*` / `web-v*` / `websocket-v*` 태그는 이제 아무 run 도 만들지
  않는다(배포 단위가 하나라는 결정에 따른 것)
- main push 마다 무거운 레인 3개가 새로 돈다 — 러너 분이 늘어난다
- `ProfileSeparationContractTest` 가 찾아낸 4개 값(cache command-timeout,
  cache positive-soft-ttl, idempotency provider, rate-limit command-timeout)이
  `.env.example` 과 인라인 기본값 사이에서 갈린다. 런타임 설정 판단이라
  건드리지 않았다

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-16 18:27:15 +09:00
co-authored by Claude Opus 5
parent 1535481794
commit 9bc2e75fe5
132 changed files with 4002 additions and 3083 deletions
+143 -148
View File
@@ -99,89 +99,127 @@ dependencies {
jpaPlatformPerformanceTestRuntimeOnly 'org.postgresql:postgresql'
}
tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' }
def registerPostgreSqlReadinessTest = { String taskName, String testClass ->
tasks.register(taskName, Test) {
group = 'verification'
// 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 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}."
testClassesDirs = sourceSets.postgresqlIntegrationTest.output.classesDirs
classpath = sourceSets.postgresqlIntegrationTest.runtimeClasspath
useJUnitPlatform()
filter {
includeTestsMatching testClass
requires(testClass)
customize = { test ->
test.jvmArgs(
'-Duser.timezone=UTC',
"-Djpa.evidence.postgresql.image=${jpaPostgreSqlEvidenceImage}")
}
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs(
'-Duser.timezone=UTC',
"-Djpa.evidence.postgresql.image=${jpaPostgreSqlEvidenceImage}")
}
}
def postgresqlLifecycleIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlLifecycleIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlLifecycleIntegrationTest')
def postgresqlSecurityBaselineIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlSecurityBaselineIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlSecurityBaselineIntegrationTest')
def postgresqlMigrationIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlMigrationIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlMigrationIntegrationTest')
def postgresqlTransactionIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlTransactionIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlTransactionIntegrationTest')
def postgresqlAggregateIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlAggregateIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlAggregateIntegrationTest')
def postgresqlQueryIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlQueryIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlQueryIntegrationTest')
def postgresqlIdempotencyIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlIdempotencyIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlIdempotencyIntegrationTest')
def postgresqlOutboxStorageIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlOutboxStorageIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlOutboxStorageIntegrationTest')
def postgresqlOutboxPollingIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlOutboxPollingIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlOutboxPollingIntegrationTest')
def postgresqlInboxIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlInboxIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlInboxIntegrationTest')
def postgresqlFileserverMigrationIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlFileserverMigrationIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlFileserverMigrationIntegrationTest')
def postgresqlFileserverMetadataIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlFileserverMetadataIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlFileserverMetadataStoreIntegrationTest')
def postgresqlFileserverReclamationIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlFileserverReclamationIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.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.
def postgresqlNotificationSchemaActivationIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlNotificationSchemaActivationIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlNotificationSchemaActivationIntegrationTest')
// 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')
}
def verifyJpaSqlConstructionSafety = tasks.register('verifyJpaSqlConstructionSafety') {
// 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 gradle/jpa-evidence.gradle 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 = 'Rejects concatenated SQL construction and non-parameterized PostgreSQL timeout configuration.'
File vendorSource = file('src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql')
inputs.dir(vendorSource)
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 = []
vendorSource.eachFileRecurse { File source ->
mainSource.eachFileRecurse { File source ->
if (!source.name.endsWith('.java')) {
return
}
String text = source.getText('UTF-8')
def concatenatedSql = text =~ /(?s)(createNativeQuery|queryForObject|update)\s*\([^;]*"\s*\+/
if (concatenatedSql.find()) {
violations << "${source}: concatenated SQL construction"
}
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"
}
@@ -193,36 +231,13 @@ def verifyJpaSqlConstructionSafety = tasks.register('verifyJpaSqlConstructionSaf
violations.join('\n '))
}
logger.lifecycle(
'verifyJpaSqlConstructionSafety: OK — no concatenated SQL construction and all set_config values are parameterized.')
"verifyJpaSqlConstructionSafety: OK — every set_config value in ${mainSource} binds a parameter.")
}
}
def verifyJpaSecurityFixtures = tasks.register('verifyJpaSecurityFixtures') {
group = 'verification'
description = 'Verifies the no-skip PostgreSQL security fixture covers runtime-role namespace denial.'
File fixture = file(
'src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/readiness/PostgreSqlSecurityBaselineIntegrationTest.java')
inputs.file(fixture)
doLast {
if (!fixture.isFile()) {
throw new GradleException("verifyJpaSecurityFixtures: missing ${fixture}")
}
String text = fixture.getText('UTF-8')
['runtimeRoleCannotCreateInApplicationSchema', 'assertDockerAvailable', '42501'].each {
String required ->
if (!text.contains(required)) {
throw new GradleException(
"verifyJpaSecurityFixtures: ${fixture} is missing '${required}'")
}
}
logger.lifecycle(
'verifyJpaSecurityFixtures: OK — no-skip Docker and runtime-role namespace denial fixtures are present.')
}
}
postgresqlSecurityBaselineIntegrationTest.configure {
dependsOn verifyJpaSqlConstructionSafety
dependsOn verifyJpaSecurityFixtures
tasks.named('postgresqlSecurityBaselineIntegrationTest') {
// Cross-leaf task edge: see the handoff. verifyCleanArchitectureDependencies inspects
// configurations, not the task graph, so this edge is invisible to it.
dependsOn project(':adapter:inbound:web').tasks.named('jpaPersistenceRedactionContractTest')
}
@@ -233,46 +248,33 @@ postgresqlSecurityBaselineIntegrationTest.configure {
// 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.
Closure<Void> registerJpaPlatformLane = { String taskName, String tag, String description ->
tasks.register(taskName, Test) {
group = 'verification'
it.description = description
testClassesDirs = sourceSets.postgresqlIntegrationTest.output.classesDirs
classpath = sourceSets.postgresqlIntegrationTest.runtimeClasspath
useJUnitPlatform {
includeTags tag
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()
}
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs('-Duser.timezone=UTC')
// The Stable matrix selection. An unknown or empty value is an error in
// PostgreSqlVersion.parseSelection rather than an empty run.
systemProperty 'jpa.matrix.versions',
(project.findProperty('jpa.matrix.versions') ?: '16').toString()
}
}
def jpaPlatformContractTest = registerJpaPlatformLane(
'jpaPlatformContractTest',
'jpa-contract',
'Runs the JPA platform contract suite against real PostgreSQL (design §40).')
def jpaPlatformMigrationTest = registerJpaPlatformLane(
'jpaPlatformMigrationTest',
'jpa-migration',
'Runs the Flyway upgrade snapshot scenarios (design §31).')
def jpaPlatformFailureTest = registerJpaPlatformLane(
'jpaPlatformFailureTest',
'jpa-failure',
'Reproduces deadlock, serialization, and commit-ambiguity failures (design §39).')
def jpaPlatformQueryPlanTest = registerJpaPlatformLane(
'jpaPlatformQueryPlanTest',
'jpa-queryplan',
'Asserts query plan structure and planner estimate error (design §33).')
def jpaPlatformSecurityTest = registerJpaPlatformLane(
'jpaPlatformSecurityTest',
'jpa-security',
'Verifies runtime role privileges and search_path safety (design §36).')
// The pool behaviour contract. Named for what it does.
//
// It was `jpaPlatformPerformanceTest`, described as certifying pool and REQUIRES_NEW pressure, and
@@ -289,15 +291,12 @@ def jpaPlatformSecurityTest = registerJpaPlatformLane(
// — 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.
def jpaPlatformPoolContractTest = tasks.register('jpaPlatformPoolContractTest', Test) {
group = 'verification'
description = 'Verifies Hikari pool and REQUIRES_NEW connection behaviour (design §38).'
testClassesDirs = sourceSets.jpaPlatformPerformanceTest.output.classesDirs
classpath = sourceSets.jpaPlatformPerformanceTest.runtimeClasspath
useJUnitPlatform()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs('-Duser.timezone=UTC')
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
@@ -306,12 +305,8 @@ tasks.register('jpaPlatformReleaseGate') {
group = 'verification'
description = 'Runs every JPA platform lane required for a release (design §41).'
dependsOn tasks.named('test')
dependsOn jpaPlatformContractTest
dependsOn jpaPlatformMigrationTest
dependsOn jpaPlatformFailureTest
dependsOn jpaPlatformQueryPlanTest
dependsOn jpaPlatformSecurityTest
dependsOn jpaPlatformPoolContractTest
jpaPlatformLanes.keySet().each { String laneName -> dependsOn tasks.named(laneName) }
dependsOn tasks.named('jpaPlatformPoolContractTest')
}
// The unit lane reads three files that are not Java sources: the release registry and its two