설계 패키지의 studio-v1.yaml(v3.0.0, 응답 봉투)을 이 저장소에 배선하고 슬라이스 1의 기반을 세운다. 19개 operation 중 getStudioSession과 listStudioCatalog를 구현했다. 계약과 생성 - src/config/openapi/studio-v1.yaml 을 vendor하고 MANIFEST에 출처 커밋을 기록 - openapi-generator로 DTO(model)만 생성한다. generateApis 대신 globalProperties.set(['models': '']) — 그 두 속성은 플러그인 7.18.0에 없다 - useOneOfInterfaces=false. 그 대가로 discriminator union 5종의 Jackson 배선이 깨진다(spec §3.1). 그 5종을 쓰는 7개 operation은 Plan 02에서 전략을 정한 뒤 구현한다 - 생성 코드는 별도 generatedOpenapi sourceSet에 둔다. -Werror가 생성물의 deprecated API 사용을 빌드 실패로 승격하기 때문이다. jar와 test 클래스패스에 별도로 얹는다 오류 계약 - StudioError 23종(계약 ApiError.code와 1:1) + StudioException(ApiErrorCarrier) - StudioExceptionHandler는 techlog 패키지로 범위를 좁힌다. 다른 기능의 오류 응답을 바꾸지 않기 위해서다 - 클라이언트 문구는 레지스트리의 client_safe_message에서 가져오고 예외 메시지는 로그 전용이다(ApiErrorCarrier javadoc의 요구) - 바인딩 예외를 봉투로 옮긴다. 그러지 않으면 bare RFC 7807이 새어 나가 ADR-006을 위반한다 게이트 - TechLogBoundaryArchTest 7종 — spec §4.3의 bounded context 경계. Gradle leaf를 늘릴 수 없어 이 규칙이 경계의 유일한 방어선이다 - StudioErrorRegistryTest — enum ↔ 레지스트리 ↔ 계약 3축 대조, vendor 사본 해시 검증 - StudioContractDriftTest — springdoc 표면이 계약을 벗어나면 실패. @ComponentScan이라 새 컨트롤러가 자동으로 걸린다 - StudioSessionCsrfHeaderProfileContractTest — 배포 가능한 세 프로파일이 계약의 csrf-header-name const로 해소되는지 고정. 이 저장소는 실제 composition root를 테스트에서 부팅할 수 없어 파일 단언으로 그 층을 덮는다 스키마 - V7__techlog_core.sql, 28 테이블. 설계 DDL에서 studio_idempotency(기존 idempotency_record 재사용)와 범위 밖 6종을 제외했다 - 원본의 tech_log 스키마 대신 public을 쓴다. 원본의 SET search_path는 Flyway 세션에만 적용되고 런타임 커넥션 풀은 상속하지 않는다 알려진 제약 - getStudioSession은 세션 인프라(redis-session)가 없어 503 STUDIO_UNAVAILABLE을 반환한다. 계약이 이 operation에 허용하는 유일한 실패 코드다. 가짜 CSRF 토큰으로 200을 만들지 않았다 - 따라서 슬라이스 1의 "프론트 로그인 실동작" 목표는 아직 달성되지 않았다 이 커밋은 AGENTS.md의 human-only 커밋 정책에 대한 저장소 소유자의 명시적 지시로 작성됐다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
269 lines
15 KiB
Groovy
269 lines
15 KiB
Groovy
// Application entry point. Wires the default runtime module set and runs Spring Boot.
|
|
// Optional leaves require an explicit registry allowance plus a composition-root dependency.
|
|
apply plugin: 'org.springframework.boot'
|
|
|
|
apply from: "${rootProject.projectDir}/gradle/strict-qualification-test.gradle"
|
|
|
|
sourceSets {
|
|
sampleOffTest {
|
|
java.srcDirs = sourceSets.test.java.srcDirs
|
|
java.srcDir 'src/sampleOffTest/java'
|
|
resources.srcDirs = sourceSets.test.resources.srcDirs
|
|
compileClasspath += sourceSets.main.output
|
|
runtimeClasspath += sourceSets.main.output
|
|
}
|
|
functionalTest {
|
|
java.srcDir 'src/functionalTest/java'
|
|
resources.srcDir 'src/functionalTest/resources'
|
|
// feature-techlog-studio-backend Task 10 — StudioContractDriftTest reuses
|
|
// RepositoryContractResources (test-sourceSet-owned, see
|
|
// dev.caskeleton.bootstrap.contract.support) for fail-closed repo-root resolution instead
|
|
// of a hand-rolled relative Path.of(..), matching the sibling contract tests' convention.
|
|
compileClasspath += sourceSets.test.output
|
|
runtimeClasspath += sourceSets.test.output
|
|
}
|
|
conditionalTransportTest {
|
|
java.srcDir 'src/conditionalTransportTest/java'
|
|
resources.srcDir 'src/conditionalTransportTest/resources'
|
|
compileClasspath += sourceSets.main.output
|
|
runtimeClasspath += sourceSets.main.output
|
|
}
|
|
}
|
|
|
|
configurations {
|
|
sampleOffTestImplementation.extendsFrom testImplementation
|
|
sampleOffTestCompileOnly.extendsFrom testCompileOnly
|
|
sampleOffTestRuntimeOnly.extendsFrom testRuntimeOnly
|
|
sampleOffTestAnnotationProcessor.extendsFrom testAnnotationProcessor
|
|
}
|
|
|
|
dependencies {
|
|
implementation project(':domain-core')
|
|
implementation project(':application-core')
|
|
implementation project(':adapter:outbound:persistence-jpa')
|
|
implementation project(':adapter:outbound:support')
|
|
implementation project(':adapter:outbound:messaging')
|
|
implementation project(':adapter:outbound:cache-redis')
|
|
implementation project(':adapter:outbound:notification')
|
|
implementation project(':adapter:outbound:fileserver')
|
|
implementation project(':adapter:outbound:httpclient')
|
|
implementation project(':adapter:outbound:identifier')
|
|
implementation project(':adapter:inbound:web')
|
|
implementation project(':shared-contract')
|
|
implementation 'org.springframework.boot:spring-boot-starter'
|
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
|
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
|
implementation 'me.paulschwarz:spring-dotenv:4.0.0'
|
|
// Boot 4 Flyway API/autoconfiguration: the composition root drives startup migration
|
|
// (MigrationStartupConfig). See README.
|
|
implementation 'org.springframework.boot:spring-boot-flyway'
|
|
// Flyway API: MigrationStartupRunner directly invokes Flyway. Kept explicit for readability.
|
|
implementation 'org.flywaydb:flyway-core'
|
|
|
|
// Micrometer core for OutboxMetrics meters (no-op without a MeterRegistry). See README.
|
|
implementation 'io.micrometer:micrometer-core'
|
|
|
|
// OTel/Micrometer tracer runtime (exporter stays off until an OTLP endpoint is set). See README.
|
|
implementation 'io.micrometer:micrometer-tracing-bridge-otel'
|
|
implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
|
|
|
|
// Actuator + Prometheus registry (health/info/prometheus/loggers endpoints).
|
|
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
|
// The composition root wires the OAuth2 credential provider for the HTTP Client Platform,
|
|
// so it must see OAuth2AuthorizedClientManager. The adapter keeps the dependency internal.
|
|
implementation 'org.springframework.security:spring-security-oauth2-client'
|
|
implementation 'io.micrometer:micrometer-registry-prometheus'
|
|
// Security types for ManagementSecurityConfig (not reachable via adapter-web's implementation dep). See README.
|
|
implementation 'org.springframework.boot:spring-boot-starter-security'
|
|
|
|
// test-only: ArchUnit needs actuator types to verify the health-shape guardrail. See README.
|
|
testImplementation 'org.springframework.boot:spring-boot-starter-actuator'
|
|
// test-only: @WithMockUser for the actuator security authorization tests. See README.
|
|
testImplementation 'org.springframework.security:spring-security-test'
|
|
|
|
// test-only: Testcontainers PostgreSQL for the outbox contract tests.
|
|
testImplementation 'org.testcontainers:testcontainers-postgresql'
|
|
testImplementation 'org.testcontainers:testcontainers-junit-jupiter'
|
|
// test-only: Boot 4 split JPA slice annotations into dedicated test modules.
|
|
testImplementation 'org.springframework.boot:spring-boot-data-jpa-test'
|
|
// test-only: PG JDBC driver (the vendor module's runtimeOnly does not leak here). See README.
|
|
testRuntimeOnly 'org.postgresql:postgresql'
|
|
// test-only: JPA/Hikari for the outbox tests' minimal context (not leaked from rdbms). See README.
|
|
testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
|
testImplementation 'com.zaxxer:HikariCP'
|
|
|
|
// test-only: JdbcLockRegistry for the distributed-lock contract test (two simulated instances). See README.
|
|
testImplementation 'org.springframework.integration:spring-integration-jdbc'
|
|
|
|
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
|
|
// test-only: jackson-databind for the deserialization-policy boundary test. See README.
|
|
testImplementation 'org.springframework.boot:spring-boot-starter-json'
|
|
// test-only: parses checked-in Messaging capability registries for the fail-closed drift gate.
|
|
testImplementation 'org.yaml:snakeyaml'
|
|
// test-only: ArchUnit violation fixtures intentionally import forbidden types. See README.
|
|
testCompileOnly 'org.springframework:spring-tx'
|
|
// test-only: streaming/websocket violation fixtures import these forbidden packages. See README.
|
|
testCompileOnly 'org.springframework:spring-webmvc' // SseEmitter, ResponseBodyEmitter, StreamingResponseBody
|
|
testCompileOnly 'org.springframework:spring-websocket' // org.springframework.web.socket..
|
|
testCompileOnly 'jakarta.websocket:jakarta.websocket-api' // jakarta.websocket..
|
|
// test-only: transport-free domain-event fixtures import these forbidden broker/wire packages. See README.
|
|
testCompileOnly 'org.apache.kafka:kafka-clients' // org.apache.kafka..
|
|
testCompileOnly 'jakarta.ws.rs:jakarta.ws.rs-api' // jakarta.ws.rs..
|
|
// test-only: @RefreshScope for the no-refresh-scope violation fixture (version pinned; not in the BOM). See README.
|
|
testCompileOnly 'org.springframework.cloud:spring-cloud-context:4.1.4' // org.springframework.cloud.context..
|
|
|
|
// JSON log encoder; implementation (not runtimeOnly) because StartupFailures uses StructuredArguments at compile time. See README.
|
|
implementation 'net.logstash.logback:logstash-logback-encoder:8.0'
|
|
|
|
// test-only: ApprovalTests JSON snapshot verification for the contract-verification suite
|
|
// (feature-contract-verification-test-suite D7 — envelope/error shape snapshots). See README.
|
|
testImplementation 'com.approvaltests:approvaltests:31.0.0'
|
|
// test-only: JUnit Platform Test Kit — proves optional-adapter tests report SKIPPED (never FAILED)
|
|
// when their enable-flag env var is unset (feature-contract-verification-test-suite D3, Claims #7). See README.
|
|
testImplementation 'org.junit.platform:junit-platform-testkit'
|
|
// functional-test-only: executes isolated Gradle fixtures without placing Gradle's SLF4J
|
|
// provider on the ordinary test runtime classpath.
|
|
functionalTestImplementation gradleTestKit()
|
|
functionalTestImplementation 'org.junit.jupiter:junit-jupiter'
|
|
functionalTestImplementation 'org.assertj:assertj-core'
|
|
functionalTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
|
// feature-techlog-studio-backend Task 10 — StudioContractDriftTest boots a minimal Studio web
|
|
// slice (real StudioSessionController/StudioCatalogController, no persistence/messaging/cache)
|
|
// to diff springdoc's published /api/v1/studio/** surface against studio-v1.yaml. Only the two
|
|
// modules the slice actually needs — deliberately not the full app-bootstrap runtime graph, so
|
|
// no DataSource/Flyway/Redis auto-configuration is even on this classpath to exclude.
|
|
functionalTestImplementation project(':adapter:inbound:web')
|
|
functionalTestImplementation project(':application-core')
|
|
// test-only: @SpringBootTest/MockMvc/@AutoConfigureMockMvc — mirrors the root build.gradle
|
|
// subprojects{} pair every non-core module already gets on its ordinary `test` sourceSet.
|
|
functionalTestImplementation 'org.springframework.boot:spring-boot-starter-test'
|
|
functionalTestImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
|
|
// test-only: classic Jackson (2.x) ObjectMapper/JsonNode to read /v3/api-docs and convert the
|
|
// SnakeYaml-parsed contract into a comparable tree. adapter-inbound-web/application-core pull
|
|
// this in only as a project-dependency `implementation` (hidden from a consumer's
|
|
// compileClasspath by Gradle's api/implementation split), so it must be declared directly here
|
|
// — mirrors the existing `testImplementation 'org.springframework.boot:spring-boot-starter-json'`
|
|
// pattern below for app-bootstrap's own `test` sourceSet.
|
|
functionalTestImplementation 'com.fasterxml.jackson.core:jackson-databind'
|
|
// test-only: org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration (excluded
|
|
// below) is part of spring-boot-security, only pulled in transitively by spring-boot-starter-security
|
|
// — same api/implementation-hiding reason as jackson-databind above. app-bootstrap declares this
|
|
// on `implementation` for its own main/test sourceSets, which functionalTest does not extend.
|
|
functionalTestImplementation 'org.springframework.boot:spring-boot-starter-security'
|
|
// test-only: parses config/openapi/studio-v1.yaml with the same library StudioErrorRegistryTest
|
|
// already uses for docs/registries/error-codes.yaml — avoids adding jackson-dataformat-yaml
|
|
// (present only on runtimeClasspath repo-wide, transitively via springdoc, not compileClasspath).
|
|
functionalTestImplementation 'org.yaml:snakeyaml'
|
|
// Explicit qualification-only composition. These projects remain absent from main
|
|
// api/implementation/compileOnly/runtimeOnly and therefore from both shipped runtime graphs.
|
|
conditionalTransportTestImplementation project(':adapter:inbound:graphql')
|
|
conditionalTransportTestImplementation project(':adapter:inbound:grpc')
|
|
conditionalTransportTestImplementation project(':adapter:inbound:websocket')
|
|
conditionalTransportTestImplementation 'org.junit.jupiter:junit-jupiter'
|
|
conditionalTransportTestImplementation 'org.assertj:assertj-core'
|
|
conditionalTransportTestImplementation 'org.yaml:snakeyaml'
|
|
conditionalTransportTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
|
}
|
|
|
|
def repositoryRootForContractTests = rootProject.projectDir.parentFile.absolutePath
|
|
def contractRegistriesDirectory = rootProject.projectDir.parentFile.toPath()
|
|
.resolve('docs/registries').toFile()
|
|
tasks.withType(Test).configureEach {
|
|
systemProperty 'ca.repository.root', repositoryRootForContractTests
|
|
}
|
|
|
|
// Pin UTC for the TEST JVM so timestamp tests are host-locale-independent (production UTC owned elsewhere). See README.
|
|
tasks.named('test') {
|
|
inputs.dir(contractRegistriesDirectory)
|
|
.withPathSensitivity(PathSensitivity.RELATIVE)
|
|
jvmArgs '-Duser.timezone=UTC'
|
|
}
|
|
|
|
tasks.register('sampleOffCompile') {
|
|
group = 'verification'
|
|
description = 'Compiles the complete app-bootstrap test corpus without sample-portfolio.'
|
|
dependsOn tasks.named(sourceSets.sampleOffTest.classesTaskName)
|
|
}
|
|
|
|
def sampleOffQualification = registerStrictQualificationTest(
|
|
name: 'sampleOffTest',
|
|
sourceSet: sourceSets.sampleOffTest,
|
|
requiredClasses: [
|
|
'dev.caskeleton.bootstrap.contract.SampleOffClasspathContractTest'
|
|
],
|
|
description: 'Runs the exact no-skip sample-off classpath qualification.')
|
|
sampleOffQualification.configure {
|
|
shouldRunAfter tasks.named('test')
|
|
systemProperty 'ca.sample.mode', 'off'
|
|
}
|
|
|
|
tasks.register('functionalTest', Test) {
|
|
group = 'verification'
|
|
description = 'Runs isolated Gradle TestKit contracts for repository build behavior, plus the ' +
|
|
'feature-techlog-studio-backend Studio contract drift gate.'
|
|
testClassesDirs = sourceSets.functionalTest.output.classesDirs
|
|
classpath = sourceSets.functionalTest.runtimeClasspath
|
|
useJUnitPlatform()
|
|
failOnNoDiscoveredTests = true
|
|
shouldRunAfter tasks.named('test')
|
|
jvmArgs '-Duser.timezone=UTC'
|
|
// feature-techlog-studio-backend Task 10 — StudioContractDriftTest boots a real (minimal)
|
|
// Spring Boot context that needs Logback, on the same classpath as gradleTestKit() (whose own
|
|
// SLF4J provider — org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext —
|
|
// wins classpath scanning over the real one). Spring Boot's LogbackLoggingSystem then finds
|
|
// Logback's jar present but the bound ILoggerFactory is Gradle's fake context, and fails fast
|
|
// with IllegalStateException before the context even starts. LoggingSystem=none skips Boot's
|
|
// logging bootstrap entirely — this task doesn't assert on log output, so there is nothing lost.
|
|
systemProperty 'org.springframework.boot.logging.LoggingSystem', 'none'
|
|
}
|
|
|
|
def conditionalTransportCompositionQualification = registerStrictQualificationTest(
|
|
name: 'conditionalTransportCompositionTest',
|
|
sourceSet: sourceSets.conditionalTransportTest,
|
|
requiredClasses: [
|
|
'dev.caskeleton.bootstrap.transport.ConditionalTransportCompositionContractTest'
|
|
],
|
|
description: 'Proves the explicit test-only GraphQL/gRPC/WebSocket opt-in classpath.')
|
|
conditionalTransportCompositionQualification.configure {
|
|
shouldRunAfter tasks.named('test')
|
|
}
|
|
|
|
tasks.named('check') {
|
|
dependsOn tasks.named('functionalTest')
|
|
}
|
|
|
|
bootJar {
|
|
mainClass = 'dev.caskeleton.bootstrap.CaSkeletonApplication'
|
|
}
|
|
|
|
tasks.register('stageDockerJar', Sync) {
|
|
dependsOn tasks.named('bootJar')
|
|
from(tasks.named('bootJar').flatMap { it.archiveFile })
|
|
into(layout.buildDirectory.dir('docker'))
|
|
rename { 'application.jar' }
|
|
}
|
|
|
|
// Run from the repo's src/ root and inject src/.env into the Java process environment.
|
|
// Boot 4 initializes profiles/logging before spring-dotenv can reliably contribute .env values.
|
|
bootRun {
|
|
workingDir = rootProject.projectDir
|
|
doFirst {
|
|
File envFile = rootProject.file('.env')
|
|
if (!envFile.isFile()) {
|
|
return
|
|
}
|
|
envFile.eachLine { raw ->
|
|
String line = raw.trim()
|
|
if (line.isEmpty() || line.startsWith('#') || !line.contains('=')) {
|
|
return
|
|
}
|
|
int separator = line.indexOf('=')
|
|
String key = line.substring(0, separator).trim()
|
|
String value = line.substring(separator + 1).trim()
|
|
if (!key.isEmpty() && System.getenv(key) == null && !environment.containsKey(key)) {
|
|
environment key, value
|
|
}
|
|
}
|
|
}
|
|
}
|