fix(build,ci): 정의만 되고 안 돌던 레인 13개를 배선하고 버전 11개를 카탈로그로

감사 remediation 의 마지막 항목 두 개.

## 아무 데서도 안 돌던 레인

등록 태스크 97개 중 어느 CI 경로로도 도달하지 않는 것이 20개였다.
있는 줄 알았는데 안 도는 상태 — 이번에 고친 `*CleanArchitectureTest` 와 같은
종류의 문제다. 각각 판단해서 처리했고, 삭제는 0건이다.

- stage 2(`integration-main.yml`, push:main + 03:00) 에 잡 3개 신설:
  mongo 컨테이너 레인 6개 / messaging 계약 증거 3개 / app-bootstrap integrationTest.
  컨테이너가 필요한 레인은 PR 에 두지 않는다 — PR 예산은 5분이고, 단계를 나눈
  이유가 이것이다
- stage 3(`release.yml`) 에 `grpc-stable-release-gate` 신설
  (inprocess/netty/fault) + `app-image-release` 의 needs 로 연결
- 수동 확정 3개: `grpcPerformanceTest`, `openapiCheckSnapshot`(드리프트 검사는
  이미 stage 1 의 `check` 안에 있고 이 태스크는 승인 지점),
  `sampleOffCompile`(stage 1 `sampleOffTest` 의 진부분집합).
  전용 레지스트리 대신 루트 README 에 적었다 — `verifyReadmeCommands` 가
  거기 적힌 태스크의 실재를 검증하므로, 문서가 곧 검사 대상이 된다
- 게이트 매트릭스 행 11개 신설. 잡↔행 양방향 대조 결과 68개 잡 전부 행이 있고
  행 없는 잡도, 어디서도 안 도는 잡도 없다

측정이 틀린 4건은 배선하지 않았다 — 이미 도달하고 있었다:
`jpaPlatformReleaseGate`(`jpaReleaseGate dependsOn`),
`generateJpaEvidenceManifests`(`verifyJpaCandidateEvidence` 경유),
`messagingCertificationTest`(`verifyMessagingCertificationEvidence` 경유),
`stageDockerJar`(호출자가 Gradle 이 아니라 `release.yml` 의 `docker build`).

## 버전 카탈로그 이관

카탈로그를 우회해 문자열로 박혀 있던 값 11개를 `gradle/libs.versions.toml` 로
옮겼다. plugin 5개는 `[plugins]` + `alias(...)`, 툴 3개는 `libs.versions.*.get()`.

`grpcVersion`/`protobufVersion`/`awsSdkVersion` 은 이관이 불가하다고 넘어온
항목이었으나, `ext.x` 를 접근자로 남기고 값만 카탈로그에서 읽으면 소비 파일 9개와
`ca.grpc-platform-module.gradle:28` 의 `findProperty` 계약이 그대로이고 해석
결과도 동일하다. **lockfile 재생성 0건.**

`commons-lang3` / `netty` 는 BOM 오버라이드라 그대로 둔다 — 오버라이드하는
이유가 주석과 분리되면 값만 남고 근거가 사라진다.

## 검증 (깨끗한 체크아웃, 커밋 전)

`verify-gate-matrix.sh` → 107 gates, 101 verified, drift 0 ·
`verify-gradle-wrapper.sh` PASS · 워크플로 YAML 21개 파싱 OK ·
`gradlew help` · `verifyCleanArchitectureDependencies` · `build-logic test` ·
`:app-bootstrap:test` **1001 tests 실패 0** · `:domain-core:check` ·
`verifyDocumentationContracts` · `verifyDependencyLocks` · `verifyReadmeCommands`.

## 남은 문제

