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
+108 -2
View File
@@ -487,6 +487,82 @@ gates:
job: documentation-contracts
execution: job
#
# The six Docker-backed MongoDB lanes. Registered in the leaf, excluded from `test` by tag, absent
# from `check`, and named by no workflow until now — scripts/verify-mongodb-platform.sh was the
# only caller and nothing in .github runs it. One row per lane because each is its own single-line
# command, which is what makes `execution: explicit` checkable.
#
# release_blocking: false is the honest value, not a demotion: stage 2 runs after the merge, so no
# release gate can wait on it. Promoting these to stage 1 would put a replica set, a three-node
# failover and a Toxiproxy on the pull-request budget.
- id: mongo-replica-set-lane
release_blocking: false
mechanism: gradle-custom-task
ref: mongoReplicaSetTest
workflow: integration-main.yml
job: mongo-container-lanes
execution: explicit
- id: mongo-failover-lane
release_blocking: false
mechanism: gradle-custom-task
ref: mongoFailoverTest
workflow: integration-main.yml
job: mongo-container-lanes
execution: explicit
- id: mongo-migration-lane
release_blocking: false
mechanism: gradle-custom-task
ref: mongoMigrationTest
workflow: integration-main.yml
job: mongo-container-lanes
execution: explicit
- id: mongo-compatibility-lane
release_blocking: false
mechanism: gradle-custom-task
ref: mongoCompatibilityTest
workflow: integration-main.yml
job: mongo-container-lanes
execution: explicit
- id: mongo-security-integration-lane
release_blocking: false
mechanism: gradle-custom-task
ref: mongoSecurityIntegrationTest
workflow: integration-main.yml
job: mongo-container-lanes
execution: explicit
- id: mongo-performance-lane
release_blocking: false
mechanism: gradle-custom-task
ref: mongoPerformanceTest
workflow: integration-main.yml
job: mongo-container-lanes
execution: explicit
#
# The messaging contract evidence DAG. One row, because one job is one control and the control is
# the aggregate: `verifyMessagingContracts` reaches five strict qualification tasks, and through
# them `prepareMessagingContractEvidence`, plus both manifest schema validators
# (validateMessagingJsonSchemaV1EvidenceManifestSchema by dependsOn,
# validateMessagingContractsEvidenceManifestSchema by finalizedBy). Strict qualification tasks are
# registered outside `check` by design, so before this job none of the seven executed anywhere.
- id: messaging-contract-evidence
release_blocking: false
mechanism: gradle-custom-task
ref: verifyMessagingContracts
workflow: integration-main.yml
job: messaging-contract-evidence
execution: explicit
#
# app-bootstrap's Testcontainers lane. Its own source set exists so that `:app-bootstrap:test`
# needs no Docker daemon; the unclosed consequence was that a source set outside `test` is also
# outside `check`, so these contracts compiled everywhere and ran nowhere.
- id: bootstrap-integration-contracts
release_blocking: false
mechanism: gradle-custom-task
ref: integrationTest
workflow: integration-main.yml
job: bootstrap-integration
execution: explicit
#
# notification-platform.yml — `pr` is path-filtered and also runs on push to main; nightly-chaos
# is schedule/dispatch only. The release-blocking notification controls are the verifyNotification*
# rows above, which run inside check on every pull request.
@@ -768,7 +844,7 @@ gates:
job: httpclient-release-gate
execution: explicit
#
# Tag-triggered (`v*`, `web-v*`). The Stable web release gate.
# Tag-triggered (`v*`). The Stable web release gate.
- id: web-stable-release-gate
release_blocking: true
mechanism: workflow-job
@@ -777,7 +853,7 @@ gates:
job: web-stable-release-gate
execution: job
#
# Tag-triggered (`v*`, `websocket-v*`). The Stable websocket release gate.
# Tag-triggered (`v*`). The Stable websocket release gate.
- id: websocket-stable-release-gate
release_blocking: true
mechanism: workflow-job
@@ -786,6 +862,36 @@ gates:
job: websocket-stable-release-gate
execution: job
#
# Tag-triggered (`v*`). The three gRPC certification lanes, one row each because each runs as its
# own single-line command and `execution: explicit` is only verifiable that way.
#
# These were registered lanes that no workflow named. Their tests are not new coverage — the
# `grpc-inprocess`, `grpc-netty` and `grpc-fault` tags run inside `:grpc:grpc-testkit:test`, which
# is inside `check` — so what the lanes add is the fail-on-nothing-discovered guard that a tag
# filter inside `test` cannot give you. Same argument as web-advanced and websocket-advanced above,
# and the same placement.
- id: grpc-inprocess-contract-lane
release_blocking: true
mechanism: gradle-custom-task
ref: grpcInProcessContractTest
workflow: release.yml
job: grpc-stable-release-gate
execution: explicit
- id: grpc-netty-transport-lane
release_blocking: true
mechanism: gradle-custom-task
ref: grpcNettyContractTest
workflow: release.yml
job: grpc-stable-release-gate
execution: explicit
- id: grpc-fault-lane
release_blocking: true
mechanism: gradle-custom-task
ref: grpcFaultTest
workflow: release.yml
job: grpc-stable-release-gate
execution: explicit
#
# Tag-triggered (`v*`). The only job in this repository that produces a deployable artifact.
#
# One row, not three, because one job is one control: the job builds the app-bootstrap image,
+2 -2
View File
@@ -34,7 +34,7 @@ readonly EXPECTED_WORKFLOW_LOCK=(
'2fa9c8081df1679c1feb9aa101aff47d7d2c24995c155aff6d1e4799eaad8f21 .github/workflows/fileserver-nightly.yml'
'1686b7b637611c8cd5eb87b2cc759f5cd2c6b878154363fc336c16b93c635ada .github/workflows/fileserver-pr.yml'
'b47932200c9ac9db57070b43bc70c40c89c152e9235d7a1325baab407df215e9 .github/workflows/fileserver-release.yml'
'3dc5a3e989043725133a1bbc90636c261406fafaf0158672323ae29dda95c5dd .github/workflows/integration-main.yml'
'a18a0f08982b393177a843c1bdd03a881d9d12491819cebb44b6891a87ff2a6d .github/workflows/integration-main.yml'
'4345d5cfb5a139a11cf3647c58fff61ab08397ace186919cdc7a769cdfc4d4b7 .github/workflows/jpa-next-hibernate8.yml'
'726b3d91603a2529205d1d5568253b57d85fcbb9d10d3efe182491c9da744d78 .github/workflows/jpa-next-jpa4.yml'
'3c073a928dfb266051a1a52f4d66bf6d6903b9dbd2cdb6459fab661228f27e88 .github/workflows/jpa-next-postgresql19.yml'
@@ -47,7 +47,7 @@ readonly EXPECTED_WORKFLOW_LOCK=(
'e685bc846108503ee2cf1e06b6cec040174d49348bd205400f891828f24dda68 .github/workflows/object-storage-qualification.yml'
'67ef53adb80551629a482e2610a0753dd0fadf85f523e985c4693354df543748 .github/workflows/pr-adapters.yml'
'376a71f7a2b9990e1e96937ad3dd46a33f266cc742ca499b208bc909897b67f3 .github/workflows/redis-sdk-topology.yml'
'3f1ff34053bb455587ab9f03305331310b4df4aa4a49bddf875b6969c9afaa05 .github/workflows/release.yml'
'42b57385c1f87170ba6d882345c709c11dff019f1860e72ad989b0c5c1a67ece .github/workflows/release.yml'
)
readonly EXPECTED_COMPOSITE_ACTION_LOCK=(
'7ec6591f26a1bd76658c55472e16b195b80db2c4792b429efda5a0dcbde61a45 .github/actions/setup-gradle-java/action.yml'
+115
View File
@@ -22,6 +22,13 @@ name: integration-main
# shared. They now run on every push to main as well as nightly, which is strictly more often
# than before.
#
# 3. Lanes that were registered in Gradle and invoked by nothing. Ten Gradle tasks — six MongoDB
# container lanes, app-bootstrap's Testcontainers `integrationTest`, and the three messaging
# evidence tasks that `verifyMessagingContracts` reaches — existed, failed closed, and executed
# in no workflow. A lane nobody runs is not coverage; it is a file that looks like coverage. They
# are here rather than in stage 1 because every one of them either starts containers or re-runs
# suites the PR gate already covers, and the pull-request budget is minutes for the whole gate.
#
# What is deliberately NOT here: the web and WebSocket "Advanced capability" nightly lanes that used
# to exist as web-advanced-nightly.yml and websocket-advanced-nightly.yml. Both leaves' build files
# say it outright — "They also run inside `test`, deliberately ... excluding them from the PR gate to
@@ -154,3 +161,111 @@ jobs:
-Phttp3.tests.enabled=true
--no-daemon
--stacktrace
# The six Docker-backed MongoDB lanes. Until now they ran in no workflow at all: the leaf excludes
# every one of their tags from `test` (build.gradle "Docker-backed lanes are excluded from the
# default unit run"), `check` gains only the hermetic `mongoStableContractTest`, and the only thing
# that named them was scripts/verify-mongodb-platform.sh, which nothing in .github invokes. Six
# lanes that fail closed without Docker, and no machine with Docker was ever asked to run them.
#
# Stage 2 rather than stage 1 because each lane starts real MongoDB containers — mongo:8.0.16,
# mongo:7.0.28 and a Toxiproxy in front of a three-node replica set. That is minutes per lane, and
# the pull-request budget is minutes for the whole gate.
#
# One single-line `./gradlew <task>` step per lane, not one folded command running six, because
# .github/scripts/verify-gate-matrix.sh reads these command lines to prove each registered lane is
# actually executed — a folded command would leave six matrix rows unverifiable. It also means a
# red replica-set lane does not hide the compatibility lane behind it.
mongo-container-lanes:
runs-on: ubuntu-latest
timeout-minutes: 90
env:
# Reuse would hand the failover lane a replica set another lane had already faulted.
TESTCONTAINERS_REUSE_ENABLE: "false"
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: ./.github/actions/setup-gradle-java
- name: Single-node replica set contract lane
working-directory: src
run: ./gradlew :adapter:outbound:persistence-mongo:mongoReplicaSetTest --no-daemon --stacktrace
- name: Three-node failover lane
working-directory: src
run: ./gradlew :adapter:outbound:persistence-mongo:mongoFailoverTest --no-daemon --stacktrace
- name: Migration and backfill restart lane
working-directory: src
run: ./gradlew :adapter:outbound:persistence-mongo:mongoMigrationTest --no-daemon --stacktrace
- name: MongoDB 7.0 compatibility lane
working-directory: src
run: ./gradlew :adapter:outbound:persistence-mongo:mongoCompatibilityTest --no-daemon --stacktrace
- name: Credential, TLS and redaction lane
working-directory: src
run: ./gradlew :adapter:outbound:persistence-mongo:mongoSecurityIntegrationTest --no-daemon --stacktrace
- name: Resource-bound certification lane
working-directory: src
run: ./gradlew :adapter:outbound:persistence-mongo:mongoPerformanceTest --no-daemon --stacktrace
- name: Publish the MongoDB lane reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: mongo-lane-reports
path: src/adapter/outbound/persistence-mongo/build/reports/tests/
if-no-files-found: warn
# The messaging contract evidence DAG. `verifyMessagingContracts` is the root of a chain that ran
# nowhere: it depends on five qualification tasks (application-core, shared-contract,
# sample-portfolio and two in adapter:outbound:messaging), each of which depends on
# `prepareMessagingContractEvidence`; it is finalizedBy
# `validateMessagingContractsEvidenceManifestSchema`; and it depends on
# `validateMessagingJsonSchemaV1EvidenceManifestSchema`, which depends on
# `verifyMessagingJsonSchemaV1`. Strict qualification tasks are registered outside `check` by
# design (ca.strict-qualification.gradle), so none of the seven was reachable from any workflow.
#
# The schema validators are the part that matters. They re-read the manifest bytes the run just
# wrote and validate them against config/messaging/evidence/build-evidence-manifest-v1.schema.json
# — a manifest that claims a qualification nobody executed is exactly the failure they exist to
# catch, and until now nothing executed them either.
#
# Stage 2 rather than stage 1: no containers, but it runs five qualification suites across four
# leaves plus two JavaExec validators, and the tests it re-runs are already inside the PR gate's
# `check`. What this job adds is the evidence manifest, which is a main-branch artifact.
messaging-contract-evidence:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: ./.github/actions/setup-gradle-java
- name: Qualify the messaging contract, catalog, binding and schema evidence
working-directory: src
run: ./gradlew verifyMessagingContracts --no-daemon --stacktrace
- name: Publish the messaging evidence manifest
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: messaging-contract-evidence
path: src/build/messaging-evidence/
if-no-files-found: warn
# app-bootstrap's Testcontainers lane. The leaf gave it a source set of its own precisely so that
# `./gradlew :app-bootstrap:test` would not require a Docker daemon — and the consequence nobody
# closed is that a source set outside `test` is also outside `check`, so the real-PostgreSQL
# outbox and idempotency contracts compiled on every build and executed on none.
bootstrap-integration:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
TESTCONTAINERS_REUSE_ENABLE: "false"
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: ./.github/actions/setup-gradle-java
- name: Run the real-PostgreSQL integration contracts
working-directory: src
run: ./gradlew :app-bootstrap:integrationTest --no-daemon --stacktrace
+42
View File
@@ -131,6 +131,47 @@ jobs:
path: src/adapter/inbound/websocket/build/reports/tests/
if-no-files-found: error
# The three gRPC certification lanes. Their tests already run on every pull request — the
# `grpc-inprocess`, `grpc-netty` and `grpc-fault` tags are NOT excluded from
# `:grpc:grpc-testkit:test` (only `grpc-performance` is), and that task runs inside the root
# `check`. So this job adds exactly what the web and WebSocket Advanced lanes above add: the lane
# fails closed when its tag selects nothing, which is the one thing a tag-filtered suite inside
# `test` cannot tell you. A renamed or deleted @Tag would otherwise leave the in-process,
# transport and fault evidence grades claiming coverage that stopped existing.
#
# Release rather than nightly, for the same reason web-stable-release-gate is: these lanes need no
# container and no fixed cadence — grpcNettyContractTest opens an ephemeral socket, not a broker —
# so the guard is worth asserting once per tag and is not worth a runner every night.
#
# grpcPerformanceTest is deliberately absent. The leaf excludes it from `test` and says why: "a
# measurement in the release gate is a flaky test on a shared CI runner; it runs when somebody asks
# for it, by name." It is recorded as a manual entrypoint in the repository README instead.
grpc-stable-release-gate:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: ./.github/actions/setup-gradle-java
- name: In-process contract lane
working-directory: src
run: ./gradlew :grpc:grpc-testkit:grpcInProcessContractTest --no-daemon --stacktrace
- name: Real Netty transport lane
working-directory: src
run: ./gradlew :grpc:grpc-testkit:grpcNettyContractTest --no-daemon --stacktrace
- name: Fault-injection lane
working-directory: src
run: ./gradlew :grpc:grpc-testkit:grpcFaultTest --no-daemon --stacktrace
- name: Publish the gRPC release evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: grpc-release-evidence
path: src/grpc/grpc-testkit/build/reports/tests/
if-no-files-found: warn
# Each declared gate runs as its own single-line `./gradlew <task>` step, because
# .github/scripts/verify-gate-matrix.sh reads these commands to prove the gate is actually
# executed — a folded or flag-laden command would make the declaration in
@@ -185,6 +226,7 @@ jobs:
- architecture-and-surface
- web-stable-release-gate
- websocket-stable-release-gate
- grpc-stable-release-gate
- httpclient-release-gate
- httpclient-documentation
# Job-level, because a job that declares `permissions:` replaces the workflow set entirely: this