refactor(build,ci): CI를 단계별로 재편하고 감사 잔여 114건 정리

빌드·CI 레이어 전수 감사(133건) 중 Track A 에서 처리한 E등급 19건을 뺀
나머지를 마무리한다. 한 커밋인 이유는 서로 얽혀 있기 때문이다 — 워크플로가
`checkstyleMain` 을 부르려면 루트가 먼저 Checkstyle 을 붙여야 하고, 모듈 43개가
convention 을 적용하려면 build-logic 이 먼저 그 플러그인을 등록해야 한다.
영역별로 쪼개면 중간 커밋이 빌드되지 않는다.

## CI 단계 분리 (워크플로 29파일 3,360줄 → 19파일 2,692줄, 잡 69 → 64)

모듈이 아니라 단계로 자른다. 기존 28개는 `web-*` `jpa-*` 처럼 모듈로 갈려
있어서 같은 일이 파일마다 중복됐다.

- stage 1 `ci-quality-gates.yml` + `pr-adapters.yml`(신규, 잡 단위 경로 필터) — pull_request
- stage 2 `integration-main.yml`(신규) — push:main + 03:00. 문서 게이트는 여기에 둔다
- stage 3 `release.yml`(신규, 릴리스 워크플로 7개 중 5개 흡수) — push: tags v*

setup 블록 59회 복붙 → `.github/actions/setup-gradle-java` 1개(잡당 13줄 → 5줄).
잡 8개 삭제, 각각 대체 잡을 확인했다. `verifyCleanArchitectureDependencies` 실행
횟수가 태그당 9 → 6, PR당 8 → 4 로 줄었다.

## 컨테이너 릴리스 신설

이미지를 만드는 것이 아무것도 없었다. Dockerfile 은 있었지만
build-push-action / bootBuildImage / jib 사용처가 0건이고, `*-release.yml` 8개는
테스트 후 아티팩트만 올렸다 — 이름만 릴리스였다.

Boot 레이어드 추출 + thin-JAR 엔트리포인트로 Dockerfile 을 고치고 릴리스
워크플로에 이미지 빌드·푸시·SBOM·스캔을 넣었다. 로컬 빌드로 검증했다:
레지스트리 content 241MB, 기동 3.7초, uid 1000, 헬스체크 200.
코드만 바뀐 릴리스는 7.68MB 만 재푸시한다(이전이라면 156MB).
CI 는 배포하지 않는다 — 매니페스트와 ArgoCD 는 별도 repo 로 간다.

## 게이트 정리

- gate-matrix 의 개수 고정 해제: `EXPECTED_GATE_COUNT=49` 와 하드코딩된 49개 id
  목록을 지우고 불변식으로 대체(필드·enum, 워크플로/잡 실재, id 중복,
  `release_blocking: true` 는 실제로 release-gate 의 needs 여야 함).
  행을 추가하려면 테스트부터 고쳐야 하던 구조를 풀었다. 커버리지 8/28 → 28/28
- 문서 게이트 4개를 `check` 에서 떼어 `verifyDocumentationContracts` 로 묶고
  stage 2 에 배치. 어겨도 런타임은 멀쩡하므로 개발을 막지 않는다
- `verifyOneTypePerFile`(정규식 Java 파싱, 126파일 미탐) → Checkstyle
  `OneTopLevelClass` + `OuterTypeFilename`. main 위반 0건, test 의 fixture 29건은
  정책을 넓히지 않고 suppressions 에 사유와 함께 명시 제외
- leaf 하나의 `check` 가 끌고 오던 저장소 전역 게이트 18개를 재배치.
  `:domain-core:check` 가 13 태스크 11초로 끝난다
- convention 플러그인 2개 신설(`ca.platform-module`, `ca.grpc-platform-module`),
  플랫폼 모듈 43개에 적용. 손수 짠 Test 태스크 17개를 `strictTestLanes` 로 전환
  (태스크 이름 전부 보존 — CI 가 이름으로 부른다)
- `ca.api-surface` 의 정규식 Java 파서를 javac parse-only 로 교체
  (기존 베이스라인 3개와 바이트 동일 확인)
- 죽은 태스크 5개 삭제, `src/gradle` 1,713 → 1,440줄, 모듈 build.gradle
  3,072 → 2,977줄

## 검사가 검사를 못 하고 있던 것들

- 11개 계약 테스트가 gitignore 된 `src/.env` 를 요구했다. `.gitignore` 자신이
  "examples beside it are the tracked contract, never a real one" 이라고 적어둔
  규칙과 어긋난다. 깨끗한 체크아웃에는 그 파일이 없으므로 CI 에서 돌 수 없었다.
  추적되는 `.env.example` 로 돌린다
- **`.env.local.example` 이 5432 를 가리키는데 compose 는 5433 을 게시한다.**
  이 파일을 복사해 시작하는 신규 개발자는 DB 연결에 실패한다. 이걸 잡으라고
  만든 테스트가 추적 안 되는 파일을 읽어서, 이미 설정이 끝난 머신에서만 돌고
  정작 처음 받는 사람에겐 아무 검사도 안 하고 있었다. 포트를 고치고 테스트를
  추적 파일로 돌렸다
- `MongoModuleBoundaryTest` 의 `DO_NOT_INCLUDE_JARS` 때문에 임포트가 0개가 되어
  규칙 10개가 "failed to check any classes" 로 실패 중이었다. 이 레인에서는
  모듈 자기 클래스가 jar 로 올라온다. `importPackages(ROOT)` 가 이미 서드파티를
  거르므로 옵션은 불필요했다
- `ReleaseManifestTaskExistenceTest` 가 build 파일 텍스트에서 `tasks.register(`
  만 찾아, convention 의 `lane('...')` 로 바뀐 태스크를 미등록으로 오판했다
- `ProfileSeparationContractTest` 는 런처가 주입하는 `src/.env` 가 맞는 대상이라
  그대로 두되, 파일이 없으면 명시적으로 skip 한다 — "안 돌았다" 가 "통과했다"
  로 읽히지 않게

## 검증 (전부 깨끗한 체크아웃에서, 커밋 전에 실행)

`verify-gradle-wrapper.sh` PASS · `verify-gate-matrix.sh` OK(drift 0) ·
워크플로 YAML 전수 파싱 OK · actionlint 지적 0 · `gradlew help` ·
`verifyCleanArchitectureDependencies` · `build-logic test` ·
`:app-bootstrap:test` **1001 tests 실패 0 스킵 5** · `:domain-core:check` ·
`verifyDocumentationContracts`.

## 남은 문제

- 첫 `v*` 태그는 이미지 취약점 스캔에서 실패한다(CRITICAL/HIGH 9건:
  ubuntu 베이스 2, tomcat-embed-core 3, amqp-client 3, httpcore5 2).
  억제를 넣지 않았다 — 릴리스 1회차를 초록으로 만들려고 임계값을 내리면
  게이트가 장식이 된다. 의존성·베이스 갱신이 선행돼야 한다
- `fileserver-v*` / `web-v*` / `websocket-v*` 태그는 이제 아무 run 도 만들지
  않는다(배포 단위가 하나라는 결정에 따른 것)