mongo 6레인 · `bootstrap-integration` · messaging 매니페스트 스키마 검증은
CI 에서 한 번도 돈 적이 없다. Docker 가 없으면 실패하도록 설계돼 있으므로
**첫 main push 와 03:00 run 이 빨간 것이 정상 시나리오**다. 로컬에서 Docker
레인을 돌려보지 않았고, `mongo-container-lanes` 의 timeout 90분은 실측이 아니라
추정치다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-16 18:46:34 +09:00
co-authored by Claude Opus 5
parent 9bc2e75fe5
commit 40ee9f1e83
7 changed files with 378 additions and 29 deletions
+29 -22
View File
@@ -9,12 +9,16 @@ import org.gradle.api.tasks.bundling.Jar
import java.security.MessageDigest
plugins {
id 'org.springframework.boot' version '4.0.8' apply false
id 'io.spring.dependency-management' version '1.1.6' apply false
// Versions come from gradle/libs.versions.toml, not from this block. They used to be five
// literals here and three more inside configure(subprojects) below, which is how the errorprone
// plugin and the errorprone checker jar it puts on the compile classpath ended up pinned in two
// different files with nothing able to show them together.
alias(libs.plugins.spring.boot) apply false
alias(libs.plugins.spring.dependency.management) apply false
// feature-static-analysis-quality-contract — static analysis / code quality baseline.
id 'com.diffplug.spotless' version '8.6.0' apply false // D1 formatter (google-java-format)
id 'com.github.spotbugs' version '6.5.6' apply false // D3 bytecode bug finder (+ D4 FindSecBugs)
id 'net.ltgt.errorprone' version '5.1.0' apply false // D5 compile-time checker
alias(libs.plugins.spotless) apply false // D1 formatter (google-java-format)
alias(libs.plugins.spotbugs) apply false // D3 bytecode bug finder (+ D4 FindSecBugs)
alias(libs.plugins.errorprone) apply false // D5 compile-time checker
// Convention plugins from the included build-logic build. No version: an included build supplies
// the plugin itself, so there is no coordinate to drift. `apply false` puts them on the
// classpath for the configure(subprojects) block below to apply per leaf.
@@ -143,25 +147,28 @@ messagingVerificationSkeletons.each { String taskName, List<String> evidencePath
// io.grpc:grpc-bom + protobuf-bom platforms (not the shared dependencyManagement block below),
// keeping the strict-locking blast radius to the grpc module alone.
//
// NOT the single SSOT for protobuf, and the comment here used to claim it was — it said "this repo
// has no version catalog", which is false: gradle/libs.versions.toml exists and this same file
// reads it (see libs.findsecbugs.plugin / libs.errorprone.core / libs.jmh.core below). Three
// protobuf versions are live today and only two of them are visible in the catalog's comparison:
// ext.protobufVersion 3.25.5 (here) -> adapter/inbound/grpc
// libs protobuf 4.33.2 (catalog) -> adapter/inbound/websocket
// libs protobufLegacy 4.29.3 (catalog) -> messaging/messaging-schema-protobuf
// Moving these three `ext` pins into gradle/libs.versions.toml is the fix; it also rewrites the
// consuming module build.gradle files and regenerates their lockfiles, so it is tracked separately
// rather than done here.
ext.grpcVersion = '1.68.1'
ext.protobufVersion = '3.25.5'
// The numbers now live in gradle/libs.versions.toml. These two lines stay because they are the
// accessor, not the answer: eight leaf build files interpolate `${grpcVersion}` / `${protobufVersion}`
// into coordinate strings, and ca.grpc-platform-module.gradle — a convention plugin in the included
// build — reads `rootProject.findProperty('grpcVersion')` and fails closed when it is absent. Moving
// the number is what the catalog is for; moving the accessor would rewrite those nine files and
// regenerate their lockfiles for no change in what resolves.
//
// All three live protobuf versions are visible side by side in the catalog now, which is the whole
// point — the comment here used to say "this repo has no version catalog", which was already false:
// libs protobufGrpc 3.25.5 -> adapter/inbound/grpc (read through ext.protobufVersion)
// libs protobuf 4.33.2 -> adapter/inbound/websocket
// libs protobufLegacy 4.29.3 -> messaging/messaging-schema-protobuf
ext.grpcVersion = libs.versions.grpc.get()
ext.protobufVersion = libs.versions.protobufGrpc.get()
// Outbound objectstorage adapter (adapter:outbound:objectstorage) — the Spring Boot BOM does NOT
// manage software.amazon.awssdk:* versions. Pinned here so the objectstorage module imports
// software.amazon.awssdk:bom as a platform at MODULE scope (not the shared dependencyManagement
// block below) — mirroring the grpc approach above and keeping the strict-locking blast radius to
// the objectstorage module alone. Same catalog caveat as above.
ext.awsSdkVersion = '2.30.0'
// the objectstorage module alone. The version is in the catalog; this is the accessor three leaves
// interpolate, for the same reason as `ext.grpcVersion` above.
ext.awsSdkVersion = libs.versions.awsSdk.get()
apply from: "${rootProject.projectDir}/gradle/archive-hygiene.gradle"
apply from: "${rootProject.projectDir}/gradle/public-path-snapshot.gradle"
@@ -389,7 +396,7 @@ configure(subprojects.findAll { it.childProjects.isEmpty() }) {
// spotlessCheck (wired into check) verifies. CI must NEVER run spotlessApply.
spotless {
java {
googleJavaFormat('1.35.0')
googleJavaFormat(libs.versions.googleJavaFormat.get())
importOrder()
removeUnusedImports()
}
@@ -397,7 +404,7 @@ configure(subprojects.findAll { it.childProjects.isEmpty() }) {
// D2 — naming + logical ruleset; formatter-owned modules suppressed in the XML.
checkstyle {
toolVersion = '13.5.0'
toolVersion = libs.versions.checkstyle.get()
configFile = rootProject.file('config/checkstyle/checkstyle.xml')
configDirectory = rootProject.file('config/checkstyle')
ignoreFailures = false
@@ -414,7 +421,7 @@ configure(subprojects.findAll { it.childProjects.isEmpty() }) {
// — strictness is a user trade-off; default is functionally valid). Confirmed false positives
// go in config/spotbugs/exclude.xml.
spotbugs {
toolVersion = '4.10.2'
toolVersion = libs.versions.spotbugs.get()
reportLevel = com.github.spotbugs.snom.Confidence.valueOf('HIGH')
excludeFilter = rootProject.file('config/spotbugs/exclude.xml')
}
+57 -3
View File
@@ -1,8 +1,9 @@
# Version catalog — every dependency version this build pins itself.
#
# Versions managed by an imported BOM (Spring Boot, AWS SDK v2) are NOT here: a BOM already owns
# them, and restating a managed version would create a second answer to the same question. What is
# here is the set a module used to spell out inline, once per usage.
# Versions managed by an imported BOM are NOT here: a BOM already owns them, and restating a managed
# version would create a second answer to the same question. The BOM's own coordinate is a different
# question and it is here — `awsSdk` and `grpc` below are the versions of the BOMs themselves, which
# this build does choose. What is here is the set a module used to spell out inline, once per usage.
#
# The reason is visibility, not reproducibility — `gradle.lockfile` already fixes what resolves.
# Before this file, 59 coordinate strings were scattered across 15 build files and nothing could
@@ -14,15 +15,44 @@
# Both are kept as separate aliases rather than unified. Unifying them here would change what
# resolves, in a change whose whole purpose is that it does not; the aliases make the split
# reviewable, and whoever converges them does so deliberately with the lockfiles regenerated.
#
# Tool and plugin versions live here too. They are not dependencies of any module, so they never
# appear in a gradle.lockfile and nothing but this file can show them side by side. Four of them
# (Spring Boot, spotless, spotbugs, errorprone) were spelled out in the root `plugins {}` block, and
# three more (google-java-format, the Checkstyle tool, the SpotBugs tool) inside the shared
# `configure(subprojects)` block; the errorprone *plugin* and the errorprone *checker* it runs were
# pinned in two different files, which is exactly the pair this file exists to put next to each
# other.
#
# What is deliberately NOT here: `ext['commons-lang3.version']` and `ext['netty.version']` in
# src/build.gradle. Neither is a version this build chooses — both are overrides of a version the
# Spring Boot BOM already manages, and each is valid only next to the sentence that says what breaks
# without it (a SpotBugs worker NoClassDefFoundError, and two named CVEs on shared runtime surface).
# Moving the number here would leave the reason behind.
[versions]
approvaltests = "31.0.0"
archunit = "1.3.0"
avro = "1.12.0"
# AWS SDK v2 BOM coordinate. Imported at MODULE scope by adapter:outbound:objectstorage,
# sample-portfolio and app-bootstrap through `ext.awsSdkVersion`, which reads this key.
awsSdk = "2.30.0"
blockhound = "1.0.17.RELEASE"
# Checkstyle *tool* version (the `checkstyle` extension), not a plugin: Checkstyle is a Gradle
# built-in plugin with no coordinate of its own.
checkstyle = "13.5.0"
cloudevents = "4.0.1"
errorprone = "2.49.0"
# The Gradle plugin that runs the checker above. Two different artifacts, two different version
# lines, deliberately adjacent so a bump of one is visibly not a bump of the other.
errorpronePlugin = "5.1.0"
findsecbugs = "1.14.0"
# Spotless's google-java-format step. A formatter version is a repository-wide decision: a bump
# reformats every file.
googleJavaFormat = "1.35.0"
# io.grpc BOM. Read through `ext.grpcVersion`; adapter:inbound:grpc and the five grpc:* leaves
# import it at MODULE scope to keep the strict-locking blast radius off the shared platform.
grpc = "1.68.1"
jmh = "1.37"
jnats = "2.26.2"
jqwik = "1.9.1"
@@ -34,6 +64,10 @@ junitJupiter = "5.11.3"
logstashLogbackEncoder = "8.0"
okhttp = "4.12.0"
protobuf = "4.33.2"
# The third live protobuf version, and the reason this file now shows all three. adapter:inbound:grpc
# imports protobuf-bom at this major to match the gRPC BOM above; it is read through
# `ext.protobufVersion`.
protobufGrpc = "3.25.5"
# The websocket leaf's proto contract is generated against an older runtime and has not been
# requalified; see the coordinate's own comment in that leaf.
protobufLegacy = "4.29.3"
@@ -42,7 +76,13 @@ resilience4j = "2.2.0"
# Two Groovy generations, deliberately not merged. See the header.
spockGroovy4 = "2.3-groovy-4.0"
spockGroovy5 = "2.4-groovy-5.0"
# SpotBugs *tool* version (the `spotbugs` extension). The plugin that runs it is spotbugsPlugin.
spotbugs = "4.10.2"
spotbugsPlugin = "6.5.6"
spotless = "8.6.0"
springBoot = "4.0.8"
springCloudContext = "4.1.4"
springDependencyManagement = "1.1.6"
springDotenv = "4.0.0"
springdoc = "3.0.0"
toxiproxy = "2.1.7"
@@ -84,3 +124,17 @@ spring-dotenv = { module = "me.paulschwarz:spring-dotenv", version.ref = "spring
springdoc-openapi-starter-webmvc-api = { module = "org.springdoc:springdoc-openapi-starter-webmvc-api", version.ref = "springdoc" }
toxiproxy-java = { module = "eu.rekawek.toxiproxy:toxiproxy-java", version.ref = "toxiproxy" }
uuid-creator = { module = "com.github.f4b6a3:uuid-creator", version.ref = "uuidCreator" }
# Plugin coordinates for the root `plugins {}` block. Declared here so that a plugin version and the
# library version it governs cannot drift in two files — errorprone is the live example: the plugin
# (errorpronePlugin) and the checker jar it puts on the compile classpath (errorprone, referenced by
# libs.errorprone.core below) are now one screen apart.
#
# The convention plugins from the included build (`ca.*`) are NOT here and cannot be: an included
# build supplies the plugin itself, so there is no coordinate and no version to pin.
[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" }
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "springDependencyManagement" }
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
spotbugs = { id = "com.github.spotbugs", version.ref = "spotbugsPlugin" }
errorprone = { id = "net.ltgt.errorprone", version.ref = "errorpronePlugin" }