refactor(build,ci): 현재 상태 검증을 걷어내고 불변조건만 남기는 검증 표면 축소
외부 리뷰("현재 상태를 유지하기 위한 검증이 너무 많고, 그 검증 자체를
다시 검증하는 구조까지 생겼다")를 설계 문서로 정리하고 코드로 반영한다.
설계·판단 근거는 docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md.
삭제
- .github/ci-gate-matrix.yml(1,025줄) + verify-gate-matrix.sh(568줄):
Gradle task graph와 workflow graph에 이미 있는 정보의 3중 복제
- verify-gradle-wrapper.sh(799줄): workflow 바이트 해시 잠금.
wrapper 검증은 gradle/actions/wrapper-validation(full SHA 핀)에 위임
- DeveloperExperienceContractTest 등의 CI YAML mutation 테스트:
애플리케이션 test suite가 GitHub Actions YAML 파서를 검증하던 계층 역전
- 문서 drift 파서: verifyReadmeCommands, verifyRunbookReferences,
verifyDocumentedLeafCount, verifyTestSourceSetRegistry
- 빈 레지스트리를 지키던 커스텀 YAML 파서: verifyTrivyignore,
verifyQuarantineSunset, flaky-quarantine.yaml
- verifyConfigurationPropertiesProcessor, verifyOneTypePerFile:
각각 ca.spring-config convention과 Checkstyle OneTopLevelClass가 대체
- 정상 입력으로도 성공할 수 없던 messaging always-fail task
- ModuleRegistry의 JSON 필드 집합 정확 일치, sample-portfolio negative guard
이동
- java/quality/spring 공통 설정을 configure(subprojects) 블록에서
ca.java-conventions / ca.quality-conventions / ca.java-library /
ca.spring-library convention plugin으로
- 아키텍처 검증을 ca.architecture로, JPA·messaging qualification을
gradle/qualification/ 아래로, verifyEnvKeys를 :app-bootstrap 소유로
완화
- Git revision은 releaseCheck·아카이브 생성에서만 요구. 일반 빌드는 SNAPSHOT
- SpotBugs/FindSecBugs는 로컬 check에서 빼고 qualityCheck 레인으로
task 계층
- leaf check는 그 leaf만. architectureCheck / qualityCheck /
configContractCheck / integrationCheck / ci / releaseCheck로 이름 분리
CI
- _reusable-gradle.yml 신규. checkout + wrapper validation + JDK/캐시 공통화
- fileserver-release.yml -> fileserver-certification.yml (CD가 아니라 certification)
- GitHub Actions = CI + artifact, Argo CD = CD 경계를 docs/ci-cd/boundary.md로 고정
순증감 +3,274 / -7,483.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d00c76241c
commit
ef947e5bb0
@@ -3,6 +3,9 @@
|
||||
// The design models the SDK as separate Gradle modules. This repository's fail-closed module
|
||||
// registry outranks that layout, so the module boundaries are packages under
|
||||
// dev.caskeleton.adapter.outbound.cache.redis.sdk and RedisSdkModuleBoundaryTest enforces them.
|
||||
apply plugin: 'ca.spring-library'
|
||||
apply plugin: 'ca.spring-config'
|
||||
|
||||
dependencies {
|
||||
// Registered edges the semantic port adapters need. The SDK's *main* source imports nothing from
|
||||
// them today — the semantic cache/session/idempotency/rate-limit adapters that did were removed
|
||||
@@ -35,7 +38,6 @@ dependencies {
|
||||
// broken compilation of the SDK's own published API, so it is declared directly.
|
||||
implementation 'io.projectreactor:reactor-core'
|
||||
implementation 'org.slf4j:slf4j-api'
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
|
||||
// Deliberately absent:
|
||||
// org.springframework.data:spring-data-redis — the SDK owns its own typed API and command
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// R2 provider is local-persistent; shared-mounted/NFS and SFTP are not stand-ins or implemented
|
||||
// capabilities. Its IO path uses only the JDK. Spring Boot autoconfigure supplies explicit,
|
||||
// disabled-default R1/R2 composition and SLF4J remains the diagnostics API.
|
||||
apply plugin: 'ca.spring-library'
|
||||
apply plugin: 'ca.spring-config'
|
||||
|
||||
description = 'Outbound adapter: file publication (R1 CSV export, R2 local-persistent) plus the ' + \
|
||||
'local filesystem content platform behind the HTTP Fileserver'
|
||||
|
||||
@@ -18,5 +21,4 @@ dependencies {
|
||||
// (BOOT-017). The composition root still decides whether to wire it — that part is assembly.
|
||||
implementation 'io.micrometer:micrometer-core'
|
||||
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// Shared test code as a Gradle test-fixtures variant — ADR-BUILD-001.
|
||||
apply plugin: 'ca.spring-library'
|
||||
|
||||
apply plugin: 'java-test-fixtures'
|
||||
|
||||
// Outbound HTTP Client Platform leaf — see
|
||||
|
||||
+4
-5
@@ -17,16 +17,15 @@ import com.tngtech.archunit.core.importer.ImportOption;
|
||||
* depends on the fixtures, which the fixtures themselves trivially do.
|
||||
*
|
||||
* <p>The path moved when this leaf adopted {@code java-test-fixtures} (ADR-BUILD-001) and the rule
|
||||
* caught it: the exclusion still named {@code /classes/java/testkit/}, so the fixtures were suddenly
|
||||
* production and the boundary test failed on the first run. That is the check working — an import
|
||||
* filter that silently stops matching is a rule asserted against the wrong corpus.
|
||||
* caught it: the exclusion still named {@code /classes/java/testkit/}, so the fixtures were
|
||||
* suddenly production and the boundary test failed on the first run. That is the check working — an
|
||||
* import filter that silently stops matching is a rule asserted against the wrong corpus.
|
||||
*/
|
||||
public final class PlatformClasses {
|
||||
|
||||
private static final ImportOption NOT_THE_FIXTURES_SOURCE_SET =
|
||||
location ->
|
||||
!location.contains("/classes/java/testFixtures/")
|
||||
&& !location.contains("test-fixtures");
|
||||
!location.contains("/classes/java/testFixtures/") && !location.contains("test-fixtures");
|
||||
|
||||
private static final JavaClasses PRODUCTION =
|
||||
new ClassFileImporter()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// groovy: compiles the UuidCodec Spock specs under src/test/groovy. See README.
|
||||
plugins {
|
||||
id 'ca.spring-library'
|
||||
// groovy: compiles the UuidCodec Spock specs under src/test/groovy. See README.
|
||||
id 'groovy'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
|
||||
apply plugin: 'ca.spring-library'
|
||||
apply plugin: 'ca.spring-config'
|
||||
|
||||
dependencies {
|
||||
implementation project(':application-core')
|
||||
implementation project(':shared-contract')
|
||||
@@ -10,7 +13,6 @@ dependencies {
|
||||
exclude group: 'com.fasterxml.jackson.dataformat', module: 'jackson-dataformat-yaml'
|
||||
}
|
||||
implementation 'org.slf4j:slf4j-api'
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
}
|
||||
tasks.withType(Test).configureEach {
|
||||
systemProperty 'messaging.commonEvidenceSchema',
|
||||
@@ -24,6 +26,9 @@ configurations.configureEach {
|
||||
exclude group: 'org.snakeyaml', module: 'snakeyaml-engine'
|
||||
}
|
||||
|
||||
import org.gradle.api.artifacts.MinimalExternalModuleDependency
|
||||
import org.gradle.api.artifacts.ModuleIdentifier
|
||||
|
||||
tasks.register('verifyJsonSchemaRuntimeGraph') {
|
||||
group = 'verification'
|
||||
description = 'Verifies the closed Jackson 3 / NetworkNT graph contains no YAML or Jackson 2 runtime.'
|
||||
@@ -46,16 +51,39 @@ tasks.register('verifyJsonSchemaRuntimeGraph') {
|
||||
throw new GradleException(
|
||||
"Messaging JSON runtime contains forbidden Jackson 2/YAML modules: ${forbidden}")
|
||||
}
|
||||
// The catalog accessors are Providers of a dependency, not coordinate strings.
|
||||
//
|
||||
// This block read `.each { String required -> ... }` over them, so Groovy tried to call the
|
||||
// closure with a TransformBackedProvider and the task threw
|
||||
// `No signature of method: doCall() ... (TransformBackedProvider)` before comparing
|
||||
// anything. It had never passed: the forbidden-module half above ran first and found
|
||||
// nothing, and then this half failed on its own argument types. `check` reached it, but
|
||||
// only ever after some earlier failure had already stopped the build.
|
||||
[
|
||||
libs.json.schema.validator,
|
||||
libs.jackson3.core,
|
||||
libs.jackson3.databind
|
||||
].each { String required ->
|
||||
if (!modules.contains(required)) {
|
||||
].collect { Provider<MinimalExternalModuleDependency> accessor ->
|
||||
ModuleIdentifier module = accessor.get().module
|
||||
"${module.group}:${module.name}".toString()
|
||||
}.each { String requiredModule ->
|
||||
if (!modules.any { it.startsWith(requiredModule + ':') }) {
|
||||
throw new GradleException(
|
||||
"Messaging JSON runtime is missing required locked module ${required}")
|
||||
"Messaging JSON runtime is missing required module ${requiredModule}; " +
|
||||
"resolved runtime modules are ${modules.toSorted()}")
|
||||
}
|
||||
}
|
||||
// Module, not module-and-version.
|
||||
//
|
||||
// The first working version of this compared the full `group:name:version` string from the
|
||||
// catalog against the resolved graph, and the gate failed on its first real run: the
|
||||
// catalog pins tools.jackson.core:jackson-core 3.0.2 while the Jackson 3 BOM resolves
|
||||
// 3.1.5. That is not drift — it is dependency management doing its job, and this task is
|
||||
// not the place that decides versions (gradle.lockfile is). What this task owns is the
|
||||
// shape of the runtime graph: the Jackson 3 + NetworkNT engine present, no YAML engine, no
|
||||
// Jackson 2 databind. Pinning the version here would have made a BOM patch bump a build
|
||||
// failure in a leaf that never asked for the version.
|
||||
|
||||
// Jackson 3 intentionally retains the 2.x-namespace annotations artifact. It is not a
|
||||
// Jackson 2 databind/runtime engine and is part of the official Jackson 3 BOM graph.
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
apply plugin: 'ca.spring-library'
|
||||
apply plugin: 'ca.spring-config'
|
||||
|
||||
dependencies {
|
||||
implementation project(':application-core')
|
||||
implementation project(':shared-contract')
|
||||
@@ -44,7 +47,6 @@ dependencies {
|
||||
exclude group: 'com.fasterxml.jackson.dataformat', module: 'jackson-dataformat-yaml'
|
||||
}
|
||||
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
|
||||
testImplementation 'io.projectreactor:reactor-test'
|
||||
}
|
||||
@@ -58,3 +60,22 @@ dependencyPolicy {
|
||||
absent 'tools.jackson.dataformat:jackson-dataformat-yaml',
|
||||
because: 'the Jackson 3 coordinate of the same parser, excluded for the same reason'
|
||||
}
|
||||
|
||||
// The three notification gates run with the leaf they are about.
|
||||
//
|
||||
// All three existed and passed for months while nothing ran them, and the cost was measurable the
|
||||
// first time they were: twenty-nine environment variables bound in application.yml were absent from
|
||||
// the configuration reference — the whole SMTP relay and all eight key-material purposes — and
|
||||
// thirteen public types had entered the notification API surface without the reviewed baseline
|
||||
// recording any of them.
|
||||
//
|
||||
// They ran on all 62 leaves once, which reached them 62 times and told the developer who changed
|
||||
// :domain-core about the notification surface. An API surface baseline and a configuration reference
|
||||
// for one adapter are that adapter's contract, so they belong to the command a developer runs after
|
||||
// changing it — and the wiring is declared here, in that leaf, rather than reached into from the
|
||||
// root. `.github/workflows/notification-platform.yml` also invokes all three by name.
|
||||
tasks.named('check') {
|
||||
dependsOn rootProject.tasks.named('verifyNotificationApiSurface')
|
||||
dependsOn rootProject.tasks.named('verifyNotificationConfiguration')
|
||||
dependsOn rootProject.tasks.named('verifyNotificationEvidence')
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
// This sentence used to end "and this repo has no version catalog". That is false, and this file
|
||||
// disproves it twice below with `libs.archunit.junit5` and `libs.jqwik`. Module scope is a locking
|
||||
// decision; the catalog just has no awssdk entry.
|
||||
apply plugin: 'ca.spring-library'
|
||||
apply plugin: 'ca.spring-config'
|
||||
|
||||
description = 'Outbound adapter: object storage (S3/MinIO + local filesystem)'
|
||||
|
||||
|
||||
@@ -43,7 +46,6 @@ dependencies {
|
||||
implementation 'software.amazon.awssdk:s3'
|
||||
implementation 'software.amazon.awssdk:netty-nio-client'
|
||||
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
|
||||
// test-only: Testcontainers MinIO integration test for the S3 backend. Uses the core
|
||||
// GenericContainer (no dedicated module) so the S3 round-trip runs against a real MinIO when
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// Shared test code as a Gradle test-fixtures variant — ADR-BUILD-001.
|
||||
apply plugin: 'ca.spring-library'
|
||||
apply plugin: 'ca.spring-config'
|
||||
|
||||
apply plugin: 'java-test-fixtures'
|
||||
|
||||
// JPA persistence adapter — merged RDBMS base + PostgreSQL vendor module.
|
||||
@@ -48,7 +51,6 @@ dependencies {
|
||||
// driver above. Not `developmentOnly`: local is a deployable profile of this artifact, and the
|
||||
// vendor selector, not the packaging, decides which driver a deployment loads.
|
||||
runtimeOnly 'com.h2database:h2'
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
|
||||
// JPA platform observability (design §37). Micrometer's observation API already arrives with
|
||||
// Spring; the meter registry does not, and the platform's transaction/query/retry metrics need
|
||||
@@ -355,3 +357,9 @@ apiSurface {
|
||||
|
||||
apply from: rootProject.file('gradle/jpa-evidence.gradle')
|
||||
|
||||
// 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
|
||||
// gradle/qualification/jpa-qualification.gradle, which the root applies.
|
||||
tasks.named('check') {
|
||||
dependsOn rootProject.tasks.named('verifyJpaReadinessRegistry')
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Shared test code as a Gradle test-fixtures variant — ADR-BUILD-001. Applied here rather than
|
||||
// from the root, the way the GraphQL leaf does: only a leaf that has shared test code needs it.
|
||||
apply plugin: 'ca.spring-library'
|
||||
apply plugin: 'ca.spring-config'
|
||||
|
||||
apply plugin: 'java-test-fixtures'
|
||||
|
||||
// MongoDB Document Persistence Platform leaf — see
|
||||
@@ -30,7 +33,6 @@ dependencies {
|
||||
implementation 'io.micrometer:micrometer-core'
|
||||
implementation 'org.slf4j:slf4j-api'
|
||||
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
|
||||
// The design's module dependency table is enforced as package rules, so ArchUnit is what keeps
|
||||
// "packages instead of modules" from meaning "no boundary at all".
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Shared base for outbound integration adapters: correlation, fail-open dependency
|
||||
// logging, and the @Configuration seam. Depended on by messaging/cache/notification/httpclient.
|
||||
apply plugin: 'ca.spring-library'
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-autoconfigure'
|
||||
implementation 'org.slf4j:slf4j-api'
|
||||
|
||||
Reference in New Issue
Block a user