diff --git a/.github/ci-gate-matrix.yml b/.github/ci-gate-matrix.yml index a1b3432d..0573ce77 100644 --- a/.github/ci-gate-matrix.yml +++ b/.github/ci-gate-matrix.yml @@ -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, diff --git a/.github/scripts/verify-gradle-wrapper.sh b/.github/scripts/verify-gradle-wrapper.sh index 6457e428..6030238c 100755 --- a/.github/scripts/verify-gradle-wrapper.sh +++ b/.github/scripts/verify-gradle-wrapper.sh @@ -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' diff --git a/.github/workflows/integration-main.yml b/.github/workflows/integration-main.yml index ffaa2246..9d7be4e2 100644 --- a/.github/workflows/integration-main.yml +++ b/.github/workflows/integration-main.yml @@ -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 ` 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 58c531f1..c38a0122 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 ` 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 diff --git a/README.md b/README.md index 0a1a8d6b..3ddea6f2 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,31 @@ cd src 두 검증 축은 [ci-quality-gates.yml](.github/workflows/ci-quality-gates.yml)의 release gate에 연결되어, 규칙 위반이 병합·릴리스를 막습니다. +## 수동 전용 Gradle 태스크 + +아래 세 태스크는 **어떤 워크플로도 실행하지 않으며, 그게 의도다.** 자동 실행이 틀린 이유를 각각 +적어 둔다. `verifyReadmeCommands`가 이 블록의 태스크 이름이 실재하는지 검사하므로, 태스크를 지우거나 +이름을 바꾸면 이 문서가 같이 틀어지고 게이트가 그것을 잡는다. + +```bash +cd src +./gradlew :grpc:grpc-testkit:grpcPerformanceTest +./gradlew :sample-portfolio:openapiCheckSnapshot -PapproveOpenApiChange +./gradlew :app-bootstrap:sampleOffCompile +``` + +- `grpcPerformanceTest` — latency percentile·saturation·drain budget을 **측정**한다. 공유 CI + runner의 측정값은 흔들리고, 흔들리는 게이트는 결국 꺼진다. leaf `build.gradle`이 이 태스크의 + 태그를 `test`에서 제외하는 이유도 같다. 성능 회귀가 의심될 때 사람이 이름으로 부른다. +- `openapiCheckSnapshot` — 드리프트 검사 자체는 이미 자동으로 돈다. 이 태스크가 감싸는 + `OpenApiDriftContractTest`는 `:sample-portfolio:test`의 일부이고, 그건 `check` 안이며 stage 1에서 + 실행된다. 이 태스크의 고유한 역할은 `-PapproveOpenApiChange`로 **커밋된 스냅샷을 다시 만드는 것** + — 의도된 API 변경을 사람이 승인하는 지점이다. 자동으로 돌리면 승인이 승인이 아니게 된다. +- `sampleOffCompile` — `sampleOffTest` 소스셋을 **컴파일만** 한다. CI가 돌리는 + `:app-bootstrap:sampleOffTest`(stage 1, `ci-quality-gates.yml`의 `sample-off` 잡)는 같은 소스셋을 + 컴파일한 뒤 실행까지 하므로, CI에 따로 넣으면 진부분집합을 한 번 더 도는 것이다. 남겨 둔 이유는 + sample 제거 작업 중 테스트를 기다리지 않고 컴파일만 빠르게 확인하는 로컬 루프가 실재하기 때문이다. + ## 더 알아보기 - 빌드·검증 게이트·환경 변수 상세: [src/README.md](src/README.md) diff --git a/src/build.gradle b/src/build.gradle index 51c9069c..7708fb51 100644 --- a/src/build.gradle +++ b/src/build.gradle @@ -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 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') } diff --git a/src/gradle/libs.versions.toml b/src/gradle/libs.versions.toml index 0fe3fffb..ebc6523c 100644 --- a/src/gradle/libs.versions.toml +++ b/src/gradle/libs.versions.toml @@ -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" }