`ObjectStorageAssetBinaryAdapter` bridges Studio's asset port to the object storage port through an `ObjectProvider`, which means it compiles whether or not an implementation is on the classpath. None was: app-bootstrap never depended on `:adapter:outbound:objectstorage`, so the provider was always empty and every upload and delete answered STUDIO_UNAVAILABLE with the message "set ca-skeleton.objectstorage.* to enable" — configuration advice for a missing dependency, which sends the reader looking in the wrong place. The AWS SDK BOM has to be imported here as well. The objectstorage module imports it at module scope on purpose (its comment explains: keep the strict locking blast radius contained), and Spring's dependency management does not propagate to consumers, so assembling the runtime here left s3 and netty-nio-client without versions. The grpc module has the same shape and did not surface it because app-bootstrap only consumes grpc from a test configuration; this is the first runtime consumer of that pattern. Lock state regenerated for the SDK's transitive set. ActuatorSecurityHttpTest.healthEndpointIsPermitAll fails on this branch before this change as well; it is untouched here.
294 lines
17 KiB
Groovy
294 lines
17 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
|
|
}
|
|
}
|
|
|
|
// objectstorage 모듈은 AWS SDK BOM 을 자기 스코프로만 import 한다(그 모듈 주석 참고 —
|
|
// strict locking 의 영향 범위를 좁히려는 의도다). Spring 의 dependency-management 는
|
|
// 소비자에게 전파되지 않으므로, 런타임을 조립하는 이쪽에서 같은 SSOT 로 한 번 더 선언한다.
|
|
// 이것이 없으면 s3/netty-nio-client 가 버전 없이 남아 bootJar 가 해석에 실패한다.
|
|
dependencyManagement {
|
|
imports {
|
|
mavenBom "software.amazon.awssdk:bom:${awsSdkVersion}"
|
|
}
|
|
}
|
|
|
|
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')
|
|
// Studio Asset 바이너리의 실제 저장 구현. 이것이 없으면 ObjectStorageAssetBinaryAdapter 의
|
|
// ObjectProvider<ObjectStoragePort> 가 항상 비어서 업로드·삭제가 STUDIO_UNAVAILABLE 로
|
|
// 거절된다 — 컴파일은 통과하므로 빌드로는 드러나지 않고 런타임에만 나타난다.
|
|
implementation project(':adapter:outbound:objectstorage')
|
|
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'
|
|
|
|
// Redis-backed HTTP session for auth-mode=redis-session (the BFF surface the Studio contract
|
|
// declares: sessionCookie TECHLOG_SESSION + X-CSRF-TOKEN). AuthenticationModeCompositionConfig
|
|
// requires the `redisVersionedSessionRepository` / `springSessionRepositoryFilter` pair once
|
|
// that mode is active; StudioSessionInfrastructureConfig supplies the first, Spring Session's
|
|
// SpringHttpSessionConfiguration the second.
|
|
// OIDC Authorization Code 로그인 자동설정(ClientRegistrationRepository 등). 기존의
|
|
// spring-security-oauth2-client 는 라이브러리만 주고 Boot 자동설정은 스타터가 준다.
|
|
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
|
|
implementation 'org.springframework.session:spring-session-data-redis'
|
|
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
}
|