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:
co-authored by
Claude Opus 5
parent
1535481794
commit
9bc2e75fe5
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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=
|
||||
|
||||
BIN
Binary file not shown.
@@ -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
|
||||
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
+8
-1
@@ -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'
|
||||
}
|
||||
|
||||
+11
-6
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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"));
|
||||
}
|
||||
|
||||
+234
-91
@@ -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")
|
||||
|
||||
+12
-1
@@ -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))
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+11
@@ -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"));
|
||||
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+20
-8
@@ -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 + "'");
|
||||
}
|
||||
|
||||
+22
-3
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
+74
@@ -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
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,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,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,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,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,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,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,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,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,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,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')
|
||||
|
||||
@@ -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,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'
|
||||
|
||||
// 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,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')
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user