- main push 마다 무거운 레인 3개가 새로 돈다 — 러너 분이 늘어난다
- `ProfileSeparationContractTest` 가 찾아낸 4개 값(cache command-timeout,
  cache positive-soft-ttl, idempotency provider, rate-limit command-timeout)이
  `.env.example` 과 인라인 기본값 사이에서 갈린다. 런타임 설정 판단이라
  건드리지 않았다

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-16 18:27:15 +09:00
co-authored by Claude Opus 5
parent 1535481794
commit 9bc2e75fe5
132 changed files with 4002 additions and 3083 deletions
+9
View File
@@ -4,6 +4,15 @@
# noise that must not enter the image build context, while keeping everything
# the builder stage needs to resolve dependencies and run bootJar.
# ---- Build recipe itself ----------------------------------------------------
# The Dockerfile is supplied with `-f` and is never needed inside the context. Leaving it in means
# `COPY . .` embeds it in the image AND makes every Dockerfile edit — a comment included — invalidate
# the cached dependency-resolution layer, which costs a full Gradle re-resolve (~3 min) for a change
# that affects nothing the builder reads.
Dockerfile
Dockerfile.*
.dockerignore
# ---- Version control --------------------------------------------------------
.git
.gitignore
+1 -1
View File
@@ -11,7 +11,7 @@ SPRING_PROFILES_ACTIVE=local
# JPA: needs the PostgreSQL service. Flyway owns the schema from dev onward, and local uses the
# same vendor semantics so the two do not diverge.
APP_PERSISTENCE_JPA_ENABLED=true
APP_DATASOURCE_URL=jdbc:postgresql://localhost:5432/ca_skeleton
APP_DATASOURCE_URL=jdbc:postgresql://localhost:5433/ca_skeleton
APP_DATASOURCE_USERNAME=ca_skeleton
APP_DATASOURCE_PASSWORD=
APP_DATASOURCE_DDL_AUTO=validate
+52 -7
View File
@@ -59,6 +59,27 @@ COPY . .
RUN ./gradlew :app-bootstrap:stageDockerJar --no-daemon -x test \
-PreleaseVersion="${RELEASE_VERSION}" -PgitRevision="${GIT_SHA}"
# ---- Layer extraction (D8) --------------------------------------------------
# Split the uber JAR into Spring Boot's four layers before it reaches the runtime stage.
#
# Without this the whole fat JAR — every dependency and the application classes in one file — is a
# single image layer, so a release that changes one line of application code re-pushes and re-pulls
# every dependency in the graph. The layers are ordered least- to most-frequently-changed
# (dependencies, loader, snapshot dependencies, application), which is what makes the expensive
# layer cacheable across releases.
#
# `extract` WITHOUT `--launcher` is the layout Spring Boot 4 documents: a thin application JAR whose
# manifest Class-Path points at the extracted lib/ directory, rather than a nested-JAR uber JAR the
# loader has to open and index on every start. It is also the AOT-cache/CDS-friendly layout, which
# is the layout any later startup-time work would need.
#
# Absolute paths on both sides, and no WORKDIR change: DeveloperExperienceContractTest asserts that
# every Dockerfile names the exact Gradle-staged artifact path rather than selecting a JAR, and the
# input here is that same fixed path Gradle wrote. Nothing in this stage may pick a JAR by pattern.
RUN java -Djarmode=tools \
-jar /build/src/app-bootstrap/build/docker/application.jar \
extract --layers --destination /build/src/app-bootstrap/build/docker/extracted
# ---- Stage 2: runtime image -------------------------------------------------
# JRE-only slim image (D3: no full JDK in production image).
# Uses eclipse-temurin:21-jre-jammy — the Adoptium-supported JRE variant.
@@ -130,24 +151,48 @@ VOLUME ["/var/lib/backend/files"]
WORKDIR /app
COPY --from=builder --chown=app:app /build/src/app-bootstrap/build/docker/application.jar app.jar
# ---- Application layers (D8) ------------------------------------------------
# One COPY per Spring Boot layer, ordered least- to most-frequently-changed. Each COPY is its own
# image layer, so a release that only changes application code re-pushes and re-pulls the last one
# instead of the whole dependency graph. This replaced a single `COPY application.jar app.jar`,
# under which every release shipped every dependency again because they lived in the same file as
# the code that changed.
#
# All four land in /app: the extracted application.jar is a thin JAR whose manifest Class-Path
# points at ./lib, which is what the dependencies layer unpacks to.
COPY --from=builder --chown=app:app /build/src/app-bootstrap/build/docker/extracted/dependencies/ ./
COPY --from=builder --chown=app:app /build/src/app-bootstrap/build/docker/extracted/spring-boot-loader/ ./
COPY --from=builder --chown=app:app /build/src/app-bootstrap/build/docker/extracted/snapshot-dependencies/ ./
COPY --from=builder --chown=app:app /build/src/app-bootstrap/build/docker/extracted/application/ ./
USER app
# ---- Ports ------------------------------------------------------------------
# 8080 — application HTTP port
# 9001 — management / actuator port (parallel actuator branch wires this endpoint)
# 9001 — management / actuator port, from management.server.port in config/observability.yml
EXPOSE 8080 9001
# ---- Health check -----------------------------------------------------------
# Targets the actuator readiness probe on the management port (9001).
# CROSS-FEATURE COUPLING: the /actuator/health/readiness endpoint is implemented
# by the parallel runtime-health + actuator branches. The HEALTHCHECK is wired here
# (container-side) and will pass once those branches are merged. In this worktree
# the endpoint may return 404; the container will be UNHEALTHY until merged.
#
# The endpoint is real: config/observability.yml sets management.server.port to 9001 and
# management.endpoint.health.probes.enabled to true, which is what publishes
# /actuator/health/readiness. (This block used to carry a note saying the path might 404 because
# the actuator work lived on an unmerged branch. It has been merged for some time, and a stale
# warning about a healthcheck is the kind of comment that gets a real red container ignored.)
#
# A readiness failure here is a correct UNHEALTHY, not a broken probe: the readiness group includes
# the datasource, so a container started with no reachable PostgreSQL is genuinely not ready.
# Kubernetes ignores HEALTHCHECK and uses its own probes against the same path; this exists for
# docker and Compose.
#
# wget is present in eclipse-temurin:21-jre-jammy, so nothing is installed for it.
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD wget --no-verbose --tries=1 --spider \
http://localhost:9001/actuator/health/readiness || exit 1
# ---- Entrypoint -------------------------------------------------------------
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
# The extracted thin JAR, not the uber JAR the builder produced. It carries only application classes
# and a Class-Path pointing at ./lib, so the loader does not open and index a nested-JAR archive on
# every start, and the layout stays AOT-cache/CDS friendly for any later startup work.
ENTRYPOINT ["java", "-jar", "application.jar"]
+17 -3
View File
@@ -63,6 +63,14 @@ COPY . .
RUN ./gradlew :sample-portfolio:stageDockerJar --no-daemon -x test \
-PreleaseVersion="${RELEASE_VERSION}" -PgitRevision="${GIT_SHA}"
# ---- Layer extraction -------------------------------------------------------
# Identical to src/Dockerfile — see the long note there. Kept in sync because the header of this
# file says the builder stages are, and a demo image whose layout has drifted from the release image
# stops being a demo of the release image. Absolute paths for the same contract-test reason.
RUN java -Djarmode=tools \
-jar /build/src/sample-portfolio/build/docker/application.jar \
extract --layers --destination /build/src/sample-portfolio/build/docker/extracted
# ---- Stage 2: runtime image -------------------------------------------------
# JRE-only slim image (no full JDK in the demo image either).
FROM eclipse-temurin:21-jre-jammy@sha256:199aebeb3adcde4910695cdebfe782ada38dadb6cc8013159b58d3724451befd AS runtime
@@ -108,7 +116,12 @@ RUN groupadd --system --gid 1000 app \
WORKDIR /app
COPY --from=builder --chown=app:app /build/src/sample-portfolio/build/docker/application.jar app.jar
# ---- Application layers -----------------------------------------------------
# One COPY per Spring Boot layer, least- to most-frequently-changed, matching src/Dockerfile.
COPY --from=builder --chown=app:app /build/src/sample-portfolio/build/docker/extracted/dependencies/ ./
COPY --from=builder --chown=app:app /build/src/sample-portfolio/build/docker/extracted/spring-boot-loader/ ./
COPY --from=builder --chown=app:app /build/src/sample-portfolio/build/docker/extracted/snapshot-dependencies/ ./
COPY --from=builder --chown=app:app /build/src/sample-portfolio/build/docker/extracted/application/ ./
USER app
@@ -125,5 +138,6 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
http://localhost:9001/actuator/health/readiness || exit 1
# ---- Entrypoint -------------------------------------------------------------
# mainClass (SamplePortfolioApplication) is baked into the bootJar manifest.
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
# mainClass (SamplePortfolioApplication) is baked into the bootJar manifest and survives the
# extraction into the thin application.jar.
ENTRYPOINT ["java", "-jar", "application.jar"]
+11 -4
View File
@@ -5,10 +5,17 @@
// NO protobuf: there is no `com.google.protobuf` plugin and no `.proto` here — health + reflection
// come from grpc-services at runtime, and a future consuming feature owns its `.proto`/services.
//
// io.grpc:* / protobuf versions are NOT managed by the Spring Boot BOM, and this repo has no version
// catalog, so the grpc-bom + protobuf-bom platforms are imported HERE (module scope) using the root
// `ext.grpcVersion` / `ext.protobufVersion` SSOT — this keeps the strict-locking blast radius to
// this module (the shared root dependencyManagement block stays io.grpc-free).
// io.grpc:* / protobuf versions are NOT managed by the Spring Boot BOM, so the grpc-bom +
// protobuf-bom platforms are imported HERE (module scope) using the root `ext.grpcVersion` /
// `ext.protobufVersion` SSOT — this keeps the strict-locking blast radius to this module (the shared
// root dependencyManagement block stays io.grpc-free).
//
// This sentence used to end "and this repo has no version catalog", which is false:
// gradle/libs.versions.toml exists and this leaf's siblings use it. Module scope is a locking
// decision, not a consequence of a missing catalog. The catalog simply has no io.grpc or protobuf
// entry, which leaves protobuf with two sources — catalog `protobuf` (used by
// adapter:inbound:websocket) and root `ext.protobufVersion` (used here) — on different majors. They
// do not meet today because neither leaf is in a composition root; see the W2A handoff.
dependencyManagement {
imports {
+43 -67
View File
@@ -175,77 +175,53 @@ dependencies {
nginxProxyTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
// The lane task. A release compatibility gate that is not wired to a task is a document.
tasks.register('webFluxContractTest', Test) {
group = 'verification'
description = 'Runs the Stable HTTP contract against a real Reactor Netty.'
testClassesDirs = sourceSets.webfluxContractTest.output.classesDirs
classpath = sourceSets.webfluxContractTest.runtimeClasspath
useJUnitPlatform()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
}
// Docker-gated, and it says so rather than skipping. A lane that quietly passes when the container
// runtime is missing is a lane that has been certifying nothing since whenever Docker last broke.
tasks.register('webNginxProxyTest', Test) {
group = 'verification'
description = 'Runs the proxy, prefix and spoofing contract behind a real Nginx.'
testClassesDirs = sourceSets.nginxProxyTest.output.classesDirs
classpath = sourceSets.nginxProxyTest.runtimeClasspath
useJUnitPlatform()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
}
// The cross-stack gate. It depends on every recording lane rather than tolerating a missing one:
// a parity check that compares whatever happens to be present would report agreement across a
// matrix with a hole in it.
tasks.register('webCrossStackParityTest', Test) {
group = 'verification'
description = 'Compares the wire contract recorded by Tomcat, Jetty and Reactor Netty.'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform {
includeTags 'web-parity'
strictTestLanes {
// A release compatibility gate that is not wired to a task is a document.
lane('webFluxContractTest') {
sourceSet = 'webfluxContractTest'
description = 'Runs the Stable HTTP contract against a real Reactor Netty.'
customize = { test -> test.jvmArgs '-Duser.timezone=UTC' }
}
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
dependsOn 'test', 'webJettyCompatTest', 'webFluxContractTest'
}
// The Advanced lane. Every capability is off unless a deployment names it, so none of them is
// exercised by anything a production deployment runs — which makes a lane that runs them all the
// only place a break is noticed before whoever enables it notices.
//
// They also run inside `test`, deliberately. They are ordinary unit tests, and excluding them from
// the PR gate to make this lane look meaningful would mean the PR gate stopped covering a fifth of
// the leaf.
tasks.register('webAdvancedTest', Test) {
group = 'verification'
description = 'Runs every web Advanced capability contract.'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform {
includeTags 'web-advanced'
// Docker-gated, and it says so rather than skipping. A lane that quietly passes when the
// container runtime is missing is a lane that has been certifying nothing since whenever Docker
// last broke.
lane('webNginxProxyTest') {
sourceSet = 'nginxProxyTest'
description = 'Runs the proxy, prefix and spoofing contract behind a real Nginx.'
customize = { test -> test.jvmArgs '-Duser.timezone=UTC' }
}
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
}
tasks.register('webJettyCompatTest', Test) {
group = 'verification'
description = 'Runs the Stable HTTP contract against a real Jetty instead of Tomcat.'
testClassesDirs = sourceSets.jettyCompatTest.output.classesDirs
classpath = sourceSets.jettyCompatTest.runtimeClasspath
useJUnitPlatform()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
lane('webJettyCompatTest') {
sourceSet = 'jettyCompatTest'
description = 'Runs the Stable HTTP contract against a real Jetty instead of Tomcat.'
customize = { test -> test.jvmArgs '-Duser.timezone=UTC' }
}
// The cross-stack gate. It depends on every recording lane rather than tolerating a missing one:
// a parity check that compares whatever happens to be present would report agreement across a
// matrix with a hole in it.
lane('webCrossStackParityTest') {
tag = 'web-parity'
description = 'Compares the wire contract recorded by Tomcat, Jetty and Reactor Netty.'
customize = { test ->
test.jvmArgs '-Duser.timezone=UTC'
test.dependsOn 'test', 'webJettyCompatTest', 'webFluxContractTest'
}
}
// The Advanced lane. Every capability is off unless a deployment names it, so none of them is
// exercised by anything a production deployment runs — which makes a lane that runs them all the
// only place a break is noticed before whoever enables it notices.
//
// They also run inside `test`, deliberately. They are ordinary unit tests, and excluding them
// from the PR gate to make this lane look meaningful would mean the PR gate stopped covering a
// fifth of the leaf.
lane('webAdvancedTest') {
tag = 'web-advanced'
description = 'Runs every web Advanced capability contract.'
customize = { test -> test.jvmArgs '-Duser.timezone=UTC' }
}
}
strictTestLanes {
@@ -95,7 +95,7 @@ public final class HmacWebCursorCodec implements WebCursorCodec {
String canonical;
try {
canonical = new String(DECODER.decode(encodedBody), StandardCharsets.UTF_8);
} catch (IllegalArgumentException notBase64) {
} catch (IllegalArgumentException malformedEncoding) {
throw new WebCursorException();
}
String[] fields = canonical.split("\\u001f", -1);
+27 -68
View File
@@ -69,13 +69,6 @@ strictTestLanes {
compilesAgainst 'main', 'testFixtures'
inherits 'implementation'
}
// The STOMP broker lane. Its own source set for the same reason the Nginx one has its own: it
// is the only other lane that needs Docker, and folding it into `test` would make every
// developer's `check` depend on a container runtime.
sourceSet('brokerRelayTest') {
compilesAgainst 'main', 'testFixtures'
inherits 'implementation'
}
}
dependencies {
@@ -90,9 +83,10 @@ dependencies {
testFixturesImplementation 'org.springframework.boot:spring-boot-autoconfigure'
testFixturesImplementation 'org.springframework.boot:spring-boot'
testFixturesImplementation libs.archunit.junit5
// No `testImplementation 'spring-boot-starter-test'` / `'spring-boot-starter-websocket'` here:
// the root gives every non-platform leaf the former (src/build.gradle), and `testImplementation`
// extends `implementation`, which already carries the latter (:24).
testImplementation libs.archunit.junit5
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-websocket'
jettyWebSocketTestImplementation('org.springframework.boot:spring-boot-starter-jetty')
jettyWebSocketTestImplementation('org.springframework.boot:spring-boot-starter-test') {
@@ -108,69 +102,34 @@ dependencies {
nginxWebSocketTestImplementation 'org.testcontainers:testcontainers'
nginxWebSocketTestImplementation 'org.testcontainers:testcontainers-junit-jupiter'
nginxWebSocketTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
brokerRelayTestImplementation 'org.springframework.boot:spring-boot-starter-test'
brokerRelayTestImplementation 'org.springframework.boot:spring-boot-starter-websocket'
brokerRelayTestImplementation 'org.testcontainers:testcontainers'
brokerRelayTestImplementation 'org.testcontainers:testcontainers-junit-jupiter'
brokerRelayTestImplementation 'org.testcontainers:testcontainers-rabbitmq'
brokerRelayTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
// Docker-gated, and it fails rather than skipping. A proxy contract that quietly passes without a
// proxy has been certifying nothing since whenever the container runtime last broke.
tasks.register('websocketNginxTest', Test) {
group = 'verification'
description = 'Runs the upgrade and forwarded-header contract behind a real Nginx.'
testClassesDirs = sourceSets.nginxWebSocketTest.output.classesDirs
classpath = sourceSets.nginxWebSocketTest.runtimeClasspath
useJUnitPlatform()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
}
// Docker-gated, and it fails rather than skipping. A broker relay contract that quietly passes with
// no broker has been certifying nothing.
tasks.register('websocketBrokerRelayTest', Test) {
group = 'verification'
description = 'Runs the STOMP broker contract against the simple broker and a real RabbitMQ.'
testClassesDirs = sourceSets.brokerRelayTest.output.classesDirs
classpath = sourceSets.brokerRelayTest.runtimeClasspath
useJUnitPlatform()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
}
// The real-container lane. Upgrade negotiation, close-frame handling and idle behaviour are
// container code, so a mock dispatcher certifies none of it.
// The Advanced lane. Every capability is off unless a deployment names it, so nothing a production
// deployment runs exercises them — which makes this the only place a break is noticed before
// whoever enables it notices. They also run inside `test`: they are ordinary unit tests, and
// excluding them to make this lane look meaningful would stop the PR gate covering them.
tasks.register('websocketAdvancedTest', Test) {
group = 'verification'
description = 'Runs every WebSocket Advanced capability contract.'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform {
includeTags 'websocket-advanced'
strictTestLanes {
// Docker-gated, and it fails rather than skipping. A proxy contract that quietly passes without
// a proxy has been certifying nothing since whenever the container runtime last broke.
lane('websocketNginxTest') {
sourceSet = 'nginxWebSocketTest'
description = 'Runs the upgrade and forwarded-header contract behind a real Nginx.'
customize = { test -> test.jvmArgs '-Duser.timezone=UTC' }
}
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
}
tasks.register('websocketJettyTest', Test) {
group = 'verification'
description = 'Runs the WebSocket runtime contract against a real Jetty instead of Tomcat.'
testClassesDirs = sourceSets.jettyWebSocketTest.output.classesDirs
classpath = sourceSets.jettyWebSocketTest.runtimeClasspath
useJUnitPlatform()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
// The real-container lane. Upgrade negotiation, close-frame handling and idle behaviour are
// container code, so a mock dispatcher certifies none of it.
lane('websocketJettyTest') {
sourceSet = 'jettyWebSocketTest'
description = 'Runs the WebSocket runtime contract against a real Jetty instead of Tomcat.'
customize = { test -> test.jvmArgs '-Duser.timezone=UTC' }
}
// The Advanced lane. Every capability is off unless a deployment names it, so nothing a
// production deployment runs exercises them — which makes this the only place a break is noticed
// before whoever enables it notices. They also run inside `test`: they are ordinary unit tests,
// and excluding them to make this lane look meaningful would stop the PR gate covering them.
lane('websocketAdvancedTest') {
tag = 'websocket-advanced'
description = 'Runs every WebSocket Advanced capability contract.'
customize = { test -> test.jvmArgs '-Duser.timezone=UTC' }
}
}
registerStrictQualificationTest(
+131 -132
View File
@@ -1,101 +1,101 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=brokerRelayTestCompileClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,nginxWebSocketTestCompileClasspath,testCompileClasspath,testkitCompileClasspath
ch.qos.logback:logback-classic:1.5.38=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
ch.qos.logback:logback-core:1.5.38=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.fasterxml.jackson.core:jackson-annotations:2.21=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.fasterxml:classmate:1.7.3=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.github.ben-manes.caffeine:caffeine:3.2.4=annotationProcessor,brokerRelayTestAnnotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
com.github.docker-java:docker-java-api:3.7.1=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
com.github.docker-java:docker-java-transport-zerodep:3.7.1=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
com.github.docker-java:docker-java-transport:3.7.1=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,brokerRelayTestAnnotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,jettyWebSocketTestCompileClasspath,nginxWebSocketTestCompileClasspath,testCompileClasspath
ch.qos.logback:logback-classic:1.5.38=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
ch.qos.logback:logback-core:1.5.38=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-annotations:2.21=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
com.fasterxml:classmate:1.7.3=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
com.github.ben-manes.caffeine:caffeine:3.2.4=annotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
com.github.docker-java:docker-java-api:3.7.1=nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
com.github.docker-java:docker-java-transport-zerodep:3.7.1=nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
com.github.docker-java:docker-java-transport:3.7.1=nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
com.github.spotbugs:spotbugs-annotations:4.10.2=spotbugs
com.github.spotbugs:spotbugs:4.10.2=spotbugs
com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,brokerRelayTestAnnotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,brokerRelayTestAnnotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
com.google.auto:auto-common:1.2.2=annotationProcessor,brokerRelayTestAnnotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
com.google.auto:auto-common:1.2.2=annotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
com.google.code.findbugs:jsr305:3.0.2=checkstyle,spotbugs
com.google.code.gson:gson:2.13.2=spotbugs
com.google.errorprone:error_prone_annotation:2.49.0=annotationProcessor,brokerRelayTestAnnotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.38.0=brokerRelayTestCompileClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,nginxWebSocketTestCompileClasspath,testCompileClasspath,testkitCompileClasspath
com.google.errorprone:error_prone_annotation:2.49.0=annotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.38.0=compileClasspath,jettyWebSocketTestCompileClasspath,nginxWebSocketTestCompileClasspath,testCompileClasspath
com.google.errorprone:error_prone_annotations:2.41.0=spotbugs
com.google.errorprone:error_prone_annotations:2.47.0=checkstyle
com.google.errorprone:error_prone_annotations:2.49.0=annotationProcessor,brokerRelayTestAnnotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
com.google.errorprone:error_prone_check_api:2.49.0=annotationProcessor,brokerRelayTestAnnotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
com.google.errorprone:error_prone_core:2.49.0=annotationProcessor,brokerRelayTestAnnotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
com.google.googlejavaformat:google-java-format:1.35.0=annotationProcessor,brokerRelayTestAnnotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
com.google.guava:failureaccess:1.0.3=annotationProcessor,brokerRelayTestAnnotationProcessor,checkstyle,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
com.google.guava:guava:33.5.0-jre=annotationProcessor,brokerRelayTestAnnotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.49.0=annotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
com.google.errorprone:error_prone_check_api:2.49.0=annotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
com.google.errorprone:error_prone_core:2.49.0=annotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
com.google.googlejavaformat:google-java-format:1.35.0=annotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
com.google.guava:failureaccess:1.0.3=annotationProcessor,checkstyle,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
com.google.guava:guava:33.5.0-jre=annotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
com.google.guava:guava:33.6.0-jre=checkstyle
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,brokerRelayTestAnnotationProcessor,checkstyle,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,brokerRelayTestAnnotationProcessor,checkstyle,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,brokerRelayTestAnnotationProcessor,brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestAnnotationProcessor,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestAnnotationProcessor,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testAnnotationProcessor,testCompileClasspath,testFixturesAnnotationProcessor,testRuntimeClasspath,testkitAnnotationProcessor,testkitCompileClasspath,testkitRuntimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,checkstyle,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,compileClasspath,jettyWebSocketTestAnnotationProcessor,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestAnnotationProcessor,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testAnnotationProcessor,testCompileClasspath,testFixturesAnnotationProcessor,testRuntimeClasspath
com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0=spotbugsPlugins
com.jayway.jsonpath:json-path:2.10.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.jayway.jsonpath:json-path:2.10.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.puppycrawl.tools:checkstyle:13.5.0=checkstyle
com.tngtech.archunit:archunit-junit5-api:1.3.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=brokerRelayTestRuntimeClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestRuntimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine:1.3.0=brokerRelayTestRuntimeClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestRuntimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
com.tngtech.archunit:archunit-junit5:1.3.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.tngtech.archunit:archunit:1.3.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.vaadin.external.google:android-json:0.0.20131108.vaadin1=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.tngtech.archunit:archunit-junit5-api:1.3.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestRuntimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine:1.3.0=jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestRuntimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5:1.3.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit:1.3.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
com.vaadin.external.google:android-json:0.0.20131108.vaadin1=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-beanutils:commons-beanutils:1.11.0=checkstyle
commons-codec:commons-codec:1.19.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
commons-codec:commons-codec:1.19.0=nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
commons-collections:commons-collections:3.2.2=checkstyle
commons-io:commons-io:2.20.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
commons-io:commons-io:2.20.0=nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
commons-io:commons-io:2.21.0=spotbugs
commons-logging:commons-logging:1.3.6=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
commons-logging:commons-logging:1.3.6=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
info.picocli:picocli:4.7.7=checkstyle
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,brokerRelayTestAnnotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,brokerRelayTestAnnotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
io.micrometer:micrometer-commons:1.16.7=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.micrometer:micrometer-observation:1.16.7=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.projectreactor:reactor-core:3.8.7=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
jakarta.activation:jakarta.activation-api:2.1.4=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
jakarta.annotation:jakarta.annotation-api:3.0.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
io.micrometer:micrometer-commons:1.16.7=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
io.micrometer:micrometer-observation:1.16.7=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
io.projectreactor:reactor-core:3.8.7=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
jakarta.activation:jakarta.activation-api:2.1.4=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
jakarta.enterprise:jakarta.enterprise.cdi-api:4.1.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath
jakarta.enterprise:jakarta.enterprise.lang-model:4.1.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath
jakarta.inject:jakarta.inject-api:2.0.1=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath
jakarta.interceptor:jakarta.interceptor-api:2.2.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath
jakarta.servlet:jakarta.servlet-api:6.1.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath
jakarta.transaction:jakarta.transaction-api:2.0.1=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath
jakarta.validation:jakarta.validation-api:3.1.1=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
jakarta.validation:jakarta.validation-api:3.1.1=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
jakarta.websocket:jakarta.websocket-api:2.2.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath
jakarta.websocket:jakarta.websocket-client-api:2.2.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath
jakarta.xml.bind:jakarta.xml.bind-api:4.0.5=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
javax.inject:javax.inject:1=annotationProcessor,brokerRelayTestAnnotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
jakarta.xml.bind:jakarta.xml.bind-api:4.0.5=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
javax.inject:javax.inject:1=annotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
jaxen:jaxen:2.0.6=spotbugs
net.bytebuddy:byte-buddy-agent:1.17.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
net.bytebuddy:byte-buddy:1.17.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
net.java.dev.jna:jna:5.18.1=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
net.minidev:accessors-smart:2.6.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
net.minidev:json-smart:2.6.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.17.8=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.17.8=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
net.java.dev.jna:jna:5.18.1=nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
net.minidev:accessors-smart:2.6.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.minidev:json-smart:2.6.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.sf.saxon:Saxon-HE:12.9=checkstyle,spotbugs
org.antlr:antlr4-runtime:4.13.2=checkstyle
org.apache.bcel:bcel:6.12.0=spotbugs
org.apache.commons:commons-compress:1.28.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
org.apache.commons:commons-lang3:3.20.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,checkstyle,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,spotbugs
org.apache.commons:commons-compress:1.28.0=nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
org.apache.commons:commons-lang3:3.20.0=checkstyle,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,spotbugs
org.apache.commons:commons-text:1.15.0=spotbugs
org.apache.commons:commons-text:1.3=checkstyle
org.apache.httpcomponents:httpclient:4.5.13=checkstyle
org.apache.httpcomponents:httpcore:4.4.16=checkstyle
org.apache.logging.log4j:log4j-api:2.25.5=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,spotbugs,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.logging.log4j:log4j-api:2.25.5=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,spotbugs,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.apache.logging.log4j:log4j-core:2.25.5=spotbugs
org.apache.logging.log4j:log4j-to-slf4j:2.25.5=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.logging.log4j:log4j-to-slf4j:2.25.5=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.apache.maven.doxia:doxia-core:1.12.0=checkstyle
org.apache.maven.doxia:doxia-logging-api:1.12.0=checkstyle
org.apache.maven.doxia:doxia-module-xdoc:1.12.0=checkstyle
org.apache.maven.doxia:doxia-sink-api:1.12.0=checkstyle
org.apache.tomcat.embed:tomcat-embed-core:11.0.24=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-el:11.0.24=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-websocket:11.0.24=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-core:11.0.24=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-el:11.0.24=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-websocket:11.0.24=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.apache.xbean:xbean-reflect:3.7=checkstyle
org.apiguardian:apiguardian-api:1.1.2=brokerRelayTestCompileClasspath,jettyWebSocketTestCompileClasspath,nginxWebSocketTestCompileClasspath,testCompileClasspath,testFixturesCompileClasspath,testkitCompileClasspath
org.assertj:assertj-core:3.27.7=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.awaitility:awaitility:4.3.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apiguardian:apiguardian-api:1.1.2=jettyWebSocketTestCompileClasspath,nginxWebSocketTestCompileClasspath,testCompileClasspath,testFixturesCompileClasspath
org.assertj:assertj-core:3.27.7=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.awaitility:awaitility:4.3.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.codehaus.plexus:plexus-classworlds:2.6.0=checkstyle
org.codehaus.plexus:plexus-component-annotations:2.1.0=checkstyle
org.codehaus.plexus:plexus-container-default:2.1.0=checkstyle
@@ -131,92 +131,91 @@ org.eclipse.jetty:jetty-server:12.1.12=jettyWebSocketTestCompileClasspath,jettyW
org.eclipse.jetty:jetty-session:12.1.12=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath
org.eclipse.jetty:jetty-util:12.1.12=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath
org.eclipse.jetty:jetty-xml:12.1.12=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath
org.hamcrest:hamcrest:3.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.hibernate.validator:hibernate-validator:9.0.1.Final=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.hamcrest:hamcrest:3.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hibernate.validator:hibernate-validator:9.0.1.Final=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.javassist:javassist:3.28.0-GA=checkstyle
org.jboss.logging:jboss-logging:3.6.3.Final=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.jetbrains:annotations:17.0.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
org.jspecify:jspecify:1.0.1=annotationProcessor,brokerRelayTestAnnotationProcessor,brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,checkstyle,compileClasspath,jettyWebSocketTestAnnotationProcessor,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestAnnotationProcessor,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testFixturesAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitAnnotationProcessor,testkitCompileClasspath,testkitRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:6.0.3=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:6.0.3=brokerRelayTestRuntimeClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:6.0.3=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.junit.jupiter:junit-jupiter:6.0.3=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.junit.platform:junit-platform-commons:6.0.3=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.junit.platform:junit-platform-engine:6.0.3=brokerRelayTestRuntimeClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestRuntimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
org.junit.platform:junit-platform-launcher:6.0.3=brokerRelayTestRuntimeClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
org.junit:junit-bom:6.0.3=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.jboss.logging:jboss-logging:3.6.3.Final=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.jetbrains:annotations:17.0.0=nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
org.jspecify:jspecify:1.0.1=annotationProcessor,checkstyle,compileClasspath,jettyWebSocketTestAnnotationProcessor,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestAnnotationProcessor,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testFixturesAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:6.0.3=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:6.0.3=jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestRuntimeClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:6.0.3=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter:6.0.3=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:6.0.3=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:6.0.3=jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestRuntimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-launcher:6.0.3=jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestRuntimeClasspath,testRuntimeClasspath
org.junit:junit-bom:6.0.3=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.junit:junit-bom:6.1.0=spotbugs
org.mockito:mockito-core:5.20.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,mockitoAgent,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.mockito:mockito-junit-jupiter:5.20.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.objenesis:objenesis:3.3=brokerRelayTestRuntimeClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.osgi:org.osgi.annotation.bundle:2.0.0=brokerRelayTestCompileClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,nginxWebSocketTestCompileClasspath,testCompileClasspath,testkitCompileClasspath
org.osgi:org.osgi.annotation.versioning:1.1.2=brokerRelayTestCompileClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,nginxWebSocketTestCompileClasspath,testCompileClasspath,testkitCompileClasspath
org.osgi:org.osgi.resource:1.0.0=brokerRelayTestCompileClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,nginxWebSocketTestCompileClasspath,testCompileClasspath,testkitCompileClasspath
org.osgi:org.osgi.service.serviceloader:1.0.0=brokerRelayTestCompileClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,nginxWebSocketTestCompileClasspath,testCompileClasspath,testkitCompileClasspath
org.mockito:mockito-core:5.20.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,mockitoAgent,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-junit-jupiter:5.20.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.objenesis:objenesis:3.3=jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestRuntimeClasspath,testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,jettyWebSocketTestCompileClasspath,nginxWebSocketTestCompileClasspath,testCompileClasspath
org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,jettyWebSocketTestCompileClasspath,nginxWebSocketTestCompileClasspath,testCompileClasspath
org.osgi:org.osgi.resource:1.0.0=compileClasspath,jettyWebSocketTestCompileClasspath,nginxWebSocketTestCompileClasspath,testCompileClasspath
org.osgi:org.osgi.service.serviceloader:1.0.0=compileClasspath,jettyWebSocketTestCompileClasspath,nginxWebSocketTestCompileClasspath,testCompileClasspath
org.ow2.asm:asm-analysis:9.10.1=spotbugs
org.ow2.asm:asm-commons:9.10.1=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,spotbugs
org.ow2.asm:asm-tree:9.10.1=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,spotbugs
org.ow2.asm:asm-util:9.10.1=spotbugs
org.ow2.asm:asm:9.10.1=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,spotbugs
org.ow2.asm:asm:9.7.1=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.pcollections:pcollections:4.0.1=annotationProcessor,brokerRelayTestAnnotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor
org.reactivestreams:reactive-streams:1.0.4=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.ow2.asm:asm:9.7.1=nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.pcollections:pcollections:4.0.1=annotationProcessor,jettyWebSocketTestAnnotationProcessor,nginxWebSocketTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor
org.reactivestreams:reactive-streams:1.0.4=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.reflections:reflections:0.10.2=checkstyle
org.rnorth.duct-tape:duct-tape:1.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
org.skyscreamer:jsonassert:1.5.3=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.slf4j:jul-to-slf4j:2.0.18=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.slf4j:slf4j-api:2.0.18=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.rnorth.duct-tape:duct-tape:1.0.8=nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
org.skyscreamer:jsonassert:1.5.3=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:jul-to-slf4j:2.0.18=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.slf4j:slf4j-api:2.0.18=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j
org.slf4j:slf4j-simple:2.0.18=checkstyle
org.springframework.boot:spring-boot-autoconfigure:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-autoconfigure:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-configuration-processor:4.0.8=annotationProcessor
org.springframework.boot:spring-boot-http-converter:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-jackson:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-http-converter:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jackson:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jetty:4.0.8=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath
org.springframework.boot:spring-boot-resttestclient:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-servlet:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson-test:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-resttestclient:4.0.8=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-servlet:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson-test:4.0.8=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jetty-runtime:4.0.8=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath
org.springframework.boot:spring-boot-starter-jetty:4.0.8=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath
org.springframework.boot:spring-boot-starter-logging:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-test:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-validation:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc-test:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-websocket:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-test-autoconfigure:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-test:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-tomcat:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-validation:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-web-server:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-webmvc-test:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-webmvc:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-websocket:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot:4.0.8=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-aop:7.0.9=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-beans:7.0.9=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-context:7.0.9=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-core:7.0.9=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-expression:7.0.9=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-messaging:7.0.9=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-test:7.0.9=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-web:7.0.9=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-webflux:7.0.9=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-webmvc:7.0.9=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-websocket:7.0.9=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.testcontainers:testcontainers-junit-jupiter:2.0.5=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
org.testcontainers:testcontainers-rabbitmq:2.0.5=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath
org.testcontainers:testcontainers:2.0.5=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
org.springframework.boot:spring-boot-starter-logging:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-test:4.0.8=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-validation:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc-test:4.0.8=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-websocket:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-test-autoconfigure:4.0.8=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-test:4.0.8=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-tomcat:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-validation:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-web-server:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-webmvc-test:4.0.8=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-webmvc:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-websocket:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot:4.0.8=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-aop:7.0.9=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-beans:7.0.9=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-context:7.0.9=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-core:7.0.9=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-expression:7.0.9=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-messaging:7.0.9=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-test:7.0.9=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-web:7.0.9=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-webflux:7.0.9=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-webmvc:7.0.9=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-websocket:7.0.9=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
org.testcontainers:testcontainers-junit-jupiter:2.0.5=nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
org.testcontainers:testcontainers:2.0.5=nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath
org.xmlresolver:xmlresolver:5.3.3=checkstyle,spotbugs
org.xmlunit:xmlunit-core:2.10.4=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.yaml:snakeyaml:2.5=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
tools.jackson.core:jackson-core:3.1.5=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
tools.jackson.core:jackson-databind:3.1.5=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
tools.jackson.dataformat:jackson-dataformat-cbor:3.1.5=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
tools.jackson:jackson-bom:3.1.5=brokerRelayTestCompileClasspath,brokerRelayTestRuntimeClasspath,compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.xmlunit:xmlunit-core:2.10.4=jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.yaml:snakeyaml:2.5=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
tools.jackson.core:jackson-core:3.1.5=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
tools.jackson.core:jackson-databind:3.1.5=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
tools.jackson.dataformat:jackson-dataformat-cbor:3.1.5=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson:jackson-bom:3.1.5=compileClasspath,jettyWebSocketTestCompileClasspath,jettyWebSocketTestRuntimeClasspath,nginxWebSocketTestCompileClasspath,nginxWebSocketTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
empty=
+14 -8
View File
@@ -101,14 +101,20 @@ adaptation rationale are in
Focused tests use fakes for contract, key, catalog, and typed-facade behavior. R1/R2 promotion
requires a separate real Redis service lane; it may never be silently skipped when selected.
`redisTopologyTest` is the only real-server lane. It is opt-in and fail-closed in seven ways: the
lane must be one of `standalone`, `sentinel`, `cluster`, `tls`; the endpoint properties must be
present (`sentinel` additionally needs `redis.topology.master`, `tls` needs
`redis.topology.trust-material`); a test class carrying the lane's tag must exist; a run that
executes zero tests fails; the classes the lane exists to run must actually have run; the executed
count must reach the lane's declared floor; and a skipped test fails the run rather than counting
as executed. `tls` is a lane, not a deployment mode — its shape is standalone and the task maps it
so, because what it qualifies is the transport.
`redisTopologyTest` is the only real-server lane, declared through `ca.strict-test-lane`. It is
opt-in and fail-closed in five ways: the lane must be one of `standalone`, `sentinel`, `cluster`,
`tls`; the endpoint properties must be present (`sentinel` additionally needs
`redis.topology.master`, `tls` needs `redis.topology.trust-material`); a run that discovers or
executes zero tests fails (the convention, not this leaf, owns that); the classes the lane exists to
run must actually have run; and a skipped test fails the run rather than counting as executed.
`tls` is a lane, not a deployment mode — its shape is standalone and the task maps it so, because
what it qualifies is the transport.
Two former checks are gone. A scan of the test sources for the literal text `@Tag("redis-topology")`
only improved the message for a failure `failOnNoDiscoveredTests` already produced, and a comment
containing the same text satisfied it. A hand-maintained per-mode floor on the executed count
(20/20/24/4) had to be edited whenever a case was added or removed, and the thing it failed on —
"coverage shrank" — is not a runtime, deployment, data, security or compile failure.
## Composition
+115 -123
View File
@@ -4,12 +4,19 @@
// registry outranks that layout, so the module boundaries are packages under
// dev.caskeleton.adapter.outbound.cache.redis.sdk and RedisSdkModuleBoundaryTest enforces them.
dependencies {
// Registered edges the semantic port adapters need. The SDK itself imports nothing from them
// today (0 imports across main source) — the semantic cache/session/idempotency/rate-limit
// adapters that did were removed and are restored by Phase E of
// docs/superpowers/plans/2026-08-10-redis-optionality-and-composition.md. They stay declared
// because that restoration is the module's stated responsibility, not because anything here
// compiles against them.
// 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
// and are restored by Phase E of
// docs/superpowers/plans/2026-08-10-redis-optionality-and-composition.md.
//
// Two of the three are nonetheless load-bearing right now, which the earlier wording hid: this
// leaf declares no `testImplementation project(...)`, so `test` reaches
// dev.caskeleton.application.* and dev.caskeleton.shared.* through these `implementation` edges
// alone. Dropping either breaks compileTestJava today, not at Phase E.
//
// ':adapter:outbound:support' is the one with no reference in any source set; it stays for the
// stated restoration reason, and that is the only one of the three for which that reason is
// doing the work.
implementation project(':application-core')
implementation project(':shared-contract')
implementation project(':adapter:outbound:support')
@@ -39,8 +46,6 @@ dependencies {
// Zero imports.
}
tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' }
// The topology lane is opt-in and fail-closed. The default unit task excludes it, and selecting it
// without an endpoint is an error rather than a skip: a topology test that silently passes because
// it never connected is worse than not having one.
@@ -68,9 +73,20 @@ tasks.named('test') {
def REDIS_TOPOLOGY_MODES = ['standalone', 'sentinel', 'cluster', 'tls'] as Set
def REDIS_TOPOLOGY_DEPLOYMENT_MODE = ['standalone': 'standalone', 'sentinel': 'sentinel',
'cluster': 'cluster', 'tls': 'standalone']
// The classes each lane exists to run, and the floor below which its coverage has shrunk. Both are
// declarations rather than observations: a lane that lost a class to a rename, or lost half its
// cases to a filter, otherwise still reports success.
// The classes each lane exists to run. A declaration, not an observation: a lane that lost a class
// to a rename otherwise still reports success on whatever remains.
//
// This list stays hand-written and that is a deliberate refusal, not an oversight. The repository's
// own mechanism for "a named test must actually have run" is `strictTestLanes { lane { requires(…) } }`,
// and ca.strict-test-lane refuses a lane that declares both a tag and required tests — "pick one
// selection". This lane's selection is a tag expression computed from the mode, so `requires` is not
// available to it. Deriving the list instead would mean reading @Tag off the compiled test classes,
// which is a bytecode dependency a leaf build file should not grow.
//
// What did go: REDIS_TOPOLOGY_MINIMUM_TESTS, a per-mode floor of 20/20/24/4 that had to be edited
// whenever a case was added or removed, and whose failure sentence was "coverage shrank" — not a
// runtime, deployment, data, security or compile failure. The class list below covers the case that
// mattered (a lane class silently leaving the lane); the number did not add a second one.
def REDIS_TOPOLOGY_REQUIRED_CLASSES = [
'standalone': ['LiveRedisCompositionTest', 'LiveRedisSemanticPortsTest',
'RedisTopologyContractTest', 'LiveRedisGuardrailTest'],
@@ -80,123 +96,99 @@ def REDIS_TOPOLOGY_REQUIRED_CLASSES = [
'LiveRedisClusterTransactionTest', 'LiveRedisSemanticPortsTest'],
'tls' : ['LiveRedisTlsTest'],
]
def REDIS_TOPOLOGY_MINIMUM_TESTS = ['standalone': 20, 'sentinel': 20, 'cluster': 24, 'tls': 4]
tasks.register('redisTopologyTest', Test) {
description = 'Runs the Redis SDK contracts against a real topology declared in infra/redis-sdk.'
group = 'verification'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
// Never up to date. This task's result depends on a server outside the build, so Gradle's
// inputs say nothing about whether it would still pass: re-running it against a lane that was
// restarted, reconfigured, or promoted reports the previous run's verdict as the current one.
// That is the same silent-pass failure mode the fail-closed endpoint check exists to prevent.
outputs.upToDateWhen { false }
def declaredMode = (project.findProperty('redis.topology.mode') ?: 'unset').toString().toLowerCase()
useJUnitPlatform {
includeTags "redis-topology & lane-${declaredMode}".toString()
}
// A filter that matches nothing is a configuration mistake, never a pass.
failOnNoDiscoveredTests = true
['redis.topology.host', 'redis.topology.port',
'redis.topology.master', 'redis.topology.username', 'redis.topology.password',
'redis.topology.trust-material']
.each { key ->
if (project.hasProperty(key)) {
systemProperty key, project.property(key)
String declaredMode = (project.findProperty('redis.topology.mode') ?: 'unset').toString().toLowerCase()
// Declared through the convention rather than hand-rolled. `ca.strict-test-lane` owns
// testClassesDirs, classpath, the tag filter, failOnNoDiscoveredTests, the refusal to serve an
// up-to-date result, and the "executed nothing" check — the same six things this task spelled out.
// What stays here is what is true of this lane only: the mode allowlist, the endpoint properties and
// the class-coverage check.
//
// The @Tag source-text scan that used to sit in `doFirst` is gone. It read every .java file in the
// test source set looking for the literal strings `@Tag("redis-topology")` and `@Tag("lane-<mode>")`,
// which a comment satisfied and a tag held in a constant defeated — and by its own comment it only
// improved the message for a failure `failOnNoDiscoveredTests` already produces.
strictTestLanes {
lane('redisTopologyTest') {
tag = "redis-topology & lane-${declaredMode}".toString()
description = 'Runs the Redis SDK contracts against a real topology declared in infra/redis-sdk.'
customize = { test ->
['redis.topology.host', 'redis.topology.port',
'redis.topology.master', 'redis.topology.username', 'redis.topology.password',
'redis.topology.trust-material']
.each { String key ->
if (project.hasProperty(key)) {
test.systemProperty key, project.property(key)
}
}
// The lane name and the deployment mode are different things, and only the TLS lane makes
// that visible: its shape is standalone, so the tests must see `standalone` while the tag
// filter and the required properties come from the lane. Passing the lane name through as
// the mode would fail RedisDeploymentMode.valueOf on a value that is not a topology.
test.systemProperty 'redis.topology.mode',
REDIS_TOPOLOGY_DEPLOYMENT_MODE.getOrDefault(declaredMode, declaredMode)
test.systemProperty 'redis.topology.tls', (declaredMode == 'tls').toString()
// Executed, not merely reported. `afterTest` fires for a skipped test too, so counting
// every callback would let a lane whose tests all skipped satisfy the checks below.
def skipped = new java.util.concurrent.atomic.AtomicInteger()
def classes = java.util.Collections.synchronizedSet(new java.util.LinkedHashSet<String>())
test.afterTest { descriptor, result ->
if (result.resultType == org.gradle.api.tasks.testing.TestResult.ResultType.SKIPPED) {
skipped.incrementAndGet()
} else {
classes.add(descriptor.className.tokenize('.').last())
}
}
// The lane name and the deployment mode are different things, and only the TLS lane makes that
// visible: its shape is standalone, so the tests must see `standalone` while the tag filter and
// the required properties come from the lane. Passing the lane name through as the mode would
// fail RedisDeploymentMode.valueOf on a value that is not a topology.
systemProperty 'redis.topology.mode', REDIS_TOPOLOGY_DEPLOYMENT_MODE.getOrDefault(declaredMode, declaredMode)
systemProperty 'redis.topology.tls', (declaredMode == 'tls').toString()
// Executed, not merely reported. `afterTest` fires for a skipped test too, so counting every
// callback meant a lane whose tests all skipped could still satisfy the "ran something" check —
// the exact green-for-nothing this gate exists to prevent, one level further in.
def executed = new java.util.concurrent.atomic.AtomicInteger()
def skipped = new java.util.concurrent.atomic.AtomicInteger()
def classes = java.util.Collections.synchronizedSet(new java.util.LinkedHashSet<String>())
afterTest { descriptor, result ->
if (result.resultType == org.gradle.api.tasks.testing.TestResult.ResultType.SKIPPED) {
skipped.incrementAndGet()
} else {
executed.incrementAndGet()
classes.add(descriptor.className.tokenize('.').last())
}
}
test.doFirst {
if (!REDIS_TOPOLOGY_MODES.contains(declaredMode)) {
throw new GradleException(
"redisTopologyTest was selected with redis.topology.mode='${declaredMode}'; " +
'the supported modes are ' + REDIS_TOPOLOGY_MODES.sort().join(', ') +
'. An unrecognised mode selects no test and would otherwise report success.')
}
def required = ['redis.topology.host', 'redis.topology.port']
if (declaredMode == 'sentinel') {
required += 'redis.topology.master'
}
if (declaredMode == 'tls') {
// Without the trust material the client would have to disable verification to
// connect, and a TLS lane that trusts anything qualifies nothing.
required += 'redis.topology.trust-material'
}
def missing = required.findAll { !project.hasProperty(it) }
if (!missing.isEmpty()) {
throw new GradleException(
'redisTopologyTest was selected without ' + missing.join(', ') +
'; start a lane from infra/redis-sdk and pass -P<key>=<value>.')
}
}
doFirst {
if (!REDIS_TOPOLOGY_MODES.contains(declaredMode)) {
throw new GradleException(
"redisTopologyTest was selected with redis.topology.mode='${declaredMode}'; " +
'the supported modes are ' + REDIS_TOPOLOGY_MODES.sort().join(', ') +
'. An unrecognised mode selects no test and would otherwise report success.')
test.doLast {
// What a lane must cover, named rather than counted by accident. A tag filter matching
// one trivial class satisfied "ran something" while the class the lane exists for had
// been renamed out of the filter, and nothing said so.
def absent = REDIS_TOPOLOGY_REQUIRED_CLASSES[declaredMode].findAll {
!classes.contains(it)
}
if (!absent.isEmpty()) {
throw new GradleException(
"redisTopologyTest ran the ${declaredMode} lane without ${absent.join(', ')}. " +
'These classes are what the lane qualifies; a run that skipped them proves ' +
'less than the lane claims.')
}
if (skipped.get() > 0) {
throw new GradleException(
"redisTopologyTest skipped ${skipped.get()} test(s) on the ${declaredMode} " +
'lane. A qualification lane has no conditional coverage: what it cannot prove ' +
'must not be selected, and what is selected must run.')
}
test.logger.lifecycle(
"redisTopologyTest: ${declaredMode} lane covered ${classes.size()} class(es).")
}
}
def required = ['redis.topology.host', 'redis.topology.port']
if (declaredMode == 'sentinel') {
required += 'redis.topology.master'
}
if (declaredMode == 'tls') {
// Without the trust material the client would have to disable verification to connect,
// and a TLS lane that trusts anything qualifies nothing.
required += 'redis.topology.trust-material'
}
def missing = required.findAll { !project.hasProperty(it) }
if (!missing.isEmpty()) {
throw new GradleException(
'redisTopologyTest was selected without ' + missing.join(', ') +
'; start a lane from infra/redis-sdk and pass -P<key>=<value>.')
}
// The lane's tag must actually exist in the compiled suite. failOnNoDiscoveredTests catches
// an empty run, but this names the cause — a renamed or deleted lane class — instead of
// leaving an operator to guess whether the filter or the server is at fault.
def laneTag = "lane-${declaredMode}"
def tagged = sourceSets.test.allJava.matching { include '**/*.java' }.files.any { file ->
def text = file.text
text.contains('@Tag("redis-topology")') && text.contains("@Tag(\"${laneTag}\")")
}
if (!tagged) {
throw new GradleException(
"redisTopologyTest found no test class tagged 'redis-topology' and " +
"'${laneTag}'. The ${declaredMode} lane has no coverage to run, so a green " +
'result would prove nothing.')
}
}
doLast {
if (executed.get() < 1) {
throw new GradleException(
"redisTopologyTest completed without executing a single test for the " +
"${declaredMode} lane. A qualification lane that runs nothing must not report " +
'success.')
}
// What a lane must cover, named rather than counted by accident. A tag filter matching one
// trivial class satisfied "ran something" while the class the lane exists for had been
// renamed out of the filter, and nothing said so.
def required = REDIS_TOPOLOGY_REQUIRED_CLASSES[declaredMode]
def absent = required.findAll { !classes.contains(it) }
if (!absent.isEmpty()) {
throw new GradleException(
"redisTopologyTest ran the ${declaredMode} lane without ${absent.join(', ')}. " +
'These classes are what the lane qualifies; a run that skipped them proves ' +
'less than the lane claims.')
}
def floor = REDIS_TOPOLOGY_MINIMUM_TESTS[declaredMode]
if (executed.get() < floor) {
throw new GradleException(
"redisTopologyTest executed ${executed.get()} tests for the ${declaredMode} " +
"lane, below the declared floor of ${floor}. Coverage that silently shrank is " +
'a gate that silently weakened.')
}
if (skipped.get() > 0) {
throw new GradleException(
"redisTopologyTest skipped ${skipped.get()} test(s) on the ${declaredMode} " +
'lane. A qualification lane has no conditional coverage: what it cannot prove ' +
'must not be selected, and what is selected must run.')
}
logger.lifecycle("redisTopologyTest: ${declaredMode} lane executed ${executed.get()} tests.")
}
}
+16 -18
View File
@@ -158,7 +158,7 @@ tasks.named('test', Test) {
// and fails closed without it, and the BlockHound lane rewrites core JDK bytecode, which must
// not be imposed on every unit run.
useJUnitPlatform {
excludeTags 'quarantine', 'httpclient-fault', 'httpclient-blockhound'
excludeTags 'httpclient-fault', 'httpclient-blockhound'
}
}
@@ -200,6 +200,14 @@ strictTestLanes {
description = 'Runs the SSRF, credential-leak, and cardinality suite (design §28.5, §28.7).'
customize = { test -> applyContractSelection(test) }
}
// Spring 6.2 / 7.0 compatibility lanes. This repository's Spring Boot 4.0 baseline pins Spring
// Framework 7, so the 6.2 lane verifies the *API surface* the common packages compile against
// rather than executing on a 6.2 distribution; the limitation is recorded in
// docs/httpclient/support-matrix.md instead of being hidden behind a green check.
lane('spring62ApiSurfaceScan') {
tag = 'httpclient-spring62-surface'
description = 'Scans the common packages for Spring 6.2 API-surface confinement. NOT a 6.2 runtime.'
}
// Its own source set rather than a tag, so the source set is the selection.
lane('httpClientPerformanceTest') {
sourceSet = 'httpClientPerformanceTest'
@@ -216,11 +224,15 @@ strictTestLanes {
'(design §28.3).'
customize = { test ->
applyContractSelection(test)
// The upstream image is mutable by default. Passing a digest here is what makes a red
// fault run attributable to this repository rather than to someone else's image push.
// The upstream image is mutable by default. A digest here is what makes a red fault run
// attributable to this repository rather than to someone else's image push — and the
// default said `:latest`, which is the exact thing this comment forbade. The digest is
// the registry manifest digest of the image the lane has been running.
test.systemProperty 'httpclient.fault.httpbin.image',
(project.findProperty('httpclient.fault.httpbin.image')
?: 'kennethreitz/httpbin:latest').toString()
?: 'kennethreitz/httpbin@sha256:' +
'599fe5e5073102dbb0ee3dbb65f049dab44fa9fc251f6835c9990f8fb196a72b')
.toString()
}
}
}
@@ -234,20 +246,6 @@ tasks.register('jmh', JavaExec) {
args '-rf', 'json', '-rff', layout.buildDirectory.file('reports/jmh/result.json').get().asFile.absolutePath
}
// Spring 6.2 / 7.0 compatibility lanes. This repository's Spring Boot 4.0 baseline pins Spring
// Framework 7, so the 6.2 lane verifies the *API surface* the common packages compile against
// rather than executing on a 6.2 distribution; the limitation is recorded in
// docs/httpclient/support-matrix.md instead of being hidden behind a green check.
tasks.register('spring62ApiSurfaceScan', Test) {
group = 'verification'
description = 'Scans the common packages for Spring 6.2 API-surface confinement. NOT a 6.2 runtime.'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform { includeTags 'httpclient-spring62-surface' }
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
}
// Which lanes gate an ordinary build, and which do not.
//
// The specialised lanes existed but hung off nothing: `check` ran only `test`, so the SSRF suite,
@@ -14,7 +14,3 @@ tasks.withType(GroovyCompile).configureEach {
groovyOptions.encoding = 'UTF-8'
options.encoding = 'UTF-8'
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}
@@ -12,7 +12,6 @@ dependencies {
implementation 'org.slf4j:slf4j-api'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
}
tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' }
tasks.withType(Test).configureEach {
systemProperty 'messaging.commonEvidenceSchema',
rootProject.file('config/messaging/evidence/build-evidence-manifest-v1.schema.json')
@@ -48,7 +48,6 @@ dependencies {
testImplementation 'io.projectreactor:reactor-test'
}
tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' }
// The exclusion above, stated as something the build verifies rather than something a comment
// asserts. verifyDependencyPolicy resolves runtimeClasspath and fails if the coordinate is present.
@@ -2,17 +2,30 @@
// provider. Canonical app.object-storage activation is disabled by default. The old whole-byte[]
// filesystem/S3 adapters remain isolated, explicit legacy compatibility only.
//
// software.amazon.awssdk:* versions are NOT managed by the Spring Boot BOM, and this repo has no
// version catalog, so the AWS SDK v2 BOM platform is imported HERE (module scope) using the root
// `ext.awsSdkVersion` SSOT — this keeps the strict-locking blast radius to this module (the shared
// root dependencyManagement block stays awssdk-free), mirroring the grpc module's grpc-bom import.
// software.amazon.awssdk:* versions are NOT managed by the Spring Boot BOM, so the AWS SDK v2 BOM
// platform is imported HERE (module scope) using the root `ext.awsSdkVersion` SSOT — this keeps the
// strict-locking blast radius to this module (the shared root dependencyManagement block stays
// awssdk-free), mirroring the grpc module's grpc-bom import.
//
// 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.
description = 'Outbound adapter: object storage (S3/MinIO + local filesystem)'
// The three qualification source sets compile against `main` only.
//
// They used to name `'test'` as well, which is what ADR-BUILD-001's testFixtures migration exists to
// remove: a source set that reaches into another source set's output directory instead of a
// consumable variant. The migration's answer elsewhere in this repository is `java-test-fixtures`,
// and that is deliberately NOT what happened here, because there is nothing to publish — these three
// source sets import no type from `dev.caskeleton` at all (they drive MinIO and S3 through the AWS
// SDK), so the `test` edge was carrying nothing and a `testFixtures` variant would have been an
// empty one. Verified by compiling all three with the edge removed.
strictTestLanes {
sourceSet('objectStorageMinioContractTest') { compilesAgainst 'main', 'test' }
sourceSet('objectStorageMinioFaultTest') { compilesAgainst 'main', 'test' }
sourceSet('objectStorageAwsQualificationTest') { compilesAgainst 'main', 'test' }
sourceSet('objectStorageMinioContractTest') { compilesAgainst 'main' }
sourceSet('objectStorageMinioFaultTest') { compilesAgainst 'main' }
sourceSet('objectStorageAwsQualificationTest') { compilesAgainst 'main' }
}
dependencyManagement {
+143 -148
View File
@@ -99,89 +99,127 @@ dependencies {
jpaPlatformPerformanceTestRuntimeOnly 'org.postgresql:postgresql'
}
tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' }
def registerPostgreSqlReadinessTest = { String taskName, String testClass ->
tasks.register(taskName, Test) {
group = 'verification'
// The fourteen no-skip PostgreSQL readiness lanes, declared rather than assembled.
//
// They were fourteen calls to a local `tasks.register(..., Test)` factory that re-spelled the five
// lines `ca.strict-test-lane` owns. The convention adds what the factory could not: naming the test
// through `requires(...)` turns on `failOnNoMatchingTests` AND the post-run check that the named
// selector actually executed, so a renamed readiness class fails its lane instead of leaving it
// with nothing to run.
String readinessPackage = 'dev.caskeleton.adapter.outbound.persistence.readiness'
Map<String, String> postgresqlReadinessLanes = [
postgresqlLifecycleIntegrationTest : 'PostgreSqlLifecycleIntegrationTest',
postgresqlSecurityBaselineIntegrationTest : 'PostgreSqlSecurityBaselineIntegrationTest',
postgresqlMigrationIntegrationTest : 'PostgreSqlMigrationIntegrationTest',
postgresqlTransactionIntegrationTest : 'PostgreSqlTransactionIntegrationTest',
postgresqlAggregateIntegrationTest : 'PostgreSqlAggregateIntegrationTest',
postgresqlQueryIntegrationTest : 'PostgreSqlQueryIntegrationTest',
postgresqlIdempotencyIntegrationTest : 'PostgreSqlIdempotencyIntegrationTest',
postgresqlOutboxStorageIntegrationTest : 'PostgreSqlOutboxStorageIntegrationTest',
postgresqlOutboxPollingIntegrationTest : 'PostgreSqlOutboxPollingIntegrationTest',
postgresqlInboxIntegrationTest : 'PostgreSqlInboxIntegrationTest',
postgresqlFileserverMigrationIntegrationTest : 'PostgreSqlFileserverMigrationIntegrationTest',
postgresqlFileserverMetadataIntegrationTest : 'PostgreSqlFileserverMetadataStoreIntegrationTest',
postgresqlFileserverReclamationIntegrationTest : 'PostgreSqlFileserverReclamationIntegrationTest',
// The notification stream is opt-in and lives outside the default Flyway location, so "is it
// applied and promoted" is a real deployment question with a real wrong answer. This lane
// asks it against a real server; the entity-scan half is a unit test.
postgresqlNotificationSchemaActivationIntegrationTest :
'PostgreSqlNotificationSchemaActivationIntegrationTest'
]
postgresqlReadinessLanes.each { String taskName, String simpleName ->
String testClass = "${readinessPackage}.${simpleName}"
strictTestLanes.lane(taskName) {
sourceSet = 'postgresqlIntegrationTest'
description = "Runs the no-skip real PostgreSQL readiness scenario ${testClass}."
testClassesDirs = sourceSets.postgresqlIntegrationTest.output.classesDirs
classpath = sourceSets.postgresqlIntegrationTest.runtimeClasspath
useJUnitPlatform()
filter {
includeTestsMatching testClass
requires(testClass)
customize = { test ->
test.jvmArgs(
'-Duser.timezone=UTC',
"-Djpa.evidence.postgresql.image=${jpaPostgreSqlEvidenceImage}")
}
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs(
'-Duser.timezone=UTC',
"-Djpa.evidence.postgresql.image=${jpaPostgreSqlEvidenceImage}")
}
}
def postgresqlLifecycleIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlLifecycleIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlLifecycleIntegrationTest')
def postgresqlSecurityBaselineIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlSecurityBaselineIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlSecurityBaselineIntegrationTest')
def postgresqlMigrationIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlMigrationIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlMigrationIntegrationTest')
def postgresqlTransactionIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlTransactionIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlTransactionIntegrationTest')
def postgresqlAggregateIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlAggregateIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlAggregateIntegrationTest')
def postgresqlQueryIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlQueryIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlQueryIntegrationTest')
def postgresqlIdempotencyIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlIdempotencyIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlIdempotencyIntegrationTest')
def postgresqlOutboxStorageIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlOutboxStorageIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlOutboxStorageIntegrationTest')
def postgresqlOutboxPollingIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlOutboxPollingIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlOutboxPollingIntegrationTest')
def postgresqlInboxIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlInboxIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlInboxIntegrationTest')
def postgresqlFileserverMigrationIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlFileserverMigrationIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlFileserverMigrationIntegrationTest')
def postgresqlFileserverMetadataIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlFileserverMetadataIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlFileserverMetadataStoreIntegrationTest')
def postgresqlFileserverReclamationIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlFileserverReclamationIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlFileserverReclamationIntegrationTest')
// The notification stream is opt-in and lives outside the default Flyway location, so "is it
// applied and promoted" is a real deployment question with a real wrong answer. This lane asks it
// against a real server; the entity-scan half is a unit test.
def postgresqlNotificationSchemaActivationIntegrationTest = registerPostgreSqlReadinessTest(
'postgresqlNotificationSchemaActivationIntegrationTest',
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlNotificationSchemaActivationIntegrationTest')
// The rule verifyJpaSecurityFixtures was reaching for is now a selector.
//
// It read PostgreSqlSecurityBaselineIntegrationTest.java as text and failed when three strings were
// absent from it — which three strings sitting in a comment would have satisfied, and which said
// nothing about whether the scenario ran. Naming the method on the lane means the convention fails
// when the runtime-role denial scenario is renamed or stops executing.
strictTestLanes.lanes.named('postgresqlSecurityBaselineIntegrationTest').configure { lane ->
lane.requires("${readinessPackage}.PostgreSqlSecurityBaselineIntegrationTest" +
'.runtimeRoleCannotCreateInApplicationSchema')
}
def verifyJpaSqlConstructionSafety = tasks.register('verifyJpaSqlConstructionSafety') {
// The task itself stays, and its name is not negotiable: config/jpa/readiness-cards.yaml lists it as
// a support task of the `jpa-security-baseline` card, and gradle/jpa-evidence.gradle resolves every
// listed path through `tasks.findByName` and fails the build when one is missing. What changed is
// what it checks. Grepping the fixture's source text for 'runtimeRoleCannotCreateInApplicationSchema',
// 'assertDockerAvailable' and '42501' passed on three strings in a comment and proved nothing about
// execution; the lane's `requires(...)` above is the thing that now enforces the scenario, so this
// task verifies that the enforcement is declared rather than re-deriving it from source text.
tasks.register('verifyJpaSecurityFixtures') {
group = 'verification'
description = 'Rejects concatenated SQL construction and non-parameterized PostgreSQL timeout configuration.'
File vendorSource = file('src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql')
inputs.dir(vendorSource)
description = 'Verifies the no-skip PostgreSQL security lane still names the runtime-role namespace denial scenario.'
String laneName = 'postgresqlSecurityBaselineIntegrationTest'
String requiredSelector = "${readinessPackage}.PostgreSqlSecurityBaselineIntegrationTest" +
'.runtimeRoleCannotCreateInApplicationSchema'
// A live reference to the lane spec's own list, captured at configuration time. Reading it in
// `doLast` therefore sees the final declaration without touching `Task.project` at execution
// time, which Gradle 9 deprecates and the `--warning-mode=fail` gates reject.
List<String> declaredSelectors = strictTestLanes.lanes.getByName(laneName).requiredTests
doLast {
if (!declaredSelectors.contains(requiredSelector)) {
throw new GradleException(
"verifyJpaSecurityFixtures: strict test lane '${laneName}' no longer requires " +
"'${requiredSelector}'. Without that selector the lane can run the " +
'security baseline class with the runtime-role namespace denial scenario ' +
"renamed or deleted and still report success. It requires ${declaredSelectors}.")
}
logger.lifecycle(
"verifyJpaSecurityFixtures: OK — '${laneName}' requires the runtime-role namespace denial scenario.")
}
}
// verifyJpaSqlConstructionSafety keeps its name for the same registry reason, and gives up the half
// of its job that a real tool already does.
//
// It used to also match `(createNativeQuery|queryForObject|update)\s*\([^;]*"\s*\+` against Java
// source text: a regex that matches any method named `update`, and that stops at the first `;`
// inside a string literal, so it over- and under-reported at once. Concatenated SQL is covered
// repo-wide and inter-procedurally by FindSecBugs, which the root build puts on every leaf
// (`spotbugsPlugins libs.findsecbugs.plugin`) with SpotBugs' `ignoreFailures` left at its blocking
// default and no SQL_INJECTION / SQL_NONCONSTANT exclusion in config/spotbugs/exclude.xml. A
// bytecode dataflow check with no package restriction is strictly better than that regex, so the
// regex is gone rather than duplicated.
//
// What no tool covers is the PostgreSQL-specific rule: a `set_config` value must be bound, never
// interpolated, because that value carries the tenant id and the search_path. That check stays, and
// two things about it changed. It no longer parses Java — it matches the SQL token `set_config('`
// and asks whether the same line binds a parameter. And it scans the whole main source root: it was
// pinned to `.../persistence/postgresql`, which is why it never saw the two real call sites in
// experimental/rls/RlsTenantSessionBinder.java and
// experimental/schema/SchemaMultiTenantConnectionProvider.java.
tasks.register('verifyJpaSqlConstructionSafety') {
group = 'verification'
description = 'Rejects non-parameterized PostgreSQL set_config values anywhere in this leaf.'
File mainSource = file('src/main/java')
inputs.dir(mainSource)
doLast {
List<String> violations = []
vendorSource.eachFileRecurse { File source ->
mainSource.eachFileRecurse { File source ->
if (!source.name.endsWith('.java')) {
return
}
String text = source.getText('UTF-8')
def concatenatedSql = text =~ /(?s)(createNativeQuery|queryForObject|update)\s*\([^;]*"\s*\+/
if (concatenatedSql.find()) {
violations << "${source}: concatenated SQL construction"
}
source.readLines().eachWithIndex { String line, int index ->
String trimmed = line.trim()
// Javadoc and line comments mention set_config to explain why it is used; a comment
// is not a call site, and treating one as a violation is how a correct build turns
// red for a sentence.
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) {
return
}
if (line.contains("set_config('") && !line.contains('?')) {
violations << "${source}:${index + 1}: set_config value is not parameterized"
}
@@ -193,36 +231,13 @@ def verifyJpaSqlConstructionSafety = tasks.register('verifyJpaSqlConstructionSaf
violations.join('\n '))
}
logger.lifecycle(
'verifyJpaSqlConstructionSafety: OK — no concatenated SQL construction and all set_config values are parameterized.')
"verifyJpaSqlConstructionSafety: OK — every set_config value in ${mainSource} binds a parameter.")
}
}
def verifyJpaSecurityFixtures = tasks.register('verifyJpaSecurityFixtures') {
group = 'verification'
description = 'Verifies the no-skip PostgreSQL security fixture covers runtime-role namespace denial.'
File fixture = file(
'src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/readiness/PostgreSqlSecurityBaselineIntegrationTest.java')
inputs.file(fixture)
doLast {
if (!fixture.isFile()) {
throw new GradleException("verifyJpaSecurityFixtures: missing ${fixture}")
}
String text = fixture.getText('UTF-8')
['runtimeRoleCannotCreateInApplicationSchema', 'assertDockerAvailable', '42501'].each {
String required ->
if (!text.contains(required)) {
throw new GradleException(
"verifyJpaSecurityFixtures: ${fixture} is missing '${required}'")
}
}
logger.lifecycle(
'verifyJpaSecurityFixtures: OK — no-skip Docker and runtime-role namespace denial fixtures are present.')
}
}
postgresqlSecurityBaselineIntegrationTest.configure {
dependsOn verifyJpaSqlConstructionSafety
dependsOn verifyJpaSecurityFixtures
tasks.named('postgresqlSecurityBaselineIntegrationTest') {
// Cross-leaf task edge: see the handoff. verifyCleanArchitectureDependencies inspects
// configurations, not the task graph, so this edge is invisible to it.
dependsOn project(':adapter:inbound:web').tasks.named('jpaPersistenceRedactionContractTest')
}
@@ -233,46 +248,33 @@ postgresqlSecurityBaselineIntegrationTest.configure {
// Every lane fails closed. `failOnNoDiscoveredTests` matters more here than usual: a selected lane
// that discovers nothing reports success, and a contract suite that silently stopped running is
// indistinguishable from one that passes.
Closure<Void> registerJpaPlatformLane = { String taskName, String tag, String description ->
tasks.register(taskName, Test) {
group = 'verification'
it.description = description
testClassesDirs = sourceSets.postgresqlIntegrationTest.output.classesDirs
classpath = sourceSets.postgresqlIntegrationTest.runtimeClasspath
useJUnitPlatform {
includeTags tag
Map<String, List<String>> jpaPlatformLanes = [
jpaPlatformContractTest : ['jpa-contract',
'Runs the JPA platform contract suite against real PostgreSQL (design §40).'],
jpaPlatformMigrationTest : ['jpa-migration',
'Runs the Flyway upgrade snapshot scenarios (design §31).'],
jpaPlatformFailureTest : ['jpa-failure',
'Reproduces deadlock, serialization, and commit-ambiguity failures (design §39).'],
jpaPlatformQueryPlanTest : ['jpa-queryplan',
'Asserts query plan structure and planner estimate error (design §33).'],
jpaPlatformSecurityTest : ['jpa-security',
'Verifies runtime role privileges and search_path safety (design §36).']
]
jpaPlatformLanes.each { String taskName, List<String> spec ->
strictTestLanes.lane(taskName) {
sourceSet = 'postgresqlIntegrationTest'
tag = spec[0]
description = spec[1]
customize = { test ->
test.jvmArgs('-Duser.timezone=UTC')
// The Stable matrix selection. An unknown or empty value is an error in
// PostgreSqlVersion.parseSelection rather than an empty run.
test.systemProperty 'jpa.matrix.versions',
(project.findProperty('jpa.matrix.versions') ?: '16').toString()
}
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs('-Duser.timezone=UTC')
// The Stable matrix selection. An unknown or empty value is an error in
// PostgreSqlVersion.parseSelection rather than an empty run.
systemProperty 'jpa.matrix.versions',
(project.findProperty('jpa.matrix.versions') ?: '16').toString()
}
}
def jpaPlatformContractTest = registerJpaPlatformLane(
'jpaPlatformContractTest',
'jpa-contract',
'Runs the JPA platform contract suite against real PostgreSQL (design §40).')
def jpaPlatformMigrationTest = registerJpaPlatformLane(
'jpaPlatformMigrationTest',
'jpa-migration',
'Runs the Flyway upgrade snapshot scenarios (design §31).')
def jpaPlatformFailureTest = registerJpaPlatformLane(
'jpaPlatformFailureTest',
'jpa-failure',
'Reproduces deadlock, serialization, and commit-ambiguity failures (design §39).')
def jpaPlatformQueryPlanTest = registerJpaPlatformLane(
'jpaPlatformQueryPlanTest',
'jpa-queryplan',
'Asserts query plan structure and planner estimate error (design §33).')
def jpaPlatformSecurityTest = registerJpaPlatformLane(
'jpaPlatformSecurityTest',
'jpa-security',
'Verifies runtime role privileges and search_path safety (design §36).')
// The pool behaviour contract. Named for what it does.
//
// It was `jpaPlatformPerformanceTest`, described as certifying pool and REQUIRES_NEW pressure, and
@@ -289,15 +291,12 @@ def jpaPlatformSecurityTest = registerJpaPlatformLane(
// — and this name does not promise a number nobody measured. A real performance gate needs a
// dedicated runner, warmup and sample counts, and recorded thresholds; when that exists it belongs
// in a lane of its own rather than behind a boolean on this one.
def jpaPlatformPoolContractTest = tasks.register('jpaPlatformPoolContractTest', Test) {
group = 'verification'
description = 'Verifies Hikari pool and REQUIRES_NEW connection behaviour (design §38).'
testClassesDirs = sourceSets.jpaPlatformPerformanceTest.output.classesDirs
classpath = sourceSets.jpaPlatformPerformanceTest.runtimeClasspath
useJUnitPlatform()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs('-Duser.timezone=UTC')
strictTestLanes {
lane('jpaPlatformPoolContractTest') {
sourceSet = 'jpaPlatformPerformanceTest'
description = 'Verifies Hikari pool and REQUIRES_NEW connection behaviour (design §38).'
customize = { test -> test.jvmArgs('-Duser.timezone=UTC') }
}
}
// The JPA release gate (design §41). Aggregates every lane whose absence would let one of the
@@ -306,12 +305,8 @@ tasks.register('jpaPlatformReleaseGate') {
group = 'verification'
description = 'Runs every JPA platform lane required for a release (design §41).'
dependsOn tasks.named('test')
dependsOn jpaPlatformContractTest
dependsOn jpaPlatformMigrationTest
dependsOn jpaPlatformFailureTest
dependsOn jpaPlatformQueryPlanTest
dependsOn jpaPlatformSecurityTest
dependsOn jpaPlatformPoolContractTest
jpaPlatformLanes.keySet().each { String laneName -> dependsOn tasks.named(laneName) }
dependsOn tasks.named('jpaPlatformPoolContractTest')
}
// The unit lane reads three files that are not Java sources: the release registry and its two
@@ -105,8 +105,7 @@ Closure<Void> applyMongoImageSelection = { task ->
// comment.
tasks.named('test', Test) {
useJUnitPlatform {
excludeTags 'quarantine',
'mongodb-contract',
excludeTags 'mongodb-contract',
'mongodb-replicaset',
'mongodb-failover',
'mongodb-migration',
@@ -149,6 +148,12 @@ strictTestLanes {
customize = { test -> applyMongoImageSelection(test) }
}
lane('mongoStableContractTest') {
tag = 'mongodb-contract'
description = 'Hermetic stable contract suite: manifests, guardrails, retry scopes, ' +
'redaction (design §30).'
}
// Driven by its own source set rather than a tag: for this shape the source set is the
// selection, so the convention asks for no tag.
lane('mongoPerformanceTest') {
@@ -259,16 +264,6 @@ tasks.register('verifyMongoReleaseContractLanes') {
}
}
tasks.register('mongoStableContractTest', Test) {
group = 'verification'
description = 'Hermetic stable contract suite: manifests, guardrails, retry scopes, ' +
'redaction (design §30).'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform { includeTags 'mongodb-contract' }
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
}
// verifyMongoApiSurface — every public type this leaf exposes is a committed decision.
//
@@ -104,10 +104,17 @@ class MongoModuleBoundaryTest {
java.util.Map.entry("architecture", java.util.Set.of("api")),
java.util.Map.entry("<root>", java.util.Set.of("api", "autoconfigure")));
// DO_NOT_INCLUDE_JARS is deliberately NOT set. On this lane the module's own production classes
// reach the test classpath as a jar rather than as build/classes/java/main, so excluding jars
// emptied the import and every rule below failed with "failed to check any classes" — ten rules
// that read as architecture enforcement while enforcing nothing.
//
// Restricting the import to ROOT is what keeps third-party jars out; the option was never what
// made this scan cheap. The rules are also left WITHOUT allowEmptyShould, so an empty import
// keeps failing loudly instead of passing green.
private static final JavaClasses PLATFORM =
new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
.importPackages(ROOT);
@Test
@@ -4,7 +4,3 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-autoconfigure'
implementation 'org.slf4j:slf4j-api'
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}
@@ -105,18 +105,23 @@ class FileserverPlatformEnvRoundTripTest {
}
/**
* Reads the repository's own {@code src/.env}.
* Reads the repository's tracked {@code src/.env.example}.
*
* <p>A copy in the test resources would drift from the file operators actually use, which is the
* drift this test exists to catch.
* <p>A copy in the test resources would drift from the shipped contract, which is the drift this
* test exists to catch — so it reads the repository file rather than a fixture.
*
* <p>It reads {@code .env.example}, not {@code .env}. {@code .gitignore} states the rule: the real
* {@code .env} is operator input and the examples beside it are the tracked contract. A real
* {@code .env} exists only on a developer machine, so pointing this test at it made the test pass
* locally and fail on every clean checkout — which is where CI runs.
*/
private static List<String> readEnvFile() {
Path fromModule = Path.of(System.getProperty("user.dir")).resolve(".env");
Path env = Files.exists(fromModule) ? fromModule : Path.of("..").resolve(".env");
Path fromModule = Path.of(System.getProperty("user.dir")).resolve(".env.example");
Path env = Files.exists(fromModule) ? fromModule : Path.of("..").resolve(".env.example");
try {
return Files.readAllLines(env);
} catch (IOException exception) {
throw new UncheckedIOException("src/.env could not be read", exception);
throw new UncheckedIOException("src/.env.example could not be read", exception);
}
}
}
@@ -131,7 +131,7 @@ class HttpClientPlatformEnvManifestTest {
@Test
@DisplayName("src/.env ships the platform disabled")
void theShippedEnvironmentKeepsThePlatformOff() {
assertThat(readLines(repositoryRoot().resolve("src/.env")))
assertThat(readLines(repositoryRoot().resolve("src/.env.example")))
.as("src/.env must ship the master switch, and ship it off")
.anySatisfy(line -> assertThat(line.trim()).isEqualTo("APP_HTTPCLIENT_ENABLED=false"));
}
@@ -16,6 +16,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.yaml.snakeyaml.LoaderOptions;
@@ -24,22 +25,27 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
class ConditionalTransportQualificationContractTest {
// 29 pre-existing controls, the eight release-blocking HTTP Client Platform gates (design §38),
// the two the Gradle convention wave registered, the six the final qualification wave found —
// four verification tasks that existed and ran nowhere, and two API-surface gates the convention
// had already wired into check without anyone recording them as controls — and the two runtime
// claims that are real and run in no workflow, registered delegated-pending so the gap is
// tracked.
private static final int EXPECTED_GATE_COUNT = 49;
/**
* Filler gates a well-formed fixture carries beside its one target gate.
*
* <p>Two, and the number does not matter. It used to be one less than a gate count this test and
* the validator both hard-coded, so every fixture had to be built to that size or it failed on
* the count rather than on whatever the test was about. Neither pins a count now; the fillers
* remain only so the duplicate-id and shape cases have a second row to mutate.
*/
private static final int FILLER_GATE_COUNT = 2;
/**
* Filler gates a well-formed fixture needs beside its one target gate.
* The release gate {@code release_blocking: true} is measured against.
*
* <p>Derived rather than written down. The validator refuses a matrix whose size differs from its
* own embedded count, so a fixture built from a stale literal fails for the wrong reason and
* hides whatever the test was actually about.
* <p>Named here and in {@code verify-gate-matrix.sh} rather than inferred from a filename: a
* workflow called "release" is a naming convention, and this job's {@code needs:} is a fact.
*/
private static final int FILLER_GATE_COUNT = EXPECTED_GATE_COUNT - 1;
private static final String RELEASE_GATE_WORKFLOW = "ci-quality-gates.yml";
private static final String RELEASE_GATE_JOB = "release-gate";
private static final Pattern WHITESPACE = Pattern.compile("\\s+");
private static final Duration VALIDATOR_TIMEOUT = Duration.ofSeconds(10);
private static final Set<String> EXPECTED_GATE_FIELDS =
@@ -52,70 +58,6 @@ class ConditionalTransportQualificationContractTest {
"workflow-job",
"delegated-pending");
private static final Set<String> ALLOWED_EXECUTIONS = Set.of("check", "explicit", "job");
private static final Set<String> EXPECTED_GATE_IDS =
Set.of(
"format-lint",
"unit-and-contract-tests",
"conditional-transport-qualification",
"clean-architecture-dependencies",
"environment-contract",
// Registered by the Gradle convention wave. documented-leaf-count had existed as a task
// for months with nothing running it, which is how five module CLAUDE.md files and four
// leaf build files kept claiming a leaf count the registry had long since left behind.
"documented-leaf-count",
"declared-dependency-absence",
// Registered by the final qualification wave. The first three had existed as tasks that
// nothing ran, and the cost of that was measured rather than guessed: twenty-nine
// environment variables and thirteen public types had drifted past them.
"notification-api-surface",
"notification-configuration-contract",
"notification-support-grade-evidence",
"runbook-reference-drift",
"graphql-api-surface",
"mongo-api-surface",
// Broker certification. `CertifiedEvidence` was a hand-authored list and
// "certified against a live broker" was a sentence a developer could type; the gate runs
// the fault lane and compares the committed manifest against what the run produced.
"messaging-broker-certification",
// Real evidence that CI does not run. The Compose matrix is the strongest proof this
// repository produces and no workflow invokes it; the GraphQL JWT claim lives inside it,
// while the registered GraphQL control is a boundary test with in-memory Basic Auth.
"runtime-smoke-matrix",
"graphql-runtime-jwt",
"one-type-per-file",
"readme-command-drift",
"trivy-suppression-governance",
"quarantine-sunset",
"public-path-snapshot",
"dependency-locks",
"architecture-contract-test",
"sample-off",
"gate-matrix-lint",
"redis-sdk",
"jpa-candidate-evidence",
"jpa-r2-evidence",
"quality-release-gate",
"flaky-quarantine",
"dependency-review",
"dependency-submission",
"filesystem-vulnerability-scan",
"documentation-links",
"object-storage-minio-managed-contract",
"poster-image-migration",
"object-storage-minio-managed-fault",
"object-storage-aws-protected-qualification",
"redis-sdk-support-matrix",
"redis-sdk-topology-evidence",
// HTTP Client Platform release gates (design §38).
"httpclient-stable-contract",
"httpclient-security-suite",
"httpclient-fault-injection",
"httpclient-performance-certification",
"httpclient-spring62-api-surface",
"httpclient-spring62-runtime",
"httpclient-spring70-compatibility",
"httpclient-documentation-drift",
"httpclient-event-loop-blocking");
@Test
void ownerQualificationsNameEveryRequiredWireClassAndRootOnlyAggregates() throws IOException {
@@ -170,7 +112,13 @@ class ConditionalTransportQualificationContractTest {
.contains("ref: conditionalTransportQualification")
.contains("job: quality-gates")
.contains("execution: explicit");
assertThat(validator).contains("readonly EXPECTED_GATE_COUNT=" + EXPECTED_GATE_COUNT);
// The count literal is gone and must stay gone. While it existed, adding a control meant
// editing the guard whose stated purpose was to stop the matrix changing, and it caught
// nothing the per-row rules do not: a row whose task, workflow or job has disappeared fails
// below at any matrix size.
assertThat(validator)
.doesNotContain("EXPECTED_GATE_COUNT")
.contains("matrix declares no gates");
}
@Test
@@ -182,13 +130,14 @@ class ConditionalTransportQualificationContractTest {
ScriptResult result = runValidator(fixtureRoot);
int fixtureGateCount = FILLER_GATE_COUNT + 1;
assertThat(result.exitCode()).isZero();
assertThat(result.output())
.contains(
"gate-matrix-lint: "
+ EXPECTED_GATE_COUNT
+ fixtureGateCount
+ " gates, "
+ EXPECTED_GATE_COUNT
+ fixtureGateCount
+ " verified, 0 delegated-pending")
.contains("gate-matrix-lint: OK");
}
@@ -339,19 +288,21 @@ class ConditionalTransportQualificationContractTest {
}
@Test
void validatorRejectsWrongCountDuplicateIdUnregisteredTaskAndMissingJob(@TempDir Path tempDir)
void validatorRejectsEmptyMatrixDuplicateIdUnregisteredTaskAndMissingJob(@TempDir Path tempDir)
throws IOException {
Path shortMatrix =
// A matrix of a particular size is not a property. A matrix of no gates is: the file exists,
// the lint runs, and every per-row rule passes vacuously. That is the one thing the deleted
// count literal protected, and it is kept.
Path emptyMatrix =
writeFixture(
tempDir.resolve("short-matrix"),
tempDir.resolve("empty-matrix"),
"Run target",
"./gradlew targetGate",
FILLER_GATE_COUNT - 1);
ScriptResult shortMatrixResult = runValidator(shortMatrix);
assertThat(shortMatrixResult.exitCode()).isNotZero();
assertThat(shortMatrixResult.output())
.contains(
"matrix has " + (EXPECTED_GATE_COUNT - 1) + " gates; expected " + EXPECTED_GATE_COUNT);
FILLER_GATE_COUNT);
Files.writeString(emptyMatrix.resolve(".github/ci-gate-matrix.yml"), "gates: []\n");
ScriptResult emptyMatrixResult = runValidator(emptyMatrix);
assertThat(emptyMatrixResult.exitCode()).isNotZero();
assertThat(emptyMatrixResult.output()).contains("matrix declares no gates");
Path duplicateId =
writeFixture(
@@ -474,7 +425,9 @@ class ConditionalTransportQualificationContractTest {
assertThat(root.keySet().stream().map(String::valueOf).toList()).containsExactly("gates");
assertThat(root.get("gates")).isInstanceOf(List.class);
List<?> gates = (List<?>) root.get("gates");
assertThat(gates).hasSize(EXPECTED_GATE_COUNT);
// No expected size. A matrix that grew by a row is a registered control, not drift; what has
// to hold is that every row is well-formed, and that is asserted below for all of them.
assertThat(gates).isNotEmpty();
Set<String> ids = new LinkedHashSet<>();
for (Object rawGate : gates) {
@@ -501,7 +454,6 @@ class ConditionalTransportQualificationContractTest {
assertThat(releaseBlocking).isEqualTo("conditional");
}
}
assertThat(ids).containsExactlyInAnyOrderElementsOf(EXPECTED_GATE_IDS);
Map<?, ?> posterGate =
gates.stream()
@@ -515,6 +467,197 @@ class ConditionalTransportQualificationContractTest {
assertThat(requireString(posterGate, "execution")).isEqualTo("explicit");
}
@Test
void everyGateNamesAWorkflowAndJobThatExist() throws IOException {
Path root = repositoryRoot();
for (Map<?, ?> gate : realGates(root)) {
String workflowName = requireString(gate, "workflow");
Path workflowFile = root.resolve(".github/workflows").resolve(workflowName);
assertThat(workflowFile).as("gate '%s' workflow", gate.get("id")).isRegularFile();
Map<?, ?> jobs = requireMapValue(parseYamlMap(workflowFile), "jobs");
assertThat(jobs.keySet().stream().map(String::valueOf).toList())
.as("gate '%s' job in %s", gate.get("id"), workflowName)
.contains(requireString(gate, "job"));
}
}
/**
* {@code release_blocking: true} has to be a fact about the build, not a label.
*
* <p>It was read by nothing but an enum check, so a gate could claim to block a release that no
* job anywhere waited on: the filesystem vulnerability scan was release_blocking and could be red
* while the release gate reported green. A gate earns {@code true} by being required on a path a
* release actually takes — the release gate itself, one of its {@code needs:}, a name in its
* {@code REQUIRED_CHECKS}, or a job in a workflow that only runs on a release tag. Everything
* else is {@code conditional}, which is what the enum is for.
*/
@Test
void everyReleaseBlockingGateIsRequiredBySomeReleaseGate() throws IOException {
Path root = repositoryRoot();
Path releaseGateFile = root.resolve(".github/workflows").resolve(RELEASE_GATE_WORKFLOW);
Map<?, ?> releaseGate =
requireMapValue(requireMapValue(parseYamlMap(releaseGateFile), "jobs"), RELEASE_GATE_JOB);
Set<String> needs = new LinkedHashSet<>();
needs.add(RELEASE_GATE_JOB);
for (Object need : requireListValue(releaseGate, "needs")) {
needs.add(String.valueOf(need));
}
Set<String> requiredChecks = new LinkedHashSet<>();
for (Object rawStep : requireListValue(releaseGate, "steps")) {
assertThat(rawStep).isInstanceOf(Map.class);
Object stepEnvironment = ((Map<?, ?>) rawStep).get("env");
if (!(stepEnvironment instanceof Map<?, ?> environment)) {
continue;
}
Object declared = environment.get("REQUIRED_CHECKS");
if (declared == null) {
continue;
}
WHITESPACE
.splitAsStream(String.valueOf(declared).trim())
.filter(check -> !check.isBlank())
.forEach(requiredChecks::add);
}
assertThat(needs).as("jobs the release gate waits on").hasSizeGreaterThan(1);
assertThat(requiredChecks).as("cross-workflow checks the release gate requires").isNotEmpty();
for (Map<?, ?> gate : realGates(root)) {
if (!"true".equals(String.valueOf(gate.get("release_blocking")))) {
continue;
}
String workflowName = requireString(gate, "workflow");
String job = requireString(gate, "job");
boolean enforced =
(RELEASE_GATE_WORKFLOW.equals(workflowName) && needs.contains(job))
|| requiredChecks.contains(job)
|| runsOnlyForAReleaseTag(root.resolve(".github/workflows").resolve(workflowName));
assertThat(enforced)
.as(
"gate '%s' is release_blocking: true, so %s::%s must be %s::%s, one of its needs, a"
+ " name in its REQUIRED_CHECKS, or a job in a tag-triggered workflow",
gate.get("id"), workflowName, job, RELEASE_GATE_WORKFLOW, RELEASE_GATE_JOB)
.isTrue();
}
}
@Test
void validatorRejectsAReleaseBlockingGateNoReleaseGateRequires(@TempDir Path tempDir)
throws IOException {
Path unrequired =
writeFixture(
tempDir.resolve("unrequired"), "Run target", "./gradlew targetGate", FILLER_GATE_COUNT);
declareTargetGateReleaseBlocking(unrequired);
ScriptResult unrequiredResult = runValidator(unrequired);
assertThat(unrequiredResult.exitCode()).isNotZero();
assertThat(unrequiredResult.output())
.contains(
"gate 'target-gate' is release_blocking: true but no release gate requires job"
+ " 'target-job' in 'fixture.yml'");
Path tagTriggered =
writeFixture(
tempDir.resolve("tag-triggered"),
"Run target",
"./gradlew targetGate",
FILLER_GATE_COUNT);
declareTargetGateReleaseBlocking(tagTriggered);
replaceLiteral(
tagTriggered.resolve(".github/workflows/fixture.yml"),
"on: [push]\n",
"on:\n push:\n tags:\n - \"v*\"\n");
ScriptResult tagTriggeredResult = runValidator(tagTriggered);
assertThat(tagTriggeredResult.output()).contains("gate-matrix-lint: OK");
assertThat(tagTriggeredResult.exitCode()).isZero();
Path requiredCheck =
writeFixture(
tempDir.resolve("required-check"),
"Run target",
"./gradlew targetGate",
FILLER_GATE_COUNT);
declareTargetGateReleaseBlocking(requiredCheck);
Files.writeString(
requiredCheck.resolve(".github/workflows").resolve(RELEASE_GATE_WORKFLOW),
"""
name: fixture-quality-gates
on: [push]
jobs:
release-gate:
runs-on: ubuntu-latest
steps:
- name: Require the cross-workflow release-blocking checks
env:
REQUIRED_CHECKS: target-job
run: echo required
""");
ScriptResult requiredCheckResult = runValidator(requiredCheck);
assertThat(requiredCheckResult.output()).contains("gate-matrix-lint: OK");
assertThat(requiredCheckResult.exitCode()).isZero();
}
private static void declareTargetGateReleaseBlocking(Path fixtureRoot) throws IOException {
replaceLiteral(
fixtureRoot.resolve(".github/ci-gate-matrix.yml"),
"release_blocking: false",
"release_blocking: true");
}
private static boolean runsOnlyForAReleaseTag(Path workflowFile) throws IOException {
Map<?, ?> workflow = parseYamlMap(workflowFile);
// A bare `on:` key is YAML 1.1, where it resolves to the boolean true rather than the string.
Object triggers = workflow.get("on") != null ? workflow.get("on") : workflow.get(true);
if (!(triggers instanceof Map<?, ?> triggerMap)) {
return false;
}
Object push = triggerMap.get("push");
return push instanceof Map<?, ?> pushTrigger && pushTrigger.get("tags") != null;
}
private static List<Map<?, ?>> realGates(Path root) throws IOException {
Object gates = parseYamlMap(root.resolve(".github/ci-gate-matrix.yml")).get("gates");
assertThat(gates).isInstanceOf(List.class);
List<Map<?, ?>> parsed = new ArrayList<>();
for (Object gate : (List<?>) gates) {
assertThat(gate).isInstanceOf(Map.class);
parsed.add((Map<?, ?>) gate);
}
assertThat(parsed).isNotEmpty();
return parsed;
}
private static Map<?, ?> parseYamlMap(Path path) throws IOException {
LoaderOptions options = new LoaderOptions();
options.setAllowDuplicateKeys(false);
options.setMaxAliasesForCollections(0);
Object loaded = new Yaml(new SafeConstructor(options)).load(Files.readString(path));
assertThat(loaded).as("%s", path).isInstanceOf(Map.class);
return (Map<?, ?>) loaded;
}
private static Map<?, ?> requireMapValue(Map<?, ?> parent, String key) {
Object value = parent.get(key);
assertThat(value).as("field %s", key).isInstanceOf(Map.class);
return (Map<?, ?>) value;
}
private static List<?> requireListValue(Map<?, ?> parent, String key) {
Object value = parent.get(key);
assertThat(value).as("field %s", key).isInstanceOf(List.class);
return (List<?>) value;
}
private static void replaceLiteral(Path path, String target, String replacement)
throws IOException {
String original = Files.readString(path);
assertThat(original).contains(target);
int index = original.indexOf(target);
Files.writeString(
path,
original.substring(0, index) + replacement + original.substring(index + target.length()));
}
private static void assertRejectedAsNotExplicit(ScriptResult result) {
assertThat(result.exitCode()).isNotZero();
assertThat(result.output())
@@ -564,7 +707,7 @@ class ConditionalTransportQualificationContractTest {
new StringBuilder()
.append("gates:\n")
.append(" - id: target-gate\n")
.append(" release_blocking: true\n")
.append(" release_blocking: false\n")
.append(" mechanism: gradle-custom-task\n")
.append(" ref: targetGate\n")
.append(" workflow: fixture.yml\n")
@@ -1064,9 +1064,20 @@ class DeveloperExperienceContractTest {
return Files.readString(REPOSITORY_ROOT.resolve(relative));
}
/**
* Reads {@code src/.env.local.example}, the tracked file a developer copies to start the local
* stack — not {@code src/.env}, which is operator input and absent from every clean checkout.
*
* <p>Pointing this at {@code src/.env} meant the port contract below was only ever checked on a
* machine that already had a working setup. It was checking nothing on the one machine where a
* new developer's first run happens. It caught a real drift the moment it was pointed here: the
* example named port 5432 while Compose publishes 5433.
*
* <p>Not {@code .env.example} either — that one deliberately leaves connection values blank.
*/
private static String envValue(String key) throws IOException {
String prefix = key + "=";
return read("src/.env")
return read("src/.env.local.example")
.lines()
.map(String::trim)
.filter(line -> line.startsWith(prefix))
@@ -39,7 +39,7 @@ class EnvProfileMatrixContractTest {
@Test
void prodUnsafeTogglesShipDisabledInEnv() throws IOException {
Path env = resources().requireTrackedFile("src/.env");
Path env = resources().requireTrackedFile("src/.env.example");
String text = Files.readString(env);
assertThat(envValue(text, ERROR_DETAIL_TOGGLE))
@@ -79,7 +79,7 @@ class EnvProfileMatrixContractTest {
+ "SPRING_PROFILES_ACTIVE alone (env-keys.yaml D6, 2026-06-06)")
.isNull();
Path env = resources.requireTrackedFile("src/.env");
Path env = resources.requireTrackedFile("src/.env.example");
assertThat(envValue(Files.readString(env), "APP_PROFILE"))
.as("src/.env must not declare APP_PROFILE — SPRING_PROFILES_ACTIVE is the sole selector")
.isNull();
@@ -111,7 +111,7 @@ class PiiTokenBodyForbiddenContractTest {
@Test
void requestBodyCaptureIsDisabledByDefault() throws IOException {
Path env = RepositoryContractResources.fromSystemProperty().requireTrackedFile("src/.env");
Path env = RepositoryContractResources.fromSystemProperty().requireTrackedFile("src/.env.example");
String value = readEnv(env, "APP_LOG_BODY_CAPTURE_ENABLED");
assertThat(value)
@@ -16,6 +16,7 @@ import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@@ -218,6 +219,16 @@ class ProfileSeparationContractTest {
@Test
void localProfilePinsEveryValueWhoseInlineDefaultDisagreesWithTheCommittedEnvironment()
throws IOException {
// src/.env is operator input and gitignored, so it is absent on every clean checkout and this
// check does not run in CI. That is stated here rather than hidden: the file the launcher
// actually injects is the only thing this contract is about, and comparing against
// .env.example instead would be a green check about a file no launcher ever loads.
//
// It skips rather than passing, so "did not run" never reads as "passed".
Assumptions.assumeTrue(
Files.isRegularFile(REPOSITORY_ROOT.resolve("src/.env")),
"src/.env is absent (gitignored operator input) — this local-only contract cannot run here");
Map<String, String> environment = committedEnvironment();
Map<String, String> localValues = flatten(profile("local"));
@@ -22,7 +22,7 @@ import org.junit.jupiter.api.Test;
class SqlLoggingForbiddenContractTest {
private Properties loadEnv() throws Exception {
Path env = RepositoryContractResources.fromSystemProperty().requireTrackedFile("src/.env");
Path env = RepositoryContractResources.fromSystemProperty().requireTrackedFile("src/.env.example");
Properties props = new Properties();
try (InputStream in = Files.newInputStream(env)) {
props.load(in);
@@ -326,18 +326,30 @@ class MessagingCapabilityRegistryContractTest {
void rootBuildDeclaresEveryFailClosedVerificationTaskThroughTheSharedGuard() throws Exception {
String build = Files.readString(repositorySrcRoot().resolve("build.gradle"));
assertThat(build).contains("messagingFailClosedEvidenceGuard");
// This test protects a contract: every skeleton routes through the shared guard, and that guard
// fails closed. The wording the guard happens to use is not the contract.
//
// It used to assert six individual message fragments from the guard's body. Those fragments
// belonged to ~45 lines of evidence validation whose result was discarded, because the guard
// threw unconditionally either way. When that dead validation was removed the implementation was
// fine and this test broke — the test was pinning source text, not behaviour, which is how a
// guard stops being a guard and becomes a reason not to touch the file.
assertThat(build)
.contains(
"messagingVerificationSkeletons.each",
"tasks.register(taskName)",
"messagingFailClosedEvidenceGuard(taskName, evidencePaths)",
"qualification producer/tests and common-schema validator are not implemented",
"missing evidence",
"contains skipped evidence",
"is stale or future-dated",
"has wrong source digest",
"has mismatched profile hash");
"messagingFailClosedEvidenceGuard(taskName, evidencePaths)");
// The property worth pinning: the guard throws. If it is ever changed to report and continue,
// every messaging R2 skeleton would start passing without a qualification producer existing.
int guardStart = build.indexOf("Closure<Void> messagingFailClosedEvidenceGuard");
assertThat(guardStart).as("the shared guard closure must exist").isNotNegative();
int guardEnd = build.indexOf("\n}", guardStart);
assertThat(guardEnd).as("the shared guard closure must be terminated").isGreaterThan(guardStart);
assertThat(build.substring(guardStart, guardEnd))
.as("the shared guard must fail closed rather than report and continue")
.contains("throw new GradleException")
.contains("FAIL_CLOSED");
for (String taskName : REQUIRED_VERIFICATION_TASKS) {
assertThat(build).contains("'" + taskName + "'");
}
@@ -32,6 +32,22 @@ class ReleaseManifestTaskExistenceTest {
private static final Pattern REGISTERED_TASK =
Pattern.compile("tasks\\.register\\(\\s*'([A-Za-z0-9_]+)'");
/**
* The second way a lane's {@code Test} task comes into existence.
*
* <p>The {@code ca.strict-test-lane} convention registers the task from a {@code lane('name')}
* declaration, so a lane moved onto the convention stops matching {@link #REGISTERED_TASK} while
* still being a perfectly real, runnable task. Matching only {@code tasks.register} made this
* guard report {@code mongoStableContractTest} as missing on the day it was converted — the task
* ran fine; the detector had gone stale.
*
* <p>This is still a text scan, so it stays wrong in the same way for any third registration
* form. It is a cheap guard against a manifest naming a task nobody wrote, not a substitute for
* asking Gradle.
*/
private static final Pattern REGISTERED_LANE =
Pattern.compile("\\blane\\(\\s*'([A-Za-z0-9_]+)'");
/**
* Tasks the Java plugin supplies, which no build file registers explicitly.
*
@@ -87,10 +103,13 @@ class ReleaseManifestTaskExistenceTest {
}
private static List<String> registeredTaskNamesIn(Path buildFile) throws IOException {
Matcher matcher = REGISTERED_TASK.matcher(Files.readString(buildFile));
String source = Files.readString(buildFile);
List<String> names = new ArrayList<>();
while (matcher.find()) {
names.add(matcher.group(1));
for (Pattern pattern : List.of(REGISTERED_TASK, REGISTERED_LANE)) {
Matcher matcher = pattern.matcher(source);
while (matcher.find()) {
names.add(matcher.group(1));
}
}
return names;
}
+14 -3
View File
@@ -13,12 +13,23 @@ without any check noticing.
| `ca.strict-qualification` | qualification lanes that cannot pass without executing every named class, re-checked against the JUnit XML |
| `ca.evidence` | the JUnit XML reader and the no-skip / required-class rules built on it |
| `ca.api-surface` | read-only API surface verification with an explicit, separate update task |
| `ca.testkit-publisher` | a leaf's testkit source set consumers and its optional consumable artifact |
| `ca.dependency-policy` | declared absences, checked against the resolved graph rather than against a comment |
| `ca.runtime-membership` | the resolved runtime project closure against the registry's memberships |
| `ca.platform-module` | what a vendored platform leaf (`messaging:*`, `grpc:*`, `grpc-advanced:*`) is: `java-library` |
| `ca.grpc-platform-module` | `ca.platform-module` plus the module-scope `io.grpc:grpc-bom` import, for the leaves that already had it |
`dev.caskeleton.buildlogic.ModuleRegistry` and `JUnitEvidence` are plain classes rather than plugins,
because settings and projects load plugins through different mechanisms and both need them.
`ca.testkit-publisher` was here and is not any more: every leaf with a `testkit` source set moved to
Gradle's own `java-test-fixtures` (ADR-BUILD-001).
Plain classes rather than plugins, because settings and projects load plugins through different
mechanisms and more than one caller needs each:
| Class | Owns |
| --- | --- |
| `dev.caskeleton.buildlogic.ModuleRegistry` | reading and validating `config/architecture/modules.json`, including which runtime compositions exist |
| `dev.caskeleton.buildlogic.JUnitEvidence` | one JUnit XML reader, with DOCTYPE processing off and no defaulting of absent counts |
| `dev.caskeleton.buildlogic.RequiredTestExecution` | the single decision "a test this build names must actually have run", asked by `ca.strict-test-lane` and by `ca.evidence` |
| `dev.caskeleton.buildlogic.JavaPublicTypes` | public top-level types, parsed by javac rather than matched with a regular expression |
## What the design named and this build does not have
@@ -31,6 +31,15 @@ class ApiSurfaceExtension {
/** Source root scanned for public top-level types. */
String sourceRoot = 'src/main/java'
/**
* Further source roots, for a surface that does not live under one directory.
*
* <p>Project-relative paths, added to {@code sourceRoot}. A platform whose API and its adapter
* are separate directories has one surface and should not need a second implementation of this
* convention to say so.
*/
List<String> additionalSourceRoots = []
}
def apiSurface = extensions.create('apiSurface', ApiSurfaceExtension)
@@ -47,36 +56,44 @@ def updateName = { "update${apiSurface.label}ApiSurface" }
def approvalProperty = { "approve${apiSurface.label}ApiSurfaceChange" }
def ceilingProperty = { "raise${apiSurface.label}ApiSurfaceCeiling" }
def renderSurface = { ->
File sourceRoot = project.file(apiSurface.sourceRoot)
def typePattern = ~/(?m)^public\s+(?:final\s+|abstract\s+|sealed\s+|non-sealed\s+)*(class|interface|enum|record|@interface)\s+(\w+)/
def packagePattern = ~/(?m)^package\s+([\w.]+)\s*;/
List<String> types = []
if (sourceRoot.isDirectory()) {
sourceRoot.eachFileRecurse { candidate ->
if (!candidate.isFile() || !candidate.name.endsWith('.java')) {
return
}
String text = candidate.getText('UTF-8')
def packageMatcher = packagePattern.matcher(text)
if (!packageMatcher.find()) {
return
}
String packageName = packageMatcher.group(1)
def typeMatcher = typePattern.matcher(text)
while (typeMatcher.find()) {
types << "${packageName}.${typeMatcher.group(2)}".toString()
}
}
// Resolved at configuration time, so the task action never reaches for `Task.project`.
def sourceRootFiles = { ->
([apiSurface.sourceRoot] + apiSurface.additionalSourceRoots)
.findAll { it?.trim() }
.collect { project.file(it) }
}
def renderSurface = { List<File> roots ->
// Parsed with javac, not matched with a regular expression. The expression this replaces kept
// its own hand-maintained list of modifiers — already missing `strictfp` — and a second copy of
// it in another build script had drifted to a different list, so two files disagreed about what
// "public" means. A surface check that under-reports reads as "types were removed", which is the
// one answer it must not produce by accident.
List<String> types
try {
types = dev.caskeleton.buildlogic.JavaPublicTypes.render(roots)
} catch (IllegalStateException unparseable) {
// Not prefixed with the verify task's name: the same render backs the update task, and a
// parse failure reported under the wrong task name sends the reader to the wrong place.
throw new GradleException(
"${owningProjectPath} ${apiSurface.label} API surface: ${unparseable.message}",
unparseable)
}
if (types.isEmpty()) {
// An empty rendering is a moved source root, not a leaf with no public types: it would
// compare equal to nothing and report every committed type as removed, or — after an
// approved update — silently blank the baseline.
throw new GradleException(
"${owningProjectPath}: found no public types under ${roots.join(', ')}. " +
'The source roots moved; fix the paths rather than accepting an empty surface.')
}
types = types.unique().toSorted()
StringBuilder header = new StringBuilder()
header.append("# ").append(apiSurface.description).append('\n')
apiSurface.rationale.each { header.append('# ').append(it).append('\n') }
header.append("# Update only after review with:\n")
header.append("# ./gradlew ${owningProjectPath}:${updateName()} -P${approvalProperty()}\n")
header.append("# types: ${types.size()}\n")
return header.toString() + (types.isEmpty() ? '' : types.join('\n') + '\n')
return header.toString() + types.join('\n') + '\n'
}
def countTypes = { String surface ->
@@ -116,6 +133,8 @@ project.afterEvaluate {
// the approval; a change that raises the total says so out loud.
boolean ceilingRaiseApproved = project.hasProperty(ceilingProperty())
List<File> roots = sourceRootFiles()
tasks.register(verifyName()) {
group = 'verification'
description = "Fails without mutation when the committed ${apiSurface.label} public API " +
@@ -126,7 +145,7 @@ project.afterEvaluate {
"${verifyName()} is read-only; use ${updateName()} to record an approved " +
"change.")
}
String rendered = renderSurface()
String rendered = renderSurface(roots)
if (!apiSurface.baseline.isFile()) {
throw new GradleException(
"${verifyName()}: missing committed baseline ${apiSurface.baseline}")
@@ -159,7 +178,7 @@ project.afterEvaluate {
"${updateName()} requires -P${approvalProperty()}: growing the public " +
"surface is a review decision, not a build step.")
}
String rendered = renderSurface()
String rendered = renderSurface(roots)
if (apiSurface.baseline.isFile() && !ceilingRaiseApproved) {
int committedCount = countTypes(apiSurface.baseline.getText('UTF-8'))
int renderedCount = countTypes(rendered)
@@ -1,4 +1,5 @@
import dev.caskeleton.buildlogic.JUnitEvidence
import dev.caskeleton.buildlogic.RequiredTestExecution
// JUnit evidence: what a lane actually executed, read one way.
//
@@ -50,13 +51,11 @@ Closure<Map<String, Object>> verifyRequiredJUnitClasses = {
String evidenceName, File resultDirectory, List<String> requiredClasses ->
Map<String, Object> evidence = verifyNoSkipJUnitXml(evidenceName, resultDirectory)
Set<String> executedClasses = evidence.executedClasses as Set<String>
// A nested class counts for its outer class: a required class whose cases all live in
// @Nested inner classes is executed, and matching on exact names alone would call it missing.
List<String> missingClasses = requiredClasses.findAll { String requiredClass ->
!executedClasses.any { String executedClass ->
executedClass == requiredClass || executedClass.startsWith(requiredClass + '$')
}
}
// The same decision ca.strict-test-lane applies to a lane's `requires(...)`, from the same
// implementation. A nested class counts for its outer class — a required class whose cases
// all live in @Nested inner classes did execute, and exact-name matching would call it
// missing — and that rule is now stated once rather than once per convention.
List<String> missingClasses = RequiredTestExecution.absent(requiredClasses, executedClasses)
if (!missingClasses.isEmpty()) {
throw new GradleException(
"${evidenceName}: no executed test cases for required classes: ${missingClasses}")
@@ -0,0 +1,52 @@
// A vendored platform leaf that compiles against io.grpc: `ca.platform-module` plus the grpc BOM.
//
// io.grpc is not managed by the Spring Boot BOM, so four `grpc:*` leaves each imported grpc-bom at
// module scope with the same five lines:
//
// dependencyManagement {
// imports {
// mavenBom "io.grpc:grpc-bom:${grpcVersion}"
// }
// }
//
// Module scope rather than the root `dependencyManagement` block is the decision those four made and
// this plugin keeps: importing grpc-bom for all sixty-two leaves would put io.grpc versions into the
// resolution of every leaf that has nothing to do with gRPC, and every configuration in this build is
// dependency-locked in STRICT mode, so that is not a tidier spelling of the same thing — it is a
// rewrite of lockfiles across the repository.
//
// The same reason is why this is a second plugin rather than a flag on `ca.platform-module`. Only the
// leaves that already import the BOM may acquire it; giving it to the other thirty-nine would change
// their resolved graphs and invalidate their lock state.
plugins {
id 'ca.platform-module'
}
// Read from the root's `ext.grpcVersion` SSOT, which is where the four leaves read it from. Resolved
// at apply time, which is the same moment their inline blocks resolved it: the root sets the property
// while evaluating its own build file, long before any leaf is evaluated.
Object declaredGrpcVersion = project.rootProject.findProperty('grpcVersion')
if (declaredGrpcVersion == null || declaredGrpcVersion.toString().isBlank()) {
throw new GradleException(
"${project.path} applies ca.grpc-platform-module, which imports io.grpc:grpc-bom, but " +
'the root project declares no `ext.grpcVersion`. Importing an unversioned BOM ' +
'would leave every io.grpc coordinate in this leaf unmanaged.')
}
String grpcVersion = declaredGrpcVersion.toString()
// Fail-closed rather than silently skipped. `dependencyManagement` is Spring's extension, so without
// that plugin there is nothing to import into — and a BOM that was never imported does not announce
// itself: it surfaces later as an io.grpc coordinate with no version, in whichever leaf asks first.
if (!project.pluginManager.hasPlugin('io.spring.dependency-management')) {
throw new GradleException(
"${project.path} applies ca.grpc-platform-module before " +
"'io.spring.dependency-management'. The grpc BOM is imported through that " +
'plugin, so applying it afterwards would leave io.grpc versions unmanaged ' +
'without failing anything here.')
}
dependencyManagement {
imports {
mavenBom "io.grpc:grpc-bom:${grpcVersion}"
}
}
@@ -0,0 +1,23 @@
// A leaf of a vendored platform: `messaging:*`, `grpc:*`, `grpc-advanced:*`.
//
// Those families are not layers of this application. They are libraries that happen to live in this
// repository — their `*-api` leaves are ports, their broker and transport leaves are adapters, their
// starters are composition roots — and the thing every one of them needs that an application leaf
// does not is `java-library`: a consumer compiles against their types, so they have an `api`
// configuration and the distinction between `api` and `implementation` is load-bearing for them.
//
// Forty-three build files said that by each writing `apply plugin: 'java-library'` at line 1. That is
// not merely repetition. The root build applies every other plugin a leaf gets, centrally, and states
// why: "leaves in this repository have no plugins {} block — the root is where a leaf acquires its
// plugins, and splitting that would mean two places to look" (src/build.gradle). These forty-three
// files were the exception, so there were two places to look, and the one with forty-three copies is
// the one that drifts — a platform leaf added without the line compiles until the first consumer
// writes `api`, and then fails somewhere else.
//
// Deliberately thin. Everything else these leaves share — the toolchain, Spotless, Checkstyle,
// SpotBugs, Error Prone, dependency locking, the strict lane conventions — the root already applies
// to every leaf, and duplicating any of it here would be the second place to look this plugin exists
// to remove. What belongs here is what is true of the vendored platform and false of the rest.
plugins {
id 'java-library'
}
@@ -1,5 +1,3 @@
import groovy.json.JsonSlurper
// Where the registry's repository-root-relative source paths are resolved from.
//
// Defaults to the parent of the Gradle root, which is this repository's layout: the build lives in
@@ -7,7 +5,7 @@ import groovy.json.JsonSlurper
// fixture, whose projects sit beside its registry — says so rather than having the plugin guess.
ext.moduleRegistryRepositoryRoot = rootProject.projectDir.parentFile
def verifyRuntimeModuleMembership = tasks.register('verifyRuntimeModuleMembership') {
tasks.register('verifyRuntimeModuleMembership') {
group = 'verification'
description = 'Verifies registry runtime membership against both shipped composition roots.'
@@ -31,7 +29,9 @@ def verifyRuntimeModuleMembership = tasks.register('verifyRuntimeModuleMembershi
} catch (IllegalStateException invalid) {
throw new GradleException(invalid.message, invalid)
}
List<String> compositionIds = registry.RUNTIME_COMPOSITIONS.toList()
// From the registry file, not from a constant in the reader: the registry owns which runtime
// compositions exist, so a derived project adds or drops one by editing JSON.
List<String> compositionIds = registry.runtimeCompositions
Map<String, String> moduleIdByGradlePath =
registry.modules.collectEntries { [(it.gradlePath): it.id] }
// Resolved here rather than through `rootProject` inside the action: that is `Task.project` at
@@ -102,5 +102,3 @@ def verifyRuntimeModuleMembership = tasks.register('verifyRuntimeModuleMembershi
'match the registry')
}
}
rootProject.ext.verifyRuntimeModuleMembership = verifyRuntimeModuleMembership
@@ -294,15 +294,13 @@ strictTestLanes.lanes.all { StrictTestLaneSpec lane ->
// reading of the docs. Coverage that silently shrank is a gate that silently weakened,
// and this is the shape that produces it: a test renamed, the lane not updated.
if (!lane.requiredTests.isEmpty()) {
List<String> absent = lane.requiredTests.findAll { String required ->
!executedSelectors.any { String executed ->
// A parameterized test executes as `method(String)[1]`, so an exact-equality
// check would report a test that ran as absent.
executed == required ||
executed.startsWith(required + '(') ||
executed.startsWith(required + '[')
}
}
// The rule is not spelled out here. "A named test must actually have run" is also
// what ca.evidence decides for a qualification lane, and the two copies of it had
// each learned only the suffix rules their own input happened to produce — this one
// knew about `method(String)[1]` and not about `Outer$Inner`, the other the reverse.
// One implementation, two observation mechanisms.
List<String> absent = dev.caskeleton.buildlogic.RequiredTestExecution.absent(
lane.requiredTests, executedSelectors)
if (!absent.isEmpty()) {
throw new GradleException(
"strict test lane '${lane.name}' in ${owningProjectPath} required " +
@@ -0,0 +1,122 @@
package dev.caskeleton.buildlogic
import com.sun.source.tree.ClassTree
import com.sun.source.tree.CompilationUnitTree
import com.sun.source.tree.ExpressionTree
import com.sun.source.tree.Tree
import com.sun.source.util.JavacTask
import javax.lang.model.element.Modifier
import javax.tools.Diagnostic
import javax.tools.DiagnosticCollector
import javax.tools.JavaCompiler
import javax.tools.JavaFileObject
import javax.tools.StandardJavaFileManager
import javax.tools.ToolProvider
import java.nio.charset.StandardCharsets
/**
* Public top-level types under a set of source roots, read with the Java compiler's own parser.
*
* <p>This used to be a regular expression over the text of each {@code .java} file, matching
* {@code ^public (final|abstract|sealed|non-sealed)* (class|interface|enum|record|@interface) Name}.
* A regular expression cannot be a Java parser, and the ways it fails here are not hypothetical:
* the modifier alternation had to be maintained by hand and was already missing {@code strictfp},
* so {@code public strictfp class Foo} would have been left out of a surface whose whole purpose is
* to be complete; a {@code public class} written at column zero inside a block comment or a text
* block is matched; a copy of the same expression in another build script had drifted to a different
* modifier list, which is how two files came to disagree about what "public" means.
*
* <p>javac's parser answers the same question by construction. It is parse-only — no attribution, no
* classpath, no annotation processing — so it needs nothing the regex did not and it cannot be
* wrong about Java's own grammar.
*
* <p>Fail-closed twice over. A JVM with no compiler is refused rather than rendering an empty
* surface, and a file that does not parse is refused rather than contributing no types: both would
* otherwise read as "this leaf exposes less than it did", which is the one answer a surface check
* must never produce by accident.
*/
final class JavaPublicTypes {
private JavaPublicTypes() {}
/**
* Fully-qualified names of every public top-level type under the roots, sorted and unique.
*
* <p>A file with no package declaration contributes nothing, which is what the text-matching
* version did and what the surface means: an unnamed package is not reachable from an adopter.
*
* @param sourceRoots directories to walk; a root that does not exist contributes nothing
*/
static List<String> render(Collection<File> sourceRoots) {
List<File> sources = []
(sourceRoots ?: []).each { File root ->
if (root == null || !root.isDirectory()) {
return
}
root.eachFileRecurse { File candidate ->
if (candidate.isFile() && candidate.name.endsWith('.java')) {
sources << candidate
}
}
}
if (sources.isEmpty()) {
return []
}
sources = sources.toSorted { File left, File right -> left.path <=> right.path }
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler()
if (compiler == null) {
throw new IllegalStateException(
'No Java compiler on this JVM, so the public API surface cannot be parsed. ' +
'Run the build on a JDK rather than a JRE — rendering an empty surface ' +
'instead would report every public type as removed.')
}
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>()
StandardJavaFileManager fileManager =
compiler.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8)
Set<String> types = new TreeSet<>()
try {
// `-proc:none`: parsing is the whole job. An annotation processor would need a resolved
// classpath this deliberately does not build, and could contribute generated types that
// are not in the source root the surface is declared over.
JavacTask task = (JavacTask) compiler.getTask(
new StringWriter(), fileManager, diagnostics, ['-proc:none'], null,
fileManager.getJavaFileObjectsFromFiles(sources))
Iterable<? extends CompilationUnitTree> units = task.parse()
List<Diagnostic<? extends JavaFileObject>> errors = diagnostics.diagnostics
.findAll { it.kind == Diagnostic.Kind.ERROR }
if (!errors.isEmpty()) {
throw new IllegalStateException(
'The public API surface could not be parsed:\n ' +
errors.take(5).collect { it.toString() }.join('\n ') +
(errors.size() > 5 ? "\n (${errors.size() - 5} more)" : ''))
}
units.each { CompilationUnitTree unit ->
ExpressionTree packageName = unit.packageName
if (packageName == null) {
return
}
String packageText = packageName.toString()
unit.typeDecls.each { Tree declaration ->
// Only top-level declarations are visited here; a nested public type is reachable
// only through its owner and is part of that owner's surface, not a separate one.
if (!(declaration instanceof ClassTree)) {
return
}
ClassTree type = declaration as ClassTree
if (!type.modifiers.flags.contains(Modifier.PUBLIC)) {
return
}
types << "${packageText}.${type.simpleName}".toString()
}
}
} finally {
fileManager.close()
}
return new ArrayList<String>(types)
}
}
@@ -21,9 +21,6 @@ import groovy.json.JsonSlurper
*/
final class ModuleRegistry {
/** The runtime compositions this repository recognises. */
static final Set<String> RUNTIME_COMPOSITIONS = ['app-bootstrap', 'sample-portfolio'] as Set
/** Exactly the fields a module entry carries — extra or missing is a failure, not a default. */
private static final Set<String> MODULE_FIELDS =
['id', 'gradle_path', 'source_path', 'allowed_dependencies', 'runtime_memberships'] as Set
@@ -33,11 +30,24 @@ final class ModuleRegistry {
/** Every registered module, in registry order. */
final List<Module> modules
/**
* The runtime compositions this registry declares, in registry order.
*
* <p>Read from {@code runtime_compositions}, not from a constant. The list used to exist twice —
* once here as {@code RUNTIME_COMPOSITIONS} and once in the JSON — and {@code read} only checked
* that the two copies agreed, so the JSON field looked like configuration while deciding nothing.
* The cost of that was not cosmetic: a derived project that drops or renames a composition root
* fails in <em>settings</em>, before any project exists, with no way to recover short of editing
* this file. The JSON is the registry, so the JSON is where the list lives.
*/
final List<String> runtimeCompositions
/** The file this was read from, for failure messages that name it. */
final File source
private ModuleRegistry(List<Module> modules, File source) {
private ModuleRegistry(List<Module> modules, List<String> runtimeCompositions, File source) {
this.modules = Collections.unmodifiableList(modules)
this.runtimeCompositions = Collections.unmodifiableList(runtimeCompositions)
this.source = source
}
@@ -82,12 +92,24 @@ final class ModuleRegistry {
if (!(parsed.modules instanceof List) || parsed.modules.isEmpty()) {
throw new IllegalStateException("Module registry has no modules: ${registryFile}")
}
if (!(parsed.runtime_compositions instanceof List) ||
parsed.runtime_compositions.collect { it as String }.toSet() != RUNTIME_COMPOSITIONS ||
parsed.runtime_compositions.size() != RUNTIME_COMPOSITIONS.size()) {
if (!(parsed.runtime_compositions instanceof List) || parsed.runtime_compositions.isEmpty()) {
throw new IllegalStateException(
"Module registry runtime_compositions must be exactly ${RUNTIME_COMPOSITIONS}: ${registryFile}")
"Module registry needs a nonempty 'runtime_compositions' list: ${registryFile}")
}
List<String> runtimeCompositions = parsed.runtime_compositions.withIndex().collect {
value, index ->
if (!(value instanceof String) || (value as String).isBlank()) {
throw new IllegalStateException(
"Module registry has a non-string or blank runtime_compositions entry at " +
"index ${index}: ${registryFile}")
}
value as String
}
if (runtimeCompositions.toSet().size() != runtimeCompositions.size()) {
throw new IllegalStateException(
"Module registry contains duplicate runtime_compositions: ${registryFile}")
}
Set<String> declaredCompositions = runtimeCompositions.toSet()
File canonicalRoot = repositoryRoot.canonicalFile
String rootPrefix = canonicalRoot.path + File.separator
Set<String> ids = new LinkedHashSet<>()
@@ -130,7 +152,7 @@ final class ModuleRegistry {
throw new IllegalStateException(
"Module registry entry '${id}' contains duplicate runtime memberships.")
}
Set<String> unknown = runtimeMemberships.toSet() - RUNTIME_COMPOSITIONS
Set<String> unknown = runtimeMemberships.toSet() - declaredCompositions
if (!unknown.isEmpty()) {
throw new IllegalStateException(
"Module registry entry '${id}' references unknown runtime memberships ${unknown.toSorted()}.")
@@ -175,7 +197,7 @@ final class ModuleRegistry {
allowedDependencies, runtimeMemberships)
}
RUNTIME_COMPOSITIONS.each { compositionId ->
runtimeCompositions.each { compositionId ->
Module composition = modules.find { it.id == compositionId }
if (composition == null || !composition.runtimeMemberships.contains(compositionId)) {
throw new IllegalStateException(
@@ -203,7 +225,7 @@ final class ModuleRegistry {
}
}
return new ModuleRegistry(modules, registryFile)
return new ModuleRegistry(modules, runtimeCompositions, registryFile)
}
/** Modules whose runtime_memberships name the given composition. */
@@ -0,0 +1,74 @@
package dev.caskeleton.buildlogic
/**
* "A test this build names must actually have run", decided in one place.
*
* <p>Two conventions enforced this rule with two copies of the decision. {@code ca.strict-test-lane}
* compared a lane's {@code requires(...)} selectors against what its {@code afterTest} listener saw;
* {@code ca.evidence} compared a qualification lane's required FQCNs against the classes it read back
* out of JUnit XML. Both answered the same question — is this named test in the set of things that
* ran — and they answered it differently, because each had written only the suffix rules its own
* input shape happened to produce.
*
* <p>That is the failure mode worth naming. The lane knew a parameterized method executes as
* {@code method(String)[1]} and the evidence reader did not; the evidence reader knew a class whose
* cases all live in {@code @Nested} inner classes executes as {@code Outer$Inner} and the lane did
* not. Neither gap shows up as a red build. Both show up as a required test reported absent when it
* ran, or — the direction that matters — as a gate that is weaker on one side than the reader of
* either plugin would guess.
*
* <p>How the observation is made stays where it was, deliberately. A lane watches a live Test task
* because it has one; a qualification lane re-reads the recorded XML precisely so its claim does not
* rest on a task's exit code. Those are different evidence sources for good reasons. What is shared
* is the rule applied to whatever they observed, and that is what lives here.
*/
final class RequiredTestExecution {
/**
* Characters that begin a sub-identity of a named test.
*
* <p>An executed identity that starts with a required name followed by one of these is that
* required test, reported at a finer grain than the name asked for:
*
* <ul>
* <li>{@code $} — a {@code @Nested} inner class, reported as {@code Outer$Inner};</li>
* <li>{@code (} — a method's parameter list, reported as {@code method(String)};</li>
* <li>{@code [} — one invocation of a parameterized test, reported as {@code method[1]}.</li>
* </ul>
*
* <p>A plain {@code .} is not here and must not be: {@code com.example.FooTest} would then be
* satisfied by {@code com.example.FooTestHelper}, and a required class would be provable by a
* different class whose name merely starts the same way.
*/
private static final List<String> SUB_IDENTITY_SEPARATORS = ['$', '(', '[']
private RequiredTestExecution() {}
/** Whether one executed identity proves the required selector ran. */
static boolean satisfies(String executed, String required) {
if (executed == null || required == null) {
return false
}
if (executed == required) {
return true
}
return SUB_IDENTITY_SEPARATORS.any { String separator -> executed.startsWith(required + separator) }
}
/**
* The required selectors nothing in {@code executed} accounts for, in declaration order.
*
* <p>Every one of them, not the first. A lane naming five contracts of which four still exist
* would otherwise report a single miss and leave the reader believing the other four were the
* only ones checked.
*/
static List<String> absent(Collection<String> required, Collection<String> executed) {
if (required == null || required.isEmpty()) {
return []
}
Collection<String> observed = executed ?: []
return required.findAll { String requiredSelector ->
!observed.any { String executedSelector -> satisfies(executedSelector, requiredSelector) }
}
}
}
@@ -107,6 +107,75 @@ class ApiSurfaceConventionTest {
"the failure should name the added type:\n${result.output}")
}
@Test
@DisplayName("a modifier the old regex did not list still reaches the surface")
void aStrictfpTypeIsRendered() {
// The renderer used to keep its own alternation of modifiers — final, abstract, sealed,
// non-sealed — and `strictfp` was not in it, so a public type declared with it rendered as
// absent. That is the direction a surface check must never be wrong in: a type nobody can
// see in the baseline is a type nobody reviews. javac has no list to forget.
Files.writeString(projectDir.resolve('src/main/java/app/Strict.java'),
"package app;\npublic strictfp class Strict {}\n")
runner('updateFixtureApiSurface', '-PapproveFixtureApiSurfaceChange').build()
assertTrue(Files.readString(projectDir.resolve('surface.txt')).contains('app.Strict'),
'a strictfp public type belongs to the surface like any other')
}
@Test
@DisplayName("a public class written inside a comment is not a public class")
void commentedOutCodeIsNotASurface() {
// The other direction of parsing text instead of Java: a line that begins with `public class`
// at column zero inside a block comment matched, and the baseline gained a type that does not
// exist. Reviewing an addition that is not there is the same waste as missing one that is.
Files.writeString(projectDir.resolve('src/main/java/app/Commented.java'),
"package app;\n/*\npublic class Ghost {}\n*/\npublic final class Commented {}\n")
runner('updateFixtureApiSurface', '-PapproveFixtureApiSurfaceChange').build()
String surface = Files.readString(projectDir.resolve('surface.txt'))
assertTrue(surface.contains('app.Commented'), 'the real type belongs to the surface')
assertEquals(false, surface.contains('app.Ghost'),
"a commented-out declaration is not a public type:\n${surface}")
}
@Test
@DisplayName("a source root that renders nothing is an error, not an empty surface")
void anEmptyRenderingIsRefused() {
// A moved source root would otherwise report every committed type as removed on verify, and
// blank the committed baseline on an approved update.
Files.writeString(projectDir.resolve('build.gradle'), """
plugins {
id 'java'
id 'ca.api-surface'
}
apiSurface {
label = 'Fixture'
sourceRoot = 'src/main/moved-away'
baseline = file('surface.txt')
description = 'The fixture leaf public surface.'
}
""".stripIndent())
def result = runner('verifyFixtureApiSurface').buildAndFail()
assertTrue(result.output.contains('found no public types'),
"an empty rendering must be refused rather than compared:\n${result.output}")
}
@Test
@DisplayName("a source file that does not parse fails the surface rather than shrinking it")
void anUnparseableSourceIsRefused() {
Files.writeString(projectDir.resolve('src/main/java/app/Broken.java'),
"package app;\npublic class Broken {\n")
def result = runner('verifyFixtureApiSurface').buildAndFail()
assertTrue(result.output.contains('could not be parsed'),
"a file javac cannot read must not silently contribute nothing:\n${result.output}")
}
@Test
@DisplayName("a leaf that declares no surface gets no tasks")
void aLeafWithoutASurfaceGetsNoTasks() {
@@ -145,6 +145,60 @@ class ModuleRegistryTest {
assertTrue(failure.message.contains('fields must be exactly'), failure.message)
}
@Test
@DisplayName("the registry decides which runtime compositions exist, so a derived project may drop one")
void theRegistryOwnsItsCompositionList() {
// The list used to be a constant here as well as a field in the JSON, and read() only checked
// that the two agreed. A derived project that drops the sample fixture then failed in
// *settings* — before any project exists — with no recovery short of editing this class.
String json = """{"runtime_compositions":["app-bootstrap"],
"modules":[${entry('app-bootstrap', ':app-bootstrap', 'src/alpha', '[]',
'["app-bootstrap"]')}]}"""
def parsed = read(json)
assertEquals(['app-bootstrap'], parsed.runtimeCompositions)
assertEquals(['app-bootstrap'], parsed.membersOf('app-bootstrap').collect { it.id })
}
@Test
@DisplayName("a composition the registry names is still checked, whatever it is called")
void aRenamedCompositionIsStillChecked() {
// Dropping the constant must not drop the rule. A membership naming something the registry
// does not declare is still refused, against the declared list rather than a fixed one.
String json = """{"runtime_compositions":["service-bootstrap"],
"modules":[${entry('service-bootstrap', ':service-bootstrap', 'src/alpha', '[]',
'["service-bootstrap"]')},
${entry('beta', ':beta', 'src/beta', '[]', '["app-bootstrap"]')}]}"""
def failure = assertThrows(IllegalStateException) { read(json) }
assertTrue(failure.message.contains('unknown runtime memberships'), failure.message)
}
@Test
@DisplayName("an empty runtime_compositions list is refused")
void anEmptyCompositionListIsRefused() {
String json = """{"runtime_compositions":[],
"modules":[${entry('alpha', ':alpha', 'src/alpha')}]}"""
def failure = assertThrows(IllegalStateException) { read(json) }
assertTrue(failure.message.contains("nonempty 'runtime_compositions'"), failure.message)
}
@Test
@DisplayName("a duplicated runtime composition is refused")
void aDuplicatedCompositionIsRefused() {
String json = """{"runtime_compositions":["app-bootstrap","app-bootstrap"],
"modules":[${entry('app-bootstrap', ':app-bootstrap', 'src/alpha', '[]',
'["app-bootstrap"]')}]}"""
def failure = assertThrows(IllegalStateException) { read(json) }
assertTrue(failure.message.contains('duplicate runtime_compositions'), failure.message)
}
@Test
@DisplayName("a runtime composition that does not include itself is refused")
void compositionMustIncludeItself() {
@@ -0,0 +1,120 @@
import java.nio.file.Files
import java.nio.file.Path
import org.gradle.testkit.runner.GradleRunner
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertTrue
/**
* The vendored-platform conventions give a leaf what its forty-three build files each wrote by hand,
* and refuse to give it half of that silently.
*
* <p>The grpc BOM is the part worth testing rather than reading. Its two preconditions — a root that
* declares {@code ext.grpcVersion}, and Spring's dependency-management plugin to import into — are
* both satisfied today by the order in which the root build applies things, and both are invisible
* at the call site. A convention that skipped the import when either was missing would not fail
* here; it would surface much later as an io.grpc coordinate with no version, in whichever leaf
* asked for one first.
*/
class PlatformModuleConventionTest {
Path projectDir
@BeforeEach
void setUp() {
projectDir = Files.createTempDirectory('platform-module')
Files.writeString(projectDir.resolve('settings.gradle'), "rootProject.name = 'fixture'\n")
}
private void buildFile(String body) {
Files.writeString(projectDir.resolve('build.gradle'), body.stripIndent())
}
private GradleRunner runner(String... args) {
return GradleRunner.create()
.withProjectDir(projectDir.toFile())
.withPluginClasspath()
.withArguments(args)
}
@Test
@DisplayName("ca.platform-module gives a leaf the api configuration java-library provides")
void platformModuleProvidesJavaLibrary() {
// `api` is the reason these leaves are java-library rather than java: a consumer compiles
// against their types. Asserting the configuration exists asserts the thing that would break.
buildFile('''
plugins {
id 'ca.platform-module'
}
tasks.register('reportApiConfiguration') {
boolean present = configurations.findByName('api') != null
doLast { logger.lifecycle("api-configuration-present=" + present) }
}
''')
def result = runner('reportApiConfiguration').build()
assertTrue(result.output.contains('api-configuration-present=true'),
"the platform convention should apply java-library:\n${result.output}")
}
@Test
@DisplayName("the grpc convention imports the BOM, so io.grpc coordinates need no version")
void grpcConventionImportsTheBom() {
// The four leaves that wrote this block by hand did so to declare `io.grpc:grpc-api` without
// a version. Asserting the managed version is asserting exactly that, and it resolves the
// BOM's POM rather than downloading any jar.
buildFile('''
plugins {
id 'io.spring.dependency-management' version '1.1.7'
id 'ca.grpc-platform-module'
}
repositories { mavenCentral() }
tasks.register('reportManagedVersion') {
String managed = dependencyManagement.managedVersions['io.grpc:grpc-api']
doLast { logger.lifecycle('managed-grpc-api=' + managed) }
}
''')
Files.writeString(projectDir.resolve('gradle.properties'), "grpcVersion=1.68.1\n")
def result = runner('reportManagedVersion').build()
assertTrue(result.output.contains('managed-grpc-api=1.68.1'),
"the BOM should manage io.grpc versions for the leaf:\n${result.output}")
}
@Test
@DisplayName("the grpc convention refuses a root that declares no grpcVersion")
void grpcConventionRefusesAMissingVersion() {
buildFile('''
plugins {
id 'ca.grpc-platform-module'
}
''')
def result = runner('tasks').buildAndFail()
assertTrue(result.output.contains('ext.grpcVersion'),
"the refusal should name the property that is missing:\n${result.output}")
}
@Test
@DisplayName("the grpc convention refuses to be applied before dependency-management")
void grpcConventionRefusesAMissingDependencyManagement() {
// Without Spring's plugin there is no `dependencyManagement` block to import the BOM into.
// Skipping the import quietly is the failure mode this refuses.
buildFile('''
plugins {
id 'ca.grpc-platform-module'
}
''')
Files.writeString(projectDir.resolve('gradle.properties'), "grpcVersion=1.68.1\n")
def result = runner('tasks').buildAndFail()
assertTrue(result.output.contains('io.spring.dependency-management'),
"the refusal should name the plugin the import needs:\n${result.output}")
}
}
@@ -0,0 +1,87 @@
import dev.caskeleton.buildlogic.RequiredTestExecution
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertTrue
/**
* One rule, and it is the union of what the two copies each knew.
*
* <p>"A test this build names must actually have run" was decided twice — once in
* {@code ca.strict-test-lane} against an {@code afterTest} listener, once in {@code ca.evidence}
* against JUnit XML. Each copy handled only the identity suffixes its own input happened to produce,
* so the lane could not see a {@code @Nested} class and the evidence reader could not see a
* parameterized invocation. Neither gap fails a build; both report a test that ran as absent, and a
* gate that cries wolf is a gate somebody eventually loosens.
*
* <p>These cases pin the whole rule rather than each caller's half of it, which is the point of there
* being one implementation.
*/
class RequiredTestExecutionTest {
@Test
@DisplayName("an exact match accounts for a required selector")
void exactMatchCounts() {
assertEquals([], RequiredTestExecution.absent(['com.example.FooTest'], ['com.example.FooTest']))
}
@Test
@DisplayName("a @Nested inner class accounts for the outer class it lives in")
void nestedClassCountsForItsOuterClass() {
// ca.evidence knew this; the lane did not. A required class whose cases all live in @Nested
// inner classes is reported by JUnit as Outer$Inner and did execute.
assertEquals([], RequiredTestExecution.absent(
['com.example.FooTest'], ['com.example.FooTest$WhenEmpty']))
}
@Test
@DisplayName("a parameterized invocation accounts for the method it came from")
void parameterizedInvocationCountsForItsMethod() {
// The lane knew this; ca.evidence did not.
assertEquals([], RequiredTestExecution.absent(
['com.example.FooTest.rejects'], ['com.example.FooTest.rejects(String)[1]']))
assertEquals([], RequiredTestExecution.absent(
['com.example.FooTest.rejects'], ['com.example.FooTest.rejects[2]']))
}
@Test
@DisplayName("a longer name that merely starts the same way proves nothing")
void aPrefixOfADifferentNameIsNotAMatch() {
// The separator list has no '.' in it precisely for this: FooTestHelper must not be able to
// stand in for FooTest, or a required class is provable by a different class.
assertEquals(['com.example.FooTest'], RequiredTestExecution.absent(
['com.example.FooTest'], ['com.example.FooTestHelper']))
}
@Test
@DisplayName("every absent selector is reported, not the first one")
void everyAbsentSelectorIsReported() {
// failOnNoMatchingTests fails only when the whole filter matches nothing, so a lane naming
// five contracts of which four still exist passes. Reporting one miss out of two would
// recreate the same half-truth one level up.
List<String> absent = RequiredTestExecution.absent(
['com.example.A', 'com.example.B', 'com.example.C'],
['com.example.B'])
assertEquals(['com.example.A', 'com.example.C'], absent)
}
@Test
@DisplayName("nothing required is nothing absent, and nothing executed leaves everything absent")
void emptyInputs() {
assertEquals([], RequiredTestExecution.absent([], ['com.example.A']))
assertEquals([], RequiredTestExecution.absent(null, ['com.example.A']))
assertEquals(['com.example.A'], RequiredTestExecution.absent(['com.example.A'], []))
assertEquals(['com.example.A'], RequiredTestExecution.absent(['com.example.A'], null))
}
@Test
@DisplayName("satisfies is the single predicate both conventions ask")
void satisfiesIsThePredicate() {
assertTrue(RequiredTestExecution.satisfies('com.example.FooTest$Inner', 'com.example.FooTest'))
assertTrue(RequiredTestExecution.satisfies('com.example.FooTest.bar(int)', 'com.example.FooTest.bar'))
assertEquals(false, RequiredTestExecution.satisfies(null, 'com.example.FooTest'))
assertEquals(false, RequiredTestExecution.satisfies('com.example.FooTest', null))
}
}
+158 -164
View File
@@ -554,17 +554,15 @@ configure(subprojects.findAll { it.childProjects.isEmpty() }) {
jvmArgs '-Duser.timezone=UTC'
}
tasks.named('check') {
dependsOn verifySpotBugsAnalysisFailureContract
dependsOn rootProject.tasks.named('verifyCleanArchitectureDependencies')
dependsOn rootProject.tasks.named('verifyRuntimeModuleMembership')
dependsOn rootProject.tasks.named('verifyEnvKeys')
dependsOn rootProject.tasks.named('verifyNoStaleTraceableJars')
dependsOn rootProject.tasks.named('verifyOneTypePerFile')
dependsOn rootProject.tasks.named('verifyNoIgnoredSourcePackages')
dependsOn rootProject.tasks.named('verifyTrivyignore')
dependsOn rootProject.tasks.named('verifyQuarantineSunset')
}
// A leaf's `check` checks that leaf. Repository-wide gates hang off the ROOT `check` (below),
// not off all 62 leaves.
//
// They used to hang off every leaf, and the reason was sound — a drift gate nobody runs reports
// whatever was true the last time somebody typed its name. The cost, though, was that
// `./gradlew :domain-core:check` compiled cache-redis, walked the whole repository twice, and
// parsed every runbook and policy document, which is the opposite of the "run the focused check"
// instruction in CLAUDE.md. Coverage is unchanged: CI runs `./gradlew check`
// (.github/workflows/ci-quality-gates.yml), which matches the task name in the root project too.
}
Map<String, Provider<Directory>> conditionalTransportEvidence = [
@@ -657,111 +655,102 @@ def prepareMessagingContractEvidence = tasks.register('prepareMessagingContractE
}
}
// JUnit XML through the shared reader, not a second XmlSlurper.
//
// This closure used to parse TEST-*.xml itself with `new XmlSlurper(false, false)`. That is the
// same construction src/gradle/jpa-evidence.gradle removed, and it left the reason in a comment:
// the shared reader additionally sets `disallow-doctype-decl`, so two readers of the same files did
// not agree on how to read them, and only one of them could be what the author meant. It is also
// where the counts come from — dev.caskeleton.buildlogic.JUnitEvidence takes them from the suite
// attributes rather than by counting <testcase> elements, so a suite that failed to initialise
// (one error in the header, no test cases at all) counts as a failure instead of as nothing.
//
// The class is called directly rather than through rootProject.ext.readJUnitEvidence because the
// scenario IDs below need executedSelectors, which that closure does not return.
def messagingEvidenceFromXml = { List<String> resultDirectories ->
List<Map<String, String>> cases = []
int executed = 0
int failed = 0
int skipped = 0
Set<String> selectors = new TreeSet<>()
resultDirectories.each { String directory ->
File resultDirectory = messagingEvidenceResultRoot.get().dir(directory).asFile
fileTree(resultDirectory).matching { include 'TEST-*.xml' }.files.sort().each { File xml ->
def suite = new XmlSlurper(false, false).parse(xml)
suite.testcase.each { testCase ->
boolean failed = !testCase.failure.isEmpty() || !testCase.error.isEmpty()
boolean skipped = !testCase.skipped.isEmpty()
String simpleClass = testCase.@classname.text().tokenize('.').last()
String rawId = "${simpleClass}.${testCase.@name.text()}"
String scenarioId = rawId
.replace('()', '')
.replaceAll('[^A-Za-z0-9._:-]', '-')
.replaceAll('-+', '-')
cases << [id: scenarioId, failed: failed.toString(), skipped: skipped.toString()]
}
def results
try {
results = dev.caskeleton.buildlogic.JUnitEvidence.read(
"messaging-evidence/${directory}", resultDirectory)
} catch (IllegalStateException unreadable) {
throw new GradleException(unreadable.message, unreadable)
}
executed += results.tests
failed += results.failures + results.errors
skipped += results.skipped
selectors.addAll(results.executedSelectors)
}
if (cases.isEmpty()) {
if (executed <= 0) {
throw new GradleException('Messaging qualification XML contains no discovered test cases.')
}
List<String> scenarioIds = cases.collect { it.id }.sort()
// `pkg.ClassName#method` -> `ClassName.method`, then sanitised to the manifest's identifier
// grammar. The uniqueness check is on the simple-name form on purpose: two classes with the same
// simple name in different packages produce one scenario ID between them, and a manifest whose
// scenario list silently merges two scenarios is the failure this refuses.
List<String> scenarioIds = selectors.collect { String selector ->
selector.replaceFirst(/^.*\./, '')
.replace('#', '.')
.replaceAll('[^A-Za-z0-9._:-]', '-')
.replaceAll('-+', '-')
}.sort()
if (scenarioIds.toSet().size() != scenarioIds.size()) {
throw new GradleException('Messaging qualification scenario IDs are not unique.')
}
int failed = cases.count { it.failed == 'true' }
int skipped = cases.count { it.skipped == 'true' }
[
scenarioIds: scenarioIds,
counts: [
executed: cases.size(),
passed: cases.size() - failed - skipped,
executed: executed,
passed: executed - failed - skipped,
failed: failed,
skipped: skipped
]
]
}
// What the JSON Schema cannot say, and nothing else.
//
// The manifest used to be validated three times: this closure before the write, this closure again
// on the bytes it had just written, and MessagingEvidenceManifestSchemaValidator over the same bytes
// as a finalizer. Three validators is three definitions of "valid evidence", and the day they
// disagree there is no way to say which one is the schema.
// config/messaging/evidence/build-evidence-manifest-v1.schema.json is now the only structural
// answer — field set, types, SHA-256 patterns, identifier grammar, counts' bounds — and the second
// pass over the written bytes is gone because the finalizer already reads exactly those bytes.
//
// Four rules are kept here because the schema genuinely does not express them:
// 1. the manifest names the task that produced it (the schema lists all eleven legal producers);
// 2. executed == passed + failed + skipped (a schema cannot relate two numbers);
// 3. a run with a failure or a skip cannot be PASS evidence (the whole point of the artifact);
// 4. generatedAt parses as an instant — `format: date-time` is an annotation, not an assertion,
// unless a validator is configured to assert it.
def validateMessagingEvidenceStructure = { Map manifest, String expectedProducer ->
Set<String> exactRootKeys = [
'schemaVersion', 'sourceDigest', 'artifactDigest', 'producerTask', 'scenarioIds',
'counts', 'command', 'generatedAt', 'hashes', 'failures', 'skips',
'unsupportedClaims'
] as Set
Set<String> exactCountKeys = ['executed', 'passed', 'failed', 'skipped'] as Set
Set<String> exactHashKeys = ['profile', 'catalog', 'schema', 'settings'] as Set
List<String> violations = []
if (manifest.keySet() != exactRootKeys) {
violations << 'root fields do not match the common manifest schema'
if (manifest.producerTask != expectedProducer) {
violations << "producerTask is '${manifest.producerTask}', not '${expectedProducer}'"
}
if (manifest.schemaVersion != 1 || manifest.producerTask != expectedProducer) {
violations << 'schemaVersion or producerTask is wrong'
}
['sourceDigest', 'artifactDigest'].each { String field ->
if (!(manifest[field] instanceof String) ||
!(manifest[field] ==~ /sha256:[a-f0-9]{64}/)) {
violations << "${field} is not a canonical SHA-256"
}
}
if (!(manifest.scenarioIds instanceof List) || manifest.scenarioIds.isEmpty() ||
manifest.scenarioIds.toSet().size() != manifest.scenarioIds.size() ||
manifest.scenarioIds.any {
!(it instanceof String) ||
!(it ==~ /[A-Za-z0-9][A-Za-z0-9._:-]{0,159}/)
}) {
violations << 'scenarioIds violate the common schema'
}
if (!(manifest.counts instanceof Map) || manifest.counts.keySet() != exactCountKeys ||
!(manifest.counts.executed instanceof Integer) || manifest.counts.executed < 1 ||
manifest.counts.values().any { !(it instanceof Integer) || it < 0 } ||
manifest.counts.executed !=
manifest.counts.passed + manifest.counts.failed + manifest.counts.skipped) {
violations << 'counts are invalid or inconsistent'
if (manifest.counts?.executed !=
(manifest.counts?.passed ?: 0) + (manifest.counts?.failed ?: 0) +
(manifest.counts?.skipped ?: 0)) {
violations << "counts do not add up: ${manifest.counts}"
}
if (manifest.counts?.failed != 0 || manifest.counts?.skipped != 0 ||
manifest.failures != [] || manifest.skips != []) {
violations << 'failed or skipped qualification cannot produce PASS evidence'
}
if (!(manifest.hashes instanceof Map) || manifest.hashes.keySet() != exactHashKeys ||
manifest.hashes.values().any {
!(it instanceof String) || !(it ==~ /sha256:[a-f0-9]{64}/)
}) {
violations << 'hashes violate the common schema'
}
if (!(manifest.command instanceof String) || manifest.command.isBlank() ||
manifest.command.length() > 2048) {
violations << 'command is missing or unbounded'
}
try {
Instant.parse(manifest.generatedAt as String)
} catch (RuntimeException ignored) {
violations << 'generatedAt is not UTC date-time evidence'
}
if (!(manifest.unsupportedClaims instanceof List) ||
manifest.unsupportedClaims.toSet().size() != manifest.unsupportedClaims.size() ||
manifest.unsupportedClaims.any {
!(it instanceof String) ||
!(it ==~ /[A-Za-z0-9][A-Za-z0-9._:-]{0,159}/)
}) {
violations << 'unsupportedClaims violate the common schema'
violations << "generatedAt '${manifest.generatedAt}' is not UTC date-time evidence"
}
if (!violations.isEmpty()) {
throw new GradleException(
"Messaging evidence fails the common schema structural validator:\n " +
"Messaging evidence fails the rules the manifest schema cannot express:\n " +
violations.join('\n '))
}
}
@@ -819,8 +808,6 @@ def writeMessagingEvidence = {
File output = messagingEvidenceFile.get().asFile
output.parentFile.mkdirs()
output.text = JsonOutput.prettyPrint(JsonOutput.toJson(manifest)) + System.lineSeparator()
Map reloaded = new JsonSlurper().parse(output) as Map
validateMessagingEvidenceStructure(reloaded, producerTask)
logger.lifecycle(
"${producerTask}: wrote payload-free evidence with ${result.counts.executed} scenarios.")
}
@@ -1199,12 +1186,6 @@ tasks.register('verifyReadmeCommands') {
}
}
configure(subprojects.findAll { it.childProjects.isEmpty() }) {
tasks.named('check') {
dependsOn rootProject.tasks.named('verifyReadmeCommands')
}
}
// MSG-023 — a leaf count written in prose drifts the moment a leaf is added, and it did: the root
// policy documents claimed 19 leaves long after the registry held 44. The registry is the only
// authority on the list and its size, so any document that restates a count has to agree with it.
@@ -1457,35 +1438,24 @@ tasks.register('verifyTestSourceSetRegistry') {
}
}
// Wired into `check`, not left to whoever remembers to type it.
// The three notification gates run with the leaf they are about.
//
// The gate existed and was green for months while five module CLAUDE.md files and four leaf build
// files claimed a leaf count the registry had not held since the messaging platform landed — because
// nothing ran it. A drift check nobody runs is a drift check that reports whatever was true when it
// was last invoked by hand.
configure(subprojects.findAll { it.childProjects.isEmpty() }) {
tasks.named('check') {
dependsOn rootProject.tasks.named('verifyDocumentedLeafCount')
dependsOn rootProject.tasks.named('verifyTestSourceSetRegistry')
}
}
// The three notification gates, and the runbook drift gate, run as part of `check`.
//
// All four existed and passed for months while nothing ran them, and the cost was measurable the
// 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. Each gate would have caught its own drift on the commit that introduced it.
//
// A verification task that only runs when somebody types its name reports on whatever was true the
// last time somebody did.
configure(subprojects.findAll { it.childProjects.isEmpty() }) {
tasks.named('check') {
dependsOn rootProject.tasks.named('verifyNotificationApiSurface')
dependsOn rootProject.tasks.named('verifyNotificationConfiguration')
dependsOn rootProject.tasks.named('verifyNotificationEvidence')
dependsOn rootProject.tasks.named('verifyRunbookReferences')
}
// Reachability is why they are wired into a `check` at all. Which `check` is a separate question,
// and the answer is the notification leaf's: 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. They ran on all 62 leaves before, which reached them 62 times and told the developer
// who changed :domain-core about the notification surface.
// `.github/workflows/notification-platform.yml` also invokes all three by name.
project(':adapter:outbound:notification').tasks.named('check') {
dependsOn rootProject.tasks.named('verifyNotificationApiSurface')
dependsOn rootProject.tasks.named('verifyNotificationConfiguration')
dependsOn rootProject.tasks.named('verifyNotificationEvidence')
}
tasks.register('verifyCleanArchitectureDependencies') {
@@ -2172,10 +2142,10 @@ def verifyJpaReadinessRegistry = tasks.register('verifyJpaReadinessRegistry') {
}
}
configure(subprojects.findAll { it.childProjects.isEmpty() }) {
tasks.named('check') {
dependsOn verifyJpaReadinessRegistry
}
// The registry describes the JPA platform's lanes and resolves their task paths, so it runs with
// that platform's `check` rather than with all 62.
project(':adapter:outbound:persistence-jpa').tasks.named('check') {
dependsOn verifyJpaReadinessRegistry
}
Project applicationCoreProject = project(':application-core')
@@ -2398,54 +2368,32 @@ def verifyConfigurationPropertiesProcessor = tasks.register('verifyConfiguration
}
}
configure(subprojects.findAll { it.childProjects.isEmpty() }) {
tasks.named('check') {
dependsOn verifyConfigurationPropertiesProcessor
}
}
// verifyOneTypePerFile — one public top-level type per file, file name == type name
// (code-conventions I6). Rationale in README.md.
// verifyOneTypePerFile — code-conventions I6, now enforced by Checkstyle.
//
// This used to be 43 lines that ran
// ^public\s+(final|abstract|sealed|non-sealed)*\s*(class|interface|record|enum|@interface)\s+(\w+)
// line by line over src/main/java. Checkstyle's OneTopLevelClass and OuterTypeFilename ask the same
// two questions against a parsed file (config/checkstyle/checkstyle.xml), and the regex was wrong in
// three ways they are not:
// * package-private top-level types were invisible to it — 126 main sources matched it zero times,
// so a file with five package-private top-level types passed;
// * it read src/main/java only;
// * `^public` anchors at column zero, so a block-comment or text-block line beginning with
// `public` counted as a declaration.
// Checkstyle also runs per leaf, which is what makes `./gradlew :<leaf>:check` able to answer this
// for that leaf alone.
//
// The name survives as an aggregate because three consumers still call it:
// .github/workflows/jpa-pr.yml:56, .github/workflows/jpa-release.yml:146, and the `jpaReleaseGate`
// below. It runs every leaf's `checkstyleMain`, so it is a superset of the rule it is named after —
// it cannot pass anything the deleted task would have failed, and it additionally reports the rest
// of the D2 ruleset. That superset is the reason to retire the name rather than keep it: once those
// three callers say `checkstyleMain`, this registration can go.
tasks.register('verifyOneTypePerFile') {
group = 'verification'
description = 'code-conventions I6: one public top-level type per file; file name == type name.'
doLast {
def typeDecl = ~/^public\s+(?:final\s+|abstract\s+|sealed\s+|non-sealed\s+)*(?:class|interface|record|enum|@interface)\s+([A-Za-z0-9_]+)/
List<String> violations = []
rootProject.subprojects.each { sub ->
File mainJava = sub.file('src/main/java')
if (!mainJava.exists()) {
return
}
mainJava.eachFileRecurse { File f ->
if (!f.name.endsWith('.java') || f.name == 'package-info.java' || f.name == 'module-info.java') {
return
}
List<String> names = []
f.eachLine { String line ->
def m = (line =~ typeDecl)
if (m.find()) {
names << m.group(1)
}
}
if (names.size() > 1) {
violations << "${f.path}: ${names.size()} public top-level types ${names}".toString()
} else if (names.size() == 1) {
String expected = f.name.replaceFirst(/\.java$/, '')
if (names[0] != expected) {
violations << "${f.path}: file name != public type name (type is '${names[0]}')".toString()
}
}
}
}
if (!violations.isEmpty()) {
throw new GradleException(
"verifyOneTypePerFile: ${violations.size()} violation(s) of code-conventions I6:\n " +
violations.join("\n "))
}
logger.lifecycle("verifyOneTypePerFile: OK — one public top-level type per file, names match.")
}
description = 'Runs every leaf\'s main Checkstyle analysis, which owns code-conventions I6.'
dependsOn subprojects.findAll { it.childProjects.isEmpty() }
.collect { it.tasks.named('checkstyleMain') }
}
// verifyNoIgnoredSourcePackages — a Java package must never be invisible to Git.
@@ -3208,3 +3156,49 @@ tasks.register('verifyQuarantineSunset') {
"(${sunsetDays}-day sunset enforced).")
}
}
// ---------------------------------------------------------------------------------------------
// Repository-wide verification, wired once at the root.
//
// Registered here rather than in each leaf because these gates answer repository questions —
// "does any module break the dependency direction", "does the env registry match the three files
// that read it", "does any Trivy suppression outlive its window". Running them 62 times answered
// the same question 62 times and made `./gradlew :<leaf>:check` a repository build.
//
// Placed at the end of the file so every root task above is registered before it is named.
// ---------------------------------------------------------------------------------------------
tasks.register('check') {
group = 'verification'
description = 'Runs the repository-wide gates. Leaf checks cover their own leaf.'
dependsOn verifySpotBugsAnalysisFailureContract
dependsOn tasks.named('verifyCleanArchitectureDependencies')
dependsOn tasks.named('verifyRuntimeModuleMembership')
dependsOn tasks.named('verifyEnvKeys')
dependsOn tasks.named('verifyNoStaleTraceableJars')
dependsOn tasks.named('verifyNoIgnoredSourcePackages')
dependsOn tasks.named('verifyTrivyignore')
dependsOn tasks.named('verifyQuarantineSunset')
dependsOn verifyConfigurationPropertiesProcessor
}
// Gates whose subject is a document. Not wired into any `check`.
//
// Each of these compares prose against the build: README commands against the task graph, a stated
// leaf count against the registry, runbook identifiers against the source tree, a strategy table
// against the declared source sets. All four are worth keeping and all four are worth running — but
// a stale sentence is not a defect that a build can be failed for, and making it one means a
// documentation fix is required before unrelated code can compile. They stay individually runnable
// (`./gradlew verifyDocumentedLeafCount`), their failure messages are unchanged, and this aggregate
// is the single task a CI documentation stage invokes.
//
// Reachability is the thing that made them worth wiring into `check` in the first place, and it is
// preserved by the CI stage, not by the local build: if no workflow calls this task, these gates are
// back to reporting whatever was true the last time somebody typed their names.
tasks.register('verifyDocumentationContracts') {
group = 'verification'
description = 'Runs the documentation-drift gates (README, leaf count, runbooks, test source sets).'
dependsOn rootProject.tasks.named('verifyReadmeCommands')
dependsOn rootProject.tasks.named('verifyDocumentedLeafCount')
dependsOn rootProject.tasks.named('verifyRunbookReferences')
dependsOn rootProject.tasks.named('verifyTestSourceSetRegistry')
}
@@ -13,4 +13,20 @@
<!-- ApprovalTests discovers this package setting by exact public static field name. -->
<suppress checks="StaticVariableName"
files="[\\/]src[\\/]test[\\/]java[\\/]dev[\\/]caskeleton[\\/]bootstrap[\\/]contract[\\/]PackageSettings\.java"/>
<!-- code-conventions I6 outside main source.
OneTopLevelClass and OuterTypeFilename replace the hand-rolled `verifyOneTypePerFile`
Gradle task, which read `src/main/java` and nothing else. Applying them to every source set
at the same time would be a policy change smuggled in as a tool change: 28 OneTopLevelClass
violations and 1 OuterTypeFilename violation exist today, all of them co-located test
fixtures (`...Test.java` holding a `...Fixtures` / `Fake...` / `Recording...` type below the
test class), and none of them in main source — main is clean on both rules, including the
package-private top-level types the old regex could not see.
Extending the rule to test sources is a real question with a real answer either way, and it
belongs to whoever moves those 29 fixtures into their own files or decides they should stay.
The regex matches any src/<sourceSet>/java path whose source set is not `main`. -->
<suppress checks="OneTopLevelClass|OuterTypeFilename"
files="[\\/]src[\\/](?!main[\\/])[^\\/]+[\\/]java[\\/]"/>
</suppressions>
+20
View File
@@ -62,6 +62,26 @@
</module>
<module name="PatternVariableName"/>
<!-- ================= file/type structure (code-conventions I6) =================
One top-level type per file, and the file is named after it.
These replace the hand-rolled `verifyOneTypePerFile` Gradle task, which matched
`^public ...` line by line. That regex could not see a package-private top-level type
(126 main sources declared none that it matched at all), read only `src/main/java`,
and anchored at column 0 so a block comment or text-block line starting with `public`
would have counted as a declaration. Checkstyle parses the file, so all three go away.
Neither rule is redundant with javac. javac rejects a *public* type whose name does not
match the file; both of these also hold for package-private top-level types, which
javac accepts.
Scope is main source only, which is exactly what the task it replaces checked. Test
sources declare 29 co-located fixture types that these rules would reject, and widening
the rule while claiming to replace a task is two changes wearing one name — the
exemption is in checkstyle-suppressions.xml with the count. -->
<module name="OneTopLevelClass"/>
<module name="OuterTypeFilename"/>
<!-- ================= logical / design (KEEP) — google_checks CS-C1 ================= -->
<module name="NeedBraces"/>
<module name="FallThrough"/>
+24 -71
View File
@@ -19,14 +19,25 @@
// mistake repository-wide, and the required-class check below blocks the other half of it, where
// the boundary test quietly disappears and the lane still reports green.
//
// Lanes (design §24, Stable plan Task 1 / Task 48):
// graphqlStableTest Stable platform unit + boundary tests (default lane)
// graphqlContractTest cross-module contract suites (@Tag("graphql-contract"))
// graphqlAdvancedTest Advanced/Experimental capability tests (@Tag("graphql-advanced"))
// graphqlPerformanceTest load/soak/fault scenarios (@Tag("graphql-performance"))
// One lane survives here: `graphqlStableTest`, the Stable platform unit + boundary lane that CI
// runs (.github/workflows/ci-quality-gates.yml). It earns its place next to the default `test` task
// for exactly one reason — the required-class check below, which refuses a green lane that executed
// no case of the module-boundary test.
//
// `graphql-performance` is excluded from the default `test` task so external load and soak work can
// never run inside the unit lane.
// Three further lanes were registered here and are gone. `graphqlContractTest` and
// `graphqlAdvancedTest` re-selected `@Tag("graphql-contract")` and `@Tag("graphql-advanced")` tests
// that the default `test` task already runs, so deleting them changes the set of executed tests by
// nothing, and no workflow, build file or `check` ever named either one. `graphqlPerformanceTest`
// demanded load, soak and fault scenarios that do not exist — no test in this repository carries
// `@Tag("graphql-performance")` — so the lane failed by construction on every invocation, which is
// why nothing ever invoked it. A lane that always fails and that nobody runs blocks nothing.
//
// The `graphql-performance` exclusion went with them, from this lane and from the default `test`
// task (`excludeTags 'quarantine'` already reaches every leaf's `test` from src/build.gradle).
// Keeping an exclusion after deleting the only lane that selected the tag would mean a future
// `@Tag("graphql-performance")` test runs nowhere and says so nowhere. When a real load environment
// exists, declare the lane through the `ca.strict-test-lane` convention plugin rather than
// re-deriving `failOnNoDiscoveredTests` and an empty-result check by hand.
ext.registerGraphQlPlatformTestLanes = { ->
String platformPackage = 'dev.caskeleton.adapter.inbound.graphql'
@@ -46,25 +57,15 @@ ext.registerGraphQlPlatformTestLanes = { ->
}
Closure<Map<String, Object>> readJUnitEvidence = rootProject.ext.readJUnitEvidence
tasks.named('test') {
useJUnitPlatform {
excludeTags 'quarantine', 'graphql-performance'
}
}
Closure<Void> configureLane = { org.gradle.api.tasks.testing.Test lane ->
lane.group = 'verification'
lane.testClassesDirs = sourceSets.test.output.classesDirs
lane.classpath = sourceSets.test.runtimeClasspath
lane.jvmArgs '-Duser.timezone=UTC'
lane.outputs.upToDateWhen { false }
}
tasks.register('graphqlStableTest', Test) {
description = 'Runs the Stable GraphQL platform test lane (Stable plan Task 1-48).'
configureLane(it)
group = 'verification'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
jvmArgs '-Duser.timezone=UTC'
outputs.upToDateWhen { false }
useJUnitPlatform {
excludeTags 'quarantine', 'graphql-performance', 'graphql-advanced'
excludeTags 'quarantine', 'graphql-advanced'
}
filter {
includeTestsMatching "${platformPackage}.*"
@@ -99,52 +100,4 @@ ext.registerGraphQlPlatformTestLanes = { ->
}
}
}
tasks.register('graphqlContractTest', Test) {
description = 'Runs the GraphQL cross-module contract lane (Stable plan Task 47).'
configureLane(it)
useJUnitPlatform {
includeTags 'graphql-contract'
excludeTags 'quarantine'
}
failOnNoDiscoveredTests = true
}
tasks.register('graphqlAdvancedTest', Test) {
description = 'Runs the Advanced/Experimental GraphQL capability lane (Advanced plan Task 1-19).'
configureLane(it)
useJUnitPlatform {
includeTags 'graphql-advanced'
excludeTags 'quarantine'
}
failOnNoDiscoveredTests = true
}
tasks.register('graphqlPerformanceTest', Test) {
description = 'Runs the GraphQL load, soak and fault scenario lane (design §24.3, §24.4).'
configureLane(it)
useJUnitPlatform {
includeTags 'graphql-performance'
excludeTags 'quarantine'
}
// The Stable gate requires real load/fault evidence before a Stable release claim, so an
// empty run here is a missing-evidence condition rather than a pass.
//
// `failOnNoDiscoveredTests` alone does NOT cover this: it reacts to an empty candidate class
// scan, and this lane always scans a non-empty test tree that JUnit then tag-filters down to
// zero. Without the explicit result check below the lane reports BUILD SUCCESSFUL while
// proving nothing. Verified empirically on Gradle 9.0.0.
failOnNoDiscoveredTests = true
doLast {
File resultsDir = reports.junitXml.outputLocation.get().asFile
File[] executed = resultsDir.listFiles({ File file -> file.name.endsWith('.xml') } as FileFilter)
if (executed == null || executed.length == 0) {
throw new org.gradle.api.GradleException(
'graphqlPerformanceTest ran no scenario: the Stable release gate requires real ' +
'load, soak and fault evidence, so an empty performance lane is a missing-evidence ' +
'failure, not a pass. Register @Tag("graphql-performance") scenarios or run the ' +
'lane against the external load environment that owns them.')
}
}
}
}
+76 -350
View File
@@ -157,251 +157,106 @@ Closure<Map<String, String>> resolvedJpaEvidenceVersions = {
] as Map<String, String>
}
Set<String> expectedJpaEvidenceManifestKeys = [
'schemaVersion',
'cardId',
'cardVersion',
'declaredState',
'attainedReadiness',
'evidenceGrade',
'profile',
'prerequisites',
'source',
'producer',
'testResult',
'requiredEvidence',
'coveredEvidence',
'missingEvidence',
'readinessBlockers',
'postgresql',
'dependencies',
'generatedAt',
'date',
'topology',
'artifactLocation',
'migration',
'dispatchModes'
] as Set
// Only the checks whose answer the writer does not already know.
//
// This validator used to assert thirty-odd properties of a manifest that
// `generateJpaEvidenceManifests` had written a few hundred lines earlier in the same process: that
// `schemaVersion` was the literal 1 the writer wrote, that the key set was the key set of its own
// map literal, that `missingEvidence` equalled `required - covered` — which is the expression the
// writer evaluates. No path in this repository accepts a manifest from anywhere else. The generator
// deletes the output directory and writes every file the verifier then reads, in the same build, so
// there is no hand-written manifest to reject and no forgery to detect. Those assertions could not
// fail, and a check that cannot fail proves nothing about the evidence while still having to be
// maintained, read and trusted.
//
// What is left is what the build learned from outside itself and could therefore be wrong about:
// the JUnit XML (executed and skipped counts), `docker image inspect`, the resolved dependency
// graph, and the R2 profile's provenance inputs.
Closure<List<String>> validateJpaEvidenceManifest = { Map<String, Object> manifest ->
List<String> violations = []
String cardId = manifest.cardId as String
Closure<List<String>> validateJpaEvidenceManifest = {
Map<String, Object> card,
Map<String, Object> manifest ->
List<String> violations = []
String cardId = manifest.cardId as String
Set<String> actualKeys = manifest.keySet().collect { it as String }.toSet()
if (actualKeys != expectedJpaEvidenceManifestKeys) {
violations << "${cardId}: manifest keys must be exactly ${expectedJpaEvidenceManifestKeys}"
}
if (manifest.schemaVersion != 1) {
violations << "${cardId}: schemaVersion must be 1"
}
if (cardId == null || cardId.isBlank()) {
violations << 'manifest cardId must be non-blank'
}
if (!(manifest.declaredState in ['selected', 'implemented-candidate'])) {
violations << "${cardId}: invalid declaredState '${manifest.declaredState}'"
}
if (!(manifest.attainedReadiness in ['R1', 'R2'])) {
violations << "${cardId}: invalid attainedReadiness '${manifest.attainedReadiness}'"
}
if (!(manifest.evidenceGrade in ['E1', 'E2', 'E3'])) {
violations << "${cardId}: invalid evidenceGrade '${manifest.evidenceGrade}'"
}
if (!(manifest.profile in ['candidate', 'r2'])) {
violations << "${cardId}: invalid profile '${manifest.profile}'"
// The reason this file exists. Gradle's `Test` fails a build on a failing test and passes it on
// a skipped one, so a PostgreSQL container that never started — every integration test skipped
// by an unmet assumption — is BUILD SUCCESSFUL. A card's evidence claims its scenarios ran, and
// a skip is not a result.
Map<String, Object> testResult = (manifest.testResult ?: [:]) as Map<String, Object>
if (((testResult.executedTestCount ?: 0) as int) <= 0) {
violations << "${cardId}: executed test count must be positive"
}
['skippedOrAbortedCount', 'failureCount', 'errorCount'].each { String countKey ->
if (((testResult[countKey] ?: 0) as int) != 0) {
violations << "${cardId}: ${countKey} must be zero"
}
}
if (testResult.noSkipResult != true) {
violations << "${cardId}: no-skip sentinel must be true"
}
Map<String, Object> source = manifest.source instanceof Map
? manifest.source as Map<String, Object>
: [:]
if (source.keySet().collect { it as String }.toSet() !=
['revision', 'worktreeDirty', 'worktreeStatusDigest'] as Set) {
violations << "${cardId}: invalid source metadata keys"
}
if (!((source.revision as String) ==~ /[0-9a-f]{7,40}/)) {
violations << "${cardId}: invalid source revision '${source.revision}'"
}
if (!(source.worktreeDirty instanceof Boolean)) {
violations << "${cardId}: worktreeDirty must be boolean"
}
if (!((source.worktreeStatusDigest as String) ==~ /[0-9a-f]{64}/)) {
violations << "${cardId}: invalid worktree status digest"
}
// `docker image inspect` on an image that was never pulled by digest prints nothing, and a
// manifest that cannot name the image its tests ran against is not evidence about a PostgreSQL
// version.
Map<String, Object> postgresql = (manifest.postgresql ?: [:]) as Map<String, Object>
if (!((postgresql.imageDigest as String) ==~ /.+@sha256:[0-9a-f]{64}/)) {
violations << "${cardId}: PostgreSQL image digest must be immutable"
}
Map<String, Object> producer = manifest.producer instanceof Map
? manifest.producer as Map<String, Object>
: [:]
if (producer.keySet().collect { it as String }.toSet() !=
['gradleTask', 'ciJob'] as Set) {
violations << "${cardId}: invalid producer metadata keys"
}
if (!((producer.gradleTask as String)?.startsWith(':'))) {
violations << "${cardId}: producer Gradle task must be absolute"
}
if ((producer.ciJob as String)?.isBlank()) {
violations << "${cardId}: producer CI job must be non-blank"
// Resolved from the integration-test runtime classpath, so a renamed or dropped module leaves a
// blank here rather than a wrong version.
Map<String, Object> dependencies = (manifest.dependencies ?: [:]) as Map<String, Object>
['pgjdbc', 'hibernate', 'flyway'].each { String component ->
if (((dependencies[component] ?: '') as String).isBlank()) {
violations << "${cardId}: ${component} version must be present"
}
}
Map<String, Object> testResult = manifest.testResult instanceof Map
? manifest.testResult as Map<String, Object>
: [:]
Set<String> expectedTestKeys = [
'tasks',
'resultDirectories',
'executedTestCount',
'skippedOrAbortedCount',
'failureCount',
'errorCount',
'noSkipResult',
'executedSelectors'
] as Set
if (testResult.keySet().collect { it as String }.toSet() != expectedTestKeys) {
violations << "${cardId}: invalid testResult keys"
}
if (!((testResult.executedTestCount ?: 0) instanceof Number) ||
(testResult.executedTestCount as int) <= 0) {
violations << "${cardId}: executed test count must be positive"
}
['skippedOrAbortedCount', 'failureCount', 'errorCount'].each { String countKey ->
if (!((testResult[countKey] ?: 0) instanceof Number) ||
(testResult[countKey] as int) != 0) {
violations << "${cardId}: ${countKey} must be zero"
}
}
if (testResult.noSkipResult != true) {
violations << "${cardId}: no-skip sentinel must be true"
}
List<String> required = manifest.requiredEvidence instanceof List
? (manifest.requiredEvidence as List).collect { it as String }.toSorted()
: []
List<String> covered = manifest.coveredEvidence instanceof List
? (manifest.coveredEvidence as List).collect { it as String }.toSorted()
: []
// R2 is the release claim, and every input below comes from the environment the lane ran in
// rather than from this build's own literals.
if (manifest.attainedReadiness == 'R2') {
Map<String, Object> source = (manifest.source ?: [:]) as Map<String, Object>
Map<String, Object> producer = (manifest.producer ?: [:]) as Map<String, Object>
List<String> missing = manifest.missingEvidence instanceof List
? (manifest.missingEvidence as List).collect { it as String }.toSorted()
? (manifest.missingEvidence as List).collect { it as String }
: []
if (required != requiredJpaEvidence(card)) {
violations << "${cardId}: required evidence drifted from registry"
if (manifest.profile != 'r2') {
violations << "${cardId}: R2 requires the r2 profile"
}
if (missing != (required - covered).toSorted()) {
violations << "${cardId}: missing evidence is not required minus covered"
if (source.worktreeDirty != false) {
violations << "${cardId}: R2 requires a clean worktree"
}
Map<String, Object> postgresql = manifest.postgresql instanceof Map
? manifest.postgresql as Map<String, Object>
: [:]
if (postgresql.keySet().collect { it as String }.toSet() !=
['image', 'imageDigest', 'managedEngineVersion'] as Set) {
violations << "${cardId}: invalid PostgreSQL metadata keys"
if (!missing.isEmpty()) {
violations << "${cardId}: R2 has missing evidence ${missing}"
}
if (!((postgresql.imageDigest as String) ==~ /.+@sha256:[0-9a-f]{64}/)) {
violations << "${cardId}: PostgreSQL image digest must be immutable"
if (((producer.ciJob ?: '') as String).isBlank() || producer.ciJob == 'local-unpublished') {
violations << "${cardId}: R2 requires a real CI job identity"
}
Map<String, Object> dependencies = manifest.dependencies instanceof Map
? manifest.dependencies as Map<String, Object>
: [:]
if (dependencies.keySet().collect { it as String }.toSet() !=
['pgjdbc', 'hibernate', 'flyway'] as Set ||
dependencies.values().any { Object version -> (version as String)?.isBlank() }) {
violations << "${cardId}: pgjdbc/Hibernate/Flyway versions must be present"
if (!((manifest.artifactLocation as String) ==~ /(?i)(https|s3|gs):\/\/\S+/)) {
violations << "${cardId}: R2 requires an externally retained artifact location"
}
try {
Instant.parse(manifest.generatedAt as String)
} catch (RuntimeException ignored) {
violations << "${cardId}: generatedAt must be an ISO-8601 instant"
}
if (!((manifest.date as String) ==~ /\d{4}-\d{2}-\d{2}/)) {
violations << "${cardId}: date must be ISO-8601"
}
if ((manifest.topology as String)?.isBlank()) {
violations << "${cardId}: topology must be non-blank"
}
if ((manifest.artifactLocation as String)?.isBlank()) {
violations << "${cardId}: artifactLocation must be non-blank"
}
List<Object> prerequisites = manifest.prerequisites instanceof List
? manifest.prerequisites as List<Object>
: []
prerequisites.eachWithIndex { Object rawPrerequisite, int index ->
Map<String, Object> prerequisite = rawPrerequisite instanceof Map
? rawPrerequisite as Map<String, Object>
: [:]
if (prerequisite.keySet().collect { it as String }.toSet() !=
['cardId', 'cardVersion', 'manifestId', 'attainedReadiness'] as Set) {
violations << "${cardId}: prerequisite ${index} has invalid keys"
}
if (!((prerequisite.manifestId as String) ==~ /sha256:[0-9a-f]{64}/)) {
violations << "${cardId}: prerequisite ${index} has invalid manifest ID"
}
}
if (card.migration instanceof Map) {
Map<String, Object> migration = manifest.migration instanceof Map
? manifest.migration as Map<String, Object>
: [:]
Set<String> expectedMigrationKeys = [
'location',
'historyTable',
'requiredCoreEpoch',
'featureRevision',
'streamLifecycleEvidenceIds'
] as Set
if (migration.keySet().collect { it as String }.toSet() != expectedMigrationKeys) {
violations << "${cardId}: schema-bearing manifest has invalid migration metadata"
}
} else if (manifest.migration != null) {
violations << "${cardId}: non-schema card must not contain migration metadata"
}
if (card['dispatch-modes'] instanceof List) {
if (manifest.dispatchModes != card['dispatch-modes']) {
violations << "${cardId}: dispatch modes drifted from registry"
}
} else if (manifest.dispatchModes != []) {
violations << "${cardId}: non-outbox card must have empty dispatch modes"
}
if (manifest.attainedReadiness == 'R2') {
if (manifest.profile != 'r2') {
violations << "${cardId}: R2 requires the r2 profile"
}
if (source.worktreeDirty != false) {
violations << "${cardId}: R2 requires a clean worktree"
}
if (!missing.isEmpty()) {
violations << "${cardId}: R2 has missing evidence ${missing}"
}
if (producer.ciJob == 'local-unpublished') {
violations << "${cardId}: R2 requires a real CI job identity"
}
if (!((manifest.artifactLocation as String) ==~
/(?i)(https|s3|gs):\/\/\S+/)) {
violations << "${cardId}: R2 requires an externally retained artifact location"
}
}
violations
}
violations
}
Closure<Map<String, Object>> loadJpaEvidenceRegistry = {
new JsonSlurper().parse(jpaEvidenceRegistryFile) as Map<String, Object>
}
// Reads back what generateJpaEvidenceManifests just wrote. It does not re-derive the content hash
// from the file name: the generator names each file after the hash it computed one statement
// earlier, so that comparison only ever proved that JsonOutput and JsonSlurper round-trip. The same
// goes for the prerequisite manifest-ID cross-check, whose two sides were both filled in from the
// generator's own `manifestIds` map.
Closure<Map<String, Object>> verifyJpaEvidenceDirectory = {
File outputDirectory,
Map<String, Object> registry ->
List<String> violations = []
Map<String, Object> manifests = [:]
Map<String, String> manifestIds = [:]
Map<String, Object> activeCards = (registry.cards as Map<String, Object>).findAll {
Set<String> activeCardIds = (registry.cards as Map<String, Object>).findAll {
String ignored, Object rawCard ->
((rawCard as Map).state as String) != 'not-implemented'
}
}.keySet()
activeCards.each { String cardId, Object rawCard ->
activeCardIds.each { String cardId ->
File cardDirectory = new File(outputDirectory, cardId)
List<File> files = cardDirectory.isDirectory()
? (cardDirectory.listFiles() ?: [] as File[])
@@ -411,143 +266,18 @@ Closure<Map<String, Object>> verifyJpaEvidenceDirectory = {
violations << "${cardId}: expected exactly one content-addressed manifest; got ${files.size()}"
return
}
File manifestFile = files[0]
String fileHash = manifestFile.name.substring(0, manifestFile.name.length() - '.json'.length())
Map<String, Object> manifest =
new JsonSlurper().parse(manifestFile) as Map<String, Object>
String contentHash = sha256JpaEvidence(canonicalJpaEvidenceJson(manifest))
if (fileHash != contentHash) {
violations << "${cardId}: filename hash ${fileHash} does not match content ${contentHash}"
}
if ((manifest.cardId as String) != cardId) {
violations << "${cardId}: manifest cardId is '${manifest.cardId}'"
}
violations.addAll(validateJpaEvidenceManifest(
rawCard as Map<String, Object>,
manifest))
new JsonSlurper().parse(files[0]) as Map<String, Object>
violations.addAll(validateJpaEvidenceManifest(manifest))
manifests[cardId] = manifest
manifestIds[cardId] = "sha256:${contentHash}".toString()
}
manifests.each { String cardId, Object rawManifest ->
Map<String, Object> manifest = rawManifest as Map<String, Object>
(manifest.prerequisites as List).each { Object rawPrerequisite ->
Map<String, Object> prerequisite = rawPrerequisite as Map<String, Object>
String prerequisiteId = prerequisite.cardId as String
if (manifestIds[prerequisiteId] != prerequisite.manifestId) {
violations << "${cardId}: prerequisite ${prerequisiteId} manifest ID does not match"
}
}
}
[violations: violations, manifests: manifests, manifestIds: manifestIds]
}
def verifyJpaEvidenceHarnessContract = tasks.register('verifyJpaEvidenceHarnessContract') {
group = 'verification'
description = 'Mutation-tests JPA evidence schema, no-skip, content hash, and R2 provenance checks.'
doLast {
Map<String, Object> card = [
state: 'selected',
'required-evidence': ['real-postgresql', 'no-skip']
]
Map<String, Object> valid = [
schemaVersion: 1,
cardId: 'jpa-contract-fixture',
cardVersion: '1',
declaredState: 'selected',
attainedReadiness: 'R1',
evidenceGrade: 'E2',
profile: 'candidate',
prerequisites: [],
source: [
revision: 'b3add0162df8',
worktreeDirty: true,
worktreeStatusDigest: '0' * 64
],
producer: [
gradleTask: ':adapter:outbound:persistence-jpa:contractFixture',
ciJob: 'local-unpublished'
],
testResult: [
tasks: [':adapter:outbound:persistence-jpa:contractFixture'],
resultDirectories: ['build/test-results/contractFixture'],
executedTestCount: 1,
skippedOrAbortedCount: 0,
failureCount: 0,
errorCount: 0,
noSkipResult: true,
executedSelectors: ['dev.caskeleton.ContractFixture#passes']
],
requiredEvidence: ['no-skip', 'real-postgresql'],
coveredEvidence: ['no-skip', 'real-postgresql'],
missingEvidence: [],
readinessBlockers: ['candidate-profile-is-not-release-evidence'],
postgresql: [
image: 'postgres:16-alpine',
imageDigest: "postgres@sha256:${'1' * 64}".toString(),
managedEngineVersion: '16'
],
dependencies: [
pgjdbc: '42.7.8',
hibernate: '7.1.8.Final',
flyway: '11.14.1'
],
generatedAt: '2026-07-28T00:00:00Z',
date: '2026-07-28',
topology: 'single-postgresql-testcontainer',
artifactLocation: 'build/jpa-evidence/manifests',
migration: null,
dispatchModes: []
]
List<String> baseline = validateJpaEvidenceManifest(card, valid)
if (!baseline.isEmpty()) {
throw new GradleException(
"verifyJpaEvidenceHarnessContract: valid fixture failed ${baseline}")
}
Map<String, Object> skipped =
new JsonSlurper().parseText(JsonOutput.toJson(valid)) as Map<String, Object>
(skipped.testResult as Map).skippedOrAbortedCount = 1
(skipped.testResult as Map).noSkipResult = false
List<String> skippedViolations = validateJpaEvidenceManifest(card, skipped)
if (!skippedViolations.any { String violation -> violation.contains('must be zero') } ||
!skippedViolations.any { String violation -> violation.contains('sentinel must be true') }) {
throw new GradleException(
"verifyJpaEvidenceHarnessContract: skip mutation escaped ${skippedViolations}")
}
Map<String, Object> dirtyR2 =
new JsonSlurper().parseText(JsonOutput.toJson(valid)) as Map<String, Object>
dirtyR2.attainedReadiness = 'R2'
dirtyR2.profile = 'r2'
List<String> dirtyViolations = validateJpaEvidenceManifest(card, dirtyR2)
if (!dirtyViolations.any { String violation -> violation.contains('clean worktree') } ||
!dirtyViolations.any { String violation -> violation.contains('real CI job') }) {
throw new GradleException(
"verifyJpaEvidenceHarnessContract: R2 provenance mutation escaped ${dirtyViolations}")
}
String validHash = sha256JpaEvidence(canonicalJpaEvidenceJson(valid))
Map<String, Object> mutated =
new JsonSlurper().parseText(JsonOutput.toJson(valid)) as Map<String, Object>
mutated.topology = 'mutated-topology'
String mutatedHash = sha256JpaEvidence(canonicalJpaEvidenceJson(mutated))
if (validHash == mutatedHash) {
throw new GradleException(
'verifyJpaEvidenceHarnessContract: content mutation did not change manifest ID')
}
logger.lifecycle(
'verifyJpaEvidenceHarnessContract: OK — skip, dirty/local R2, and content mutation fail closed.')
}
[violations: violations, manifests: manifests]
}
def generateJpaEvidenceManifests = tasks.register('generateJpaEvidenceManifests') {
group = 'verification'
description = 'Runs active JPA card producers and writes content-addressed candidate/R2 manifests.'
dependsOn verifyJpaEvidenceHarnessContract
dependsOn rootProject.tasks.named('verifyJpaReadinessRegistry')
Map<String, Object> configuredRegistry = loadJpaEvidenceRegistry()
@@ -936,7 +666,3 @@ tasks.register('verifyJpaPrimaryFoundationEvidence') {
'verifyJpaPrimaryFoundationEvidence: OK — six immutable R2 base manifests and the primary DAG are verified.')
}
}
tasks.named('check') {
dependsOn verifyJpaEvidenceHarnessContract
}
+64 -76
View File
@@ -1,105 +1,93 @@
// NTF-025 — the configuration reference, the YAML tree and the env-key registry say one thing.
// NTF-025 — an env key the registry lists and nothing reads.
//
// The reference named three properties the binding never had (max-retry-concurrency,
// scheduler-poll-interval, callback-worker-concurrency) and omitted three it did, while
// application.yml carried no platform tree at all. A template user could only discover the settings
// by guessing environment variable names out of Boot's relaxed binding — which works, and gives no
// way to find out which profile, secret, callback and activation settings have to line up.
// This task used to check four relationships at once: application.yml against the configuration
// reference document in both directions, and application.yml against the env-key registry in both
// directions. Three of the four are now owned elsewhere or were never a build concern.
//
// Three artifacts, one fact. This task fails when they disagree in any direction.
// * application.yml -> env-keys.yaml is `verifyEnvKeys` check D (src/build.gradle), which makes
// the same comparison over every `APP_*` reference, optional inline defaults included. The
// notification platform's keys all carry the `APP_` prefix, so they were being compared twice by
// two implementations that could disagree.
// * application.yml <-> docs/notification/configuration-reference.md was documentation drift. A
// reference that names a property the binding never had is a bad document, not a broken
// platform: nothing fails to start, no request is mishandled, no data moves. It was failing the
// `check` of every leaf in the repository over prose.
//
// What remains is the one direction nothing else covers: a key registered in env-keys.yaml that no
// binding reads. That one is worth a build failure because the registry is what an operator
// configures from — a key listed there that reaches no binding is an instruction to set an
// environment variable that does nothing, and it is indistinguishable from one that works.
//
// It reads the composition root's whole YAML set, not application.yml alone. Two of the deleted
// checks located the platform tree by slicing application.yml between the literals
// ` notification:\n platform:` and `\n persistence:` — an indent width and the NAME OF A
// SIBLING KEY. The tree has since moved into config/notification.yml, imported by
// application.yml's `spring.config.import`, so both literals stopped matching and this task failed
// on every `check` in the repository at its first assertion. Scanning application.yml plus every
// config/*.yml it imports means the same keys are found wherever the composition root chooses to
// keep them.
tasks.register('verifyNotificationConfiguration') {
group = 'verification'
description = 'Fails when the notification configuration reference, application.yml and the env-key registry disagree.'
description = 'Fails when docs/registries/env-keys.yaml registers a notification platform key no binding reads.'
File applicationYaml =
rootProject.file('app-bootstrap/src/main/resources/application.yml')
File referenceDocument =
rootProject.file('../docs/notification/configuration-reference.md')
File resourceRoot = rootProject.file('app-bootstrap/src/main/resources')
File applicationYaml = new File(resourceRoot, 'application.yml')
File configurationDirectory = new File(resourceRoot, 'config')
File environmentRegistry = rootProject.file('../docs/registries/env-keys.yaml')
inputs.files(applicationYaml, referenceDocument, environmentRegistry)
inputs.files(applicationYaml, environmentRegistry)
inputs.dir(configurationDirectory)
doLast {
[applicationYaml, referenceDocument, environmentRegistry].each { File required ->
[applicationYaml, environmentRegistry].each { File required ->
if (!required.isFile()) {
throw new GradleException("verifyNotificationConfiguration: missing ${required}")
}
}
// Environment variables the platform tree in application.yml actually references. The tree
// is delimited by its own comment marker rather than by indentation counting, so a reformat
// does not silently empty this set.
String yaml = applicationYaml.getText('UTF-8')
List<File> boundSources = [applicationYaml]
if (configurationDirectory.isDirectory()) {
boundSources.addAll(
configurationDirectory.listFiles()
.findAll { File file -> file.isFile() && file.name.endsWith('.yml') }
.toSorted { File file -> file.name })
}
// The tree's own comment tells a template user where the reference is. It named a file that
// does not exist, which is the same failure as an out-of-date reference and harder to
// notice: the reader concludes the documentation is missing rather than that the pointer
// is. Checked here because this task already owns the agreement between the two.
if (!yaml.contains('docs/notification/' + referenceDocument.name)) {
Set<String> boundVariables = new TreeSet<>()
boundSources.each { File source ->
def placeholder =
(source.getText('UTF-8') =~ /\$\{(APP_NOTIFICATION_PLATFORM_[A-Z0-9_]*)(:[^}]*)?\}/)
while (placeholder.find()) {
boundVariables << placeholder.group(1)
}
}
if (boundVariables.isEmpty()) {
// Fail closed. An empty set makes every registry entry look unread, but it far more
// likely means the platform tree moved again, and a check comparing nothing against
// nothing passes forever.
throw new GradleException(
'verifyNotificationConfiguration: application.yml does not point at ' +
"docs/notification/${referenceDocument.name}, so the tree tells a " +
'reader to consult a document this task does not verify.')
'verifyNotificationConfiguration: no APP_NOTIFICATION_PLATFORM_* placeholder is ' +
"bound anywhere in ${rootProject.relativePath(resourceRoot)}, so there is " +
'nothing to compare the registry against.')
}
int treeStart = yaml.indexOf(' notification:\n platform:')
if (treeStart < 0) {
throw new GradleException(
'verifyNotificationConfiguration: application.yml has no ' +
'ca-skeleton.notification.platform tree. Without it this check would ' +
'compare the reference against nothing and pass.')
}
int treeEnd = yaml.indexOf('\n persistence:', treeStart)
String tree = treeEnd < 0 ? yaml.substring(treeStart) : yaml.substring(treeStart, treeEnd)
Set<String> yamlVariables = new TreeSet<>()
def placeholder = (tree =~ /\$\{(APP_NOTIFICATION_PLATFORM_[A-Z0-9_]*)(:[^}]*)?\}/)
while (placeholder.find()) {
yamlVariables << placeholder.group(1)
}
if (yamlVariables.isEmpty()) {
throw new GradleException(
'verifyNotificationConfiguration: the platform tree references no ' +
'APP_NOTIFICATION_PLATFORM_* variable, so nothing would be compared.')
}
// Variables the reference document promises.
Set<String> documentedVariables = new TreeSet<>()
def documented = (referenceDocument.getText('UTF-8') =~ /`(APP_NOTIFICATION_PLATFORM_[A-Z0-9_]*)`/)
while (documented.find()) {
documentedVariables << documented.group(1)
}
// Variables the registry knows.
Set<String> registeredVariables = new TreeSet<>()
def registered = (environmentRegistry.getText('UTF-8') =~ /(?m)^\s*- name:\s*(APP_NOTIFICATION_PLATFORM_[A-Z0-9_]+)\s*$/)
def registered = (environmentRegistry.getText('UTF-8')
=~ /(?m)^\s*- name:\s*(APP_NOTIFICATION_PLATFORM_[A-Z0-9_]+)\s*$/)
while (registered.find()) {
registeredVariables << registered.group(1)
}
List<String> problems = []
(yamlVariables - documentedVariables).each {
problems << "${it} is bound in application.yml and absent from the configuration reference"
List<String> problems = (registeredVariables - boundVariables).collect {
"${it} is registered in env-keys.yaml and bound by nothing".toString()
}
(documentedVariables - yamlVariables).each {
problems << "${it} is documented in the configuration reference and bound nowhere — " +
'this is the shape of the three properties the reference promised and the binding never had'
}
(yamlVariables - registeredVariables).each {
problems << "${it} is bound in application.yml and unregistered in env-keys.yaml"
}
(registeredVariables - yamlVariables).each {
problems << "${it} is registered in env-keys.yaml and read by nothing"
}
if (!problems.isEmpty()) {
throw new GradleException(
'verifyNotificationConfiguration: the configuration surface disagrees with ' +
"itself.\n " + problems.join('\n ') +
'\nThe binding is the fact; the reference and the registry describe it.')
'verifyNotificationConfiguration: the env-key registry promises settings the ' +
"binding does not have.\n " + problems.join('\n ') +
'\nThe binding is the fact; the registry describes it.')
}
logger.lifecycle(
"verifyNotificationConfiguration: OK — ${yamlVariables.size()} platform settings, " +
'bound, documented and registered.')
"verifyNotificationConfiguration: OK — ${registeredVariables.size()} registered " +
"platform keys, all bound under ${rootProject.relativePath(resourceRoot)}.")
}
}
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// The Advanced boundary itself: capability grades, the `ca-skeleton.grpc.advanced.*` feature-flag
// contract, the module guard that refuses an unflagged capability, and the per-capability
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// Compatibility bridges: gRPC-Web, the Servlet HTTP/2 profile, the Spring Integration bridge, the
// Reactor adapter, and the Kotlin coroutine/Flow boundary.
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// Channelz/CSDS diagnostics for administrators, with the redactor that keeps socket authority,
// credentials, certificate material, metadata and payload out of a snapshot, plus the advanced
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// Protobuf Edition lanes. Edition 2024 is an opt-in Advanced lane that must produce cross-consumer
// compile evidence before anything public moves onto it; Edition 2026 is a watch lane that records
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// Resilience and discovery capabilities that Stable refuses: read-only unary hedging, the custom
// name resolver SPI, the custom load balancer SPI, and the proxyless xDS experimental profile.
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// The streaming shapes the Stable plan deliberately excludes: client streaming sessions with
// dedup/checkpoint/resume, bidirectional sessions with independent per-direction sequences, and the
+1 -1
View File
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// Operational surface: the standard health registry, the reflection exposure policy, the drain
// coordinator, and the secret-free runtime policy snapshot an administrator reads.
+1 -6
View File
@@ -1,12 +1,7 @@
apply plugin: 'java-library'
apply plugin: 'ca.grpc-platform-module'
// Client runtime: named channel profiles, channel runtime generations with drain, the typed stub
// factory that refuses to hand a raw Channel to application code, and client metadata/credentials.
dependencyManagement {
imports {
mavenBom "io.grpc:grpc-bom:${grpcVersion}"
}
}
dependencies {
api project(':grpc:grpc-core-api')
+1 -1
View File
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// Contract governance: Buf format/lint/breaking policy, the single codegen owner declaration, and
// the descriptor/schema-hash release artifact with its consumer-compile gate.
+1 -1
View File
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// The platform's port layer: identifiers, method policy, execution evidence, failure model,
// deadline primitives and request context.
+1 -1
View File
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// Stable discovery: Static/DNS resolvers, pick_first/round_robin load balancing, and the
// Kubernetes VIP / headless / mesh routing profiles. Custom resolvers, custom load balancers and
+1 -1
View File
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// Bounded observability: logical RPC vs physical attempt vs stream lifecycle, with a cardinality
// policy that refuses payload, raw metadata and any actor/tenant/object/stream/idempotency
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// Durable mutation idempotency: the operation ledger entity, its state machine, the vendor-neutral
// repository port and the Spring Data JPA binding, plus the migration that owns the unique
+1 -10
View File
@@ -1,17 +1,8 @@
apply plugin: 'java-library'
apply plugin: 'ca.grpc-platform-module'
// Validation, context propagation, status/rich-error mapping, TLS/credential profiles, deadline
// and cancellation, retry ownership and eligibility, idempotency and completion recovery, server
// streaming, payload size and compression.
//
// io.grpc versions are NOT managed by the Spring Boot BOM and this repo has no version catalog, so
// the grpc-bom is imported at MODULE scope from the root `ext.grpcVersion` SSOT — the same shape
// `adapter:inbound:grpc` uses, keeping the strict-locking blast radius local.
dependencyManagement {
imports {
mavenBom "io.grpc:grpc-bom:${grpcVersion}"
}
}
dependencies {
api project(':grpc:grpc-core-api')
+1 -1
View File
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// Schema source of truth: the `.proto` files plus the rule engine that judges them.
//
+1 -6
View File
@@ -1,15 +1,10 @@
apply plugin: 'java-library'
apply plugin: 'ca.grpc-platform-module'
// Server boundary: the ArchUnit-shaped application boundary rules, the typed service adapter SPI,
// the interceptor order contract, and the Netty server/executor/admission profiles.
//
// The Netty profiles are configuration models, not Netty wiring — no netty dependency here. Real
// Netty lives in `grpc-testkit`'s certification lane, which is where transport evidence is produced.
dependencyManagement {
imports {
mavenBom "io.grpc:grpc-bom:${grpcVersion}"
}
}
dependencies {
api project(':grpc:grpc-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// The platform's composition boundary: typed properties, auto-configuration and the startup
// validator that refuses a deployment whose configuration contradicts a Stable invariant.
+1 -6
View File
@@ -1,15 +1,10 @@
apply plugin: 'java-library'
apply plugin: 'ca.grpc-platform-module'
// Certification. The Stable plan splits this across four modules (core / in-process / netty /
// fault); this repository already expresses "these two runs are not the same kind of evidence" with
// strict test lanes rather than with module boundaries, so the four become four lanes over one
// leaf (adaptation design §2). A lane that discovers nothing fails, and none of them can serve an
// up-to-date result — which is the property the split was protecting.
dependencyManagement {
imports {
mavenBom "io.grpc:grpc-bom:${grpcVersion}"
}
}
strictTestLanes {
lane('grpcInProcessContractTest') {
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
}
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
+2 -2
View File
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -75,7 +75,7 @@ strictTestLanes {
// breaking a laptop build.
tasks.named('test', Test) {
useJUnitPlatform {
excludeTags 'quarantine', 'messaging-certification'
excludeTags 'messaging-certification'
}
}
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
+1 -1
View File
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
+1 -1
View File
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// The central publish and delivery orchestration.
//
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
// Scopes, not a flat list of `api`.
//
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
+1 -1
View File
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'ca.platform-module'
dependencies {
api project(':messaging:messaging-core-api')
+18 -14
View File
@@ -90,21 +90,25 @@ dependencies {
// OpenApiDriftContractTest runs as a normal test (so `check` is release-blocking on drift) AND
// can be invoked through this task to (re)generate the committed snapshot after an intentional
// API change: ./gradlew :sample-portfolio:openapiCheckSnapshot -PapproveOpenApiChange
tasks.register('openapiCheckSnapshot', Test) {
group = 'verification'
description = 'OpenAPI drift gate: runtime springdoc /v3/api-docs vs the committed snapshot. ' +
'-PapproveOpenApiChange regenerates the committed baseline.'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform()
filter {
includeTestsMatching 'dev.caskeleton.sample.portfolio.adapter.inbound.web.contract.OpenApiDriftContractTest'
//
// Declared through `ca.strict-test-lane` rather than hand-registered. The hand-written version set
// `includeTestsMatching` and never set `failOnNoDiscoveredTests`, so renaming or deleting
// OpenApiDriftContractTest left this gate running nothing and reporting success on the one task
// whose whole job is to notice a change. `requires(...)` turns the class name into a selector the
// convention checks after the run, and the convention supplies the fail-closed discovery this task
// had no way to opt out of losing.
strictTestLanes {
lane('openapiCheckSnapshot') {
description = 'OpenAPI drift gate: runtime springdoc /v3/api-docs vs the committed snapshot. ' +
'-PapproveOpenApiChange regenerates the committed baseline.'
requires('dev.caskeleton.sample.portfolio.adapter.inbound.web.contract.OpenApiDriftContractTest')
customize = { test ->
test.systemProperty 'openapi.snapshot.write',
project.hasProperty('approveOpenApiChange') ? 'true' : 'false'
// Pin UTC like the main test task for host-locale independence.
test.jvmArgs '-Duser.timezone=UTC'
}
}
systemProperty 'openapi.snapshot.write', project.hasProperty('approveOpenApiChange') ? 'true' : 'false'
// Always re-check drift; never serve a stale UP-TO-DATE result.
outputs.upToDateWhen { false }
// Pin UTC like the main test task for host-locale independence.
jvmArgs '-Duser.timezone=UTC'
}
def posterImageMigrationQualification = registerStrictQualificationTest(