Compare commits

...
10 Commits
Author SHA1 Message Date
donghyeon-ka 60b6a319e7 refactor: 빌드 로직 개선, gradle 파일 경량화 2026-09-18 15:22:30 +09:00
donghyeon-ka ace8aaaef6 refactor: 빌드 최적화 및 ci 수정 2026-09-17 15:24:31 +09:00
DongHyeonka 944a1e348b chore: 문서 추가 2026-09-17 15:03:36 +09:00
DongHyeonkaandClaude Opus 5 ef947e5bb0 refactor(build,ci): 현재 상태 검증을 걷어내고 불변조건만 남기는 검증 표면 축소
외부 리뷰("현재 상태를 유지하기 위한 검증이 너무 많고, 그 검증 자체를
다시 검증하는 구조까지 생겼다")를 설계 문서로 정리하고 코드로 반영한다.
설계·판단 근거는 docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md.

삭제
- .github/ci-gate-matrix.yml(1,025줄) + verify-gate-matrix.sh(568줄):
  Gradle task graph와 workflow graph에 이미 있는 정보의 3중 복제
- verify-gradle-wrapper.sh(799줄): workflow 바이트 해시 잠금.
  wrapper 검증은 gradle/actions/wrapper-validation(full SHA 핀)에 위임
- DeveloperExperienceContractTest 등의 CI YAML mutation 테스트:
  애플리케이션 test suite가 GitHub Actions YAML 파서를 검증하던 계층 역전
- 문서 drift 파서: verifyReadmeCommands, verifyRunbookReferences,
  verifyDocumentedLeafCount, verifyTestSourceSetRegistry
- 빈 레지스트리를 지키던 커스텀 YAML 파서: verifyTrivyignore,
  verifyQuarantineSunset, flaky-quarantine.yaml
- verifyConfigurationPropertiesProcessor, verifyOneTypePerFile:
  각각 ca.spring-config convention과 Checkstyle OneTopLevelClass가 대체
- 정상 입력으로도 성공할 수 없던 messaging always-fail task
- ModuleRegistry의 JSON 필드 집합 정확 일치, sample-portfolio negative guard

이동
- java/quality/spring 공통 설정을 configure(subprojects) 블록에서
  ca.java-conventions / ca.quality-conventions / ca.java-library /
  ca.spring-library convention plugin으로
- 아키텍처 검증을 ca.architecture로, JPA·messaging qualification을
  gradle/qualification/ 아래로, verifyEnvKeys를 :app-bootstrap 소유로

완화
- Git revision은 releaseCheck·아카이브 생성에서만 요구. 일반 빌드는 SNAPSHOT
- SpotBugs/FindSecBugs는 로컬 check에서 빼고 qualityCheck 레인으로

task 계층
- leaf check는 그 leaf만. architectureCheck / qualityCheck /
  configContractCheck / integrationCheck / ci / releaseCheck로 이름 분리

CI
- _reusable-gradle.yml 신규. checkout + wrapper validation + JDK/캐시 공통화
- fileserver-release.yml -> fileserver-certification.yml (CD가 아니라 certification)
- GitHub Actions = CI + artifact, Argo CD = CD 경계를 docs/ci-cd/boundary.md로 고정

순증감 +3,274 / -7,483.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 20:33:19 +09:00
DongHyeonkaandClaude Opus 5 d00c76241c docs: 감사 추정(−3,368줄)과 실제 결과(+1,652줄) 차이를 기록
빌드·CI 레이어는 줄지 않고 늘었다. 영역별 증감과 이유를 적어둔다.

줄어든 곳은 실제로 중복이던 곳이다(워크플로 30 → 19파일, setup 블록 59회 →
composite 1개, jpa-evidence −274줄). 늘어난 곳은 그동안 비어 있던 자리다
(게이트 매트릭스 커버리지 8/28 → 28/28 워크플로가 +634줄, convention 플러그인과
그 테스트가 +701줄).

추정이 빗나간 이유 셋: 감사 findings 17건이 실행 결과로 기각됐고(지울 수 있다고
본 코드에 소비자가 있었다), 작동하게 만드는 데 코드가 들고, convention 플러그인은
총량이 아니라 중복을 줄인다.

줄 수로 보고하면 틀린 보고가 되므로 그렇게 적어둔다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 18:47:15 +09:00
DongHyeonkaandClaude Opus 5 40ee9f1e83 fix(build,ci): 정의만 되고 안 돌던 레인 13개를 배선하고 버전 11개를 카탈로그로
감사 remediation 의 마지막 항목 두 개.

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

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

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

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

## 버전 카탈로그 이관

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

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

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

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

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

## 남은 문제

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 18:46:34 +09:00
DongHyeonkaandClaude Opus 5 9bc2e75fe5 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>
2026-09-16 18:27:15 +09:00
DongHyeonkaandClaude Opus 5 1535481794 refactor(build,src): testkit 소스셋 이관과 빌드 게이트 정상화, 미추적 빌드 파일 추적
한 커밋인 이유: src/build.gradle 안에서 ca.testkit-publisher 플러그인 제거와
게이트 수정이 얽혀 있다. 플러그인 적용부만 빼면 web·websocket·persistence-jpa·
persistence-mongo·app-bootstrap 이 사라진 testkitPublisher() 와 *Testkit
컨피규레이션을 계속 참조해 설정 단계에서 빌드가 죽는다. 파일 단위로 나눌 수 없다.

1) testkit 소스셋 → Gradle 표준 java-test-fixtures 이관
   web, websocket, persistence-jpa, persistence-mongo, httpclient, graphql 과
   이들의 testkit 컨피규레이션을 소비하던 app-bootstrap.
   자체 제작 ca.testkit-publisher.gradle 77줄이 사라진다.

2) 실행되지 않거나 실패할 수 없던 빌드 게이트 정상화 (E등급)
   - strict-test-lane 의 실행 카운터가 skip 을 실행으로 세던 것 수정.
     전부 skip 인 레인은 이제 실패한다 (회귀 테스트 2건 추가)
   - public-path 스냅샷이 gitignore 된 src/.env 를 읽던 것을
     config/security.yml 의 바인딩 기본값으로 교체
   - verifyEnvKeys 가 build/ 산출물을 소스로 읽어 삭제된 키를 사용 중으로
     오판하던 것 수정 (입력 4,637 → 4,630 파일)
   - jpa-evidence 가 git 실패를 "워크트리 깨끗함"으로 읽던 것을 fail-closed 로
   - notification-evidence 의 Grade 열 탐지를 헤더 기준으로 교체 +
     표 부재 시 fail-closed
   - spring70CompatibilityTest 가 레인을 복제하며 잃은 fail-closed 복구
     (태스크명 유지 — 워크플로 3곳과 gate-matrix 린트 무손상)
   - 메시징 R2 스켈레톤 주변의 도달 불가 검증 45줄을 MSG-015 명시적 실패로 교체

3) git 에 없던 빌드 필수 파일 추적
   - src/gradle/libs.versions.toml — src/build.gradle 이 9곳에서 참조하는데
     추적되지 않아 깨끗한 체크아웃에서 설정이 실패했다
   - app-bootstrap config/*.yml 15개 — application.yml 이 전부 import 한다.
     하드코딩된 시크릿은 없고 값은 secret://environment/APP_* 참조다

4) 진행 중이던 구현 작업 반영 (redis/idempotency 구성, startup 검증,
   아키텍처 테스트 클래스, notification 콜백 레지스트리 등)

검증:
- 깨끗한 체크아웃에서 ./gradlew help 통과
- :app-bootstrap:test --tests 'dev.caskeleton.bootstrap.architecture.*'
  → 20개 클래스 174 tests, 실패 0, 스킵 0 (이전에는 0개 실행)

미해결: verifyOneTypePerFile 은 손대지 않았다(Checkstyle 로 교체 권고).
B/C/D 등급 100여 건과 CI 단계 분리는 별도 작업 —
docs/superpowers/plans/2026-09-16-ci-stage-separation.md 참고.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 16:53:25 +09:00
DongHyeonkaandClaude Opus 5 e34519113b fix(ci): 실행되지 않거나 실패할 수 없던 CI 게이트 정상화
가장 큰 것: 문서화된 아키텍처 게이트가 규칙을 하나도 실행하지 않았다.
워크플로 7곳과 CLAUDE.md 가 --tests '*CleanArchitectureTest' 를 지정했으나
그 이름의 클래스는 존재하지 않는다. 4곳은 매칭 0건으로 하드 실패하고,
3곳은 다른 필터와 병기돼 아키텍처 규칙 0개를 돌고 초록으로 통과했다.
필터를 패키지 글롭 dev.caskeleton.bootstrap.architecture.* 로 교체했다.
*ArchitectureTest 글롭은 20개 중 12개만 잡고 ArchRuleDiscoveryContractTest 등
8개를 놓치므로 쓰지 않았다.

그 외:
- ci-gate-matrix 의 release_blocking 이 강제되지 않아 trivy-fs 가 빨개도
  release-gate 가 초록이던 것을 실제 의존으로 연결
- build-logic TestKit 이 어떤 CI 에서도 돌지 않던 것을 ci-quality-gates 에 연결
- jpa-next-* 3개, object-storage, fileserver-pr 의 실패할 수 없거나
  트리거되지 않던 잡 정리
- 릴리스 태그 네임스페이스 분열로 v* 태그가 web·websocket 게이트를
  건너뛰던 것 수정
- 워크플로 SHA 잠금 28개 재생성 (verify-gradle-wrapper.sh)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 16:52:44 +09:00
DongHyeonkaandClaude Opus 5 2a8d34f85c docs: 빌드·CI 감사 계획과 진행 중이던 문서 정리
- CI 단계 분리 계획 추가 (docs/superpowers/plans/2026-09-16-ci-stage-separation.md).
  빌드·CI 레이어 전수 리뷰 133건의 결론과 Track A/B/C 작업 순서를 담는다.
- public-path 보안 기준선을 실제 배포 기본값(/v1/healthcheck)으로 재생성.
  이전 값은 gitignore 된 src/.env 에서 유래해 재현이 불가능했다.
- 진행 중이던 ADR·리뷰·테스트 전략 문서 반영, 대체된 grpc 계획 문서 제거.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 16:52:37 +09:00
973 changed files with 36174 additions and 27703 deletions
@@ -0,0 +1,41 @@
name: Set up Java and Gradle
description: >-
Installs the repository's pinned Temurin JDK, then configures Gradle through the official
setup-gradle action — which validates every checked-in wrapper jar and manages the Gradle cache.
Every Gradle job used to carry the JDK block verbatim, so the JDK patch level lived in fifty-nine
places; every job also carried a separate three-line wrapper-validation step, so the pinned action
SHA lived in forty.
# Wrapper validation is INSIDE this action now.
#
# It could not be before, and the reason was not a GitHub limitation: .github/scripts/
# verify-gradle-wrapper.sh read every workflow job and required it to contain, literally and in this
# order, an `actions/checkout@` step, the exact three-field pinned wrapper-validation step, and then
# the Gradle invocation. That literalness was the whole guard — "this job validated the wrapper" had
# to be answerable from the workflow file alone — and it is what made the step uninlineable.
#
# That script is gone (it also byte-hashed all twelve workflow files, so a comment change needed a
# hash update, while an attacker with write access would simply have updated both). The guarantee it
# was protecting is now the official action's own: `gradle/actions/setup-gradle` validates all
# wrapper jars by default (`validate-wrappers`, default true), and the action is pinned to a full
# commit SHA here — which GitHub's own hardening guide calls the only immutable action reference.
#
# `actions/checkout` still cannot move here: a `./.github/actions/...` reference is resolved from the
# checked-out working copy, so this file does not exist until checkout has already run. A composite
# action cannot contain the step that makes itself readable.
#
# So a Gradle job is two lines — checkout, then this action.
runs:
using: composite
steps:
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
# Gradle's own caching, not setup-java's `cache: gradle`. The two cache the same directory with
# different keys, and running both is how a job restores one cache and saves the other.
- uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
with:
build-scan-publish: false
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
-391
View File
@@ -1,391 +0,0 @@
# Current repository CI controls. This file lists only mechanisms and jobs that exist in this
# checkout. Build/release supply-chain, image, signing, provenance, SBOM, and tag-release jobs are
# intentionally absent until their later bounded reconstruction.
#
# Fields:
# release_blocking: true, false, or conditional
# mechanism: gradle-custom-task, gradle-plugin-task, contract-test, workflow-job,
# or delegated-pending
# ref: task, plugin@task, repository-relative test path below src/, or workflow job id
# workflow/job: canonical workflow and job that execute or represent the control
# execution: check (through Gradle check), explicit (named in the job), or job
gates:
- id: format-lint
release_blocking: true
mechanism: gradle-plugin-task
ref: com.diffplug.spotless@spotlessCheck
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: unit-and-contract-tests
release_blocking: true
mechanism: gradle-plugin-task
ref: java@test
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: conditional-transport-qualification
release_blocking: true
mechanism: gradle-custom-task
ref: conditionalTransportQualification
workflow: ci-quality-gates.yml
job: quality-gates
execution: explicit
- id: clean-architecture-dependencies
release_blocking: true
mechanism: gradle-custom-task
ref: verifyCleanArchitectureDependencies
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: environment-contract
release_blocking: true
mechanism: gradle-custom-task
ref: verifyEnvKeys
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: documented-leaf-count
release_blocking: true
mechanism: gradle-custom-task
ref: verifyDocumentedLeafCount
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: declared-dependency-absence
release_blocking: true
mechanism: gradle-custom-task
ref: verifyDependencyPolicy
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: notification-api-surface
release_blocking: true
mechanism: gradle-custom-task
ref: verifyNotificationApiSurface
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: notification-configuration-contract
release_blocking: true
mechanism: gradle-custom-task
ref: verifyNotificationConfiguration
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: notification-support-grade-evidence
release_blocking: true
mechanism: gradle-custom-task
ref: verifyNotificationEvidence
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: runbook-reference-drift
release_blocking: true
mechanism: gradle-custom-task
ref: verifyRunbookReferences
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: graphql-api-surface
release_blocking: true
mechanism: gradle-custom-task
ref: verifyGraphQlApiSurface
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: mongo-api-surface
release_blocking: true
mechanism: gradle-custom-task
ref: verifyMongoApiSurface
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
# The strongest evidence this repository produces, and CI does not run it. Fifteen Compose lanes
# start real PostgreSQL, MongoDB, Kafka, MinIO, Mailpit and Keycloak, take a real client-credentials
# JWT, and prove things no in-JVM test can: that all-off boots with no external resource, that the
# notification handoff delivers exactly once across a restart on the same volume, that the startup
# log is silent. It runs from a developer's machine via scripts/run-compose-runtime-smoke.sh and
# from nowhere else — no workflow invokes it, so nothing re-runs it on a pull request.
#
# Registered delegated-pending so the gap is a tracked absence rather than an unstated one.
# Executing it in CI needs a Docker-capable runner and a decision about the minutes fifteen
# container lanes cost, which is an infrastructure choice rather than a wiring oversight.
- id: runtime-smoke-matrix
release_blocking: conditional
mechanism: delegated-pending
ref: runtime-smoke-matrix-lane
workflow: ci-quality-gates.yml
job: release-gate
execution: job
# `conditional-transport-qualification` above is the registered GraphQL control, and it is a
# boundary test: a @SpringBootTest over a nested test application with in-memory Basic Auth. Its own
# javadoc says so — "the nested application deliberately owns only test authentication and CORS
# policy". That is a legitimate transport-boundary proof and it is not release evidence for the
# security posture, which is the distinction the Definition of Done draws.
#
# The real proof exists: the local-graphql Compose lane obtains a Keycloak client-credentials token
# and posts it to /graphql on the running bootJar, asserting that anonymous and malformed
# credentials are refused and the authenticated query answers. It is part of the runtime smoke
# matrix above, so it inherits that control's pending status rather than having none of its own.
- id: graphql-runtime-jwt
release_blocking: conditional
mechanism: delegated-pending
ref: graphql-runtime-jwt-lane
workflow: ci-quality-gates.yml
job: release-gate
execution: job
- id: one-type-per-file
release_blocking: true
mechanism: gradle-custom-task
ref: verifyOneTypePerFile
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: readme-command-drift
release_blocking: true
mechanism: gradle-custom-task
ref: verifyReadmeCommands
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: trivy-suppression-governance
release_blocking: true
mechanism: gradle-custom-task
ref: verifyTrivyignore
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: quarantine-sunset
release_blocking: true
mechanism: gradle-custom-task
ref: verifyQuarantineSunset
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: public-path-snapshot
release_blocking: true
mechanism: gradle-custom-task
ref: verifyPublicPathSnapshot
workflow: ci-quality-gates.yml
job: quality-gates
execution: explicit
- id: dependency-locks
release_blocking: true
mechanism: gradle-custom-task
ref: verifyDependencyLocks
workflow: ci-quality-gates.yml
job: quality-gates
execution: explicit
- id: architecture-contract-test
release_blocking: true
mechanism: contract-test
ref: app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
- id: sample-off
release_blocking: true
mechanism: gradle-custom-task
ref: sampleOffTest
workflow: ci-quality-gates.yml
job: sample-off
execution: explicit
- id: gate-matrix-lint
release_blocking: true
mechanism: workflow-job
ref: gate-matrix-lint
workflow: ci-quality-gates.yml
job: gate-matrix-lint
execution: job
- id: redis-sdk
release_blocking: true
mechanism: workflow-job
ref: redis-sdk
workflow: ci-quality-gates.yml
job: redis-sdk
execution: job
- id: jpa-candidate-evidence
release_blocking: true
mechanism: workflow-job
ref: jpa-candidate-evidence
workflow: ci-quality-gates.yml
job: jpa-candidate-evidence
execution: job
- id: jpa-r2-evidence
release_blocking: conditional
mechanism: workflow-job
ref: jpa-r2-evidence
workflow: jpa-r2-evidence.yml
job: jpa-r2-evidence
execution: job
- id: quality-release-gate
release_blocking: true
mechanism: workflow-job
ref: release-gate
workflow: ci-quality-gates.yml
job: release-gate
execution: job
- id: flaky-quarantine
release_blocking: false
mechanism: workflow-job
ref: quarantine
workflow: ci-quality-gates.yml
job: quarantine
execution: job
- id: dependency-review
release_blocking: conditional
mechanism: workflow-job
ref: dependency-review
workflow: dependency-vulnerability.yml
job: dependency-review
execution: job
- id: dependency-submission
release_blocking: false
mechanism: workflow-job
ref: dependency-submission
workflow: dependency-vulnerability.yml
job: dependency-submission
execution: job
- id: filesystem-vulnerability-scan
release_blocking: true
mechanism: workflow-job
ref: trivy-fs
workflow: dependency-vulnerability.yml
job: trivy-fs
execution: job
- id: documentation-links
release_blocking: conditional
mechanism: workflow-job
ref: lychee
workflow: link-check.yml
job: lychee
execution: job
- id: object-storage-minio-managed-contract
release_blocking: true
mechanism: gradle-custom-task
ref: objectStorageMinioContractTest
workflow: object-storage-qualification.yml
job: minio-managed-contract
execution: explicit
- id: poster-image-migration
release_blocking: true
mechanism: gradle-custom-task
ref: posterImageMigrationTest
workflow: object-storage-qualification.yml
job: poster-image-v7-migration
execution: explicit
- id: object-storage-minio-managed-fault
release_blocking: conditional
mechanism: gradle-custom-task
ref: objectStorageMinioFaultTest
workflow: object-storage-qualification.yml
job: minio-managed-fault
execution: explicit
- id: object-storage-aws-protected-qualification
release_blocking: conditional
mechanism: delegated-pending
ref: approval-gate-b
workflow: object-storage-qualification.yml
job: aws-managed-common-subset
execution: job
- id: redis-sdk-support-matrix
release_blocking: true
mechanism: contract-test
ref: adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/RedisSupportMatrixTest.java
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
# Promoted from delegated-pending: the workflow is no longer manual-only. A pull request that
# touches the Redis leaf runs the standalone lane, and the full supported-version x topology
# matrix runs nightly and on a release candidate. While it was dispatch-only, a release could
# claim topology evidence that nobody had produced for that commit.
- id: redis-sdk-topology-evidence
release_blocking: conditional
mechanism: workflow-job
ref: topology-evidence
workflow: redis-sdk-topology.yml
job: topology-evidence
execution: job
- id: httpclient-stable-contract
release_blocking: true
mechanism: gradle-custom-task
ref: httpClientStableContractTest
workflow: httpclient-release.yml
job: release-gate
execution: explicit
- id: httpclient-security-suite
release_blocking: true
mechanism: gradle-custom-task
ref: httpClientSecurityTest
workflow: httpclient-release.yml
job: release-gate
execution: explicit
- id: httpclient-fault-injection
release_blocking: true
mechanism: gradle-custom-task
ref: httpClientFailureInjectionTest
workflow: httpclient-release.yml
job: release-gate
execution: explicit
- id: httpclient-performance-certification
release_blocking: true
mechanism: gradle-custom-task
ref: httpClientPerformanceTest
workflow: httpclient-release.yml
job: release-gate
execution: explicit
- id: httpclient-spring62-api-surface
release_blocking: true
mechanism: gradle-custom-task
ref: spring62ApiSurfaceScan
workflow: httpclient-release.yml
job: release-gate
execution: explicit
# The 6.2 API-surface scan above proves the common packages compile against the older surface. It
# does not prove they run on it, and the two were being conflated: a lane called
# "spring62CompatibilityTest" reads as a runtime compatibility proof. The Gradle task is renamed to
# say what it does, and the runtime claim is registered here as its own delegated-pending control
# so the gap is a tracked absence rather than an unstated one. Executing it needs a Spring
# Framework 6.2 distribution resolved into a separate test runtime, which this repository's
# Boot 4.0 baseline does not carry.
- id: httpclient-spring62-runtime
release_blocking: conditional
mechanism: delegated-pending
ref: spring62-runtime-lane
workflow: httpclient-release.yml
job: release-gate
execution: job
- id: httpclient-spring70-compatibility
release_blocking: true
mechanism: gradle-custom-task
ref: spring70CompatibilityTest
workflow: httpclient-release.yml
job: release-gate
execution: explicit
- id: httpclient-documentation-drift
release_blocking: true
mechanism: workflow-job
ref: httpclient-documentation
workflow: httpclient-release.yml
job: httpclient-documentation
execution: job
- id: httpclient-event-loop-blocking
release_blocking: true
mechanism: gradle-custom-task
ref: httpClientBlockHoundTest
workflow: httpclient-release.yml
job: release-gate
execution: explicit
# The messaging platform's only claim that needs a real broker to be true. The gate is the
# evidence check rather than the lane, and it depends on the lane: passing means both that every
# fault scenario produced the outcome the shared contract fixes and that the committed manifest is
# what this run wrote. Before it existed, `CertifiedEvidence` was a hand-authored list and
# "certified against a live broker" was a sentence a developer could type.
- id: messaging-broker-certification
release_blocking: true
mechanism: gradle-custom-task
ref: verifyMessagingCertificationEvidence
workflow: messaging-certification.yml
job: broker-certification
execution: explicit
+2 -2
View File
@@ -3,7 +3,7 @@
This policy is enforced by This policy is enforced by
[`dependency-vulnerability.yml`](workflows/dependency-vulnerability.yml), [`dependency-vulnerability.yml`](workflows/dependency-vulnerability.yml),
[`dependency-review-config.yml`](dependency-review-config.yml), [`dependency-review-config.yml`](dependency-review-config.yml),
[`../.trivyignore.yaml`](../.trivyignore.yaml), `verifyTrivyignore`, CODEOWNERS, and [`../.trivyignore.yaml`](../.trivyignore.yaml), CODEOWNERS, and
[`../renovate.json`](../renovate.json). [`../renovate.json`](../renovate.json).
## Execution and platform boundary ## Execution and platform boundary
@@ -73,7 +73,7 @@ dependencies; stale mirrors can delay detection.
The only suppression source is repository-root `.trivyignore.yaml`. Every Trivy scan passes it The only suppression source is repository-root `.trivyignore.yaml`. Every Trivy scan passes it
explicitly with `--ignorefile .trivyignore.yaml`. Each future entry must contain an identifier, a explicitly with `--ignorefile .trivyignore.yaml`. Each future entry must contain an identifier, a
non-empty rationale, and a future expiry no more than 90 days away. `verifyTrivyignore` validates non-empty rationale, and a future expiry no more than 90 days away. A CODEOWNERS reviewer validates
the shape and expiry; CODEOWNERS plus branch protection controls who may approve the change. the shape and expiry; CODEOWNERS plus branch protection controls who may approve the change.
Neither control substitutes for the other. Neither control substitutes for the other.
+2 -2
View File
@@ -4,8 +4,8 @@
## Verification ## Verification
- [ ] I ran the focused test for each changed leaf. - [ ] I ran focused `:<changed-leaf>:check` tasks for the modules I changed.
- [ ] I ran `cd src && ./gradlew check verifyPublicPathSnapshot verifyDependencyLocks`. - [ ] I ran `cd src && ./gradlew architectureCheck verifyPublicPathSnapshot verifyDependencyLocks` when the change touched repository structure, dependencies, or public paths.
- [ ] I did not add an unregistered production module dependency. - [ ] I did not add an unregistered production module dependency.
- [ ] Dependency changes include refreshed `gradle.lockfile` files and a strict-lock verification. - [ ] Dependency changes include refreshed `gradle.lockfile` files and a strict-lock verification.
- [ ] Trivy suppressions include an owner-reviewed reason and an expiry within 90 days. - [ ] Trivy suppressions include an owner-reviewed reason and an expiry within 90 days.
-448
View File
@@ -1,448 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
if (( $# > 1 )); then
printf '::error::gate-matrix-lint: expected zero arguments or one repository root\n' >&2
exit 2
fi
if (( $# == 1 )); then
if [[ ! -d "$1" ]]; then
printf '::error::gate-matrix-lint: repository root is not a directory: %s\n' "$1" >&2
exit 2
fi
REPO_ROOT="$(cd -- "$1" && pwd -P)"
else
REPO_ROOT="$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel)"
EXPECTED_SCRIPT_DIR="$(cd -- "${REPO_ROOT}/.github/scripts" && pwd -P)"
if [[ "${SCRIPT_DIR}" != "${EXPECTED_SCRIPT_DIR}" ]]; then
printf '::error::gate-matrix-lint: script location must be repository .github/scripts directory\n' >&2
exit 1
fi
fi
readonly REPO_ROOT
readonly MATRIX="${REPO_ROOT}/.github/ci-gate-matrix.yml"
# Deliberately a literal: a gate silently appearing or disappearing is the drift this lint exists to
# catch, so growing the matrix is an explicit edit here. 38 as of the HTTP Client platform hardening,
# which registered httpclient-spring62-runtime as a delegated-pending control — the 6.2 *runtime*
# claim, distinct from the API-surface scan that was standing in for it. 40 after the Gradle
# convention wave registered documented-leaf-count and declared-dependency-absence, then 46 after
# the final qualification wave registered the four notification/runbook gates that existed but ran
# nowhere and the two API-surface gates the convention had already wired into check. 48 once the
# Compose runtime matrix and the GraphQL runtime JWT claim were registered as delegated-pending —
# both are real and neither runs in CI. 49 once the messaging broker certification lane registered
# its evidence gate — the first control in this repository whose subject is not "did the tests pass"
# but "is the committed evidence what the run produced".
readonly EXPECTED_GATE_COUNT=49
if [[ ! -f "${MATRIX}" ]]; then
printf '::error::gate-matrix-lint: missing %s\n' "${MATRIX}" >&2
exit 1
fi
records="$(
awk '
function flush() {
if (id != "") {
printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", id, blocking, mechanism, ref, workflow, job, execution
}
}
/^[[:space:]]*-[[:space:]]+id:[[:space:]]*/ {
flush()
id=$0
sub(/^[[:space:]]*-[[:space:]]+id:[[:space:]]*/, "", id)
blocking=mechanism=ref=workflow=job=execution=""
next
}
/^[[:space:]]+release_blocking:[[:space:]]*/ {
blocking=$0
sub(/^[[:space:]]+release_blocking:[[:space:]]*/, "", blocking)
next
}
/^[[:space:]]+mechanism:[[:space:]]*/ {
mechanism=$0
sub(/^[[:space:]]+mechanism:[[:space:]]*/, "", mechanism)
next
}
/^[[:space:]]+ref:[[:space:]]*/ {
ref=$0
sub(/^[[:space:]]+ref:[[:space:]]*/, "", ref)
next
}
/^[[:space:]]+workflow:[[:space:]]*/ {
workflow=$0
sub(/^[[:space:]]+workflow:[[:space:]]*/, "", workflow)
next
}
/^[[:space:]]+job:[[:space:]]*/ {
job=$0
sub(/^[[:space:]]+job:[[:space:]]*/, "", job)
next
}
/^[[:space:]]+execution:[[:space:]]*/ {
execution=$0
sub(/^[[:space:]]+execution:[[:space:]]*/, "", execution)
next
}
END { flush() }
' "${MATRIX}"
)"
declare -A seen_ids=()
declare -a failures=()
total=0
verified=0
delegated=0
job_body() {
local workflow_file="$1"
local job_id="$2"
awk -v target="${job_id}" '
$0 ~ "^ " target ":[[:space:]]*$" { inside=1; print; next }
inside && $0 ~ "^ [A-Za-z0-9_-]+:[[:space:]]*$" { exit }
inside { print }
' "${workflow_file}"
}
gradle_command_has_safe_literal_grammar() {
local command="$1"
[[ "${command}" =~ ^\./gradlew([[:space:]]+[A-Za-z0-9_.:/@=,+-]+)+[[:space:]]*$ ]]
}
gradle_token_suppresses_execution() {
local token="$1"
case "${token}" in
'--dry-run'|'--dry-run='*|'-m'|'-x'|'-x'*|'--exclude-task'|'--exclude-task='*) return 0 ;;
*) return 1 ;;
esac
}
gradle_token_is_allowed_gate_argument() {
local token="$1"
case "${token}" in
'--no-daemon'|'--stacktrace'|'--warning-mode=fail') return 0 ;;
esac
[[ "${token}" =~ ^:?[A-Za-z0-9_][A-Za-z0-9_.-]*(:[A-Za-z0-9_][A-Za-z0-9_.-]*)*$ ]]
}
gradle_plugin_is_applied() {
local plugin_id="$1"
grep -RqsF --include='build.gradle' -- "id '${plugin_id}'" "${REPO_ROOT}/src" \
|| grep -RqsF --include='build.gradle' -- "id \"${plugin_id}\"" "${REPO_ROOT}/src" \
|| grep -RqsF --include='build.gradle' -- "apply plugin: '${plugin_id}'" "${REPO_ROOT}/src" \
|| grep -RqsF --include='build.gradle' -- "apply plugin: \"${plugin_id}\"" "${REPO_ROOT}/src"
}
gradle_custom_task_is_registered_in_build_file() {
local task_name="$1"
local build_file="$2"
if grep -qsE -- "tasks\\.register\\(['\"]${task_name}['\"]" "${build_file}"; then
return 0
fi
# A lane declared through the `ca.strict-test-lane` convention. The convention exists because the
# five lines every lane used to repeat were copied per lane and per leaf, and two copies had
# already lost `failOnNoDiscoveredTests`; registering through it is still registering, so this lint
# has to recognise the declaration or it reports every converted lane as missing.
if grep -qsE -- "lane\\(['\"]${task_name}['\"]\\)" "${build_file}"; then
return 0
fi
# An API surface gate declared through the `ca.api-surface` convention, which derives every task
# name from one label so a leaf cannot verify one surface while telling the reader about another.
# The name is computed, so there is no literal `tasks.register('verifyMongoApiSurface')` anywhere;
# what the build file says is `apiSurface { label = 'Mongo' }`.
if [[ "${task_name}" =~ ^verify(.+)ApiSurface$ ]]; then
local surface_label="${BASH_REMATCH[1]}"
if grep -qsE -- "label[[:space:]]*=[[:space:]]*['\"]${surface_label}['\"]" "${build_file}"; then
return 0
fi
fi
awk -v required_task="${task_name}" '
index($0, "registerStrictQualificationTest(") > 0 { inside_registration=1 }
inside_registration && /^[[:space:]]*name:[[:space:]]*/ {
candidate=$0
sub(/^[[:space:]]*name:[[:space:]]*/, "", candidate)
quote=substr(candidate, 1, 1)
if (quote != "\"" && quote != sprintf("%c", 39)) {
next
}
candidate=substr(candidate, 2)
closing_quote=index(candidate, quote)
if (closing_quote == 0) {
next
}
candidate=substr(candidate, 1, closing_quote - 1)
if (candidate == required_task) {
found=1
}
}
inside_registration && /\)[[:space:]]*$/ { inside_registration=0 }
END { exit found ? 0 : 1 }
' "${build_file}"
}
# Every `dependsOn ... named('x')` in the build, collected once.
#
# This used to be one recursive grep per gate. That was affordable at 38 gates and stopped being so
# at 48: the whole lint crossed the ten-second budget its own contract test asserts, and the first
# symptom was that test failing rather than anything about gate coverage. One pass, then membership
# tests against the result.
CHECK_WIRING_CACHE=""
load_check_wiring() {
[[ -n "${CHECK_WIRING_CACHE}" ]] && return 0
CHECK_WIRING_CACHE="$(grep -RhoE -- "dependsOn[^\n]*named\((['\"])[A-Za-z0-9_.-]+\1\)" \
"${REPO_ROOT}/src" --include='build.gradle' --include='ca.*.gradle' 2>/dev/null \
| grep -oE "(['\"])[A-Za-z0-9_.-]+\1" | tr -d "\"'" | sort -u)"
# A build with no such wiring at all would leave this empty and make every membership test pass by
# vacuity, so an empty result is a marker rather than an answer.
[[ -z "${CHECK_WIRING_CACHE}" ]] && CHECK_WIRING_CACHE="<none>"
return 0
}
gradle_custom_task_wired_into_check() {
local task_name="$1"
load_check_wiring
if printf '%s\n' "${CHECK_WIRING_CACHE}" | grep -qxF -- "${task_name}"; then
return 0
fi
# `ca.api-surface` wires check as `dependsOn tasks.named(verifyName())`, where verifyName() is
# derived from the leaf's label. The declaration that makes the gate real is the label, so that is
# what proves the wiring — the convention has exactly one check wiring and it is unconditional.
if [[ "${task_name}" =~ ^verify(.+)ApiSurface$ ]]; then
local surface_label="${BASH_REMATCH[1]}"
if grep -RqsE -- "label[[:space:]]*=[[:space:]]*['\"]${surface_label}['\"]" "${REPO_ROOT}/src" \
--include='build.gradle' \
&& grep -qsE -- "dependsOn tasks\.named\(verifyName\(\)\)" \
"${REPO_ROOT}/src/build-logic/src/main/groovy/ca.api-surface.gradle"; then
return 0
fi
fi
return 1
}
# The build files, found once rather than once per gate. Same reason as the wiring cache above: the
# per-gate `find` was a fixed cost multiplied by a number that grew.
GRADLE_FILE_CACHE=""
load_gradle_files() {
[[ -n "${GRADLE_FILE_CACHE}" ]] && return 0
GRADLE_FILE_CACHE="$(find "${REPO_ROOT}/src" -type f -name '*.gradle' | sort)"
return 0
}
gradle_custom_task_is_registered() {
local task_name="$1"
local build_file
load_gradle_files
while IFS= read -r build_file; do
[[ -z "${build_file}" ]] && continue
if gradle_custom_task_is_registered_in_build_file "${task_name}" "${build_file}"; then
return 0
fi
done <<< "${GRADLE_FILE_CACHE}"
return 1
}
gradle_token_matches_registered_task() {
local token="$1"
local required_task="$2"
local project_path build_file
if [[ "${token}" == "${required_task}" || "${token}" == ":${required_task}" ]]; then
return 0
fi
if [[ "${token}" != :* || "${token}" != *:"${required_task}" ]]; then
return 1
fi
project_path="${token%:"${required_task}"}"
project_path="${project_path#:}"
project_path="${project_path%:}"
build_file="${REPO_ROOT}/src/${project_path//:/\/}/build.gradle"
[[ -f "${build_file}" ]] \
&& gradle_custom_task_is_registered_in_build_file "${required_task}" "${build_file}"
}
job_runs_gradle_task() {
local workflow_file="$1"
local job_id="$2"
local required_task="$3"
local command token
local found_task suppressed
local -a tokens=()
while IFS= read -r command; do
if ! gradle_command_has_safe_literal_grammar "${command}"; then
continue
fi
read -r -a tokens <<< "${command}"
if (( ${#tokens[@]} < 2 )) || [[ "${tokens[0]}" != './gradlew' ]]; then
continue
fi
found_task=0
suppressed=0
for token in "${tokens[@]:1}"; do
case "${token}" in
'&&'|'||'|';'|'|'|'#'*) break ;;
esac
if gradle_token_suppresses_execution "${token}"; then
suppressed=1
break
fi
if ! gradle_token_is_allowed_gate_argument "${token}"; then
suppressed=1
break
fi
if gradle_token_matches_registered_task "${token}" "${required_task}"; then
found_task=1
fi
done
if (( found_task == 1 && suppressed == 0 )); then
return 0
fi
done < <(
job_body "${workflow_file}" "${job_id}" | awk '
/^[[:space:]]+(-[[:space:]]+)?run:[[:space:]]+/ {
command=$0
sub(/^[[:space:]]+(-[[:space:]]+)?run:[[:space:]]+/, "", command)
if (command !~ /^(\||>)/) {
print command
}
}
'
)
return 1
}
while IFS=$'\t' read -r id blocking mechanism ref workflow job execution; do
[[ -z "${id}" ]] && continue
total=$((total + 1))
if [[ -n "${seen_ids[${id}]:-}" ]]; then
failures+=("duplicate gate id '${id}'")
fi
seen_ids["${id}"]=1
if [[ -z "${blocking}" || -z "${mechanism}" || -z "${ref}" || -z "${workflow}" \
|| -z "${job}" || -z "${execution}" ]]; then
failures+=("gate '${id}' has an empty required field")
continue
fi
if [[ ! "${blocking}" =~ ^(true|false|conditional)$ ]]; then
failures+=("gate '${id}' has invalid release_blocking '${blocking}'")
fi
if [[ ! "${workflow}" =~ ^[A-Za-z0-9._-]+\.ya?ml$ || ! "${job}" =~ ^[A-Za-z0-9_-]+$ ]]; then
failures+=("gate '${id}' has an unsafe workflow or job identifier")
continue
fi
workflow_file="${REPO_ROOT}/.github/workflows/${workflow}"
if [[ ! -f "${workflow_file}" ]]; then
failures+=("gate '${id}' references missing workflow '.github/workflows/${workflow}'")
continue
fi
if ! grep -Eqs -- "^[[:space:]]{2}${job}:[[:space:]]*$" "${workflow_file}"; then
failures+=("gate '${id}' references missing job '${job}' in '${workflow}'")
continue
fi
case "${mechanism}" in
gradle-custom-task)
if [[ ! "${ref}" =~ ^[A-Za-z_][A-Za-z0-9_-]*$ ]]; then
failures+=("gate '${id}' has unsafe Gradle custom task ref '${ref}'")
continue
fi
if ! gradle_custom_task_is_registered "${ref}"; then
failures+=("gate '${id}' references unregistered Gradle task '${ref}'")
continue
fi
;;
gradle-plugin-task)
plugin="${ref%@*}"
task="${ref#*@}"
if [[ "${plugin}" == "${ref}" \
|| ! "${plugin}" =~ ^[A-Za-z][A-Za-z0-9.-]*$ \
|| ! "${task}" =~ ^[A-Za-z_][A-Za-z0-9_-]*$ ]]; then
failures+=("gate '${id}' has unsafe Gradle plugin task ref '${ref}'")
continue
fi
if ! gradle_plugin_is_applied "${plugin}"; then
failures+=("gate '${id}' references unapplied Gradle plugin '${plugin}'")
continue
fi
;;
contract-test)
if [[ "${ref}" == /* || "${ref}" == *".."* || ! -f "${REPO_ROOT}/src/${ref}" ]]; then
failures+=("gate '${id}' references missing or unsafe contract test 'src/${ref}'")
continue
fi
;;
workflow-job)
if [[ "${ref}" != "${job}" ]]; then
failures+=("gate '${id}' workflow-job ref '${ref}' must equal job '${job}'")
continue
fi
;;
delegated-pending)
delegated=$((delegated + 1))
printf "gate '%s': explicitly delegated-pending\n" "${id}"
continue
;;
*)
failures+=("gate '${id}' has unknown mechanism '${mechanism}'")
continue
;;
esac
case "${execution}" in
check)
if ! job_runs_gradle_task "${workflow_file}" "${job}" 'check'; then
failures+=("gate '${id}' expects Gradle check in job '${job}'")
continue
fi
# Build files *and* convention plugins. A gate can now be wired into check from an included
# build's convention rather than from a leaf's build.gradle, and a lint that only reads
# build.gradle would call such a gate unwired while it runs on every leaf — a false failure
# that teaches the next author to delete the matrix row instead of trusting it.
#
# A convention that derives the task name from a label wires check by that derived name, so
# there is no literal to grep for either; `gradle_custom_task_wired_into_check` handles both
# the literal and the derived form.
if [[ "${mechanism}" == "gradle-custom-task" ]] \
&& ! gradle_custom_task_wired_into_check "${ref}"; then
failures+=("gate '${id}' task '${ref}' exists but is not wired into Gradle check")
continue
fi
;;
explicit)
if ! job_runs_gradle_task "${workflow_file}" "${job}" "${ref}"; then
failures+=("gate '${id}' task '${ref}' is not explicit in job '${job}'")
continue
fi
;;
job)
;;
*)
failures+=("gate '${id}' has unknown execution '${execution}'")
continue
;;
esac
verified=$((verified + 1))
done <<< "${records}"
if (( total != EXPECTED_GATE_COUNT )); then
failures+=("matrix has ${total} gates; expected ${EXPECTED_GATE_COUNT}")
fi
printf 'gate-matrix-lint: %d gates, %d verified, %d delegated-pending\n' \
"${total}" "${verified}" "${delegated}"
if (( ${#failures[@]} > 0 )); then
printf '::error::gate-matrix-lint: %d drift(s) found\n' "${#failures[@]}" >&2
for failure in "${failures[@]}"; do
printf ' - %s\n' "${failure}" >&2
done
exit 1
fi
printf 'gate-matrix-lint: OK\n'
-756
View File
@@ -1,756 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
readonly EXPECTED_DISTRIBUTION_SUFFIX='/gradle-9.0.0-bin.zip'
readonly EXPECTED_DISTRIBUTION_SHA256='8fad3d78296ca518113f3d29016617c7f9367dc005f932bd9d93bf45ba46072b'
readonly EXPECTED_WRAPPER_JAR_SHA256='76805e32c009c0cf0dd5d206bddc9fb22ea42e84db904b764f3047de095493f3'
readonly EXPECTED_VALIDATION_ACTION='gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6'
readonly EXPECTED_DEPENDENCY_SUBMISSION_ACTION='gradle/actions/dependency-submission@748248ddd2a24f49513d8f472f81c3a07d4d50e1'
readonly EXPECTED_GUARDED_GRADLE_IF="\${{ always() && steps.gradle-wrapper-validation.outcome == 'success' }}"
# Workflow-lock update procedure (only after intentional review of the complete workflow diff):
# find .github/workflows -mindepth 1 -maxdepth 1 \
# \( -name '*.yml' -o -name '*.yaml' \) ! -type f -print # must print nothing
# find .github/workflows -mindepth 1 -maxdepth 1 -type f \
# \( -name '*.yml' -o -name '*.yaml' \) -print0 \
# | LC_ALL=C sort -z | xargs -0 sha256sum
# Replace this entire sorted array in the same reviewed change. Never refresh a single digest
# merely to make this verifier pass.
readonly EXPECTED_WORKFLOW_LOCK=(
'e27d981f43815294e47470e51f61671ee7047794e2a638d1b71a91ee957a18c6 .github/workflows/ci-quality-gates.yml'
'59de260a70c2c0a0d686d97035a189dc0567395977dfa18758f1a2d89d15a00d .github/workflows/dependency-vulnerability.yml'
'1b3220c922f954500f727c6a799b24e4962915845b9248e8e496e5050e829f28 .github/workflows/fileserver-nightly.yml'
'26812e16b8d6e4472543ddd49c7b16ee6b7697834ddbb653fa0424befd71c544 .github/workflows/fileserver-pr.yml'
'86a240c4ce7d0d293616e30de30ed77bcfdc700fedb8916f083eda9567099096 .github/workflows/fileserver-release.yml'
'58e28f3358d794ca08f4aa8df4516e03f50a9ee58488b3f0d2619998e069ef14 .github/workflows/httpclient-contract.yml'
'823bc346e58a58b2c0814cd1e3e55ec90d360c138419ec3d8f05deb59c62c7eb .github/workflows/httpclient-nightly.yml'
'ad84000efc438ee7439517b8f85819e62b13dab0aa4f94066c2905060f3bb581 .github/workflows/httpclient-release.yml'
'3be84c9f15fa3b2ac5a085f8d725ec6d05e7007ae0b433da9e79b3bf340d57ea .github/workflows/jpa-next-hibernate8.yml'
'a2b74bfb3af12d6d03cd2ea8a5e48490dd131afb89b79694d498c5798387ac53 .github/workflows/jpa-next-jpa4.yml'
'cd955ef4af895df477896dad9577810f010b2beea8570b09b008f9e94e928bd0 .github/workflows/jpa-next-postgresql19.yml'
'21e065880ef5d4c4ff973f52d8107ef08398ebaf9518ec6b2fd82d49c5d822c6 .github/workflows/jpa-nightly.yml'
'04851f44ba94533bfbc8fabe2b3a2b408726a9996e86ed3864986d1499d16b50 .github/workflows/jpa-pr.yml'
'59cb3a0ffc687a15eefe96bc5e3a70d42be78e1cc85d2e7f7880dac6124ca4c7 .github/workflows/jpa-r2-evidence.yml'
'cf4f80134197dd6d7dc177f0d21294a6b9ffe8709be67d05089f7ff0ce6c9429 .github/workflows/jpa-release.yml'
'5be7e931db749029d89787da042d6d7cf8e683d60698bd8a2993c29db26355fb .github/workflows/link-check.yml'
'8adafc59a2d87a6c65ef94b4726d7d036ac81b150ed3d301578308e6f9a3523f .github/workflows/messaging-certification.yml'
'4e4ccfa267ecd63b9369803d49f2dbdb2fa899517ad4cf23ab11d29104557a91 .github/workflows/notification-platform.yml'
'64245586cd5936f1a5647b57f2cd9acd316f96fd75f713b1890decb812e7d5fe .github/workflows/object-storage-qualification.yml'
'cbc104ea486c746229895e804e3be7716e056a02cce0588c537bce9f442f8b38 .github/workflows/redis-sdk-topology.yml'
'89fb84532d542f7951e11cf2925425ea84b7ef9cc22f4587f1d2cfd99c481f5f .github/workflows/web-advanced-nightly.yml'
'a3d01b73831f1f77a09edfe883e32cd63c8dc8c79b022faf7fec7bdd08c6e4db .github/workflows/web-advanced-release.yml'
'4198ce8215097ae9342167c4985455bbe6e56956a3be05bee33428381cad638d .github/workflows/web-nightly.yml'
'b07b92c43e94f674fe6c851603dd27bbed72894274f031b95c3d6e2b256650bd .github/workflows/web-pr.yml'
'a82f3eacee01165cf9c0767657a584a2524d7bb751f1d567a696241ea44cb3b6 .github/workflows/web-release.yml'
'f37b2b2598687679a3fb0ae9ea2b50cd5d84a64de7e5852f38a3e5b93bf76e4d .github/workflows/websocket-advanced-nightly.yml'
'5643fe9c9d27d9e6f5ac30a731e77a962b68bed2961566e2e64cdb3991ef2350 .github/workflows/websocket-pr.yml'
'c9fabc17fe755f9f0ee54007e48357fec9493a89ecf67fa2878f4dbc23478f30 .github/workflows/websocket-release.yml'
)
readonly EXPECTED_WRAPPER_PROPERTIES=(
'distributionBase=GRADLE_USER_HOME'
'distributionPath=wrapper/dists'
"distributionUrl=https\://services.gradle.org/distributions${EXPECTED_DISTRIBUTION_SUFFIX}"
"distributionSha256Sum=${EXPECTED_DISTRIBUTION_SHA256}"
'networkTimeout=10000'
'validateDistributionUrl=true'
'zipStoreBase=GRADLE_USER_HOME'
'zipStorePath=wrapper/dists'
)
fail() {
printf 'gradle-wrapper-contract: FAIL: %s\n' "$1" >&2
exit 1
}
if [[ $# -ne 1 ]]; then
fail 'expected exactly one repository-root argument'
fi
readonly REPOSITORY_ROOT=$1
[[ -d "${REPOSITORY_ROOT}" ]] || fail "repository root is not a directory: ${REPOSITORY_ROOT}"
readonly WRAPPER_PROPERTIES="${REPOSITORY_ROOT}/src/gradle/wrapper/gradle-wrapper.properties"
readonly WRAPPER_JAR="${REPOSITORY_ROOT}/src/gradle/wrapper/gradle-wrapper.jar"
readonly WORKFLOWS_DIRECTORY="${REPOSITORY_ROOT}/.github/workflows"
[[ -f "${WRAPPER_PROPERTIES}" ]] || fail "missing wrapper properties: ${WRAPPER_PROPERTIES}"
[[ -f "${WRAPPER_JAR}" ]] || fail "missing wrapper JAR: ${WRAPPER_JAR}"
[[ -d "${WORKFLOWS_DIRECTORY}" ]] || fail "missing workflows directory: ${WORKFLOWS_DIRECTORY}"
if ! printf '%s\n' "${EXPECTED_WRAPPER_PROPERTIES[@]}" | cmp -s - "${WRAPPER_PROPERTIES}"; then
fail 'wrapper properties must match the exact canonical Gradle 9.0.0 eight-line contract'
fi
readonly actual_wrapper_jar_sha256=$(sha256sum "${WRAPPER_JAR}" | awk '{print $1}')
[[ "${actual_wrapper_jar_sha256}" == "${EXPECTED_WRAPPER_JAR_SHA256}" ]] \
|| fail "wrapper JAR SHA-256 mismatch: ${actual_wrapper_jar_sha256}"
workflow_lock_valid=1
actual_workflow_lock=()
while IFS= read -r -d '' locked_workflow; do
locked_workflow_relative=${locked_workflow#"${REPOSITORY_ROOT}"/}
if [[ -L "${locked_workflow}" || ! -f "${locked_workflow}" ]]; then
locked_workflow_sha256='<invalid-file-type>'
else
locked_workflow_sha256=$(sha256sum -- "${locked_workflow}" | awk '{print $1}')
fi
actual_workflow_lock+=("${locked_workflow_sha256} ${locked_workflow_relative}")
done < <(
find "${WORKFLOWS_DIRECTORY}" -mindepth 1 -maxdepth 1 \
\( -name '*.yml' -o -name '*.yaml' \) -print0 \
| LC_ALL=C sort -z
)
workflow_lock_entry_count=${#EXPECTED_WORKFLOW_LOCK[@]}
if ((${#actual_workflow_lock[@]} > workflow_lock_entry_count)); then
workflow_lock_entry_count=${#actual_workflow_lock[@]}
fi
for ((workflow_lock_index = 0; workflow_lock_index < workflow_lock_entry_count; workflow_lock_index++)); do
expected_workflow_lock_entry=${EXPECTED_WORKFLOW_LOCK[workflow_lock_index]-<missing>}
actual_workflow_lock_entry=${actual_workflow_lock[workflow_lock_index]-<missing>}
if [[ "${actual_workflow_lock_entry}" != "${expected_workflow_lock_entry}" ]]; then
printf 'gradle-wrapper-contract: workflow lock mismatch: expected %q; actual %q\n' \
"${expected_workflow_lock_entry}" "${actual_workflow_lock_entry}" >&2
workflow_lock_valid=0
fi
done
workflow_count=0
gradle_job_count=0
while IFS= read -r -d '' workflow; do
if ! awk -v workflow="${workflow#"${REPOSITORY_ROOT}"/}" '
function reset_step(known_field) {
step_active = 0
run_block = 0
for (known_field in step_fields) {
delete step_fields[known_field]
}
}
function reset_job() {
job = ""
in_steps = 0
steps_count = 0
reset_step()
}
function indentation(line, first_non_space) {
if (line ~ /^ *$/) {
return length(line)
}
first_non_space = match(line, /[^ ]/)
return first_non_space - 1
}
function trim(value) {
sub(/^[[:space:]]+/, "", value)
sub(/[[:space:]]+$/, "", value)
return value
}
function grammar_error(message) {
printf "%s: job %s %s\n", workflow, job == "" ? "<unknown>" : job, message > "/dev/stderr"
invalid = 1
}
function workflow_grammar_error(message) {
printf "%s: %s\n", workflow, message > "/dev/stderr"
invalid = 1
}
function validate_job_shape() {
if (job != "" && steps_count != 1) {
grammar_error("must contain exactly one canonical steps block")
}
}
function is_allowed_step_field(field) {
return field == "name" \
|| field == "id" \
|| field == "uses" \
|| field == "run" \
|| field == "if" \
|| field == "shell" \
|| field == "with" \
|| field == "env" \
|| field == "working-directory" \
|| field == "continue-on-error" \
|| field == "timeout-minutes"
}
function validate_uses_scalar(value, first, quote, closing, index_value, suffix, action, single_quote) {
value = trim(value)
if (value == "" || index(value, "\\") != 0) {
grammar_error("has unsupported uses scalar")
return
}
first = substr(value, 1, 1)
single_quote = sprintf("%c", 39)
if (first == "\"" || first == single_quote) {
quote = first
closing = 0
for (index_value = 2; index_value <= length(value); index_value++) {
if (substr(value, index_value, 1) == quote) {
closing = index_value
break
}
}
if (closing == 0) {
grammar_error("has unsupported uses scalar")
return
}
suffix = substr(value, closing + 1)
if (suffix !~ /^[[:space:]]*(#.*)?$/) {
grammar_error("has unsupported uses scalar")
return
}
action = substr(value, 2, closing - 2)
if (index(action, quote) != 0) {
grammar_error("has unsupported uses scalar")
return
}
} else {
action = value
sub(/[[:space:]]+#.*$/, "", action)
action = trim(action)
if (action ~ /["'"'"'\\]/ || action ~ /^[*!&|>]/) {
grammar_error("has unsupported uses scalar")
return
}
}
if (action !~ /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(\/[A-Za-z0-9_.-]+)*@[A-Za-z0-9_.\/-]+$/ \
&& action !~ /^\.\/[A-Za-z0-9_.\/-]+$/ \
&& action !~ /^docker:\/\/[^[:space:]]+$/) {
grammar_error("has unsupported uses scalar")
}
}
function validate_run_scalar(value, first) {
value = trim(value)
if (value ~ /^(\||>)[+-]?([[:space:]]+#.*)?$/) {
run_block = 1
return
}
first = substr(value, 1, 1)
if (value == "" || first == "\"" || first == sprintf("%c", 39) \
|| first ~ /[*&!|>]/ || index(value, "\\") != 0) {
grammar_error("has unsupported run scalar")
}
}
function validate_step_field(content, field, value, separator) {
content = trim(content)
if (content ~ /^[{[]/) {
grammar_error("contains unsupported flow-style step syntax")
return
}
if (content ~ /^<</) {
grammar_error("contains a forbidden step merge key")
return
}
if (content ~ /^[*&!]/) {
grammar_error("contains unsupported step anchor, alias, or tag syntax")
return
}
if (content !~ /^[A-Za-z][A-Za-z0-9-]*:/) {
grammar_error("contains unsupported step field syntax")
return
}
separator = index(content, ":")
field = substr(content, 1, separator - 1)
value = substr(content, separator + 1)
sub(/^[[:space:]]*/, "", value)
if (!is_allowed_step_field(field)) {
grammar_error("contains unsupported step field: " field)
return
}
if (field in step_fields) {
grammar_error("contains duplicate step field: " field)
return
}
step_fields[field] = 1
if (field == "uses") {
validate_uses_scalar(value)
} else if (field == "run") {
validate_run_scalar(value)
}
}
BEGIN {
in_jobs = 0
invalid = 0
jobs_count = 0
single_quote = sprintf("%c", 39)
reset_job()
}
/^jobs:/ {
if ($0 !~ /^jobs:[[:space:]]*(#.*)?$/) {
workflow_grammar_error("jobs container must use a canonical block mapping")
next
}
jobs_count++
if (jobs_count != 1) {
workflow_grammar_error("workflow must contain exactly one canonical jobs block")
}
in_jobs = 1
next
}
/^"jobs":/ {
workflow_grammar_error("jobs container must use a canonical block mapping")
next
}
substr($0, 1, 7) == single_quote "jobs" single_quote ":" {
workflow_grammar_error("jobs container must use a canonical block mapping")
next
}
run_block == 0 && /^<<:/ {
workflow_grammar_error("workflow contains a forbidden merge key")
next
}
in_jobs && /^[^[:space:]#]/ {
validate_job_shape()
reset_job()
in_jobs = 0
}
in_jobs && /^ [^[:space:]#]/ {
if ($0 !~ /^ [A-Za-z0-9_.-]+:[[:space:]]*(#.*)?$/) {
grammar_error("job declaration must use a canonical block mapping")
next
}
validate_job_shape()
reset_job()
job = $0
sub(/^ /, "", job)
sub(/:.*/, "", job)
next
}
in_jobs && job != "" {
raw = $0
line_indent = indentation(raw)
if (run_block != 0) {
if (raw ~ /^ *$/ || line_indent > 8) {
next
}
run_block = 0
}
if (raw ~ /^ *#/) {
next
}
if (raw ~ /^ steps:/ || raw ~ /^ "steps":/ \
|| substr(raw, 1, 11) == " " single_quote "steps" single_quote ":") {
if (raw != " steps:") {
grammar_error("steps container must use a canonical block sequence")
next
}
steps_count++
if (steps_count != 1) {
grammar_error("must contain exactly one canonical steps block")
}
in_steps = 1
reset_step()
next
}
if (in_steps != 0 && line_indent == 4) {
in_steps = 0
reset_step()
}
if (raw ~ /^ *<<:/) {
grammar_error("contains a forbidden merge key")
next
}
if (in_steps != 0 && raw ~ /^ - /) {
reset_step()
step_active = 1
content = substr(raw, 9)
validate_step_field(content)
next
}
if (in_steps != 0 && raw ~ /^ -[[:space:]]*$/) {
grammar_error("contains unsupported empty step syntax")
next
}
if (in_steps != 0 && step_active != 0 && line_indent == 8) {
content = substr(raw, 9)
validate_step_field(content)
next
}
if (in_steps != 0 && line_indent == 6 && raw !~ /^ *$/) {
grammar_error("contains unsupported step-list syntax")
}
}
END {
validate_job_shape()
if (jobs_count != 1) {
workflow_grammar_error("workflow must contain exactly one canonical jobs block")
}
if (invalid) {
exit 1
}
}
' "${workflow}"; then
fail "workflow structural validation failed: ${workflow#"${REPOSITORY_ROOT}"/}"
fi
if ! grep -Fq -- './gradlew' "${workflow}" \
&& ! grep -Fq -- 'gradle/actions/dependency-submission@' "${workflow}"; then
continue
fi
((workflow_count += 1))
if ! jobs_in_workflow=$(
awk -v workflow="${workflow#"${REPOSITORY_ROOT}"/}" \
-v validation_action="${EXPECTED_VALIDATION_ACTION}" \
-v dependency_action="${EXPECTED_DEPENDENCY_SUBMISSION_ACTION}" \
-v guarded_gradle_if="${EXPECTED_GUARDED_GRADLE_IF}" '
function reset_step(known_field) {
step_active = 0
run_block = 0
step_kind = ""
step_name = ""
step_id = ""
step_uses = ""
step_uses_action = ""
step_if = ""
step_if_present = 0
step_continue_on_error = 0
step_gradle = 0
step_gradle_line = 0
step_unsupported_gradle = 0
step_field_count = 0
step_name_line = 0
step_id_line = 0
step_uses_line = 0
step_extra_field = ""
for (known_field in step_fields) {
delete step_fields[known_field]
delete step_field_raw[known_field]
}
}
function reset_job() {
job = ""
checkout_line = 0
validation_line = 0
gradle_line = 0
in_steps = 0
unsupported_gradle = 0
reset_step()
}
function indentation(line, first_non_space) {
if (line ~ /^ *$/) {
return length(line)
}
first_non_space = match(line, /[^ ]/)
return first_non_space - 1
}
function has_gradle_reference(line) {
return index(line, "./gradlew") != 0 \
|| index(line, "gradle/actions/dependency-submission@") != 0
}
function trim(value) {
sub(/^[[:space:]]+/, "", value)
sub(/[[:space:]]+$/, "", value)
return value
}
function normalize_action(value, scalar, first, quote, closing, index_value) {
scalar = trim(value)
first = substr(scalar, 1, 1)
if (first == "\"" || first == single_quote) {
quote = first
closing = index(substr(scalar, 2), quote)
if (closing == 0) {
return ""
}
return substr(scalar, 2, closing - 1)
}
sub(/[[:space:]]+#.*$/, "", scalar)
return trim(scalar)
}
function record_gradle(line_number) {
step_gradle = 1
if (step_gradle_line == 0) {
step_gradle_line = line_number
}
if (gradle_line == 0) {
gradle_line = line_number
}
}
function record_uses(value, line_number, action) {
if (step_kind == "run") {
if (index(value, "gradle/actions/dependency-submission@") != 0) {
step_unsupported_gradle = 1
}
return
}
step_kind = "uses"
action = normalize_action(value)
step_uses = trim(value)
step_uses_action = action
step_uses_line = line_number
if (checkout_line == 0 && action ~ /^actions\/checkout@/) {
checkout_line = line_number
}
if (action == dependency_action) {
record_gradle(line_number)
} else if (index(action, "gradle/actions/dependency-submission@") != 0) {
record_gradle(line_number)
step_unsupported_gradle = 1
}
}
function record_run(value, line_number) {
if (step_kind == "uses") {
if (index(value, "./gradlew") != 0) {
step_unsupported_gradle = 1
}
return
}
step_kind = "run"
if (value ~ /^(\||>)[+-]?([[:space:]]+#.*)?$/) {
run_block = 1
} else if (index(value, "./gradlew") != 0) {
record_gradle(line_number)
}
}
function record_step_field(content, line_number, separator, field, value) {
separator = index(content, ":")
field = substr(content, 1, separator - 1)
value = substr(content, separator + 1)
sub(/^[[:space:]]*/, "", value)
step_fields[field] = 1
step_field_raw[field] = trim(content)
step_field_count++
if (field == "name") {
step_name = trim(value)
step_name_line = line_number
} else if (field == "id") {
step_id = trim(value)
step_id_line = line_number
} else if (field == "uses") {
record_uses(value, line_number)
} else if (field == "run") {
record_run(trim(value), line_number)
} else if (field == "if") {
step_if_present = 1
step_if = trim(value)
} else if (field == "continue-on-error") {
step_continue_on_error = 1
}
if (field != "name" && field != "id" && field != "uses" && step_extra_field == "") {
step_extra_field = step_field_raw[field]
}
}
function validate_wrapper_step() {
if (step_uses_action != validation_reference) {
return
}
if (step_extra_field != "") {
printf "%s: job %s wrapper validation step contains unsupported field: %s\n", workflow, job, step_extra_field > "/dev/stderr"
invalid = 1
return
}
if (step_field_count != 3 \
|| step_name != "Validate Gradle wrapper" \
|| step_id != "gradle-wrapper-validation" \
|| step_uses != validation_action \
|| !(step_name_line < step_id_line && step_id_line < step_uses_line)) {
printf "%s: job %s wrapper validation step must contain exact name, id, and uses fields only\n", workflow, job > "/dev/stderr"
invalid = 1
return
}
if (validation_line == 0) {
validation_line = step_uses_line
}
}
function validate_gradle_step() {
if (step_gradle == 0 && step_unsupported_gradle == 0) {
return
}
if (step_unsupported_gradle != 0 || ("uses" in step_fields && "run" in step_fields)) {
unsupported_gradle = 1
}
if (step_if_present != 0 && step_if != guarded_gradle_if) {
printf "%s: job %s has Gradle step with unsupported if condition: %s\n", workflow, job, step_if > "/dev/stderr"
invalid = 1
}
if (step_continue_on_error != 0) {
printf "%s: job %s has Gradle step with unsupported field: %s\n", workflow, job, step_field_raw["continue-on-error"] > "/dev/stderr"
invalid = 1
}
}
function finalize_step() {
if (step_active == 0) {
return
}
validate_wrapper_step()
validate_gradle_step()
}
function start_step() {
finalize_step()
reset_step()
step_active = 1
}
function validate_job() {
finalize_step()
if (job == "" || (gradle_line == 0 && unsupported_gradle == 0)) {
return
}
gradle_jobs++
if (unsupported_gradle != 0) {
printf "%s: job %s uses a Gradle invocation outside the canonical workflow structure\n", workflow, job > "/dev/stderr"
invalid = 1
}
if (gradle_line == 0) {
return
} else if (checkout_line == 0) {
printf "%s: job %s invokes Gradle without checkout\n", workflow, job > "/dev/stderr"
invalid = 1
} else if (validation_line == 0) {
printf "%s: job %s invokes Gradle without the exact pinned wrapper validation action\n", workflow, job > "/dev/stderr"
invalid = 1
} else if (!(checkout_line < validation_line && validation_line < gradle_line)) {
printf "%s: job %s must order checkout, exact wrapper validation, then Gradle\n", workflow, job > "/dev/stderr"
invalid = 1
}
}
BEGIN {
in_jobs = 0
invalid = 0
gradle_jobs = 0
single_quote = sprintf("%c", 39)
validation_reference = validation_action
sub(/[[:space:]]+#.*$/, "", validation_reference)
reset_job()
}
/^jobs:[[:space:]]*(#.*)?$/ {
in_jobs = 1
next
}
in_jobs && /^[^[:space:]#]/ {
validate_job()
reset_job()
in_jobs = 0
}
in_jobs && /^ [A-Za-z0-9_.-]+:[[:space:]]*(#.*)?$/ {
validate_job()
reset_job()
job = $0
sub(/^ /, "", job)
sub(/:.*/, "", job)
next
}
in_jobs && job != "" {
raw = $0
line_indent = indentation(raw)
if (run_block != 0) {
if (raw ~ /^ *$/) {
next
}
if (line_indent > 8) {
if (index(raw, "./gradlew") != 0) {
record_gradle(NR)
}
if (index(raw, "gradle/actions/dependency-submission@") != 0) {
step_unsupported_gradle = 1
}
next
}
run_block = 0
}
if (raw ~ /^ *#/) {
next
}
if (raw == " steps:") {
in_steps = 1
reset_step()
next
}
if (in_steps != 0 && line_indent == 4) {
finalize_step()
in_steps = 0
reset_step()
}
if (in_steps != 0 && raw ~ /^ - /) {
start_step()
content = substr(raw, 9)
record_step_field(content, NR)
next
}
if (in_steps != 0 && step_active != 0 && line_indent == 8) {
content = substr(raw, 9)
record_step_field(content, NR)
next
}
if (has_gradle_reference(raw)) {
unsupported_gradle = 1
}
}
END {
validate_job()
print gradle_jobs
if (invalid) {
exit 1
}
}
' "${workflow}"
); then
fail "workflow validation failed: ${workflow#"${REPOSITORY_ROOT}"/}"
fi
[[ "${jobs_in_workflow}" =~ ^[0-9]+$ ]] \
|| fail "workflow parser returned an invalid Gradle job count: ${workflow#"${REPOSITORY_ROOT}"/}"
((jobs_in_workflow > 0)) \
|| fail "Gradle-running workflow contains no detected Gradle job: ${workflow#"${REPOSITORY_ROOT}"/}"
((gradle_job_count += jobs_in_workflow))
done < <(find "${WORKFLOWS_DIRECTORY}" -type f \( -name '*.yml' -o -name '*.yaml' \) -print0)
((workflow_count > 0)) || fail 'no Gradle-running workflow was found'
((gradle_job_count > 0)) || fail 'no individual Gradle-running job was found'
((workflow_lock_valid != 0)) \
|| fail 'workflow lock mismatch: workflow set or bytes differ from the reviewed embedded manifest'
printf 'gradle-wrapper-contract: PASS\n'
+63
View File
@@ -0,0 +1,63 @@
name: reusable-gradle
# One place that knows how a Gradle job starts.
#
# Every job in this repository opened with the same preamble: checkout, a three-line pinned
# wrapper-validation step, then the JDK/cache action. The wrapper step is gone (setup-gradle
# validates wrappers itself), and this workflow removes the rest of the repetition for the jobs whose
# only variation is the Gradle command they run.
#
# Jobs that need service containers, a matrix, artifact uploads or per-job env stay written out with
# `./.github/actions/setup-gradle-java`, because expressing those through `workflow_call` inputs
# means encoding YAML inside strings — which is how a "shared" workflow becomes less readable than
# the duplication it replaced.
on:
workflow_call:
inputs:
tasks:
description: The Gradle task list, whitespace-separated.
required: true
type: string
gradle-args:
description: Flags appended after the task list.
required: false
type: string
default: "--stacktrace"
working-directory:
description: Directory the wrapper is invoked from.
required: false
type: string
default: src
timeout-minutes:
required: false
type: number
default: 30
continue-on-error:
description: Run the job as an advisory signal rather than a gate.
required: false
type: boolean
default: false
permissions:
contents: read
jobs:
gradle:
runs-on: ubuntu-latest
timeout-minutes: ${{ inputs.timeout-minutes }}
continue-on-error: ${{ inputs.continue-on-error }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Run ${{ inputs.tasks }}
working-directory: ${{ inputs.working-directory }}
env:
GRADLE_TASKS: ${{ inputs.tasks }}
GRADLE_ARGS: ${{ inputs.gradle-args }}
run: |
set -euo pipefail
# Word-split on purpose: both inputs are task/flag lists. They come from this repository's
# own workflow files, never from a pull request.
# shellcheck disable=SC2086
./gradlew ${GRADLE_TASKS} ${GRADLE_ARGS}
+88 -103
View File
@@ -1,5 +1,14 @@
name: ci-quality-gates name: ci-quality-gates
# The pull-request gate. Everything here blocks a merge.
#
# The job list used to include `gate-matrix-lint`, which ran .github/scripts/verify-gate-matrix.sh
# against .github/ci-gate-matrix.yml: a 1,025-line register of all 107 CI controls, checked for
# consistency against the Gradle task graph and this workflow by a 568-line shell script, which was
# itself checked by contract tests in :app-bootstrap. Adding one check meant editing Gradle, a
# workflow, the matrix, the verifier's expectations and a Java test. The information was already in
# the task graph and the job graph; the matrix was a third copy that had to be kept equal to both.
on: on:
pull_request: pull_request:
push: push:
@@ -21,9 +30,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- name: Require the committed public-path security baseline - name: Require the committed public-path security baseline
run: | run: |
set -euo pipefail set -euo pipefail
@@ -36,111 +42,97 @@ jobs:
echo "::error::${snapshot} exists locally but is not committed." echo "::error::${snapshot} exists locally but is not committed."
exit 1 exit 1
fi fi
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1 - uses: ./.github/actions/setup-gradle-java
with: # `ci`, not `check`. A leaf's `check` is that leaf's — compile, its tests, Spotless, Checkstyle
distribution: temurin # and Error Prone — and the repository-wide gates are named tasks of their own:
java-version: "21.0.11+10" # ci = every leaf check + architectureCheck + qualityCheck + configContractCheck + qualificationCheck
cache: gradle # so CI runs strictly more than it used to while `./gradlew :domain-core:check` runs strictly
cache-dependency-path: | # less.
src/**/*.gradle - name: Run the pull-request gate
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Check quality, public paths, and dependency locks
working-directory: src working-directory: src
run: ./gradlew check verifyPublicPathSnapshot verifyDependencyLocks --warning-mode=fail --no-daemon --stacktrace run: ./gradlew :ci :verifyPublicPathSnapshot :verifyDependencyLocks --warning-mode=fail --stacktrace
# Named as its own step because nothing else runs it: `check` does not depend on # Named as its own step because nothing else runs it: `check` does not depend on
# graphqlStableTest, so the lane's required-class guard — the check that its module-boundary # graphqlStableTest, so the lane's required-class guard — the check that its module-boundary
# suite has not silently stopped being discovered — protected nothing in CI. A separate step # suite has not silently stopped being discovered — would protect nothing in CI.
# keeps the aggregate invocation below byte-identical, which ConditionalTransportQualification
# ContractTest asserts on, and the two tasks do not overlap.
- name: Qualify the GraphQL Stable lane - name: Qualify the GraphQL Stable lane
working-directory: src working-directory: src
run: ./gradlew :adapter:inbound:graphql:graphqlStableTest --no-daemon --stacktrace run: ./gradlew :adapter:inbound:graphql:graphqlStableTest --stacktrace
- name: Qualify opt-in inbound transports without skips - name: Qualify opt-in inbound transports without skips
working-directory: src working-directory: src
run: ./gradlew conditionalTransportQualification --no-daemon --stacktrace run: ./gradlew :conditionalTransportQualification --stacktrace
sample-off: build-logic:
# Included-build tests are independent of the main project task graph. Running them as a
# separate blocking job keeps plugin TestKit work off the quality-gates critical path.
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation - name: Test the build-logic convention plugins
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Verify the application without the sample fixture
working-directory: src working-directory: src
run: ./gradlew :app-bootstrap:sampleOffTest verifyCleanArchitectureDependencies --no-daemon --stacktrace run: ./gradlew -p build-logic test --stacktrace
gate-matrix-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Verify the gate matrix against the repository
run: bash .github/scripts/verify-gate-matrix.sh
redis-sdk: redis-sdk:
# Milestone A of the Redis wrapper/typed API plan: policy catalog, typed API parity, permit
# provenance, connection isolation, and the executor guard. There is no real-server lane yet.
#
# `verifyConfigurationPropertiesProcessor` used to be in this list. It is deleted: the parity it
# enforced — a leaf declares Spring's configuration processor exactly when it owns
# @ConfigurationProperties — is now what applying `ca.spring-config` means.
# `verifyEnvKeys` is no longer named here either; it belongs to :app-bootstrap and runs through
# `configContractCheck`, which the quality-gates job covers.
uses: ./.github/workflows/_reusable-gradle.yml
with:
tasks: ":shared-contract:edgeRateLimitContractTest"
optional-platforms:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation - name: Verify the optional gRPC platform build
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6 working-directory: src
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1 run: ./gradlew -p optional-platforms ci --stacktrace
with:
distribution: temurin configuration-cache:
java-version: "21.0.11+10" runs-on: ubuntu-latest
cache: gradle steps:
cache-dependency-path: | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
src/**/*.gradle - uses: ./.github/actions/setup-gradle-java
src/**/gradle-wrapper.properties - name: Store configuration cache for the everyday core build
src/**/gradle.lockfile
# Milestone A of the Redis wrapper/typed API plan: policy catalog, typed API parity,
# permit provenance, connection isolation, and the executor guard. There is no real-server
# lane yet — Tasks 10-17 add the contract suites that need one.
- name: Verify the Redis SDK policy, API parity, and guardrail contracts
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
:shared-contract:edgeRateLimitContractTest :domain-core:check
:adapter:outbound:cache-redis:check :application-core:check
verifyCleanArchitectureDependencies --configuration-cache
verifyEnvKeys --configuration-cache-problems=fail
verifyPublicPathSnapshot --stacktrace
verifyConfigurationPropertiesProcessor - name: Require configuration-cache reuse
--no-daemon --stacktrace working-directory: src
shell: bash
run: |
set -euo pipefail
output="$({ ./gradlew :domain-core:check :application-core:check \
--configuration-cache --configuration-cache-problems=fail --stacktrace; } 2>&1)"
printf '%s\n' "${output}"
grep -Fq 'Reusing configuration cache.' <<<"${output}" || {
echo '::error::Gradle did not reuse the configuration cache on the second identical build.'
exit 1
}
jpa-candidate-evidence: jpa-candidate-evidence:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Produce zero-skip JPA candidate manifests - name: Produce zero-skip JPA candidate manifests
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
:adapter:outbound:persistence-jpa:verifyJpaCandidateEvidence :adapter:outbound:persistence-jpa:verifyJpaCandidateEvidence
--no-daemon
--stacktrace --stacktrace
- name: Retain content-addressed JPA candidate manifests - name: Retain content-addressed JPA candidate manifests
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # actions/upload-artifact@v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # actions/upload-artifact@v7.0.1
@@ -150,53 +142,46 @@ jobs:
if-no-files-found: error if-no-files-found: error
retention-days: 14 retention-days: 14
# Advisory only. Quarantine expiry/drift remains blocking through verifyQuarantineSunset in check. # Advisory. The quarantine bucket runs so a flaky test is still executed and reported; it never
# blocks. The 14-day sunset registry that used to make an expired quarantine entry a build failure
# is gone — it was a 250-line YAML-and-Java parser guarding a registry with zero entries.
quarantine: quarantine:
runs-on: ubuntu-latest uses: ./.github/workflows/_reusable-gradle.yml
continue-on-error: true
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with: with:
distribution: temurin tasks: ":quarantineTest"
java-version: "21.0.11+10" gradle-args: "--stacktrace"
cache: gradle continue-on-error: true
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run quarantined tests as an advisory signal
working-directory: src
run: ./gradlew quarantineTest --no-daemon
release-gate: release-gate:
needs: needs:
- quality-gates - quality-gates
- sample-off - build-logic
- gate-matrix-lint
- redis-sdk - redis-sdk
- jpa-candidate-evidence - jpa-candidate-evidence
- optional-platforms
- configuration-cache
if: always() if: always()
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- name: Require every current blocking job to succeed - name: Require every current blocking job to succeed
env: env:
QUALITY_RESULT: ${{ needs.quality-gates.result }} QUALITY_RESULT: ${{ needs.quality-gates.result }}
SAMPLE_OFF_RESULT: ${{ needs.sample-off.result }} BUILD_LOGIC_RESULT: ${{ needs.build-logic.result }}
MATRIX_RESULT: ${{ needs.gate-matrix-lint.result }}
REDIS_RESULT: ${{ needs.redis-sdk.result }} REDIS_RESULT: ${{ needs.redis-sdk.result }}
JPA_CANDIDATE_RESULT: ${{ needs.jpa-candidate-evidence.result }} JPA_CANDIDATE_RESULT: ${{ needs.jpa-candidate-evidence.result }}
OPTIONAL_PLATFORMS_RESULT: ${{ needs.optional-platforms.result }}
CONFIGURATION_CACHE_RESULT: ${{ needs.configuration-cache.result }}
run: | run: |
set -euo pipefail set -euo pipefail
for result in \ for result in \
"${QUALITY_RESULT}" \ "${QUALITY_RESULT}" \
"${SAMPLE_OFF_RESULT}" \ "${BUILD_LOGIC_RESULT}" \
"${MATRIX_RESULT}" \
"${REDIS_RESULT}" \ "${REDIS_RESULT}" \
"${JPA_CANDIDATE_RESULT}"; do "${JPA_CANDIDATE_RESULT}" \
"${OPTIONAL_PLATFORMS_RESULT}" \
"${CONFIGURATION_CACHE_RESULT}"; do
if [[ "${result}" != "success" ]]; then if [[ "${result}" != "success" ]]; then
echo "::error::release-gate: required job result was ${result}" echo "::error::release-gate: required job result was ${result}"
exit 1 exit 1
+5 -13
View File
@@ -35,18 +35,7 @@ jobs:
contents: write contents: write
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Submit the resolved Gradle dependency graph - name: Submit the resolved Gradle dependency graph
uses: gradle/actions/dependency-submission@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # gradle/actions@v4.4.4 uses: gradle/actions/dependency-submission@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # gradle/actions@v4.4.4
with: with:
@@ -179,7 +168,10 @@ jobs:
trivy-kev.json | sort -u > found-cves.txt trivy-kev.json | sort -u > found-cves.txt
jq -r '.vulnerabilities[]?.cveID | select(type == "string")' \ jq -r '.vulnerabilities[]?.cveID | select(type == "string")' \
kev.json | sort -u > kev-cves.txt kev.json | sort -u > kev-cves.txt
hits="$(comm -12 found-cves.txt kev-cves.txt || true)" # No `|| true`. comm exits non-zero only when it cannot read or order its inputs, and
# swallowing that would have turned an unreadable CVE list into an empty intersection and
# printed "no catalog match" — a KEV cross-check that passes because it never ran.
hits="$(comm -12 found-cves.txt kev-cves.txt)"
if [[ -n "${hits}" ]]; then if [[ -n "${hits}" ]]; then
echo "::error::CISA KEV-listed vulnerability found regardless of CVSS:" echo "::error::CISA KEV-listed vulnerability found regardless of CVSS:"
printf '%s\n' "${hits}" printf '%s\n' "${hits}"
@@ -0,0 +1,122 @@
name: fileserver-certification
# The certification a release must clear. Its job list is deliberately the same shape as the support
# matrix: nothing may be advertised at a support level whose evidence job is absent here.
#
# Named "certification", not "release", and the name is the point. This workflow proves a storage
# topology, a support matrix and a telemetry redaction claim. It deploys nothing and holds no cluster
# credential. Calling it `fileserver-release.yml` read as if GitHub Actions released the fileserver,
# which is the CI/CD boundary this repository has now fixed in docs/ci-cd/boundary.md: GitHub Actions
# tests, scans and publishes artifacts; Argo CD deploys.
#
# It used to be workflow_dispatch only, which made that sentence false: the four jobs below are the
# only place the fileserver support matrix, the PVC manifest and the telemetry redaction proof are
# checked, and a release tag reached none of them unless somebody remembered to press a button.
#
# `v*` is the only release tag. The adapter-scoped `fileserver-v*` pattern is gone: this repository
# has one deployable unit (app-bootstrap), so an adapter-scoped tag could only ever run a subset of
# the release gates and call the result a release — the tag-namespace split that release.yml exists
# to end.
#
# These four jobs stay in their own file, and not in release.yml, for one mechanical reason:
# FileserverDocumentationCoverageTest reads job ids out of `.github/workflows/fileserver-*.yml` and
# requires every `fileserver-...` job docs/fileserver/support-matrix.md names to be defined in one
# of them. Renaming the file or moving these jobs needs that document changed in the same change.
on:
push:
tags:
- "v*"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
fileserver-full-verification:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Run the architecture-wide dependency and module verification
working-directory: src
run: >-
./gradlew
:verifyCleanArchitectureDependencies
--stacktrace
- name: Run the complete fileserver suite across every leaf
working-directory: src
run: >-
./gradlew
:application-core:check
:adapter:inbound:web:check
:adapter:outbound:fileserver:check
--stacktrace
fileserver-documentation-gate:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Prove every support claim maps to a job and every endpoint is documented
working-directory: src
run: >-
./gradlew
:app-bootstrap:test --tests '*FileserverDocumentationCoverageTest'
--stacktrace
fileserver-pvc-certification:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
# This job checks the manifest, and only the manifest. It deliberately does not apply anything
# to a cluster.
#
# There used to be a second step here that applied the job to a release cluster when
# secrets.FILESERVER_PVC_KUBECONFIG was set and `exit 0`-ed with a ::warning:: when it was
# not. With no secret configured — which is every fork of this template and was this
# repository — the step printed a warning and the job went green under the name
# "fileserver-pvc-certification", so a release read as ReadWriteOnce-certified against a
# cluster nothing had ever touched. It also wrote a `certified` output that no job, step or
# script in this repository read.
#
# The cluster result comes from an operator running infra/fileserver/kubernetes/
# pvc-certification-job.yaml against a real cluster and recording it in
# docs/fileserver/storage-certification.md. That is registered as
# docs/fileserver/storage-certification.md, and the absence of a cluster result is stated
# there rather than hidden behind a green check.
- name: Check the certification manifest still says what the claim depends on
run: |
set -euo pipefail
manifest=infra/fileserver/kubernetes/pvc-certification-job.yaml
test -f "$manifest"
grep -q 'kind: PersistentVolumeClaim' "$manifest"
grep -q 'kind: Job' "$manifest"
# ReadWriteMany is explicitly not claimed; a manifest that quietly widened the access
# mode would certify a topology the support matrix says is uncertified.
grep -q 'ReadWriteOnce' "$manifest"
! grep -q 'ReadWriteMany' "$manifest"
fileserver-sensitive-telemetry-scan:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Prove telemetry carries no filename, path, or raw identifier
working-directory: src
run: >-
./gradlew
:application-core:test --tests '*FileserverObservabilityTest'
--stacktrace
+8 -52
View File
@@ -24,18 +24,7 @@ jobs:
FILESERVER_NFS_TESTS: "true" FILESERVER_NFS_TESTS: "true"
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Start the NFSv4 certification environment - name: Start the NFSv4 certification environment
run: docker compose -f infra/fileserver/nfs/compose.yml up -d --wait run: docker compose -f infra/fileserver/nfs/compose.yml up -d --wait
- name: Run the network-filesystem ambiguity suite - name: Run the network-filesystem ambiguity suite
@@ -43,7 +32,7 @@ jobs:
run: >- run: >-
./gradlew ./gradlew
:adapter:outbound:fileserver:test --tests '*NfsAmbiguityIntegrationTest' :adapter:outbound:fileserver:test --tests '*NfsAmbiguityIntegrationTest'
--no-daemon
--stacktrace --stacktrace
- name: Tear down the NFS environment - name: Tear down the NFS environment
if: always() if: always()
@@ -54,18 +43,7 @@ jobs:
timeout-minutes: 45 timeout-minutes: 45
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the crash matrix and reconciliation suites - name: Run the crash matrix and reconciliation suites
working-directory: src working-directory: src
run: >- run: >-
@@ -73,7 +51,7 @@ jobs:
:adapter:outbound:fileserver:test --tests '*CrashRecoveryMatrixTest' :adapter:outbound:fileserver:test --tests '*CrashRecoveryMatrixTest'
:application-core:test --tests '*FileReconciliationServiceTest' :application-core:test --tests '*FileReconciliationServiceTest'
--rerun-tasks --rerun-tasks
--no-daemon
--stacktrace --stacktrace
fileserver-large-file-performance: fileserver-large-file-performance:
@@ -81,18 +59,7 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the large-file and slow-client suites under a constrained heap - name: Run the large-file and slow-client suites under a constrained heap
working-directory: src working-directory: src
env: env:
@@ -102,7 +69,7 @@ jobs:
:adapter:outbound:fileserver:test --tests '*LargeFileBoundedMemoryTest' :adapter:outbound:fileserver:test --tests '*LargeFileBoundedMemoryTest'
:adapter:outbound:fileserver:test --tests '*LocalAppendMemoryTest' :adapter:outbound:fileserver:test --tests '*LocalAppendMemoryTest'
--rerun-tasks --rerun-tasks
--no-daemon
--stacktrace --stacktrace
fileserver-multi-instance-lease: fileserver-multi-instance-lease:
@@ -110,23 +77,12 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Prove no run commits bytes from a stale lease - name: Prove no run commits bytes from a stale lease
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
:application-core:test --tests '*MultiInstanceWriterLeaseTest' :application-core:test --tests '*MultiInstanceWriterLeaseTest'
--rerun-tasks --rerun-tasks
--no-daemon
--stacktrace --stacktrace
+17 -68
View File
@@ -23,9 +23,13 @@ on:
- 'docs/registries/env-keys.yaml' - 'docs/registries/env-keys.yaml'
- 'src/Dockerfile' - 'src/Dockerfile'
- 'docker-compose.yml' - 'docker-compose.yml'
- 'infra/nginx/**' - 'infra/fileserver/nginx/**'
- 'infra/k8s/**' - 'infra/fileserver/kubernetes/**'
- 'infra/fileserver/nfs/**'
- '.github/workflows/fileserver-pr.yml' - '.github/workflows/fileserver-pr.yml'
# Every Gradle job here installs its toolchain through this composite action, so a change to
# it changes what this gate runs.
- '.github/actions/setup-gradle-java/action.yml'
permissions: permissions:
contents: read contents: read
@@ -40,25 +44,14 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the fileserver application and architecture suites - name: Run the fileserver application and architecture suites
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
:application-core:test :application-core:test
:app-bootstrap:test --tests '*CleanArchitectureTest' --tests '*Fileserver*' :app-bootstrap:test --tests 'dev.caskeleton.bootstrap.architecture.*' --tests '*Fileserver*'
--no-daemon
--stacktrace --stacktrace
fileserver-local-ext4-contract: fileserver-local-ext4-contract:
@@ -66,24 +59,13 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Certify the local content store against the shared contract - name: Certify the local content store against the shared contract
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
:adapter:outbound:fileserver:test :adapter:outbound:fileserver:test
--no-daemon
--stacktrace --stacktrace
fileserver-http-contract: fileserver-http-contract:
@@ -91,24 +73,13 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the servlet and reactive transport contracts - name: Run the servlet and reactive transport contracts
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
:adapter:inbound:web:test :adapter:inbound:web:test
--no-daemon
--stacktrace --stacktrace
fileserver-security-suite: fileserver-security-suite:
@@ -116,25 +87,14 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the path, filename, range, and problem-detail hardening suite - name: Run the path, filename, range, and problem-detail hardening suite
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
:adapter:inbound:web:test --tests '*FileserverHardeningContractTest' :adapter:inbound:web:test --tests '*FileserverHardeningContractTest'
:adapter:outbound:fileserver:test --tests '*PhysicalPathResolverTest' :adapter:outbound:fileserver:test --tests '*PhysicalPathResolverTest'
--no-daemon
--stacktrace --stacktrace
fileserver-bounded-memory: fileserver-bounded-memory:
@@ -142,23 +102,12 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Prove transfer cost does not scale with file size - name: Prove transfer cost does not scale with file size
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
:adapter:outbound:fileserver:test --tests '*LargeFileBoundedMemoryTest' :adapter:outbound:fileserver:test --tests '*LargeFileBoundedMemoryTest'
:adapter:inbound:web:test --tests '*DataBufferReleaseTest' :adapter:inbound:web:test --tests '*DataBufferReleaseTest'
--no-daemon
--stacktrace --stacktrace
-143
View File
@@ -1,143 +0,0 @@
name: fileserver-release
# The gate a release must clear. Its job list is deliberately the same shape as the support matrix:
# nothing may be advertised at a support level whose evidence job is absent here.
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
fileserver-full-verification:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the architecture-wide dependency and module verification
working-directory: src
run: >-
./gradlew
verifyCleanArchitectureDependencies
--no-daemon
--stacktrace
- name: Run the complete fileserver suite across every leaf
working-directory: src
run: >-
./gradlew
:application-core:check
:adapter:inbound:web:check
:adapter:outbound:fileserver:check
--no-daemon
--stacktrace
fileserver-documentation-gate:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Prove every support claim maps to a job and every endpoint is documented
working-directory: src
run: >-
./gradlew
:app-bootstrap:test --tests '*FileserverDocumentationCoverageTest'
--no-daemon
--stacktrace
fileserver-pvc-certification:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
# Two different things, kept apart on purpose. The manifest checks below run everywhere and
# fail on real drift; the cluster run needs a cluster and is skipped without one. The job
# used to `test -f` the manifest and report success, which read as "ReadWriteOnce certified"
# when nothing had been applied anywhere.
- name: Check the certification manifest still says what the claim depends on
run: |
set -euo pipefail
manifest=infra/fileserver/kubernetes/pvc-certification-job.yaml
test -f "$manifest"
grep -q 'kind: PersistentVolumeClaim' "$manifest"
grep -q 'kind: Job' "$manifest"
# ReadWriteMany is explicitly not claimed; a manifest that quietly widened the access
# mode would certify a topology the support matrix says is uncertified.
grep -q 'ReadWriteOnce' "$manifest"
! grep -q 'ReadWriteMany' "$manifest"
- name: Certify the ReadWriteOnce claim on the release cluster
id: pvc-cluster-run
env:
KUBECONFIG_CONTENT: ${{ secrets.FILESERVER_PVC_KUBECONFIG }}
run: |
set -euo pipefail
if [ -z "${KUBECONFIG_CONTENT:-}" ]; then
echo "::warning::no release cluster configured; PVC certification was NOT run."
echo "The support matrix records this profile as Limited for exactly this reason:"
echo "the cluster result is produced by an operator against a real cluster and read"
echo "from docs/fileserver/storage-certification.md, not by this job."
echo "certified=false" >> "$GITHUB_OUTPUT"
exit 0
fi
printf '%s' "$KUBECONFIG_CONTENT" > /tmp/kubeconfig
export KUBECONFIG=/tmp/kubeconfig
kubectl apply -f infra/fileserver/kubernetes/pvc-certification-job.yaml
kubectl wait --for=condition=complete --timeout=30m job/fileserver-pvc-certification
kubectl logs job/fileserver-pvc-certification
echo "certified=true" >> "$GITHUB_OUTPUT"
fileserver-sensitive-telemetry-scan:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Prove telemetry carries no filename, path, or raw identifier
working-directory: src
run: >-
./gradlew
:application-core:test --tests '*FileserverObservabilityTest'
--no-daemon
--stacktrace
-132
View File
@@ -1,132 +0,0 @@
name: httpclient-contract
# Per-PR gate for the HTTP Client Platform (design §29). Each transport runs the same semantic
# contract in its own job, so a transport that stops satisfying it fails on its own row instead of
# disappearing into an aggregate run.
on:
workflow_dispatch:
pull_request:
paths:
- 'src/adapter/outbound/httpclient/**'
- 'src/app-bootstrap/src/**/httpclient/**'
- 'docs/httpclient/**'
- 'scripts/verify-httpclient-docs.py'
- '.github/workflows/httpclient-contract.yml'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
httpclient-unit-and-boundaries:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the focused module suite and the architecture gate
working-directory: src
run: >-
./gradlew
:adapter:outbound:httpclient:test
verifyCleanArchitectureDependencies
--no-daemon
--stacktrace
httpclient-stable-contract:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
transport: [apache, jdk, reactor]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Certify one transport against the shared contract
working-directory: src
run: >-
./gradlew
:adapter:outbound:httpclient:httpClientStableContractTest
-Phttpclient.contract.transports=${{ matrix.transport }}
--no-daemon
--stacktrace
httpclient-security-and-compatibility:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the SSRF, cardinality, and Spring compatibility lanes
working-directory: src
run: >-
./gradlew
:adapter:outbound:httpclient:httpClientSecurityTest
:adapter:outbound:httpclient:httpClientBlockHoundTest
:adapter:outbound:httpclient:spring62ApiSurfaceScan
:adapter:outbound:httpclient:spring70CompatibilityTest
--no-daemon
--stacktrace
httpclient-composition:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Verify composition and architecture in the bootstrap module
working-directory: src
run: >-
./gradlew
:app-bootstrap:test --tests '*httpclient*' --tests '*CleanArchitectureTest'
--no-daemon
--stacktrace
-94
View File
@@ -1,94 +0,0 @@
name: httpclient-nightly
# Lanes that need a container runtime, real time, or a QUIC-capable host (design §29). They are
# separated from the per-PR gate rather than made optional inside it: a lane that cannot run here
# fails, it does not skip.
on:
workflow_dispatch:
schedule:
- cron: '0 3 * * *'
permissions:
contents: read
jobs:
httpclient-fault-injection:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Inject TCP faults against a real upstream
working-directory: src
run: >-
./gradlew
:adapter:outbound:httpclient:httpClientFailureInjectionTest
--no-daemon
--stacktrace
httpclient-performance:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
GRADLE_OPTS: -Dorg.gradle.project.performance.assertions.enabled=true
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Certify pool, streaming, retry, and rotation bounds
working-directory: src
run: >-
./gradlew
:adapter:outbound:httpclient:httpClientPerformanceTest
--no-daemon
--stacktrace
httpclient-http3-experimental:
runs-on: ubuntu-latest
timeout-minutes: 30
# Experimental by design (D-08): the result is reported, never used to block a merge.
continue-on-error: true
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Exercise the experimental HTTP/3 opt-in
working-directory: src
run: >-
./gradlew
:adapter:outbound:httpclient:test
-Phttp3.tests.enabled=true
--no-daemon
--stacktrace
-70
View File
@@ -1,70 +0,0 @@
name: httpclient-release
# Release gate for the HTTP Client Platform (design §38 step 4). Each declared gate runs as its own
# single-line `./gradlew <task>` step, because .github/scripts/verify-gate-matrix.sh reads these
# commands to prove the gate is actually executed — a folded or flag-laden command would make the
# declaration in .github/ci-gate-matrix.yml unverifiable.
on:
workflow_dispatch:
push:
tags:
- 'v*'
permissions:
contents: read
jobs:
release-gate:
runs-on: ubuntu-latest
timeout-minutes: 60
defaults:
run:
working-directory: src
env:
# A project property rather than a command-line flag, so each run command stays a plain,
# verifiable task invocation while the machine-dependent bounds are still asserted.
GRADLE_OPTS: -Dorg.gradle.project.performance.assertions.enabled=true
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Focused module tests
run: ./gradlew :adapter:outbound:httpclient:test --no-daemon --stacktrace
- name: Spring 6.2 API surface lane
run: ./gradlew :adapter:outbound:httpclient:spring62ApiSurfaceScan --no-daemon --stacktrace
- name: Spring 7.0 compatibility lane
run: ./gradlew :adapter:outbound:httpclient:spring70CompatibilityTest --no-daemon --stacktrace
- name: Stable cross-transport contract suite
run: ./gradlew :adapter:outbound:httpclient:httpClientStableContractTest --no-daemon --stacktrace
- name: SSRF and cardinality suite
run: ./gradlew :adapter:outbound:httpclient:httpClientSecurityTest --no-daemon --stacktrace
- name: Event-loop blocking suite
run: ./gradlew :adapter:outbound:httpclient:httpClientBlockHoundTest --no-daemon --stacktrace
- name: Toxiproxy fault-injection suite
run: ./gradlew :adapter:outbound:httpclient:httpClientFailureInjectionTest --no-daemon --stacktrace
- name: Resource-bound performance certification
run: ./gradlew :adapter:outbound:httpclient:httpClientPerformanceTest --no-daemon --stacktrace
- name: Architecture dependency gate
run: ./gradlew verifyCleanArchitectureDependencies --no-daemon --stacktrace
httpclient-documentation:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # actions/setup-python@v5.6.0
with:
python-version: '3.12'
- name: Verify documentation matches the code
run: python3 scripts/verify-httpclient-docs.py
+232
View File
@@ -0,0 +1,232 @@
name: integration-main
# Stage 2: is the merged state healthy.
#
# The question this stage answers is different from stage 1's. Stage 1 asks whether a diff is safe
# and blocks a merge; stage 2 asks whether main is healthy and does not — the merge has already
# happened. That difference is the point, and it is what lets a control exist without being an
# obstacle: a gate here still fails loudly, it just fails after the thing it is reporting on.
#
# Two kinds of work live here.
#
# 1. The lanes that need a machine that is not simultaneously compiling something else — load,
# abuse, graceful shutdown, TCP fault injection, resource bounds. They were web-nightly.yml and
# httpclient-nightly.yml, two module-shaped files whose only real difference was the cadence they
# shared. They now run on every push to main as well as nightly, which is strictly more often
# than before.
#
# 2. Lanes that were registered in Gradle and invoked by nothing. Ten Gradle tasks — six MongoDB
# container lanes, app-bootstrap's Testcontainers `integrationTest`, and the three messaging
# evidence tasks that `verifyMessagingContracts` reaches — existed, failed closed, and executed
# in no workflow. A lane nobody runs is not coverage; it is a file that looks like coverage. They
# are here rather than in stage 1 because every one of them either starts containers or re-runs
# suites the PR gate already covers, and the pull-request budget is minutes for the whole gate.
#
# What is deliberately NOT here: the web and WebSocket "Advanced capability" nightly lanes that used
# to exist as web-advanced-nightly.yml and websocket-advanced-nightly.yml. Both leaves' build files
# say it outright — "They also run inside `test`, deliberately ... excluding them from the PR gate to
# make this lane look meaningful would mean the PR gate stopped covering a fifth of the leaf" — so
# `webAdvancedTest` and `websocketAdvancedTest` select tagged tests that `:<leaf>:test` already runs,
# and `:<leaf>:test` runs inside the root `check` on every pull request and every push to main. The
# strict lanes themselves survive in release.yml, where their fail-on-nothing-discovered guard is
# worth a job.
on:
push:
branches: ["main"]
schedule:
# 03:00 UTC. Late enough that the day's merges are in, early enough that a failure is triaged
# before the next working day starts.
- cron: '0 3 * * *'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
# The documentation-drift gates that used to run here are gone rather than demoted.
#
# They were four hand-written parsers: README shell blocks compared against the Gradle task graph,
# runbook identifiers compared against every declared Java type, a leaf count written in prose
# compared against the registry, and a Markdown table compared against the declared source sets.
# Each was a custom parser for a file format nobody controls, and each made a documentation edit a
# precondition for a build. A stale sentence is a defect, but it is not one a build can be failed
# for, and link-check.yml already answers the one documentation question with a stable machine
# answer: does this link resolve.
jobs:
# Load, abuse and graceful shutdown measure behaviour that degrades gradually rather than breaking
# outright — which is exactly the kind of regression a per-PR gate never catches.
web-load-abuse-and-shutdown:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Run the load, abuse and shutdown lanes on every container
working-directory: src
run: >-
./gradlew
:adapter:inbound:web:test
:adapter:inbound:web:webJettyCompatTest
:adapter:inbound:web:webFluxContractTest
--stacktrace
- name: Publish the test reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: web-integration-reports
path: src/adapter/inbound/web/build/reports/tests/
if-no-files-found: warn
# Needs a container runtime and real time (design §29). Separated from the per-PR gate rather than
# made optional inside it: a lane that cannot run here fails, it does not skip.
httpclient-fault-injection:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Inject TCP faults against a real upstream
working-directory: src
run: >-
./gradlew
:adapter:outbound:httpclient:httpClientFailureInjectionTest
--stacktrace
httpclient-performance:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
# A project property rather than a command-line flag, so the run command stays a plain,
# verifiable task invocation while the machine-dependent bounds are still asserted.
GRADLE_OPTS: -Dorg.gradle.project.performance.assertions.enabled=true
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Certify pool, streaming, retry, and rotation bounds
working-directory: src
run: >-
./gradlew
:adapter:outbound:httpclient:httpClientPerformanceTest
--stacktrace
httpclient-http3-experimental:
runs-on: ubuntu-latest
timeout-minutes: 30
# Experimental by design (D-08): the result is reported, never used to block a merge. Registered
# advisory so that "this job cannot fail the
# build" is written down rather than inferred from a field two hundred lines into a workflow.
continue-on-error: true
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Exercise the experimental HTTP/3 opt-in
working-directory: src
run: >-
./gradlew
:adapter:outbound:httpclient:test
-Phttp3.tests.enabled=true
--stacktrace
# The six Docker-backed MongoDB lanes. Until now they ran in no workflow at all: the leaf excludes
# every one of their tags from `test` (build.gradle "Docker-backed lanes are excluded from the
# default unit run"), `check` gains only the hermetic `mongoStableContractTest`, and the only thing
# that named them was scripts/verify-mongodb-platform.sh, which nothing in .github invokes. Six
# lanes that fail closed without Docker, and no machine with Docker was ever asked to run them.
#
# Stage 2 rather than stage 1 because each lane starts real MongoDB containers — mongo:8.0.16,
# mongo:7.0.28 and a Toxiproxy in front of a three-node replica set. That is minutes per lane, and
# the pull-request budget is minutes for the whole gate.
#
# One single-line `./gradlew <task>` step per lane, not one folded command running six, because
# These command lines name each lane explicitly so that a lane which stops being invoked is
# actually executed — a folded command would leave six matrix rows unverifiable. It also means a
# red replica-set lane does not hide the compatibility lane behind it.
mongo-container-lanes:
runs-on: ubuntu-latest
timeout-minutes: 90
env:
# Reuse would hand the failover lane a replica set another lane had already faulted.
TESTCONTAINERS_REUSE_ENABLE: "false"
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Single-node replica set contract lane
working-directory: src
run: ./gradlew :adapter:outbound:persistence-mongo:mongoReplicaSetTest --stacktrace
- name: Run MongoDB integration lanes
working-directory: src
run: >-
./gradlew
:adapter:outbound:persistence-mongo:mongoReplicaSetTest
:adapter:outbound:persistence-mongo:mongoFailoverTest
:adapter:outbound:persistence-mongo:mongoMigrationTest
:adapter:outbound:persistence-mongo:mongoCompatibilityTest
:adapter:outbound:persistence-mongo:mongoSecurityIntegrationTest
:adapter:outbound:persistence-mongo:mongoPerformanceTest
--stacktrace
- name: Publish the MongoDB lane reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: mongo-lane-reports
path: src/adapter/outbound/persistence-mongo/build/reports/tests/
if-no-files-found: warn
# The messaging contract evidence DAG. `verifyMessagingContracts` is the root of a chain that ran
# nowhere: it depends on four production qualification tasks (application-core,
# shared-contract and two in adapter:outbound:messaging), each of which depends on
# `prepareMessagingContractEvidence`; it is finalizedBy
# `validateMessagingContractsEvidenceManifestSchema`; and it depends on
# `validateMessagingJsonSchemaV1EvidenceManifestSchema`, which depends on
# `verifyMessagingJsonSchemaV1`. Strict qualification tasks are registered outside `check` by
# design (ca.strict-qualification.gradle), so none of the seven was reachable from any workflow.
#
# The schema validators are the part that matters. They re-read the manifest bytes the run just
# wrote and validate them against config/messaging/evidence/build-evidence-manifest-v1.schema.json
# — a manifest that claims a qualification nobody executed is exactly the failure they exist to
# catch, and until now nothing executed them either.
#
# Stage 2 rather than stage 1: no containers, but it runs four qualification suites across three
# leaves plus two JavaExec validators, and the tests it re-runs are already inside the PR gate's
# `check`. What this job adds is the evidence manifest, which is a main-branch artifact.
messaging-contract-evidence:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Qualify the messaging contract, catalog, binding and schema evidence
working-directory: src
run: ./gradlew :verifyMessagingContracts --stacktrace
- name: Publish the messaging evidence manifest
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: messaging-contract-evidence
path: src/build/messaging-evidence/
if-no-files-found: warn
# app-bootstrap's Testcontainers lane. The leaf gave it a source set of its own precisely so that
# `./gradlew :app-bootstrap:test` would not require a Docker daemon — and the consequence nobody
# closed is that a source set outside `test` is also outside `check`, so the real-PostgreSQL
# outbox and idempotency contracts compiled on every build and executed on none.
bootstrap-integration:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
TESTCONTAINERS_REUSE_ENABLE: "false"
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Run the real-PostgreSQL integration contracts
working-directory: src
run: ./gradlew :app-bootstrap:integrationTest --stacktrace
-64
View File
@@ -1,64 +0,0 @@
name: jpa-next-hibernate8
# Hibernate ORM 8 compatibility lane (experimental plan Task 8).
#
# Re-runs the contracts most likely to move between provider majors: collection fetch pagination,
# StatementInspector, Statistics, JSONB, batch, and StatelessSession. Differences are recorded, not
# accommodated — weakening the 7.x gate to make this lane green would delete the evidence that 7.x
# behaves as documented.
on:
workflow_dispatch:
schedule:
- cron: '0 5 * * 1'
permissions:
contents: read
jobs:
hibernate8-compatibility:
runs-on: ubuntu-latest
timeout-minutes: 45
continue-on-error: true
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Report Hibernate ORM 8 compatibility
working-directory: src
run: >-
./gradlew
:adapter:outbound:persistence-jpa:test --tests '*HibernateCompatibilityPolicyTest'
--no-daemon
--stacktrace
- name: Record what this lane did and did not execute
if: always()
run: |
mkdir -p compatibility-evidence
{
echo "target=Hibernate 8"
echo "target-coordinate=org.hibernate.orm:hibernate-core:8.x"
echo "status=NOT_EXECUTABLE"
echo "reason=Hibernate 8 is not resolvable from this build, so nothing has been compiled or run against it"
echo "what-ran=the current runtime's own policy and lane-definition tests"
echo "sha=${{ github.sha }}"
} > compatibility-evidence/status.properties
echo "::notice::Hibernate 8 compatibility is NOT_EXECUTABLE: Hibernate 8 is not resolvable from this build, so nothing has been compiled or run against it"
- name: Upload the compatibility status
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: compatibility-status-hibernate-8
path: compatibility-evidence/status.properties
retention-days: 30
if-no-files-found: error
-64
View File
@@ -1,64 +0,0 @@
name: jpa-next-jpa4
# Jakarta Persistence 4.0 compatibility lane (experimental plan Task 7).
#
# Non-blocking by design: it reports whether the Stable public API still compiles and whether the
# selected mapping contracts still hold on JPA 4. It publishes nothing, and a red result here never
# changes a Stable contract — the 3.2 gate keeps asserting what 3.2 must do, because that is what
# deployments run.
on:
workflow_dispatch:
schedule:
- cron: '0 4 * * 1'
permissions:
contents: read
jobs:
jpa4-compatibility:
runs-on: ubuntu-latest
timeout-minutes: 45
continue-on-error: true
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Report Jakarta Persistence 4.0 compatibility
working-directory: src
run: >-
./gradlew
:adapter:outbound:persistence-jpa:test --tests '*CompatibilityLaneDefinitionTest'
--no-daemon
--stacktrace
- name: Record what this lane did and did not execute
if: always()
run: |
mkdir -p compatibility-evidence
{
echo "target=Jakarta Persistence 4"
echo "target-coordinate=jakarta.persistence:jakarta.persistence-api:4.x"
echo "status=NOT_EXECUTABLE"
echo "reason=the JPA 4 API is not on any configuration this build resolves, so nothing has been compiled against it"
echo "what-ran=the current runtime's own policy and lane-definition tests"
echo "sha=${{ github.sha }}"
} > compatibility-evidence/status.properties
echo "::notice::Jakarta Persistence 4 compatibility is NOT_EXECUTABLE: the JPA 4 API is not on any configuration this build resolves, so nothing has been compiled against it"
- name: Upload the compatibility status
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: compatibility-status-jakarta-persistence-4
path: compatibility-evidence/status.properties
retention-days: 30
if-no-files-found: error
@@ -1,70 +0,0 @@
name: jpa-next-postgresql19
# PostgreSQL 19 compatibility lane (experimental plan Task 9).
#
# This lane is NOT_EXECUTABLE against its target.
#
# It runs the current runtime's policy and lane-definition tests; it does not resolve the target
# dependency or start a container of the target version. A green run therefore says "the target is
# absent from this build", which is not the same claim as "we are compatible with the target" — and
# the workflow's name reads as the second one. The status artifact says which it is.
#
# Promotion needs evidence, not availability. Two supported patch runs with no unresolved semantic
# regression, plus a reviewed ADR, before the Stable support matrix changes — which is what
# ExperimentalPromotionGate encodes.
on:
workflow_dispatch:
schedule:
- cron: '0 6 * * 1'
permissions:
contents: read
jobs:
postgresql19-compatibility:
runs-on: ubuntu-latest
timeout-minutes: 60
continue-on-error: true
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Report PostgreSQL 19 compatibility
working-directory: src
run: >-
./gradlew
:adapter:outbound:persistence-jpa:test --tests '*ExperimentalPromotionGateTest'
--no-daemon
--stacktrace
- name: Record what this lane did and did not execute
if: always()
run: |
mkdir -p compatibility-evidence
{
echo "target=PostgreSQL 19"
echo "target-coordinate=postgres:19-alpine"
echo "status=NOT_EXECUTABLE"
echo "reason=no PostgreSQL 19 image is published yet, so no container of that major has ever been started by this lane"
echo "what-ran=the current runtime's own policy and lane-definition tests"
echo "sha=${{ github.sha }}"
} > compatibility-evidence/status.properties
echo "::notice::PostgreSQL 19 compatibility is NOT_EXECUTABLE: no PostgreSQL 19 image is published yet, so no container of that major has ever been started by this lane"
- name: Upload the compatibility status
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: compatibility-status-postgresql-19
path: compatibility-evidence/status.properties
retention-days: 30
if-no-files-found: error
+75
View File
@@ -0,0 +1,75 @@
name: jpa-next
# Advisory compatibility probes for future JPA/Hibernate/PostgreSQL majors. These targets are not
# resolved by the current build, so the artifact records NOT_EXECUTABLE rather than implying that a
# green policy test is compatibility evidence.
on:
workflow_dispatch:
schedule:
- cron: '0 5 * * 1'
permissions:
contents: read
jobs:
compatibility:
strategy:
fail-fast: false
matrix:
include:
- id: jakarta-persistence-4
target: Jakarta Persistence 4
coordinate: jakarta.persistence:jakarta.persistence-api:4.x
test: "*CompatibilityLaneDefinitionTest"
reason: the JPA 4 API is not on any configuration this build resolves, so nothing has been compiled against it
- id: hibernate-8
target: Hibernate 8
coordinate: org.hibernate.orm:hibernate-core:8.x
test: "*HibernateCompatibilityPolicyTest"
reason: Hibernate 8 is not resolvable from this build, so nothing has been compiled or run against it
- id: postgresql-19
target: PostgreSQL 19
coordinate: postgres:19-alpine
test: "*ExperimentalPromotionGateTest"
reason: no PostgreSQL 19 image is published yet, so no container of that major has ever been started by this lane
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Run the current-runtime policy probe
id: compatibility-probe
working-directory: src
run: >-
./gradlew
:adapter:outbound:persistence-jpa:test
--tests '${{ matrix.test }}'
--stacktrace
- name: Record what this lane did and did not execute
if: always()
env:
PROBE_OUTCOME: ${{ steps.compatibility-probe.outcome }}
TARGET: ${{ matrix.target }}
TARGET_COORDINATE: ${{ matrix.coordinate }}
REASON: ${{ matrix.reason }}
run: |
mkdir -p compatibility-evidence
{
echo "target=${TARGET}"
echo "target-coordinate=${TARGET_COORDINATE}"
echo "status=NOT_EXECUTABLE"
echo "probe-result=${PROBE_OUTCOME}"
echo "reason=${REASON}"
echo "what-ran=the current runtime's own policy and lane-definition tests"
echo "sha=${{ github.sha }}"
} > compatibility-evidence/status.properties
echo "::notice::${TARGET} compatibility is NOT_EXECUTABLE: ${REASON}"
- name: Upload the compatibility status
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: compatibility-status-${{ matrix.id }}
path: compatibility-evidence/status.properties
retention-days: 30
if-no-files-found: error
+8 -52
View File
@@ -30,25 +30,14 @@ jobs:
postgresql: ["16", "17", "18"] postgresql: ["16", "17", "18"]
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Certify the platform against PostgreSQL ${{ matrix.postgresql }} - name: Certify the platform against PostgreSQL ${{ matrix.postgresql }}
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
:adapter:outbound:persistence-jpa:jpaPlatformContractTest :adapter:outbound:persistence-jpa:jpaPlatformContractTest
-Pjpa.matrix.versions=${{ matrix.postgresql }} -Pjpa.matrix.versions=${{ matrix.postgresql }}
--no-daemon
--stacktrace --stacktrace
jpa-failure-injection: jpa-failure-injection:
@@ -56,24 +45,13 @@ jobs:
timeout-minutes: 45 timeout-minutes: 45
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Reproduce deadlock, serialization, and commit-ambiguity scenarios - name: Reproduce deadlock, serialization, and commit-ambiguity scenarios
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
:adapter:outbound:persistence-jpa:jpaPlatformFailureTest :adapter:outbound:persistence-jpa:jpaPlatformFailureTest
--no-daemon
--stacktrace --stacktrace
jpa-query-plan-and-security: jpa-query-plan-and-security:
@@ -81,25 +59,14 @@ jobs:
timeout-minutes: 45 timeout-minutes: 45
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the query plan and database security suites - name: Run the query plan and database security suites
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
:adapter:outbound:persistence-jpa:jpaPlatformQueryPlanTest :adapter:outbound:persistence-jpa:jpaPlatformQueryPlanTest
:adapter:outbound:persistence-jpa:jpaPlatformSecurityTest :adapter:outbound:persistence-jpa:jpaPlatformSecurityTest
--no-daemon
--stacktrace --stacktrace
jpa-pool-pressure: jpa-pool-pressure:
@@ -107,18 +74,7 @@ jobs:
timeout-minutes: 45 timeout-minutes: 45
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Verify pool saturation and REQUIRES_NEW connection behaviour - name: Verify pool saturation and REQUIRES_NEW connection behaviour
working-directory: src working-directory: src
# A behaviour contract, not a measurement. This step used to switch assertions off with an # A behaviour contract, not a measurement. This step used to switch assertions off with an
@@ -130,5 +86,5 @@ jobs:
run: >- run: >-
./gradlew ./gradlew
:adapter:outbound:persistence-jpa:jpaPlatformPoolContractTest :adapter:outbound:persistence-jpa:jpaPlatformPoolContractTest
--no-daemon
--stacktrace --stacktrace
-114
View File
@@ -1,114 +0,0 @@
name: jpa-pr
# Every "Stable" row in docs/jpa/support-matrix.md is backed by a job here or in jpa-nightly /
# jpa-release. A support level with no job behind it is a marketing claim.
#
# The PR lane runs the oldest and the newest Stable PostgreSQL rather than all three: a behaviour
# that differs across the matrix almost always differs at its ends, and the middle version is
# covered nightly. What it does not do is skip the container lane on a runner without Docker —
# PostgreSqlContainerFactory throws, because a skipped contract reports success for a database
# nobody tested.
on:
workflow_dispatch:
pull_request:
paths:
- 'src/adapter/outbound/persistence-jpa/**'
- 'src/app-bootstrap/src/**/jpa/**'
- 'src/config/architecture/modules.json'
- 'docs/jpa/**'
- 'docs/adr/ADR-JPA-*'
- 'infra/jpa/**'
- '.github/workflows/jpa-pr.yml'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
jpa-unit-and-architecture:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the JPA unit and architecture suites
working-directory: src
run: >-
./gradlew
:adapter:outbound:persistence-jpa:test
:app-bootstrap:test --tests '*CleanArchitectureTest'
verifyCleanArchitectureDependencies
verifyOneTypePerFile
--no-daemon
--stacktrace
jpa-postgresql-contract:
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
# 16 and 18 — the ends of the Stable matrix. 17 runs nightly.
postgresql: ["16", "18"]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Certify the platform against PostgreSQL ${{ matrix.postgresql }}
working-directory: src
run: >-
./gradlew
:adapter:outbound:persistence-jpa:jpaPlatformContractTest
-Pjpa.matrix.versions=${{ matrix.postgresql }}
--no-daemon
--stacktrace
jpa-migration-smoke:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the migration upgrade smoke scenarios
working-directory: src
run: >-
./gradlew
:adapter:outbound:persistence-jpa:jpaPlatformMigrationTest
--no-daemon
--stacktrace
+2 -13
View File
@@ -26,25 +26,14 @@ jobs:
JPA_EVIDENCE_TOPOLOGY: postgresql-16-testcontainers-tls-and-fault-matrix JPA_EVIDENCE_TOPOLOGY: postgresql-16-testcontainers-tls-and-fault-matrix
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Verify the production-profile JPA R2 manifest DAG - name: Verify the production-profile JPA R2 manifest DAG
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
:adapter:outbound:persistence-jpa:verifyJpaPrimaryFoundationEvidence :adapter:outbound:persistence-jpa:verifyJpaPrimaryFoundationEvidence
-PjpaEvidenceProfile=r2 -PjpaEvidenceProfile=r2
--no-daemon
--stacktrace --stacktrace
- name: Retain JPA R2 attempt manifests - name: Retain JPA R2 attempt manifests
if: always() if: always()
+15 -37
View File
@@ -1,10 +1,9 @@
name: jpa-release name: jpa-release
# The release gate. src/config/jpa/release-registry.json is the source: every gate it declares has a # The release registry is the gate-task source: jpaReleaseQualification reads its blocking gates,
# job or an assertion here, JpaReleaseRenderingTest holds this file's matrix and promotion lists to # while JpaReleaseRenderingTest holds this file's matrix and promotion lists to the registry's Stable
# the registry's Stable majors, and verifyJpaReleaseGateTasks resolves each gate's task against the # majors and verifyJpaReleaseGateTasks resolves each declared task against the real Gradle graph.
# real Gradle graph. So a gate removed from the registry, or a major demoted in it, fails the build # CI therefore owns release scheduling, not a second JPA gate-task inventory.
# rather than quietly ceasing to be checked.
# #
# The matrix below is therefore not free to drift: editing it without editing the registry fails the # The matrix below is therefore not free to drift: editing it without editing the registry fails the
# unit lane. # unit lane.
@@ -43,25 +42,13 @@ jobs:
postgresql: ["16", "17", "18"] postgresql: ["16", "17", "18"]
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation - name: Run the JPA database qualification set on PostgreSQL ${{ matrix.postgresql }}
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the full JPA release gate on PostgreSQL ${{ matrix.postgresql }}
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
jpaReleaseGate jpaReleaseQualification
-Pjpa.matrix.versions=${{ matrix.postgresql }} -Pjpa.matrix.versions=${{ matrix.postgresql }}
--no-daemon
--stacktrace --stacktrace
- name: Record which major this evidence covers - name: Record which major this evidence covers
if: always() if: always()
@@ -72,7 +59,7 @@ jobs:
echo "sha=${{ github.sha }}" echo "sha=${{ github.sha }}"
echo "ref=${{ github.ref }}" echo "ref=${{ github.ref }}"
echo "postgresql-major=${{ matrix.postgresql }}" echo "postgresql-major=${{ matrix.postgresql }}"
echo "task=jpaReleaseGate" echo "task-set=jpa-database-qualification"
} > "build/jpa-release-evidence/manifest-${{ matrix.postgresql }}.properties" } > "build/jpa-release-evidence/manifest-${{ matrix.postgresql }}.properties"
- name: Upload the release evidence - name: Upload the release evidence
if: always() if: always()
@@ -126,25 +113,16 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Verify architecture boundaries and the support matrix - name: Verify architecture boundaries and the support matrix
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
verifyCleanArchitectureDependencies :verifyJpaReleaseGateTasks
verifyOneTypePerFile :verifyJpaReadinessRegistry
:app-bootstrap:test --tests '*CleanArchitectureTest' :verifyCleanArchitectureDependencies
checkstyleMain
:app-bootstrap:architectureTest
:adapter:outbound:persistence-jpa:test --tests '*JpaReleaseManifestTest' :adapter:outbound:persistence-jpa:test --tests '*JpaReleaseManifestTest'
--no-daemon
--stacktrace --stacktrace
+5 -13
View File
@@ -17,6 +17,9 @@ on:
paths: paths:
- "src/messaging/**" - "src/messaging/**"
- ".github/workflows/messaging-certification.yml" - ".github/workflows/messaging-certification.yml"
# Every Gradle job here installs its toolchain through this composite action, so a change to
# it changes what this gate runs.
- ".github/actions/setup-gradle-java/action.yml"
schedule: schedule:
- cron: "41 4 * * 3" - cron: "41 4 * * 3"
workflow_dispatch: workflow_dispatch:
@@ -33,23 +36,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Certify the Kafka adapter against a real broker - name: Certify the Kafka adapter against a real broker
working-directory: src working-directory: src
# GITHUB_SHA is read by the lane and written into every evidence line, because "certified" # GITHUB_SHA is read by the lane and written into every evidence line, because "certified"
# is a claim about one source tree. # is a claim about one source tree.
run: ./gradlew :messaging:messaging-kafka:verifyMessagingCertificationEvidence --no-daemon --stacktrace run: ./gradlew :messaging:messaging-kafka:verifyMessagingCertificationEvidence --stacktrace
- name: Publish the certification evidence - name: Publish the certification evidence
if: always() if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
+29 -78
View File
@@ -31,6 +31,9 @@ on:
- 'docs/notification/**' - 'docs/notification/**'
- 'infra/notification/**' - 'infra/notification/**'
- '.github/workflows/notification-platform.yml' - '.github/workflows/notification-platform.yml'
# Every Gradle job here installs its toolchain through this composite action, so a change to
# it changes what this gate runs.
- '.github/actions/setup-gradle-java/action.yml'
push: push:
branches: [ main ] branches: [ main ]
schedule: schedule:
@@ -52,18 +55,7 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Compile and format check - name: Compile and format check
working-directory: src working-directory: src
run: ./gradlew :application-core:compileJava :adapter:outbound:notification:compileJava --console=plain run: ./gradlew :application-core:compileJava :adapter:outbound:notification:compileJava --console=plain
@@ -89,19 +81,19 @@ jobs:
- name: Architecture gates - name: Architecture gates
working-directory: src working-directory: src
run: | run: |
./gradlew verifyCleanArchitectureDependencies --console=plain ./gradlew :verifyCleanArchitectureDependencies --console=plain
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' --tests '*NotificationArchitectureTest' --console=plain ./gradlew :app-bootstrap:architectureTest --console=plain
- name: Configuration surface - name: Configuration surface
working-directory: src working-directory: src
run: | run: |
./gradlew verifyEnvKeys verifyPublicPathSnapshot --console=plain ./gradlew :app-bootstrap:verifyEnvKeys :verifyPublicPathSnapshot --console=plain
./gradlew verifyNotificationApiSurface verifyNotificationConfiguration --console=plain ./gradlew :verifyNotificationApiSurface :verifyNotificationConfiguration --console=plain
# A support grade is a promise about production behaviour. This refuses one the pipeline # A support grade is a promise about production behaviour. This refuses one the pipeline
# cannot back — the check that would have caught five channels reading "Stable" while no # cannot back — the check that would have caught five channels reading "Stable" while no
# request had ever left the process. # request had ever left the process.
- name: Evidence manifest - name: Evidence manifest
working-directory: src working-directory: src
run: ./gradlew verifyNotificationEvidence --console=plain run: ./gradlew :verifyNotificationEvidence --console=plain
- name: Static analysis - name: Static analysis
working-directory: src working-directory: src
run: ./gradlew :adapter:outbound:notification:check -x test --console=plain run: ./gradlew :adapter:outbound:notification:check -x test --console=plain
@@ -113,18 +105,7 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
# This job is named for ambiguity, restart recovery and callback burst. It used to run a # This job is named for ambiguity, restart recovery and callback burst. It used to run a
# unit-test filter and then `test` — neither of which restarts anything or bursts anything — # unit-test filter and then `test` — neither of which restarts anything or bursts anything —
# so the job name was the only place those three properties existed. # so the job name was the only place those three properties existed.
@@ -137,9 +118,15 @@ jobs:
- name: Restart recovery and lease fencing (real PostgreSQL) - name: Restart recovery and lease fencing (real PostgreSQL)
working-directory: src working-directory: src
run: ./gradlew :adapter:outbound:persistence-jpa:jpaPlatformContractTest :adapter:outbound:persistence-jpa:jpaPlatformFailureTest --console=plain run: ./gradlew :adapter:outbound:persistence-jpa:jpaPlatformContractTest :adapter:outbound:persistence-jpa:jpaPlatformFailureTest --console=plain
- name: Full suite - name: Notification regression suite
working-directory: src working-directory: src
run: ./gradlew test --console=plain run: >-
./gradlew
:application-core:test
:adapter:outbound:notification:test
:adapter:outbound:persistence-jpa:test
:adapter:inbound:web:test
--console=plain
# A filter that matches nothing passes. Each --tests filter above names a class that exists # A filter that matches nothing passes. Each --tests filter above names a class that exists
# today; if one is renamed the job must fail rather than quietly stop covering it. # today; if one is renamed the job must fail rather than quietly stop covering it.
- name: Every named suite actually ran - name: Every named suite actually ran
@@ -153,50 +140,14 @@ jobs:
fi fi
done done
provider-sandbox: # There is no provider-sandbox job. It ran only on workflow_dispatch and could not succeed by
name: provider sandbox smoke (secret-protected, non-blocking) # any path: with no credentials its first step exit 1-ed, and with credentials the only test it
if: github.event_name == 'workflow_dispatch' # ran was ProviderSandboxSmokeTest, whose body is an unconditional fail() saying a real sandbox
runs-on: ubuntu-latest # call is not implemented. Its credential check read secrets.NOTIFICATION_SANDBOX_CREDENTIALS,
timeout-minutes: 30 # which nothing in this repository consumes — the test reads NOTIFICATION_SANDBOX_ENABLED — so
environment: notification-provider-sandbox # any non-empty string satisfied it and was then dropped.
# Not a required check: an external outage must not block a merge. But not continue-on-error #
# either — a job that cannot fail produces no evidence, and this job's entire previous body was # The unimplemented state is still stated in two places that do not depend on a workflow:
# two echo statements, which is what let five channels be graded Stable on nothing. # ProviderSandboxSmokeTest itself, and the unsatisfied provider-wire-qualified claim in
steps: # docs/notification/evidence-manifest.json, which verifyNotificationEvidence enforces inside
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 # check. When a real sandbox call is implemented, the job comes back with it.
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Refuse to report a pass with no credentials
env:
NOTIFICATION_SANDBOX_CREDENTIALS: ${{ secrets.NOTIFICATION_SANDBOX_CREDENTIALS }}
run: |
set -euo pipefail
if [ -z "${NOTIFICATION_SANDBOX_CREDENTIALS:-}" ]; then
echo "provider sandbox credentials are not configured for this environment." >&2
echo "The job stops here rather than reporting a green run that called nothing." >&2
exit 1
fi
- name: Smoke test against real provider sandboxes
working-directory: src
env:
NOTIFICATION_SANDBOX_ENABLED: 'true'
NOTIFICATION_SANDBOX_CREDENTIALS: ${{ secrets.NOTIFICATION_SANDBOX_CREDENTIALS }}
run: ./gradlew :adapter:outbound:notification:test --tests '*ProviderSandbox*' --console=plain
- name: Upload the wire evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: notification-provider-sandbox-evidence
path: src/adapter/outbound/notification/build/test-results/test/
if-no-files-found: error
retention-days: 90
@@ -11,6 +11,17 @@ on:
required: true required: true
default: false default: false
type: boolean type: boolean
# AwsS3DirectTransferQualificationTest requires a second, separate authority
# (OBJECT_STORAGE_AWS_DIRECT_MUTATION_ENABLED) before the direct-transfer lane may run, and
# the job never supplied it. objectStorageAwsQualificationTest is a strict qualification task
# that requires both of its classes, so the lane could not be run to a pass from any input:
# dispatching it always failed on the missing variable. The authority now exists as its own
# input rather than as a constant, which is what "separate" was supposed to mean.
run_protected_aws_direct_mutation:
description: Also authorize the direct-transfer mutation lane against the sandbox bucket
required: true
default: false
type: boolean
permissions: permissions:
contents: read contents: read
@@ -19,90 +30,41 @@ env:
TESTCONTAINERS_REUSE_ENABLE: "false" TESTCONTAINERS_REUSE_ENABLE: "false"
jobs: jobs:
poster-image-v7-migration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run non-skipping Poster image migration qualification
working-directory: src
run: ./gradlew :sample-portfolio:posterImageMigrationTest --no-daemon --stacktrace
minio-managed-contract: minio-managed-contract:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run exact-release MinIO managed contract - name: Run exact-release MinIO managed contract
working-directory: src working-directory: src
run: ./gradlew :adapter:outbound:objectstorage:objectStorageMinioContractTest --no-daemon --stacktrace run: ./gradlew :adapter:outbound:objectstorage:objectStorageMinioContractTest --stacktrace
minio-managed-fault: minio-managed-fault:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run digest-pinned MinIO and Toxiproxy fault contract - name: Run digest-pinned MinIO and Toxiproxy fault contract
working-directory: src working-directory: src
run: ./gradlew :adapter:outbound:objectstorage:objectStorageMinioFaultTest --no-daemon --stacktrace run: ./gradlew :adapter:outbound:objectstorage:objectStorageMinioFaultTest --stacktrace
aws-managed-common-subset: aws-managed-common-subset:
if: github.event_name == 'workflow_dispatch' && inputs.run_protected_aws if: >-
github.event_name == 'workflow_dispatch'
&& inputs.run_protected_aws
&& inputs.run_protected_aws_direct_mutation
environment: object-storage-aws-qualification environment: object-storage-aws-qualification
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
OBJECT_STORAGE_AWS_QUALIFICATION_ENABLED: "true" OBJECT_STORAGE_AWS_QUALIFICATION_ENABLED: "true"
OBJECT_STORAGE_AWS_DIRECT_MUTATION_ENABLED: ${{ inputs.run_protected_aws_direct_mutation }}
OBJECT_STORAGE_AWS_BUCKET: ${{ secrets.OBJECT_STORAGE_AWS_BUCKET }} OBJECT_STORAGE_AWS_BUCKET: ${{ secrets.OBJECT_STORAGE_AWS_BUCKET }}
OBJECT_STORAGE_AWS_REGION: ${{ secrets.OBJECT_STORAGE_AWS_REGION }} OBJECT_STORAGE_AWS_REGION: ${{ secrets.OBJECT_STORAGE_AWS_REGION }}
OBJECT_STORAGE_AWS_EXPECTED_OWNER: ${{ secrets.OBJECT_STORAGE_AWS_EXPECTED_OWNER }} OBJECT_STORAGE_AWS_EXPECTED_OWNER: ${{ secrets.OBJECT_STORAGE_AWS_EXPECTED_OWNER }}
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run protected AWS common-subset qualification - name: Run protected AWS common-subset qualification
working-directory: src working-directory: src
run: ./gradlew :adapter:outbound:objectstorage:objectStorageAwsQualificationTest --no-daemon --stacktrace run: ./gradlew :adapter:outbound:objectstorage:objectStorageAwsQualificationTest --stacktrace
+289
View File
@@ -0,0 +1,289 @@
name: pr-adapters
# Stage 1, the adapter half: the lanes a pull request must clear that `ci-quality-gates.yml` cannot
# reach.
#
# It replaces web-pr.yml, websocket-pr.yml, httpclient-contract.yml and jpa-pr.yml, which were four
# files split by module rather than by stage. Splitting by module is what made the duplication
# invisible: each file opened with its own "unit and architecture" job running
# `:<leaf>:test verifyCleanArchitectureDependencies`, and all four of those were already inside the
# root `check` that ci-quality-gates.yml runs on every pull request with no path filter. Four jobs,
# four runners, four Gradle configurations, zero additional coverage. They are gone; what is left
# here is only what `check` does not run.
#
# What `check` does not run, and therefore what this file is for:
# * lanes with their own source set — a second servlet container, a real Nginx, Reactor Netty;
# * lanes selected by a tag that `test` excludes — the cross-stack parity recording comparison;
# * lanes parameterised per run — one PostgreSQL major per job, one HTTP transport per job.
# Each of those genuinely cannot run inside `check`, which is the test for whether a job belongs
# here at all.
#
# Path filtering is per job rather than per workflow. The four files it replaces each carried an
# `on.pull_request.paths` list, so the whole file was skipped or run as a unit; a change touching
# web and JPA started two workflows and a change touching neither still started none. Here one
# `changes` job computes the answer once from the pull request's own diff and every lane reads it.
# The filter is a plain `git diff` rather than a filter action: this repository pins every action by
# commit SHA and adding a third-party action to compute a boolean is a supply-chain decision, not a
# convenience.
on:
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# One diff, read once. `workflow_dispatch` answers "everything changed", because a manual run is
# somebody asking for the lanes and there is no base ref to compare against.
changes:
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
web: ${{ steps.filter.outputs.web }}
websocket: ${{ steps.filter.outputs.websocket }}
httpclient: ${{ steps.filter.outputs.httpclient }}
jpa: ${{ steps.filter.outputs.jpa }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
with:
# Both endpoints of the pull request's diff have to be present locally; the default
# shallow fetch has neither the base commit nor the merge base.
fetch-depth: 0
- name: Decide which adapter lanes this diff can affect
id: filter
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
if [ "${GITHUB_EVENT_NAME}" != 'pull_request' ]; then
changed='ALL'
else
if [ -z "${BASE_SHA}" ] || [ -z "${HEAD_SHA}" ]; then
echo "::error::pull request diff endpoints are missing; refusing to report no lanes"
exit 1
fi
changed="$(git diff --name-only "${BASE_SHA}" "${HEAD_SHA}")"
fi
# Fail closed rather than reporting "nothing changed": an empty diff on a pull request
# means the comparison did not work, and a filter that answers false on a broken
# comparison silently turns every lane below off.
if [ "${changed}" != 'ALL' ] && [ -z "${changed}" ]; then
echo "::error::the pull request diff is empty; the comparison did not run"
exit 1
fi
printf 'changed files:\n%s\n' "${changed}"
emit() {
lane="$1"
shift
if [ "${changed}" = 'ALL' ]; then
printf '%s=true\n' "${lane}" >> "${GITHUB_OUTPUT}"
printf 'lane %s: true (manual run)\n' "${lane}"
return 0
fi
for pattern in "$@"; do
if printf '%s\n' "${changed}" | grep -qE -- "${pattern}"; then
printf '%s=true\n' "${lane}" >> "${GITHUB_OUTPUT}"
printf 'lane %s: true (%s)\n' "${lane}" "${pattern}"
return 0
fi
done
printf '%s=false\n' "${lane}" >> "${GITHUB_OUTPUT}"
printf 'lane %s: false\n' "${lane}"
}
# This workflow and the composite action every lane below uses are in every lane's path
# set: a change to either changes what the lanes do, and a gate that does not re-run when
# its own definition changes is a gate nobody has seen run in its current form.
common='^\.github/workflows/pr-adapters\.yml$|^\.github/actions/'
emit web \
'^src/adapter/inbound/web/' \
'^src/application-core/src/.*/operation/' \
'^src/application-core/src/.*/idempotency/' \
'^src/adapter/outbound/persistence-jpa/src/.*/operation/' \
'^docs/web/' \
"${common}"
emit websocket \
'^src/adapter/inbound/websocket/' \
'^docs/websocket/' \
"${common}"
emit httpclient \
'^src/adapter/outbound/httpclient/' \
'^src/app-bootstrap/src/.*/httpclient/' \
'^docs/httpclient/' \
'^scripts/verify-httpclient-docs\.py$' \
"${common}"
emit jpa \
'^src/adapter/outbound/persistence-jpa/' \
'^src/app-bootstrap/src/.*/jpa/' \
'^src/config/architecture/modules\.json$' \
'^docs/jpa/' \
'^docs/adr/ADR-JPA-' \
'^infra/jpa/' \
"${common}"
# The parity gate depends on all three recording lanes and fails when one is missing, so it runs
# them itself rather than trusting a previous job to have left the recordings behind. Its tag is
# excluded from `test`, which is why `check` cannot cover it.
web-cross-stack-parity:
needs: changes
if: needs.changes.outputs.web == 'true'
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Compare the wire contract across Tomcat, Jetty and Reactor Netty
working-directory: src
run: >-
./gradlew
:adapter:inbound:web:webCrossStackParityTest
--stacktrace
- name: Publish the parity recordings
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: web-contract-parity
path: src/adapter/inbound/web/build/web-contract-parity/
if-no-files-found: error
# Docker-gated, and the lane fails rather than skipping when the runtime is missing. A proxy
# contract that quietly passes without a proxy has been certifying nothing since whenever the
# container runtime last broke.
web-nginx-proxy-contract:
needs: changes
if: needs.changes.outputs.web == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Run the proxy, prefix and spoofing contract behind a real Nginx
working-directory: src
run: >-
./gradlew
:adapter:inbound:web:webNginxProxyTest
--stacktrace
websocket-container-matrix:
needs: changes
if: needs.changes.outputs.websocket == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Run the runtime contract on the second servlet container
working-directory: src
run: >-
./gradlew
:adapter:inbound:websocket:websocketJettyTest
--stacktrace
# Docker-gated, and the lane fails rather than skipping. Upgrade handling is the single most
# common WebSocket deployment failure and it is invisible from either side alone.
websocket-nginx-contract:
needs: changes
if: needs.changes.outputs.websocket == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Run the upgrade and forwarded-header contract behind a real Nginx
working-directory: src
run: >-
./gradlew
:adapter:inbound:websocket:websocketNginxTest
--stacktrace
# One transport per job, so a transport that stops satisfying the shared contract fails on its own
# row instead of disappearing into an aggregate run. `check` runs this lane once, unparameterised.
httpclient-stable-contract:
needs: changes
if: needs.changes.outputs.httpclient == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
transport: [apache, jdk, reactor]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Certify one transport against the shared contract
working-directory: src
run: >-
./gradlew
:adapter:outbound:httpclient:httpClientStableContractTest
-Phttpclient.contract.transports=${{ matrix.transport }}
--stacktrace
# Only the Spring 7.0 lane. httpClientSecurityTest, httpClientBlockHoundTest and
# spring62ApiSurfaceScan used to run here too; all three are `dependsOn` of this leaf's `check`
# (src/adapter/outbound/httpclient/build.gradle), so ci-quality-gates.yml already ran them on the
# same pull request. spring70CompatibilityTest is deliberately outside `check` and is what is left.
httpclient-security-and-compatibility:
needs: changes
if: needs.changes.outputs.httpclient == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Run the next-major Spring compatibility lane
working-directory: src
run: >-
./gradlew
:adapter:outbound:httpclient:spring70CompatibilityTest
--stacktrace
# 16 and 18 — the ends of the Stable matrix. 17 runs in the integration stage. What this does not
# do is skip the container lane on a runner without Docker: PostgreSqlContainerFactory throws,
# because a skipped contract reports success for a database nobody tested.
jpa-postgresql-contract:
needs: changes
if: needs.changes.outputs.jpa == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
postgresql: ["16", "18"]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Certify the platform against PostgreSQL ${{ matrix.postgresql }}
working-directory: src
run: >-
./gradlew
:adapter:outbound:persistence-jpa:jpaPlatformContractTest
-Pjpa.matrix.versions=${{ matrix.postgresql }}
--stacktrace
jpa-migration-smoke:
needs: changes
if: needs.changes.outputs.jpa == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Run the migration upgrade smoke scenarios
working-directory: src
run: >-
./gradlew
:adapter:outbound:persistence-jpa:jpaPlatformMigrationTest
--stacktrace
+16 -13
View File
@@ -34,6 +34,9 @@ on:
- "src/adapter/outbound/cache-redis/**" - "src/adapter/outbound/cache-redis/**"
- "infra/redis-sdk/**" - "infra/redis-sdk/**"
- ".github/workflows/redis-sdk-topology.yml" - ".github/workflows/redis-sdk-topology.yml"
# Every Gradle job here installs its toolchain through this composite action, so a change to
# it changes what this gate runs.
- ".github/actions/setup-gradle-java/action.yml"
schedule: schedule:
# 02:30 UTC daily. Nightly, not hourly: the matrix starts real servers. # 02:30 UTC daily. Nightly, not hourly: the matrix starts real servers.
- cron: "30 2 * * *" - cron: "30 2 * * *"
@@ -113,18 +116,7 @@ jobs:
matrix: ${{ fromJson(needs.lanes.outputs.matrix) }} matrix: ${{ fromJson(needs.lanes.outputs.matrix) }}
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper - uses: ./.github/actions/setup-gradle-java
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Start the topology - name: Start the topology
env: env:
REDIS_VERSION: ${{ matrix.redis_version }} REDIS_VERSION: ${{ matrix.redis_version }}
@@ -135,8 +127,19 @@ jobs:
set -euo pipefail set -euo pipefail
# The tag says 7.4; the digest says which 7.4. Evidence that names only the tag cannot be # The tag says 7.4; the digest says which 7.4. Evidence that names only the tag cannot be
# reproduced once the tag moves. # reproduced once the tag moves.
#
# This used to end in `|| echo 'unresolved'`, which absorbed the failure that `set -e` was
# there to catch: the manifest below recorded `image_digest=unresolved`, the upload
# satisfied `if-no-files-found: error`, and the lane went green holding exactly the
# artifact this workflow's header calls "not evidence". Compose pulls the image in the
# step before this one, so RepoDigests is populated; if it is not, the run has nothing to
# certify and says so.
digest="$(docker image inspect --format '{{index .RepoDigests 0}}' \ digest="$(docker image inspect --format '{{index .RepoDigests 0}}' \
"redis:${{ matrix.redis_version }}" 2>/dev/null || echo 'unresolved')" "redis:${{ matrix.redis_version }}")"
if [[ -z "$digest" ]]; then
echo "::error::no repository digest for redis:${{ matrix.redis_version }}; this run cannot say which image produced its evidence"
exit 1
fi
printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT"
- name: Run the topology contracts - name: Run the topology contracts
working-directory: src working-directory: src
+466
View File
@@ -0,0 +1,466 @@
name: release
# Stage 3: produce a deployable artifact.
#
# One workflow, because there is one deployable unit. `app-bootstrap` is the composition root and
# the only thing a cluster runs; the adapters are leaves of that artifact, not independently
# shippable services. Eight files used to answer a release tag — web-release, web-advanced-release,
# websocket-release, httpclient-release, container-release, and the three that still have to live
# apart (see below) — and between them they ran `verifyCleanArchitectureDependencies` six times and
# `:app-bootstrap:test` four times for one release, on separate runners, with no job in any of them
# able to wait on a job in another.
#
# Tag scheme: `v*` only. The adapter-scoped patterns (`web-v*`, `websocket-v*`, `fileserver-v*`) are
# gone. They were the namespace-split bug: tagging `v1.2.3` and tagging `web-v1.2.3` ran different
# sets of gates, so a release could choose which gate it cleared, and the adapter-scoped half could
# not build an image because there is no per-adapter image to build.
#
# Two release workflows still stand outside this file, both for a mechanical reason rather than a
# design one:
# * jpa-release.yml — JpaReleaseRenderingTest reads that exact path and holds its PostgreSQL
# matrix and promotion list to src/config/jpa/release-registry.json.
# * fileserver-certification.yml — FileserverDocumentationCoverageTest requires every job id named
# in docs/fileserver/support-matrix.md to be defined in a `.github/workflows/fileserver-*.yml`.
# It is named "certification" rather than "release" on purpose: it certifies a storage topology
# and a support matrix, it deploys nothing, and the CI/CD boundary in docs/ci-cd/boundary.md
# says GitHub Actions does not deploy.
# Folding either one in needs its src-side test (and, for fileserver, the support document) changed
# in the same commit. Until then the image job below cannot wait on them — a stated gap.
#
# The image job DOES now wait on the evidence jobs in this file, which is new: while the image build
# lived in its own workflow it could publish while a sibling suite was still running or already red,
# because `needs:` does not reach across workflows.
on:
push:
tags:
- "v*"
permissions:
contents: read
# Never cancel a release in flight. A half-pushed manifest is worse than a slow one, and two runs
# for the same tag would race for the same registry tags.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
# The architecture-wide verification, once. Each of the four release workflows this file replaces
# ran `verifyCleanArchitectureDependencies` on its own runner, and three of them also ran the
# bootstrap architecture suite; the answers were identical because the input was one commit.
architecture-and-surface:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Verify architecture boundaries and the published surfaces
working-directory: src
run: >-
./gradlew
:verifyCleanArchitectureDependencies
:verifyPublicPathSnapshot
:app-bootstrap:verifyEnvKeys
:app-bootstrap:test --tests 'dev.caskeleton.bootstrap.architecture.*'
--stacktrace
# Every web lane that `check` cannot reach. webCrossStackParityTest depends on `test`,
# webJettyCompatTest and webFluxContractTest, so naming it runs all four — which is what
# web-advanced-release.yml spent a separate 90-minute job doing by naming the three by hand.
#
# webAdvancedTest is here rather than in a nightly of its own. Its tests run inside
# `:adapter:inbound:web:test` by design, so the lane adds exactly one thing: it fails closed when
# the `web-advanced` tag selects nothing. That is worth asserting at a release and is not worth a
# workflow file and a runner every night.
web-stable-release-gate:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Run every web lane, Stable and Advanced
working-directory: src
run: >-
./gradlew
:adapter:inbound:web:webCrossStackParityTest
:adapter:inbound:web:webNginxProxyTest
:adapter:inbound:web:webAdvancedTest
--stacktrace
- name: Publish the release evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: web-release-evidence
path: |
src/adapter/inbound/web/build/web-contract-parity/
src/adapter/inbound/web/build/reports/tests/
if-no-files-found: error
websocket-stable-release-gate:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Run every websocket lane, Stable and Advanced
working-directory: src
run: >-
./gradlew
:adapter:inbound:websocket:test
:adapter:inbound:websocket:websocketJettyTest
:adapter:inbound:websocket:websocketNginxTest
:adapter:inbound:websocket:websocketTransportQualificationTest
:adapter:inbound:websocket:websocketAdvancedTest
--stacktrace
- name: Publish the release evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: websocket-release-evidence
path: src/adapter/inbound/websocket/build/reports/tests/
if-no-files-found: error
# The three gRPC certification lanes. Their tests already run on every pull request — the
# `grpc-inprocess`, `grpc-netty` and `grpc-fault` tags are NOT excluded from
# `:grpc:grpc-testkit:test` (only `grpc-performance` is), and that task runs inside the root
# `check`. So this job adds exactly what the web and WebSocket Advanced lanes above add: the lane
# fails closed when its tag selects nothing, which is the one thing a tag-filtered suite inside
# `test` cannot tell you. A renamed or deleted @Tag would otherwise leave the in-process,
# transport and fault evidence grades claiming coverage that stopped existing.
#
# Release rather than nightly, for the same reason web-stable-release-gate is: these lanes need no
# container and no fixed cadence — grpcNettyContractTest opens an ephemeral socket, not a broker —
# so the guard is worth asserting once per tag and is not worth a runner every night.
#
# grpcPerformanceTest is deliberately absent. The leaf excludes it from `test` and says why: "a
# measurement in the release gate is a flaky test on a shared CI runner; it runs when somebody asks
# for it, by name." It is recorded as a manual entrypoint in the repository README instead.
grpc-stable-release-gate:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Run the gRPC Stable certification lanes
working-directory: src
run: >-
./gradlew -p optional-platforms
:grpc:grpc-testkit:grpcInProcessContractTest
:grpc:grpc-testkit:grpcNettyContractTest
:grpc:grpc-testkit:grpcFaultTest
--stacktrace
- name: Publish the gRPC release evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: grpc-release-evidence
path: src/grpc/grpc-testkit/build/reports/tests/
if-no-files-found: warn
# Each gate runs as its own single-line `./gradlew <task>` step so that a failure names the gate
# rather than a folded command. The architecture dependency gate that used to end this list is now
# architecture-and-surface above; it was the fourth copy of the same invocation.
httpclient-release-gate:
runs-on: ubuntu-latest
timeout-minutes: 60
defaults:
run:
working-directory: src
env:
# A project property rather than a command-line flag, so each run command stays a plain,
# verifiable task invocation while the machine-dependent bounds are still asserted.
GRADLE_OPTS: -Dorg.gradle.project.performance.assertions.enabled=true
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Run the HTTP client release qualification graph
run: >-
./gradlew
:adapter:outbound:httpclient:test
:adapter:outbound:httpclient:spring62ApiSurfaceScan
:adapter:outbound:httpclient:spring70CompatibilityTest
:adapter:outbound:httpclient:httpClientStableContractTest
:adapter:outbound:httpclient:httpClientSecurityTest
:adapter:outbound:httpclient:httpClientBlockHoundTest
:adapter:outbound:httpclient:httpClientFailureInjectionTest
:adapter:outbound:httpclient:httpClientPerformanceTest
--stacktrace
httpclient-documentation:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # actions/setup-python@v5.6.0
with:
python-version: '3.12'
- name: Verify documentation matches the code
run: python3 scripts/verify-httpclient-docs.py
app-image-release:
needs:
- architecture-and-surface
- web-stable-release-gate
- websocket-stable-release-gate
- grpc-stable-release-gate
- httpclient-release-gate
- httpclient-documentation
# Job-level, because a job that declares `permissions:` replaces the workflow set entirely: this
# is the only job that writes anything anywhere, and `packages: write` stops at its boundary.
permissions:
contents: read
packages: write
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
# The builder stage inside src/Dockerfile runs this repository's Gradle wrapper to produce the
# JAR that becomes the image. Validating the wrapper here checks the thing that is about to
# execute, before it executes, rather than after an image already exists.
# The tag is the release identity; everything below derives from it. A tag that does not parse
# stops the release here, rather than producing an image named after whatever ref happened to
# be checked out.
#
# GHCR rejects an uppercase path, and this repository's owner is mixed case — the naive
# `ghcr.io/${{ github.repository }}` fails at push time with a message about the manifest
# rather than about the case, so the lowercasing is explicit and the result is asserted.
- name: Resolve the release coordinates
env:
CONFIGURED_IMAGE_NAME: ${{ vars.APP_IMAGE_NAME }}
run: |
set -euo pipefail
readonly REGISTRY='ghcr.io'
if [[ "${GITHUB_REF_TYPE}" != 'tag' ]]; then
echo "::error::container-release runs only for a release tag; ref type was ${GITHUB_REF_TYPE}"
exit 1
fi
release_tag="${GITHUB_REF_NAME}"
# Bare MAJOR.MINOR.PATCH, because src/build.gradle's release-version guard refuses a
# pre-release or build suffix and the image tag must be the same string the JAR reports.
if [[ ! "${release_tag}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
echo "::error::release tag must be vMAJOR.MINOR.PATCH with no suffix; got '${release_tag}'"
exit 1
fi
release_version="${BASH_REMATCH[1]}"
owner_path="$(printf '%s' "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')"
image_name="${CONFIGURED_IMAGE_NAME:-${owner_path}/caskeleton}"
image_repository="${REGISTRY}/${image_name}"
if [[ "${image_repository}" != "${image_repository,,}" ]]; then
echo "::error::image repository must be lowercase; got '${image_repository}'"
exit 1
fi
if [[ "${image_repository}" =~ [[:space:]] || "${image_repository}" == *:* ]]; then
echo "::error::image repository must carry no tag and no whitespace; got '${image_repository}'"
exit 1
fi
{
printf 'REGISTRY=%s\n' "${REGISTRY}"
printf 'RELEASE_VERSION=%s\n' "${release_version}"
printf 'BUILD_VERSION=%s+%s\n' "${release_version}" "${GITHUB_SHA}"
printf 'IMAGE_REPOSITORY=%s\n' "${image_repository}"
printf 'IMAGE_VERSION_TAG=%s\n' "${release_version}"
printf 'IMAGE_REVISION_TAG=sha-%s\n' "${GITHUB_SHA}"
printf 'SOURCE_URL=%s/%s\n' "${GITHUB_SERVER_URL}" "${GITHUB_REPOSITORY}"
} >> "${GITHUB_ENV}"
printf 'container-release: %s -> %s:%s and %s:sha-%s\n' \
"${release_tag}" "${image_repository}" "${release_version}" \
"${image_repository}" "${GITHUB_SHA}"
# Byte-identical to the install in dependency-vulnerability.yml, deliberately: the same
# checksum-pinned binary at the same version scans the filesystem and the image, so the two
# gates cannot disagree because one of them silently moved to a newer database schema.
#
# This repository installs its scanner rather than calling a scanner action, which is why no
# third-party action appears in this workflow: a pinned tarball with an asserted SHA-256 is a
# supply-chain claim that can be checked offline, and an action pinned to a commit is not.
- name: Install pinned Trivy under RUNNER_TEMP
env:
TRIVY_DOWNLOAD_BASE_URL: ${{ vars.TRIVY_DOWNLOAD_BASE_URL }}
run: |
set -euo pipefail
readonly TRIVY_VERSION='0.71.2'
readonly TRIVY_SHA256_AMD64='0510e71e2fd39bf863856d499c8dc19feb4e7336546394c502a8f5cc7ab27460'
readonly TRIVY_SHA256_ARM64='fe1c7106e15a5365d485b098a8c338f91e3b7ba71cb0e4963b98a3a098763cfc'
readonly DOWNLOAD_BASE_URL="${TRIVY_DOWNLOAD_BASE_URL:-https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}}"
case "${RUNNER_ARCH:-X64}" in
X64)
asset_arch='64bit'
expected_sha256="${TRIVY_SHA256_AMD64}"
;;
ARM64)
asset_arch='ARM64'
expected_sha256="${TRIVY_SHA256_ARM64}"
;;
*)
echo "::error::unsupported runner architecture: ${RUNNER_ARCH:-unknown}"
exit 1
;;
esac
install_dir="${RUNNER_TEMP}/trivy-${TRIVY_VERSION}"
archive="${RUNNER_TEMP}/trivy-${TRIVY_VERSION}.tar.gz"
mkdir -p "${install_dir}"
curl --fail --show-error --silent --location --retry 3 \
--proto '=https' --tlsv1.2 \
"${DOWNLOAD_BASE_URL}/trivy_${TRIVY_VERSION}_Linux-${asset_arch}.tar.gz" \
--output "${archive}"
printf '%s %s\n' "${expected_sha256}" "${archive}" | sha256sum -c -
tar -xzf "${archive}" -C "${install_dir}" trivy
chmod 0755 "${install_dir}/trivy"
printf '%s\n' "${install_dir}" >> "${GITHUB_PATH}"
# SOURCE_DATE_EPOCH is the commit time, not the wall clock, so the image metadata is a function
# of the commit rather than of when the runner happened to pick the job up. Verified locally,
# and worth stating exactly because it is easy to overclaim: BuildKit uses it for the image
# config `created` field and for every history timestamp — both came back as the commit time —
# and it does NOT rewrite file mtimes inside the layers. Those still carry the build time, so
# two builds of the same commit agree on metadata but their layer digests still differ.
# Byte-identical layers additionally need `--output type=image,rewrite-timestamp=true`, which
# needs the containerd image store; that is a runner-capability change, not a flag to add
# untested to the one job that publishes releases.
#
# The OCI `created` label comes from the same commit for the same reason: `date -u` there would
# have made every rebuild a different image for no reason anybody could see.
#
# Both base images are already digest-pinned inside src/Dockerfile, and so is the Dockerfile
# frontend in its `# syntax` directive, so nothing in this build resolves a floating tag.
- name: Build the release image
run: |
set -euo pipefail
SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"
export SOURCE_DATE_EPOCH
created="$(git log -1 --format=%cI)"
printf 'SOURCE_DATE_EPOCH=%s (%s)\n' "${SOURCE_DATE_EPOCH}" "${created}"
DOCKER_BUILDKIT=1 docker build \
--file src/Dockerfile \
--tag "${IMAGE_REPOSITORY}:${IMAGE_VERSION_TAG}" \
--tag "${IMAGE_REPOSITORY}:${IMAGE_REVISION_TAG}" \
--build-arg RELEASE_VERSION="${RELEASE_VERSION}" \
--build-arg BUILD_VERSION="${BUILD_VERSION}" \
--build-arg GIT_SHA="${GITHUB_SHA}" \
--build-arg SOURCE_URL="${SOURCE_URL}" \
--label org.opencontainers.image.created="${created}" \
src
docker image inspect \
--format 'built {{.Id}} ({{.Size}} bytes, {{len .RootFS.Layers}} layers)' \
"${IMAGE_REPOSITORY}:${IMAGE_VERSION_TAG}"
# Generated before the blocking scan, and uploaded before it too, so the inventory of what is
# in the image survives the run that refuses to publish it. An SBOM you only get on a green
# build is an SBOM you cannot use to answer "what was in the one that failed".
- name: Generate the image SBOM
run: |
set -euo pipefail
trivy image \
--format cyclonedx \
--scanners license \
--output image-sbom.cdx.json \
"${IMAGE_REPOSITORY}:${IMAGE_VERSION_TAG}"
test -s image-sbom.cdx.json
- name: Upload the image SBOM
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # actions/upload-artifact@v7.0.1
with:
name: container-release-sbom
path: image-sbom.cdx.json
if-no-files-found: error
retention-days: 90
# The same policy dependency-vulnerability.yml applies to the filesystem, applied to the thing
# that actually ships: CRITICAL and HIGH block, everything else is reported. The filesystem
# scan cannot see the base image's OS packages, which is most of an image's attack surface, so
# a green trivy-fs has never been evidence about the artifact.
#
# --ignorefile is mandatory here as everywhere: .trivyignore.yaml is the single suppression
# source. Each entry carries a rationale and an expiry by policy, reviewed through CODEOWNERS
# (.github/dependency-vulnerability-policy.md); an inline --skip or a second ignore file would
# be a suppression nobody reviews.
#
# This step is the reason `docker push` is further down. A vulnerable image that was pushed and
# then reported is already pullable by everything that watches the tag.
- name: Block High and Critical vulnerabilities in the release image
run: |
set -euo pipefail
trivy image \
--scanners vuln,license \
--severity CRITICAL,HIGH \
--exit-code 1 \
--ignorefile .trivyignore.yaml \
"${IMAGE_REPOSITORY}:${IMAGE_VERSION_TAG}"
- name: Report Medium and Low vulnerabilities in the release image
run: |
set -euo pipefail
trivy image \
--scanners vuln,license \
--severity MEDIUM,LOW \
--exit-code 0 \
--ignorefile .trivyignore.yaml \
"${IMAGE_REPOSITORY}:${IMAGE_VERSION_TAG}"
- name: Sign in to the container registry
env:
REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
printf '%s' "${REGISTRY_TOKEN}" \
| docker login "${REGISTRY}" --username "${GITHUB_ACTOR}" --password-stdin
# Two tags, one digest. The semver tag is what a human reads and what a release note cites; the
# sha- tag is the one that can never be moved to different content, because the git SHA it
# names is the only commit that can produce it.
#
# Neither is what a manifest should pin. Both are mutable names in a registry: a later push can
# point `1.2.3` at something else, and nothing about a tag tells a cluster it did not. The
# digest recorded below is immutable by construction, and it is the field the GitOps repository
# pins — the tags exist so a person can find the digest, not so a cluster can resolve one.
- name: Push the release and revision tags
run: |
set -euo pipefail
docker push "${IMAGE_REPOSITORY}:${IMAGE_VERSION_TAG}"
docker push "${IMAGE_REPOSITORY}:${IMAGE_REVISION_TAG}"
# awk rather than `grep | head`, deliberately. Under `set -e` with `pipefail`, a grep that
# matches nothing exits 1 and kills the step right here — so the explicit check below,
# and its message, would never run and the failure would surface as a bare exit code.
# awk exits 0 whether or not it matched, which leaves the empty case for us to report.
pinned_reference="$(
docker image inspect \
--format '{{range .RepoDigests}}{{println .}}{{end}}' \
"${IMAGE_REPOSITORY}:${IMAGE_VERSION_TAG}" \
| awk -v prefix="${IMAGE_REPOSITORY}@sha256:" \
'index($0, prefix) == 1 { print; exit }'
)"
if [[ -z "${pinned_reference}" ]]; then
echo "::error::no registry digest for ${IMAGE_REPOSITORY} after push"
exit 1
fi
printf 'PINNED_REFERENCE=%s\n' "${pinned_reference}" >> "${GITHUB_ENV}"
printf 'container-release: pushed %s\n' "${pinned_reference}"
# The handoff to the GitOps repository, in a form a person and a script can both read. It is
# written to the job summary as well as to an artifact because the summary is where somebody
# looks first and the artifact is what survives the ninety days a release audit asks about.
- name: Record the immutable image reference
run: |
set -euo pipefail
digest="${PINNED_REFERENCE#*@}"
{
printf 'release_tag: %s\n' "${GITHUB_REF_NAME}"
printf 'git_sha: %s\n' "${GITHUB_SHA}"
printf 'image_repository: %s\n' "${IMAGE_REPOSITORY}"
printf 'version_tag: %s\n' "${IMAGE_VERSION_TAG}"
printf 'revision_tag: %s\n' "${IMAGE_REVISION_TAG}"
printf 'digest: %s\n' "${digest}"
printf 'pinned_reference: %s\n' "${PINNED_REFERENCE}"
} > image-release.txt
{
printf '### container-release\n\n'
printf 'Pin this in the GitOps manifest as the container image:\n\n'
printf '```\n%s\n```\n\n' "${PINNED_REFERENCE}"
printf -- '- release tag: `%s`\n' "${GITHUB_REF_NAME}"
printf -- '- version tag: `%s:%s`\n' "${IMAGE_REPOSITORY}" "${IMAGE_VERSION_TAG}"
printf -- '- revision tag: `%s:%s`\n' "${IMAGE_REPOSITORY}" "${IMAGE_REVISION_TAG}"
} >> "${GITHUB_STEP_SUMMARY}"
cat image-release.txt
- name: Upload the immutable image reference
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # actions/upload-artifact@v7.0.1
with:
name: container-release-image-reference
path: image-release.txt
if-no-files-found: error
retention-days: 90
@@ -1,66 +0,0 @@
name: web-advanced-nightly
# Every web Advanced capability is off in production unless a deployment names it, which means none
# of them is exercised by the ordinary PR gate. That is exactly why they need their own nightly: a
# capability nobody runs is a capability nobody notices breaking, and the first person to find out
# is whoever enabled it.
#
# The lane is tagged rather than module-scoped because Advanced lives in the same leaf as Stable.
on:
workflow_dispatch:
schedule:
# 03:30 UTC, after web-nightly. They contend for the same machine when streaming holds
# connections, and a load lane that shares a runner measures the runner.
- cron: '30 3 * * *'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
web-advanced-capabilities:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the Advanced capability lane
working-directory: src
run: >-
./gradlew
:adapter:inbound:web:webAdvancedTest
--no-daemon
--stacktrace
- name: Prove Stable behaviour is unchanged with every flag off
# The rollback assertion, run as its own step so a failure names itself. Two of the twelve
# capabilities change requests that do not use them, and this is what catches a third
# acquiring that property by accident.
working-directory: src
run: >-
./gradlew
:adapter:inbound:web:test
--tests '*WebAdvancedReleaseTest*'
--no-daemon
--stacktrace
- name: Publish the test reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: web-advanced-nightly-reports
path: src/adapter/inbound/web/build/reports/tests/
if-no-files-found: warn
@@ -1,75 +0,0 @@
name: web-advanced-release
# Promotion evidence for the web Advanced capabilities.
#
# It depends on the Stable gate rather than replacing it: the condition every Advanced capability
# must satisfy is that Stable behaviour is unchanged with the feature off, and that is only
# meaningful against a Stable suite that passed in the same run.
on:
workflow_dispatch:
push:
tags:
- 'v*'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
web-advanced-promotion-evidence:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Establish the Stable baseline
working-directory: src
run: >-
./gradlew
:adapter:inbound:web:test
:adapter:inbound:web:webJettyCompatTest
:adapter:inbound:web:webFluxContractTest
--no-daemon
--stacktrace
- name: Run the Advanced capability lane
working-directory: src
run: >-
./gradlew
:adapter:inbound:web:webAdvancedTest
--no-daemon
--stacktrace
- name: Verify the architecture boundary Stable depends on
# WEB-ARCH-ADV. A feature flag decides whether an Advanced bean is created; it does nothing
# about a Stable class that imports an Advanced type, and one such edge makes the Stable
# platform unbuildable without the Advanced code.
working-directory: src
run: >-
./gradlew
:adapter:inbound:web:test
--tests '*WebArchitectureRulesTest*'
--tests '*WebModuleBoundaryTest*'
verifyCleanArchitectureDependencies
--no-daemon
--stacktrace
- name: Publish the promotion evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: web-advanced-release-evidence
path: src/adapter/inbound/web/build/reports/tests/
if-no-files-found: warn
-55
View File
@@ -1,55 +0,0 @@
name: web-nightly
# The gates that are too slow for a pull request and too important to run only at release. Load,
# abuse and graceful shutdown all need a machine that is not simultaneously compiling something
# else, and all three measure behaviour that degrades gradually rather than breaking outright —
# which is exactly the kind of regression a per-PR gate never catches and a nightly one does.
on:
workflow_dispatch:
schedule:
# 03:00 UTC. Late enough that the day's merges are in, early enough that a failure is triaged
# before the next working day starts.
- cron: '0 3 * * *'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
web-load-abuse-and-shutdown:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the load, abuse and shutdown lanes on every container
working-directory: src
run: >-
./gradlew
:adapter:inbound:web:test
:adapter:inbound:web:webJettyCompatTest
:adapter:inbound:web:webFluxContractTest
--no-daemon
--stacktrace
- name: Publish the test reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: web-nightly-reports
path: src/adapter/inbound/web/build/reports/tests/
if-no-files-found: warn
-114
View File
@@ -1,114 +0,0 @@
name: web-pr
# Every Stable claim the web platform makes is backed by a job here. The lanes are split by what
# they need rather than by what they test: the cross-container matrix needs three source sets, the
# proxy contract needs Docker, and the load gate needs a machine that is not also compiling. A
# single job running everything would attribute every failure to "the web tests".
on:
workflow_dispatch:
pull_request:
paths:
- 'src/adapter/inbound/web/**'
- 'src/application-core/src/**/operation/**'
- 'src/application-core/src/**/idempotency/**'
- 'src/adapter/outbound/persistence-jpa/src/**/operation/**'
- 'docs/web/**'
- '.github/workflows/web-pr.yml'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
web-unit-and-architecture:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the web unit, module-boundary and architecture suites
working-directory: src
run: >-
./gradlew
:adapter:inbound:web:test
:application-core:test
verifyCleanArchitectureDependencies
--no-daemon
--stacktrace
# The parity gate depends on all three recording lanes and fails when one is missing, so it runs
# them itself rather than trusting a previous job to have left the recordings behind.
web-cross-stack-parity:
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Compare the wire contract across Tomcat, Jetty and Reactor Netty
working-directory: src
run: >-
./gradlew
:adapter:inbound:web:webCrossStackParityTest
--no-daemon
--stacktrace
- name: Publish the parity recordings
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: web-contract-parity
path: src/adapter/inbound/web/build/web-contract-parity/
if-no-files-found: error
# Docker-gated, and the lane fails rather than skipping when the runtime is missing. A proxy
# contract that quietly passes without a proxy has been certifying nothing since whenever the
# container runtime last broke.
web-nginx-proxy-contract:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the proxy, prefix and spoofing contract behind a real Nginx
working-directory: src
run: >-
./gradlew
:adapter:inbound:web:webNginxProxyTest
--no-daemon
--stacktrace
-61
View File
@@ -1,61 +0,0 @@
name: web-release
# The complete Stable gate. Everything the PR and nightly workflows run, plus the checks whose cost
# is only justified when something is about to ship: the public API surface, the environment key
# registry and the whole architecture verification.
#
# It is one workflow rather than a reference to the others because a release gate that depends on
# another workflow having run is a gate whose result depends on scheduling.
on:
workflow_dispatch:
push:
tags:
- 'web-v*'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
web-stable-release-gate:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run every web lane and the architecture-wide verification
working-directory: src
run: >-
./gradlew
:adapter:inbound:web:webCrossStackParityTest
:adapter:inbound:web:webNginxProxyTest
verifyCleanArchitectureDependencies
verifyPublicPathSnapshot
verifyEnvKeys
:app-bootstrap:test --tests '*CleanArchitectureTest'
--no-daemon
--stacktrace
- name: Publish the release evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: web-release-evidence
path: |
src/adapter/inbound/web/build/web-contract-parity/
src/adapter/inbound/web/build/reports/tests/
if-no-files-found: error
@@ -1,64 +0,0 @@
name: websocket-advanced-nightly
# The WebSocket Advanced capabilities are off unless a deployment names them, so nothing a
# production deployment runs exercises them. A capability nobody runs is a capability nobody
# notices breaking, and the first person to find out is whoever enables it.
on:
workflow_dispatch:
schedule:
# 04:00 UTC, after the web lanes. Streaming and connection work contend for the same runner,
# and a load lane sharing one measures the runner.
- cron: '0 4 * * *'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
websocket-advanced-capabilities:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the Advanced capability lane
working-directory: src
run: >-
./gradlew
:adapter:inbound:websocket:websocketAdvancedTest
--no-daemon
--stacktrace
- name: Verify the boundary Stable depends on
# WS-ARCH-6. A flag decides whether an Advanced bean is created; it does nothing about a
# Stable class that imports an Advanced type, and one such edge makes Stable unbuildable
# without Advanced.
working-directory: src
run: >-
./gradlew
:adapter:inbound:websocket:test
--tests '*WebSocketArchitectureRulesTest*'
--tests '*WebSocketModuleBoundaryTest*'
--no-daemon
--stacktrace
- name: Publish the test reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: websocket-advanced-nightly-reports
path: src/adapter/inbound/websocket/build/reports/tests/
if-no-files-found: warn
-98
View File
@@ -1,98 +0,0 @@
name: websocket-pr
# Every Stable claim the WebSocket platform makes is backed by a job here. The lanes are split by
# what they need: the runtime matrix needs two containers, and the proxy contract needs Docker.
on:
workflow_dispatch:
pull_request:
paths:
- 'src/adapter/inbound/websocket/**'
- 'docs/websocket/**'
- '.github/workflows/websocket-pr.yml'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
websocket-unit-and-architecture:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the websocket unit, boundary and runtime suites
working-directory: src
run: >-
./gradlew
:adapter:inbound:websocket:test
verifyCleanArchitectureDependencies
--no-daemon
--stacktrace
websocket-container-matrix:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the runtime contract on the second servlet container
working-directory: src
run: >-
./gradlew
:adapter:inbound:websocket:websocketJettyTest
--no-daemon
--stacktrace
# Docker-gated, and the lane fails rather than skipping. Upgrade handling is the single most
# common WebSocket deployment failure and it is invisible from either side alone.
websocket-nginx-contract:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run the upgrade and forwarded-header contract behind a real Nginx
working-directory: src
run: >-
./gradlew
:adapter:inbound:websocket:websocketNginxTest
--no-daemon
--stacktrace
-56
View File
@@ -1,56 +0,0 @@
name: websocket-release
# The complete Stable gate: every lane plus the architecture-wide verification. One workflow rather
# than a reference to the others, because a release gate that depends on another workflow having run
# is a gate whose result depends on scheduling.
on:
workflow_dispatch:
push:
tags:
- 'websocket-v*'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
websocket-stable-release-gate:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run every websocket lane and the architecture-wide verification
working-directory: src
run: >-
./gradlew
:adapter:inbound:websocket:test
:adapter:inbound:websocket:websocketJettyTest
:adapter:inbound:websocket:websocketNginxTest
:adapter:inbound:websocket:websocketTransportQualificationTest
verifyCleanArchitectureDependencies
:app-bootstrap:test --tests '*CleanArchitectureTest'
--no-daemon
--stacktrace
- name: Publish the release evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: websocket-release-evidence
path: src/adapter/inbound/websocket/build/reports/tests/
if-no-files-found: error
+13 -5
View File
@@ -1,13 +1,21 @@
# Structured Trivy suppression baseline. # Structured Trivy suppression baseline.
# #
# This repository-root file is the only CI suppression source. Every future entry must include: # This repository-root file is the only CI suppression source. Every Trivy invocation must name it
# with `--ignorefile .trivyignore.yaml`; ad-hoc ignore files and inline bypasses are not allowed.
#
# Every entry must carry:
# id: advisory, license, misconfiguration, or secret identifier # id: advisory, license, misconfiguration, or secret identifier
# statement: non-empty accepted-risk or false-positive rationale # statement: non-empty accepted-risk or false-positive rationale
# expired_at: future YYYY-MM-DD no more than 90 days from review # expired_at: future YYYY-MM-DD, no more than 90 days from review
# #
# `verifyTrivyignore` enforces those fields and the expiry window. CODEOWNERS supplies the separate # Enforced by review, not by a build task. `verifyTrivyignore` used to be a 105-line hand-written
# reviewer control. Every Trivy invocation must also name this file with # YAML parser in the root build — indentation tracking, inline-scalar handling, quote stripping — and
# `--ignorefile .trivyignore.yaml`; do not add ad-hoc ignore files or inline bypasses. # what it guarded was this file, which has been empty since it was created. A suppression is added by
# a human and merged by a CODEOWNERS reviewer (.github/dependency-vulnerability-policy.md); that
# reviewer is the control, and a parser that has never seen an entry is not a second one.
#
# If this file ever carries entries and they start drifting, that is the moment to automate the
# check — against real entries, with a real YAML library. Not before.
vulnerabilities: [] vulnerabilities: []
licenses: [] licenses: []
+21 -9
View File
@@ -52,10 +52,19 @@ root `CLAUDE.md`는 이 목록의 동기화된 요약이다. 두 문서가 어
- `src/config/architecture/modules.json`: 등록된 모든 leaf의 ID, repository-relative 소스 경로, - `src/config/architecture/modules.json`: 등록된 모든 leaf의 ID, repository-relative 소스 경로,
Gradle path, 허용 production project dependency edge, 두 composition root의 실제 runtime Gradle path, 허용 production project dependency edge, 두 composition root의 실제 runtime
membership. leaf 목록과 그 개수의 SSOT는 registry다. 문서는 개수를 복제하지 않는다 — membership. leaf 목록과 그 개수의 SSOT는 registry다. 문서는 개수를 복제하지 않는다 —
산문에 적힌 숫자는 leaf가 추가되는 순간 drift한다. `verifyDocumentedLeafCount` 강제한다. 산문에 적힌 숫자는 leaf가 추가되는 순간 drift하기 때문이다. 이제 강제하는 태스크는 없다:
- `src/settings.gradle`: registry를 fail-closed로 검증하고 등록된 Gradle project를 include/mapping `verifyDocumentedLeafCount`는 삭제됐다. 문서에 적힌 수가 틀린 것은 결함이지만 빌드를 실패시킬
- `src/build.gradle`: 같은 registry를 읽는 `verifyCleanArchitectureDependencies`와 그 밖의 사유는 아니고, 그 태스크는 모든 `CLAUDE.md``build.gradle`을 정규식으로 훑는 파서였다.
architecture-wide verification task - `src/settings.gradle`: 16줄. `ca.architecture-registry` 설정 플러그인이 registry를 읽어
project를 include/mapping 한다. registry가 project 목록이 될 수 없는 경우(중복 ID, 저장소 밖
경로, 없는 디렉터리)만 여기서 실패한다. 허용되지 않는 edge 같은 아키텍처 규칙은
`verifyCleanArchitectureDependencies`가 답한다 — settings에서 죽으면 실행할 수 있는 태스크가
하나도 없다.
- `src/build-logic/`: convention plugin. leaf는 `ca.java-library` / `ca.spring-library` /
`ca.platform-module` 중 자기 성격을 선언하고, 그 플러그인이 toolchain·락·정적분석·테스트
기본값을 준다. `ca.architecture`가 아키텍처 검증 태스크를 소유한다.
- `src/build.gradle`: 루트 라이프사이클(`ci`, `releaseCheck`, `qualityCheck`,
`configContractCheck`, `integrationCheck`)과 버전/리비전
작업 파일의 소유 leaf는 registry의 `source_path`로 판단하고 가장 가까운 `src/**/CLAUDE.md` 작업 파일의 소유 leaf는 registry의 `source_path`로 판단하고 가장 가까운 `src/**/CLAUDE.md`
함께 읽는다. focused test는 registry의 `gradle_path`에서 함께 읽는다. focused test는 registry의 `gradle_path`에서
@@ -174,14 +183,17 @@ Gradle 의존성 검증도 같은 registry를 읽는다. root 문서나 기억
```bash ```bash
cd src cd src
./gradlew <owner-gradle-path>:test --console=plain ./gradlew <owner-gradle-path>:check --console=plain # 그 leaf만: 컴파일·테스트·포맷·스타일·ErrorProne
./gradlew test ./gradlew check # 모든 leaf의 check
./gradlew check # check 가 verifyCleanArchitectureDependencies + verifyEnvKeys 2종을 전이 실행한다 (src/build.gradle) ./gradlew architectureCheck # 의존 방향·런타임 멤버십·application-core 순수성
./gradlew verifyCleanArchitectureDependencies ./gradlew qualityCheck # SpotBugs + FindSecBugs (leaf check에는 없다)
./gradlew ci # PR 게이트 = 위 셋 + configContractCheck
./gradlew verifyPublicPathSnapshot ./gradlew verifyPublicPathSnapshot
./gradlew verifyEnvKeys ./gradlew :app-bootstrap:verifyEnvKeys
``` ```
leaf의 `check`는 그 leaf만 검사한다. 저장소 전체 질문은 이름이 따로 있는 루트 태스크가 답한다.
소유 leaf의 정확한 Gradle path는 `src/config/architecture/modules.json`에서 읽고 focused test 소유 leaf의 정확한 Gradle path는 `src/config/architecture/modules.json`에서 읽고 focused test
명령을 파생한다. root 문서에 leaf별 명령 목록을 복제하지 않는다. 명령을 파생한다. root 문서에 leaf별 명령 목록을 복제하지 않는다.
+15 -7
View File
@@ -45,8 +45,9 @@ count.
## Module families ## Module families
`src/config/architecture/modules.json` owns the complete leaf list. Root guidance summarizes `src/config/architecture/modules.json` owns the complete leaf list. Root guidance summarizes
families; the nearest `src/**/CLAUDE.md` owns local rules. `verifyDocumentedLeafCount` fails the families; the nearest `src/**/CLAUDE.md` owns local rules. No task enforces this any more:
build when a policy document states a leaf count that the registry does not agree with. `verifyDocumentedLeafCount` was deleted along with the other documentation-drift parsers. A stated
count that disagrees with the registry is a defect, not a build failure — so do not state one.
| Family | Responsibility | Stable dependency direction | | Family | Responsibility | Stable dependency direction |
| --- | --- | --- | | --- | --- | --- |
@@ -105,14 +106,21 @@ From `src/`, read the owning leaf's `gradle_path` from
Architecture-wide commands: Architecture-wide commands:
```bash ```bash
./gradlew verifyCleanArchitectureDependencies --console=plain ./gradlew architectureCheck --console=plain
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' --console=plain ./gradlew :app-bootstrap:test --tests 'dev.caskeleton.bootstrap.architecture.*' --console=plain
./gradlew verifyPublicPathSnapshot --console=plain ./gradlew verifyPublicPathSnapshot --console=plain
./gradlew verifyEnvKeys --console=plain ./gradlew :app-bootstrap:verifyEnvKeys --console=plain
``` ```
Use public-path and env-key checks only when their surfaces changed. Full `test` or `check` requires A leaf's `check` covers that leaf only — compile, its tests, Spotless, Checkstyle, Error Prone.
the controller's workflow authorization. Repository-wide questions have their own names: `architectureCheck` (dependency direction, runtime
membership, application-core purity, Git-carryable sources), `qualityCheck` (SpotBugs, FindSecBugs),
`configContractCheck` (the environment contract), `integrationCheck` (the declared strict test
lanes). `ci` is check + architectureCheck + qualityCheck + configContractCheck; `releaseCheck` adds
provenance, archive hygiene and the public-path snapshot.
Use public-path and env-key checks only when their surfaces changed. Full `test`, `check` or `ci`
requires the controller's workflow authorization.
## Advisory and reporting ## Advisory and reporting
+37 -2
View File
@@ -90,14 +90,20 @@ cd src
3. `CaSkeletonApplication`을 새 애플리케이션 이름으로 바꾸고, 목표 도메인의 엔티티·repository port·유스케이스·adapter를 production 모듈에 추가합니다. 예시 코드는 `sample-portfolio`에만 둡니다. 3. `CaSkeletonApplication`을 새 애플리케이션 이름으로 바꾸고, 목표 도메인의 엔티티·repository port·유스케이스·adapter를 production 모듈에 추가합니다. 예시 코드는 `sample-portfolio`에만 둡니다.
4. 모듈 이름과 경계는 그대로 유지합니다. 4. 모듈 이름과 경계는 그대로 유지합니다.
검증은 sample-on과 sample-off를 모두 통과시킵니다. 검증은 먼저 composition root의 빠른 테스트와 sample-off 계약을 확인합니다. 루트에서
`./gradlew test`를 호출하면 등록된 모든 하위 프로젝트의 `test`를 실행하므로 일상적인 로컬
피드백 명령으로 사용하지 않습니다. 저장소 전체 qualification은 CI 또는 명시적인 `ci` task가
담당합니다.
```bash ```bash
cd src cd src
./gradlew test ./gradlew :app-bootstrap:test
./gradlew :app-bootstrap:sampleOffTest ./gradlew :app-bootstrap:sampleOffTest
./gradlew architectureCheck
``` ```
병합 전 저장소 전체 검증이 필요하면 `./gradlew ci`를 실행합니다.
`sample-portfolio`는 템플릿이 유지하는 fixture/reference 모듈이라 production 모듈이 의존하지 않고, runtime에 sample bean이나 endpoint를 넣지 않습니다. 다운스트림 fork에서 fixture가 더 필요 없을 때만 sample-off 테스트를 통과시킨 뒤 정리합니다. `sample-portfolio`는 템플릿이 유지하는 fixture/reference 모듈이라 production 모듈이 의존하지 않고, runtime에 sample bean이나 endpoint를 넣지 않습니다. 다운스트림 fork에서 fixture가 더 필요 없을 때만 sample-off 테스트를 통과시킨 뒤 정리합니다.
## 아키텍처 규칙과 검증 ## 아키텍처 규칙과 검증
@@ -122,6 +128,35 @@ cd src
두 검증 축은 [ci-quality-gates.yml](.github/workflows/ci-quality-gates.yml)의 release gate에 연결되어, 규칙 위반이 병합·릴리스를 막습니다. 두 검증 축은 [ci-quality-gates.yml](.github/workflows/ci-quality-gates.yml)의 release gate에 연결되어, 규칙 위반이 병합·릴리스를 막습니다.
## 수동 전용 Gradle 태스크
아래 세 태스크는 **어떤 워크플로도 실행하지 않으며, 그게 의도다.** 자동 실행이 틀린 이유를 각각
적어 둔다.
여기 적힌 태스크 이름이 실재하는지 검사하던 `verifyReadmeCommands`는 삭제했다. 그건 이 문서의
```bash 블록을 직접 파싱해 `./gradlew`·`docker compose`·`make` 토큰을 실제 태스크 그래프와 대조하는
Markdown 명령 파서였고, 그 결과 "README에 무엇을 쓸 수 있는가"가 그 파서가 읽을 수 있는 문법의
함수가 됐다. 문서와 코드가 어긋나는 것은 결함이지만, 빌드를 실패시켜서 고칠 일은 아니다.
```bash
cd src
./gradlew :grpc:grpc-testkit:grpcPerformanceTest
./gradlew :sample-portfolio:openapiCheckSnapshot -PapproveOpenApiChange
./gradlew :app-bootstrap:sampleOffCompile
```
- `grpcPerformanceTest` — latency percentile·saturation·drain budget을 **측정**한다. 공유 CI
runner의 측정값은 흔들리고, 흔들리는 게이트는 결국 꺼진다. leaf `build.gradle`이 이 태스크의
태그를 `test`에서 제외하는 이유도 같다. 성능 회귀가 의심될 때 사람이 이름으로 부른다.
- `openapiCheckSnapshot` — 드리프트 검사 자체는 이미 자동으로 돈다. 이 태스크가 감싸는
`OpenApiDriftContractTest`는 `:sample-portfolio:test`의 일부이고, 그건 `check` 안이며 stage 1에서
실행된다. 이 태스크의 고유한 역할은 `-PapproveOpenApiChange`로 **커밋된 스냅샷을 다시 만드는 것**
— 의도된 API 변경을 사람이 승인하는 지점이다. 자동으로 돌리면 승인이 승인이 아니게 된다.
- `sampleOffCompile` — `sampleOffTest` 소스셋을 **컴파일만** 한다. CI가 돌리는
`:app-bootstrap:sampleOffTest`(stage 1, `ci-quality-gates.yml`의 `sample-off` 잡)는 같은 소스셋을
컴파일한 뒤 실행까지 하므로, CI에 따로 넣으면 진부분집합을 한 번 더 도는 것이다. 남겨 둔 이유는
sample 제거 작업 중 테스트를 기다리지 않고 컴파일만 빠르게 확인하는 로컬 루프가 실재하기 때문이다.
## 더 알아보기 ## 더 알아보기
- 빌드·검증 게이트·환경 변수 상세: [src/README.md](src/README.md) - 빌드·검증 게이트·환경 변수 상세: [src/README.md](src/README.md)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,119 @@
# ADR-BUILD-001: `java-test-fixtures` is the standard for shared test code
- Status: Accepted
- Date: 2026-09-07
- Scope: every leaf that publishes or consumes shared test code
- Source: `docs/reviews/2026-09-07-app-bootstrap-module-code-review.md` BOOT-015
## Context
Two conventions do the same job in this repository.
`ca.testkit-publisher` — a convention plugin — gives a leaf a `testkit` source set, wires its output
onto the lanes that leaf names, and optionally publishes it as a consumable configuration. Five
leaves use it: `persistence-jpa` (published as `jpaTestkit`), `web` (`webTestkit`), `websocket`
(`websocketTestkit`), `persistence-mongo` and `httpclient` (both unpublished).
`java-test-fixtures` — Gradle's own plugin — gives a leaf a `testFixtures` source set, puts it on
`test`'s classpath automatically, and always publishes it as a variant consumers reach with
`testFixtures(project(':x'))`. One leaf uses it: `graphql`, which additionally fails its build when a
fixture is written outside `src/testFixtures/java`.
Two conventions for one purpose is the defect. A contributor adding shared test code has to know
which leaf they are in before they know where the file goes, and the two answers are not
interchangeable: a consumer of the first writes `project(path: ':x', configuration: 'jpaTestkit')`
and has to know the configuration's name, while a consumer of the second writes
`testFixtures(project(':x'))` and does not.
## Decision
**`java-test-fixtures` is the standard.** New shared test code goes in `src/testFixtures/java`, and a
consumer depends on it with `testFixtures(project(':x'))`.
Three reasons, in order of weight:
1. **The consumer side describes itself.** `testFixtures(project(':adapter:inbound:web'))` says what
it is. `project(path: ':adapter:inbound:web', configuration: 'webTestkit')` says where to look,
and only after the reader has learned that `webTestkit` is a testkit rather than a lane.
2. **The enforcement already exists and is copyable.** `graphql`'s build fails when a fixture is
declared in the wrong place. The same guard applies unchanged to any leaf that adopts the plugin.
3. **It is one fewer local concept.** A convention plugin that reimplements a Gradle plugin has to be
maintained against it.
## What the local plugin does better, and how it is replaced
This is worth writing down, because the review that prompted this ADR recommended the migration
before reading `ca.testkit-publisher`, and the plugin turns out to encode two deliberate decisions
rather than being an oversight.
**Publishing is opt-in.** `persistence-mongo` and `httpclient` have a testkit and publish nothing;
`persistence-jpa` publishes. The plugin's own comment names this as "a real difference in what each
leaf offers rather than an oversight to normalise away". `java-test-fixtures` always creates the
variant, so the distinction is lost — a leaf that never meant to offer its fixtures will offer them.
> Replacement: none at the build level. The distinction moves to review: the fixtures of a leaf that
> nobody consumes are simply unconsumed. This is a real, accepted loss.
**Lane consumption is declared.** `persistence-jpa` says `consumedBy 'test', 'postgresqlIntegrationTest'`.
`java-test-fixtures` puts fixtures on `test` only, so every other lane needs the output added
explicitly.
> Replacement: `strictTestLanes`' existing `compilesAgainst` expresses this unchanged — a lane
> declares `compilesAgainst 'main', 'testFixtures'`. The first draft of this ADR assumed the DSL
> would need a change, because `sourceSet(name)` creates what it is given and `testFixtures` already
> exists. The `persistence-mongo` migration showed otherwise: `compilesAgainst` only *looks a source
> set up*, so naming a plugin-created one works as-is. What the leaf drops is the
> `sourceSet('testkit')` declaration, not the lane's.
## Migration: done, and what it cost
Five leaves, eleven lanes, two published testkits, all migrated leaf by leaf with the suite run
between each. `ca.testkit-publisher` is deleted.
The order was chosen so a mistake would be cheap: unpublished leaves first, published ones last with
their consumer in the same step.
1. `persistence-mongo` — one leaf, two lanes, no cross-module consumer; the proof the path works.
What it took, per leaf:
- `apply plugin: 'java-test-fixtures'` at the top of the leaf build file;
- `git mv src/testkit src/testFixtures`;
- drop `sourceSet('testkit')` and the whole `testkitPublisher` block; keep every other lane's
`compilesAgainst`, renaming `'testkit'` to `'testFixtures'`;
- rename `testkitImplementation` to `testFixturesImplementation`, **and add what the old source
set was inheriting silently**. This is the one non-mechanical step: `testkit*` extended
`testImplementation`, so the fixtures saw every test library the leaf declared. Mongo's needed
four more lines (AssertJ, BSON, Spring Data commons, Toxiproxy) — none of which the leaf had
ever stated the fixtures depended on;
- regenerate the leaf's lock state.
2. `httpclient`, then `websocket` — unpublished as well, more lanes.
3. `web` and `persistence-jpa` with `app-bootstrap`'s two consumer declarations, which became
`testImplementation(testFixtures(project(':…')))`.
4. `ca.testkit-publisher` deleted, along with its `plugins {}` entry and its application in the root
build.
### Two things the migration broke, and what they taught
Both were caught by tests that exist to catch exactly this, which is the argument for having them.
**ArchUnit corpora went wrong in opposite directions.** `httpclient`'s boundary rules *excluded*
`build/classes/java/testkit`; after the move the fixtures arrived as a `…-test-fixtures.jar` on the
same classpath, so the exclusion missed them and 258 fixture-to-fixture calls were reported as
production depending on the testkit. `persistence-jpa`'s rules *included* only
`build/classes/java/main`; applying `java-test-fixtures` makes the module's own test classpath carry
the module as a **jar** rather than as a class directory, so its corpus became empty. The second is
the dangerous one — an empty corpus makes every `noClasses()` rule pass — and it surfaced only
because that suite asserts its corpus is non-empty before asserting anything about it.
**Fixtures had invisible dependencies.** `testkit*` configurations extended `testImplementation`, so
the fixtures compiled against every test library their leaf declared without ever naming one. Making
them explicit took roughly thirty `testFixturesImplementation` lines across the five leaves —
Micrometer, Spring Web, Netty, logback, Jackson, JUnit, AssertJ, Spring Data. None of them were
wrong; none of them were stated.
## Consequences
- `docs/testing/TESTING_STRATEGY.md` §5 records the standard; this ADR records why and at what cost.
- Until step 5, two conventions remain visible. The strategy document says so explicitly, so a
contributor reading it is not left to infer which one is current.
- The opt-in-publishing distinction is given up. If it later proves load-bearing — a leaf whose
fixtures genuinely must not be reachable — the answer is a separate module, not a third convention.
+73
View File
@@ -0,0 +1,73 @@
# CI/CD 경계 — GitHub Actions는 CI, Argo CD는 CD
## 결론
GitHub Actions는 **검증하고 아티팩트를 만든다**. Argo CD는 **배포한다**. 두 역할은 겹치지 않는다.
GitHub Actions 워크플로는 `kubectl apply`, `helm upgrade`, `argocd app sync` 중 어느 것도 하지
않는다. 그러므로 CI에는 클러스터 자격증명(kubeconfig, 서비스 계정 토큰)이 들어가지 않는다.
## 흐름
```text
git push / tag
GitHub Actions ─────────────── CI ───────────────┐
• 테스트 · 정적분석 · 아키텍처 검증 │
• 컨테이너 이미지 빌드 │
• 취약점 스캔 (Trivy) │
• SBOM 생성 │
• 레지스트리에 이미지 push │
│ │
│ 이미지 태그(다이제스트)를 manifest에 기록 │
▼ │
GitOps 저장소 (배포 희망 상태) ──────────────────┘
│ Argo CD가 watch
Argo CD ──────────────────── CD ───────────────
│ auto-sync
Kubernetes
```
용어 한 줄 풀이:
- **GitOps 저장소** — 클러스터에 무엇이 떠 있어야 하는지를 적어 둔 Git 저장소. 애플리케이션 소스와
분리한다.
- **manifest** — Kubernetes에 넣을 YAML(Deployment, Service 등).
- **auto-sync** — Argo CD가 GitOps 저장소의 변경을 스스로 감지해 클러스터에 반영하는 모드. 이걸 쓰면
CI가 Argo CD API 서버에 접근할 필요가 없다.
## 왜 이렇게 나누나
1. **자격증명 반경.** CI가 배포하면 CI 러너가 프로덕션 클러스터에 대한 쓰기 권한을 갖는다. 포크된
PR, 서드파티 액션, 캐시 오염이 모두 그 권한에 닿는다. auto-sync를 쓰면 그 권한은 클러스터 안의
Argo CD에만 있고, CI는 Git에 커밋만 한다.
2. **현재 상태의 소유자가 하나.** 클러스터에 무엇이 떠 있는지는 GitOps 저장소가 답한다. CI가 직접
apply 하면 답이 두 개가 된다 — Git에 적힌 것과 실제로 떠 있는 것.
3. **롤백이 revert.** 배포를 되돌리는 것이 `git revert`가 된다.
## 이 저장소의 현재 위치
| 항목 | 상태 |
| --- | --- |
| 이미지 빌드/스캔/push | `release.yml`이 수행 |
| SBOM | `release.yml`이 생성 |
| 이미지 서명 · provenance attestation | **없음.** 추가 대상 |
| GitOps 저장소 | **없음.** 별도 저장소로 만들 예정 |
| Argo CD Application 정의 | **없음.** GitOps 저장소에 둘 예정 |
| CI에서의 클러스터 접근 | 없음 — 유일했던 `kubectl apply`는 제거됨 |
`fileserver-certification.yml`은 예외처럼 보이지만 아니다. PVC 매니페스트가 여전히 ReadWriteOnce를
선언하는지 **파일만** 확인하고, 클러스터에는 아무것도 적용하지 않는다. 실제 클러스터에서의 인증은
운영자가 `infra/fileserver/kubernetes/pvc-certification-job.yaml`을 직접 실행하고
`docs/fileserver/storage-certification.md`에 기록한다. 이름을 `fileserver-release.yml`에서 바꾼 이유가
이것이다 — 이 워크플로는 릴리스하지 않는다.
## 규칙
- 워크플로에 클러스터 자격증명 secret을 추가하지 않는다.
- 배포 대상이 바뀌면 GitOps 저장소의 manifest를 바꾼다. 워크플로를 바꾸지 않는다.
- CI가 만드는 것은 **불변 다이제스트로 지정된 이미지**다. `latest` 태그로 배포하지 않는다.
+50
View File
@@ -0,0 +1,50 @@
# Template maintainer와 Template consumer의 검증은 다르다
## 결론
이 저장소에는 성격이 다른 두 종류의 검증이 섞여 있다.
1. **스켈레톤을 만드는 사람**에게 필요한 검증 — sample 모듈이 정말 제거 가능한가, optional 모듈
조합이 모두 빌드되는가, 레지스트리가 확장 가능한가.
2. **스켈레톤을 가져다 서비스를 만드는 사람**에게 필요한 검증 — 내 애플리케이션의 테스트,
아키텍처 방향, 보안, 릴리스.
파생 프로젝트가 1번을 그대로 물려받으면, 자기 서비스와 아무 상관 없는 게이트를 평생 유지하게 된다.
이 문서는 어느 쪽이 어느 쪽인지 적어 둔다.
## Template 전용 (파생 프로젝트는 삭제해도 된다)
| 대상 | 무엇을 지키는가 |
| --- | --- |
| `:app-bootstrap:sampleOffTest`, `ci-quality-gates.yml``sample-off` job | sample 픽스처를 지워도 애플리케이션이 빌드·부팅되는가 |
| `sample-portfolio` leaf 전체 | 참조 구현 |
| `Dockerfile.sample`, `docker-compose.*` 중 sample 관련 | 위와 동일 |
| `docs/superpowers/**` | 이 템플릿을 만든 과정의 설계/계획 기록 |
| `gradle/qualification/**` | 이 템플릿이 벤더링한 플랫폼(JPA, messaging)의 인증 체계 |
| `*-certification.yml`, `*-qualification.yml`, `jpa-next-*.yml` | 템플릿이 광고하는 지원 매트릭스의 근거 |
## Consumer 필수 (파생 프로젝트가 유지해야 한다)
| 대상 | 무엇을 지키는가 |
| --- | --- |
| `architectureCheck` | Clean Architecture 의존 방향. 이 템플릿의 존재 이유 |
| 각 leaf의 `check` | 컴파일 · 단위 테스트 · 포맷 · 스타일 · Error Prone |
| `qualityCheck` | SpotBugs / FindSecBugs |
| `configContractCheck` | 환경변수 계약 |
| `verifyDependencyLocks` | 재현 가능한 의존성 해석 |
| `dependency-vulnerability.yml` | dependency-review + Trivy |
| `ci-quality-gates.yml` | PR 게이트 |
| `release.yml` | 이미지 · SBOM 생산 |
| action의 full SHA 핀 | 공급망 |
## 파생 프로젝트가 할 일
1. Template 전용 표의 항목을 삭제한다. 삭제는 대부분 파일 삭제 + `config/architecture/modules.json`
에서 leaf 항목 제거로 끝난다 — 레지스트리가 leaf 목록의 SSOT이고, 개수를 따로 적어 둔 곳은 없다.
2. `docs/ci-cd/boundary.md`의 경계를 그대로 유지한 채 자기 GitOps 저장소를 연결한다.
3. `.trivyignore.yaml`과 CODEOWNERS는 그대로 쓴다.
## 아직 하지 않은 것
Template CI와 Generated Application CI를 **물리적으로** 분리하지는 않았다(생성기 없음). 지금은 이
문서가 그 경계다. 생성기를 만든다면, 위 표의 "Template 전용" 열이 생성기가 벗겨 내야 할 목록이다.
+1 -1
View File
@@ -12,7 +12,7 @@
# #
# Only APP_HTTPCLIENT_ENABLED is registered in docs/registries/env-keys.yaml and shipped in # Only APP_HTTPCLIENT_ENABLED is registered in docs/registries/env-keys.yaml and shipped in
# src/.env: it is the only key with a deployment-independent value, and it is the only one the # src/.env: it is the only key with a deployment-independent value, and it is the only one the
# three-way verifyEnvKeys gate can express. Everything below is per deployment and is set directly # three-way :app-bootstrap:verifyEnvKeys gate can express. Everything below is per deployment and is set directly
# in the environment — templating an indexed client in application.yml would materialise a nameless # in the environment — templating an indexed client in application.yml would materialise a nameless
# client in every deployment, which the settings' aggregate validation refuses. # client in every deployment, which the settings' aggregate validation refuses.
# #
+1 -1
View File
@@ -101,7 +101,7 @@ Docker-dependent lanes fail closed rather than skipping, matching the existing
| `settings.gradle.kts` module registration | Fail-closed 19-leaf registry | No registry change: leaf identity, Gradle path, allowed dependencies, and runtime memberships are unchanged. | | `settings.gradle.kts` module registration | Fail-closed 19-leaf registry | No registry change: leaf identity, Gradle path, allowed dependencies, and runtime memberships are unchanged. |
| `infra/jpa/{postgres,roles,toxiproxy}` | Repository already owns `infra/` | Created at the same repository-relative paths. | | `infra/jpa/{postgres,roles,toxiproxy}` | Repository already owns `infra/` | Created at the same repository-relative paths. |
| `docs/jpa/**`, `docs/adr/ADR-JPA-*`, `.github/workflows/jpa-*.yml` | Repository already owns `docs/` and `.github/workflows/` | Created at the same repository-relative paths. | | `docs/jpa/**`, `docs/adr/ADR-JPA-*`, `.github/workflows/jpa-*.yml` | Repository already owns `docs/` and `.github/workflows/` | Created at the same repository-relative paths. |
| `build.gradle.kts` release aggregate `jpaReleaseGate` | Root is `src/build.gradle` | Registered there against the repository lane names in §3. | | release blocking aggregate | `.github/workflows/jpa-release.yml` | CI names the blocking JPA lanes directly; Gradle only defines how each lane runs. |
| Per-task `git add` + `git commit` | `AGENTS.md`: commit policy is `human-only`; agents do not stage, commit, amend, or push | Implementation is delivered unstaged. This is the only plan step intentionally not executed, and it is recorded here. | | Per-task `git add` + `git commit` | `AGENTS.md`: commit policy is `human-only`; agents do not stage, commit, amend, or push | Implementation is delivered unstaged. This is the only plan step intentionally not executed, and it is recorded here. |
| Querydsl as an optional module dependency | Querydsl is not part of this repository's dependency set | `querydsl` is implemented against the plan's contracts with the Querydsl types kept behind `compileOnly`, so the Stable runtime classpath never carries Querydsl and a deployment opting in adds the artifact itself. | | Querydsl as an optional module dependency | Querydsl is not part of this repository's dependency set | `querydsl` is implemented against the plan's contracts with the Querydsl types kept behind `compileOnly`, so the Stable runtime classpath never carries Querydsl and a deployment opting in adds the artifact itself. |
| Hibernate Envers as a module dependency | Envers is not part of this repository's dependency set | Same treatment as Querydsl: `compileOnly` + explicit opt-in, matching the plan's "Envers is opt-in and never enabled by a global base class". | | Hibernate Envers as a module dependency | Envers is not part of this repository's dependency set | Same treatment as Querydsl: `compileOnly` + explicit opt-in, matching the plan's "Envers is opt-in and never enabled by a global base class". |
+23
View File
@@ -13,6 +13,29 @@
> 어떤 binder도 그것을 읽지 않았다 — 문서대로 설정한 배포는 아무것도 바뀌지 않았고 아무 말도 듣지 > 어떤 binder도 그것을 읽지 않았다 — 문서대로 설정한 배포는 아무것도 바뀌지 않았고 아무 말도 듣지
> 못했다 (MSG-008). > 못했다 (MSG-008).
## Application publish bridge identity
Application의 canonical integration event를 platform publish pipeline으로 보낼 때는
`app.messaging.producer-id`를 명시한다. 같은 값의 환경변수 이름은
`APP_MESSAGING_PRODUCER_ID`다. 이 값은 host/pod 이름이 아니라 배포와 무관하게 유지되는 논리적
producing-service identity다.
값이 없으면 `IntegrationEventPublishPort` bridge 자체를 만들지 않는다. `spring.application.name`이나
현재 process 이름으로 추론하지 않는다. 기존 legacy `OutboxEvent`/realtime 경로는 별도 cutover가
끝날 때까지 `app.messaging.broker` 경로를 유지한다.
## Outbox canonical transport-only cutover
`APP_OUTBOX_CANONICAL_TRANSPORT_ENABLED` / `ca-skeleton.outbox.canonical-transport-enabled`
기존 `outbox_event` writer/claim/status authority를 유지한 채 canonical row의 **transport만** platform
publish path로 보내는 compatibility gate다. 기본값은 `false`이며 `POLLING_V2`를 활성화하지 않는다.
`true`일 때는 `OutboxAppendPort``ValidatedIntegrationEvent`의 exact envelope bytes와 canonical
metadata를 기존 outbox row에 저장하고, claim된 canonical row는 `IntegrationEventPublishPort`로 간다.
legacy row는 계속 `MessageBroker`를 사용한다. 따라서 mixed-row compatibility 기간에는 relay가 켜져
있다면 `app.messaging.broker`도 계속 필요하며, canonical path를 위해 `IntegrationEventPublishPort`
추가로 필요하다. legacy backlog가 0이라는 별도 증거 없이 broker 요구를 제거하지 않는다.
## Destination profile ## Destination profile
```yaml ```yaml
+1 -1
View File
@@ -5,7 +5,7 @@
```bash ```bash
./gradlew verifyCleanArchitectureDependencies --console=plain ./gradlew verifyCleanArchitectureDependencies --console=plain
./gradlew verifyRuntimeModuleMembership --console=plain ./gradlew verifyRuntimeModuleMembership --console=plain
./gradlew verifyOneTypePerFile --console=plain ./gradlew checkstyleMain --console=plain
``` ```
destination profile은 startup에서 검증된다. 아래는 **부팅 실패**다. destination profile은 startup에서 검증된다. 아래는 **부팅 실패**다.
@@ -32,6 +32,8 @@ dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.Notification
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.ProviderRuntimeAssembler dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.ProviderRuntimeAssembler
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.ProviderType dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.ProviderType
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.SmtpProviderRuntimeAssembler dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.SmtpProviderRuntimeAssembler
dev.caskeleton.adapter.outbound.notification.platform.callback.MapProviderCallbackAdapterRegistry
dev.caskeleton.adapter.outbound.notification.platform.callback.MapProviderEventProjectorRegistry
dev.caskeleton.adapter.outbound.notification.platform.dispatch.AttemptPermit dev.caskeleton.adapter.outbound.notification.platform.dispatch.AttemptPermit
dev.caskeleton.adapter.outbound.notification.platform.dispatch.CapabilityReconciliationGateway dev.caskeleton.adapter.outbound.notification.platform.dispatch.CapabilityReconciliationGateway
dev.caskeleton.adapter.outbound.notification.platform.dispatch.ConfiguredProfileCatalog dev.caskeleton.adapter.outbound.notification.platform.dispatch.ConfiguredProfileCatalog
@@ -57,6 +59,7 @@ dev.caskeleton.adapter.outbound.notification.platform.dispatch.SingleTenantConte
dev.caskeleton.adapter.outbound.notification.platform.dispatch.UuidV7Generator dev.caskeleton.adapter.outbound.notification.platform.dispatch.UuidV7Generator
dev.caskeleton.adapter.outbound.notification.platform.observation.LoggingNotificationAudit dev.caskeleton.adapter.outbound.notification.platform.observation.LoggingNotificationAudit
dev.caskeleton.adapter.outbound.notification.platform.observation.LoggingNotificationMetrics dev.caskeleton.adapter.outbound.notification.platform.observation.LoggingNotificationMetrics
dev.caskeleton.adapter.outbound.notification.platform.observation.MicrometerNotificationMetrics
dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationHealthReporter dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationHealthReporter
dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationHealthSnapshot dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationHealthSnapshot
dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationServingThresholds dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationServingThresholds
+32 -1
View File
@@ -1897,7 +1897,7 @@ env_keys:
# Bound only by RedisSdkAutoConfiguration, which exists only while APP_REDIS_ENABLED # Bound only by RedisSdkAutoConfiguration, which exists only while APP_REDIS_ENABLED
# is true. They are deliberately absent from application.yml and src/.env: putting # is true. They are deliberately absent from application.yml and src/.env: putting
# them there would make a Redis-free deployment carry Redis configuration, which is # them there would make a Redis-free deployment carry Redis configuration, which is
# the defect the conditional composition root removes. verifyEnvKeys checks them # the defect the conditional composition root removes. :app-bootstrap:verifyEnvKeys checks them
# against spring-configuration-metadata.json instead. # against spring-configuration-metadata.json instead.
- name: APP_REDIS_ACKNOWLEDGED_WRITE_LOSS_ACCEPTED - name: APP_REDIS_ACKNOWLEDGED_WRITE_LOSS_ACCEPTED
@@ -3325,6 +3325,21 @@ env_keys:
compatibility_impact: behavior-change compatibility_impact: behavior-change
required_test: adapter-contract:messaging-broker-selection required_test: adapter-contract:messaging-broker-selection
- name: APP_MESSAGING_PRODUCER_ID
# source: canonical messaging platform bridge 2026-09-18
# Explicit logical producing-service identity for IntegrationEventPublishPort.
# Blank/absent = canonical platform bridge is not exposed; identity is never inferred.
type: string
default: null
allowed_values: null
classification: public-config
required: false
reload_policy: restart-only
owner_branch: feature-integration-adapter-templates
validation: none
compatibility_impact: additive
required_test: adapter-contract:messaging-platform-producer-id
- name: APP_MESSAGING_KAFKA_BROKERS - name: APP_MESSAGING_KAFKA_BROKERS
# source: feature-domain-event-outbox-contract — "Kafka는 optional integration adapter" # source: feature-domain-event-outbox-contract — "Kafka는 optional integration adapter"
# (broker 활성화 시 endpoint 필요) # (broker 활성화 시 endpoint 필요)
@@ -4358,6 +4373,22 @@ env_keys:
validation: boolean validation: boolean
compatibility_impact: behavior-change compatibility_impact: behavior-change
required_test: adapter-contract:outbox-capability-disabled-safe required_test: adapter-contract:outbox-capability-disabled-safe
- name: APP_OUTBOX_CANONICAL_TRANSPORT_ENABLED
# source: MSG-015 transport-only cutover 2026-09-18
# Enables canonical outbox rows/platform transport without switching publication authority.
type: boolean
default: false
allowed_values:
- "true"
- "false"
classification: public-config
required: false
reload_policy: restart-only
owner_branch: feature-integration-adapter-templates
validation: boolean
compatibility_impact: additive
required_test: app-bootstrap:outbox-canonical-transport-gate
- name: APP_OUTBOX_RELAY_ENABLED - name: APP_OUTBOX_RELAY_ENABLED
# source: five-adapter-runtime-remediation §6.3 MSG-INT-001 — starts the relay scheduler. # source: five-adapter-runtime-remediation §6.3 MSG-INT-001 — starts the relay scheduler.
# Requires APP_OUTBOX_ENABLED, APP_PERSISTENCE_JPA_ENABLED and APP_MESSAGING_ENABLED with a # Requires APP_OUTBOX_ENABLED, APP_PERSISTENCE_JPA_ENABLED and APP_MESSAGING_ENABLED with a
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
# Messaging R2 자격(qualification) — 미구현
추적: MSG-015
## 상태
**구현되지 않았다.** R2 자격을 주장할 수 있는 근거가 없다.
- qualification producer 없음
- 대응하는 Test 태스크 없음
- 공통 스키마 validator 없음
따라서 `config/messaging/readiness-cards.yaml`의 카드는 `verifyMessagingContracts`
`verifyMessagingJsonSchemaV1` 두 개를 제외하면 모두 `maturity: not-implemented`다.
## 왜 Gradle 태스크를 미리 만들어 두지 않는가
2026-09 이전에는 루트 빌드가 아래 아홉 개 태스크 이름을 미리 등록해 두고, 그 본문이 **입력과 무관하게
무조건 예외를 던졌다**.
```text
verifyMessagingPollingOutboxR2 verifyMessagingTargetBinding
verifyMessagingKafkaProducerR2 verifyMessagingDeploymentCutover
verifyMessagingSecurityR2 verifyMessagingCleanupTargetBinding
verifyMessagingReleaseProfile verifyMessagingFinalR2Profile
verifyMessagingTargetBindingPreflight
```
의도는 "fail-closed"였지만 결과는 다음과 같았다.
- `./gradlew tasks`에 게이트처럼 보이는 이름 아홉 개가 나타난다.
- `dependsOn`으로 걸 수 있다. 거는 순간 그 레인은 영원히 빨간불이다.
- 정상적인 입력으로도 성공할 수 없으므로 "검증"이 아니다.
즉 TODO를 Gradle 태스크 API로 표현한 것이었다. 미구현 사실을 기록하는 자리는 이 문서이고, 태스크는
**실제로 통과할 수 있게 된 시점에** 그 producer와 함께 추가한다.
## 구현 시 추가할 것
1. 각 시나리오를 실제로 실행하는 Test 태스크.
2. 그 실행 결과(JUnit XML)에서 payload-free manifest를 만드는 producer.
3. `config/messaging/evidence/build-evidence-manifest-v1.schema.json`으로 그 manifest 바이트를
검증하는 finalizer.
4. 위 셋이 모두 생긴 다음에 `verifyMessaging<Scenario>R2` 태스크 등록.
`gradle/qualification/messaging-qualification.gradle``verifyMessagingJsonSchemaV1`이 그 네 단계를
모두 갖춘 예시다.
+4 -2
View File
@@ -1,4 +1,6 @@
# feature-security-operational-baseline D5 — deny-by-default public path snapshot. # feature-security-operational-baseline D5 — deny-by-default public path snapshot.
# SSOT: SECURITY_PUBLIC_PATHS (src/.env) -> SecurityConfig permitAll(); anyRequest authenticated. # SSOT: ca-skeleton.security.public-paths default in app-bootstrap/src/main/resources/config/security.yml
# -> SecurityConfig permitAll(); anyRequest authenticated. An operator's own SECURITY_PUBLIC_PATHS
# overrides it at run time and is outside this snapshot.
# Update only after review with: ./gradlew updatePublicPathSnapshot -PapprovePublicPathChange # Update only after review with: ./gradlew updatePublicPathSnapshot -PapprovePublicPathChange
/api/healthcheck /v1/healthcheck
File diff suppressed because it is too large Load Diff
@@ -79,7 +79,7 @@ Run:
--tests 'dev.caskeleton.application.outbox.*' --console=plain --tests 'dev.caskeleton.application.outbox.*' --console=plain
./gradlew :adapter:outbound:messaging:test --console=plain ./gradlew :adapter:outbound:messaging:test --console=plain
./gradlew :app-bootstrap:test \ ./gradlew :app-bootstrap:test \
--tests 'dev.caskeleton.bootstrap.outbox.*' --console=plain --tests 'dev.caskeleton.bootstrap.autoconfigure.outbox.*' --console=plain
./gradlew verifyApplicationCoreDependencyPurity --console=plain ./gradlew verifyApplicationCoreDependencyPurity --console=plain
./gradlew :application-core:dependencies \ ./gradlew :application-core:dependencies \
--configuration runtimeClasspath --console=plain --configuration runtimeClasspath --console=plain
@@ -2899,12 +2899,12 @@ authority and destructive downgrade are forbidden.
dev.caskeleton.adapter.outbound.persistence.outbox.OutboxClaimRepository dev.caskeleton.adapter.outbound.persistence.outbox.OutboxClaimRepository
dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlOutboxClaimRepository dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlOutboxClaimRepository
dev.caskeleton.adapter.outbound.persistence.outbox.OutboxReaper dev.caskeleton.adapter.outbound.persistence.outbox.OutboxReaper
dev.caskeleton.bootstrap.outbox.OutboxLeaderElectionToken dev.caskeleton.bootstrap.autoconfigure.outbox.OutboxLeaderElectionToken
dev.caskeleton.bootstrap.outbox.OutboxLegacyToV2CutoverCoordinator dev.caskeleton.bootstrap.autoconfigure.outbox.OutboxLegacyToV2CutoverCoordinator
dev.caskeleton.bootstrap.outbox.OutboxLegacyPreCommitRecoveryCoordinator dev.caskeleton.bootstrap.autoconfigure.outbox.OutboxLegacyPreCommitRecoveryCoordinator
dev.caskeleton.bootstrap.outbox.LegacyOutboxRelayControlAdapter dev.caskeleton.bootstrap.autoconfigure.outbox.LegacyOutboxRelayControlAdapter
dev.caskeleton.bootstrap.outbox.MessagingAuthorityCutoverJobSettings dev.caskeleton.bootstrap.autoconfigure.outbox.MessagingAuthorityCutoverJobSettings
dev.caskeleton.bootstrap.outbox.MessagingAuthorityCutoverApplicationRunner dev.caskeleton.bootstrap.autoconfigure.outbox.MessagingAuthorityCutoverApplicationRunner
OutboxEventJpaRepository.deletePublishedBefore OutboxEventJpaRepository.deletePublishedBefore
OutboxEventJpaRepository.countGroupedByStatus OutboxEventJpaRepository.countGroupedByStatus
OutboxEventJpaRepository.findOldestUnpublishedOccurredAtByEventType OutboxEventJpaRepository.findOldestUnpublishedOccurredAtByEventType
@@ -162,7 +162,7 @@ into the new test package, rewritten to run through the single auto-configuratio
`@Bean @ConditionalOnMissingBean(Clock.class) Clock httpClientClock()`. Once the whole capability is `@Bean @ConditionalOnMissingBean(Clock.class) Clock httpClientClock()`. Once the whole capability is
gated, that bean would vanish whenever HTTP Client is off — and Redis, idempotency and the Fileserver gated, that bean would vanish whenever HTTP Client is off — and Redis, idempotency and the Fileserver
all inject `Clock`. The application context is unaffected because all inject `Clock`. The application context is unaffected because
`dev.caskeleton.bootstrap.idempotency.IdempotencyConfig#systemClock` declares one unconditionally in `dev.caskeleton.bootstrap.autoconfigure.idempotency.IdempotencyConfig#systemClock` declares one unconditionally in
a scanned package, so the httpclient copy is redundant *in the application* and dangerous *in the a scanned package, so the httpclient copy is redundant *in the application* and dangerous *in the
gate*. Isolated `ApplicationContextRunner` tests must supply their own, exactly as gate*. Isolated `ApplicationContextRunner` tests must supply their own, exactly as
`FileserverPlatformAutoConfigurationTest` supplies a `MeterRegistry`. `FileserverPlatformAutoConfigurationTest` supplies a `MeterRegistry`.
@@ -382,7 +382,7 @@ public final class AdapterActivationInventory {
NOTIFICATION( NOTIFICATION(
Set.of( Set.of(
"dev.caskeleton.adapter.outbound.notification", "dev.caskeleton.adapter.outbound.notification",
"dev.caskeleton.bootstrap.notification"), "dev.caskeleton.bootstrap.autoconfigure.notification"),
Set.of(), Set.of(),
"notification-"), "notification-"),
GRAPHQL( GRAPHQL(
@@ -668,7 +668,7 @@ class CompositionScanNarrownessTest {
Pattern excluded = Pattern.compile(regexExcludeOf(APPLICATION.getAnnotation(ComponentScan.class).excludeFilters())); Pattern excluded = Pattern.compile(regexExcludeOf(APPLICATION.getAnnotation(ComponentScan.class).excludeFilters()));
for (String type : for (String type :
new String[] { new String[] {
"dev.caskeleton.bootstrap.autoconfigure.persistencejpa.PersistenceJpaRootAutoConfiguration", "dev.caskeleton.bootstrap.autoconfigure.jpa.PersistenceJpaRootAutoConfiguration",
"dev.caskeleton.adapter.outbound.mongo.MongoPersistenceConfig", "dev.caskeleton.adapter.outbound.mongo.MongoPersistenceConfig",
"dev.caskeleton.adapter.outbound.messaging.MessagingSettings", "dev.caskeleton.adapter.outbound.messaging.MessagingSettings",
"dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationPlatformSettings", "dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationPlatformSettings",
@@ -800,7 +800,7 @@ design:
- [ ] **Step 1: Write the failing test** - [ ] **Step 1: Write the failing test**
```java ```java
package dev.caskeleton.bootstrap.autoconfigure.persistencejpa; package dev.caskeleton.bootstrap.autoconfigure.jpa;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -871,7 +871,7 @@ class JpaMasterGateTest {
- [ ] **Step 3: Write `DataSourceRequirement`** - [ ] **Step 3: Write `DataSourceRequirement`**
```java ```java
package dev.caskeleton.bootstrap.autoconfigure.persistencejpa; package dev.caskeleton.bootstrap.autoconfigure.jpa;
import dev.caskeleton.shared.activation.MasterSwitch; import dev.caskeleton.shared.activation.MasterSwitch;
import java.util.ArrayList; import java.util.ArrayList;
@@ -943,7 +943,7 @@ public final class DataSourceRequirement {
- [ ] **Step 4: Write the off-filter** - [ ] **Step 4: Write the off-filter**
```java ```java
package dev.caskeleton.bootstrap.autoconfigure.persistencejpa; package dev.caskeleton.bootstrap.autoconfigure.jpa;
import java.util.Set; import java.util.Set;
import org.springframework.boot.autoconfigure.AutoConfigurationImportFilter; import org.springframework.boot.autoconfigure.AutoConfigurationImportFilter;
@@ -1008,7 +1008,7 @@ public final class JpaOffAutoConfigurationImportFilter
Create `PersistenceJpaRootAutoConfiguration`: Create `PersistenceJpaRootAutoConfiguration`:
```java ```java
package dev.caskeleton.bootstrap.autoconfigure.persistencejpa; package dev.caskeleton.bootstrap.autoconfigure.jpa;
import dev.caskeleton.bootstrap.autoconfigure.jpa.JpaPlatformRuntimeAutoConfiguration; import dev.caskeleton.bootstrap.autoconfigure.jpa.JpaPlatformRuntimeAutoConfiguration;
import dev.caskeleton.bootstrap.autoconfigure.jpa.JpaSafetySettings; import dev.caskeleton.bootstrap.autoconfigure.jpa.JpaSafetySettings;
@@ -1048,9 +1048,9 @@ Then:
owns that import — and keep the vendor-specific beans. owns that import — and keep the vendor-specific beans.
- Remove `dev.caskeleton.bootstrap.autoconfigure.jpa.JpaPlatformRuntimeAutoConfiguration` from - Remove `dev.caskeleton.bootstrap.autoconfigure.jpa.JpaPlatformRuntimeAutoConfiguration` from
`AutoConfiguration.imports` and add `AutoConfiguration.imports` and add
`dev.caskeleton.bootstrap.autoconfigure.persistencejpa.PersistenceJpaRootAutoConfiguration`. `dev.caskeleton.bootstrap.autoconfigure.jpa.PersistenceJpaRootAutoConfiguration`.
- Create `AutoConfigurationImportFilter.imports` containing - Create `AutoConfigurationImportFilter.imports` containing
`dev.caskeleton.bootstrap.autoconfigure.persistencejpa.JpaOffAutoConfigurationImportFilter`. `dev.caskeleton.bootstrap.autoconfigure.jpa.JpaOffAutoConfigurationImportFilter`.
- [ ] **Step 6: Run the test to verify it passes.** - [ ] **Step 6: Run the test to verify it passes.**
Run: `cd src && ./gradlew :app-bootstrap:test --tests '*JpaMasterGateTest*' --console=plain --no-daemon` Run: `cd src && ./gradlew :app-bootstrap:test --tests '*JpaMasterGateTest*' --console=plain --no-daemon`
@@ -0,0 +1,212 @@
# CI 단계 분리 + 컨테이너 릴리스 도입
- 작성: 2026-09-16
- 상태: Track A 완료(커밋 3개) / Track B 진행 중 / Track C 대기
- 근거 감사: 빌드·CI 레이어 전수 리뷰 133건 (파일 110개 / 12,800줄)
## 확정된 결정
1. **배포 단위는 `app-bootstrap` 하나.** 어댑터는 독립 배포되지 않는다.
따라서 release 워크플로는 8개가 아니라 1개다. 어댑터별로 필요한 것은
release가 아니라 PR 단계의 선택적 테스트다.
2. **GitOps 매니페스트는 별도 repo.** 단, 이번 작업 범위 밖이다.
이 repo는 "이미지를 만들고 태그를 확정"하는 데서 끝난다.
ArgoCD Application 정의와 매니페스트는 이미지가 생긴 뒤 착수한다.
3. **작업은 main에서 직접 하고 커밋한다** (사용자 지시).
`CLAUDE.md:36``commit policy is human-only` 와 충돌하므로
그 줄도 이번에 함께 갱신한다. push 는 하지 않는다.
## 현재 구조의 문제 — 한 줄
**워크플로가 단계가 아니라 모듈로 쪼개져 있다.**
28개 워크플로가 전부 "어느 모듈이냐"(`web-*`, `jpa-*`, `httpclient-*` …)로 갈렸고
"어느 단계냐"로는 갈리지 않았다. 결과:
- 같은 성격의 일이 9개 파일에 흩어진다
- 한 파일 안에 PR 검증과 릴리스 게이트가 섞인다
- 동일한 13줄 setup 블록이 35회 복붙됐다 (CI 366줄)
- 어떤 게이트가 개발을 막고 어떤 게이트가 안 막는지 파일만 봐서는 모른다
이 상태에서는 "이 게이트가 쓸모 있나"를 물을 수 없다. 단계가 하나뿐이면
모든 게이트가 똑같이 개발을 막기 때문이다.
## 목표 구조
| 단계 | 답하는 질문 | 예산 | 트리거 | 실패 시 |
| --- | --- | --- | --- | --- |
| 1 PR 게이트 | 이 diff가 안전한가 | 5분 | PR, 변경 모듈만 | 머지 차단 |
| 2 통합 | 합쳐진 상태가 건강한가 | 30분 | main push | 알림, 머지는 이미 끝남 |
| 3 릴리스 | 배포 가능한 산출물 생성 | — | tag | 릴리스 중단 |
| 4 CD | 클러스터를 산출물로 수렴 | — | ArgoCD 폴링 | (이번 범위 밖) |
### 게이트 재배치 원칙
감사에서 나온 B등급 36건(어겨도 프로그램은 도는 문서·네이밍·개수 검증)은
**삭제 여부를 논쟁하지 않는다. 단계를 지정한다.**
- 1단계: 컴파일, 의존성 방향, 잠금파일, 시크릿·취약점 스캔, 변경 모듈 테스트
- 2단계: 문서-코드 일치, 공개 경로 스냅샷, env 키, 전체 테스트, 느린 통합 테스트
- 3단계: 이미지 빌드, SBOM, 서명, 릴리스 차단 게이트 집계
1단계에 있으면 개발을 막고, 2단계에 있으면 안 막는다. 이 배치가
"쓸모 있나"라는 질문을 대체한다.
## 작업 순서
### Track A — 깨진 게이트 (진행 중)
설계 논쟁이 필요 없는 E등급 24건. 구조는 건드리지 않는다.
핵심: **아키텍처 게이트가 규칙을 0개 실행하고 있다.**
워크플로 7곳과 `CLAUDE.md:109``--tests '*CleanArchitectureTest'` 를 지정하는데
그 이름의 클래스는 존재하지 않는다. 실재하는 ArchUnit 테스트는 17개
(`AdapterBoundaryArchitectureTest`, `DomainPurityArchitectureTest` 등, ArchUnit 사용 37개 클래스).
4곳은 하드 실패하고, 3곳은 다른 필터와 병기돼 아키텍처 규칙 0개를 돌고 초록으로 통과한다.
그 외: `release_blocking` 미강제(trivy-fs 가 빨개도 release-gate 초록),
`strict-test-lane` 이 skip 을 실행으로 셈, `public-path-snapshot` 이 gitignore 된 `src/.env` 를 읽음,
`verifyEnvKeys` 가 build 산출물을 소스로 읽음, `jpa-next-*` 3개가 실제 테스트 없이 continue-on-error,
`spring70CompatibilityTest` fail-closed 상실, `fileserver-pr` 존재하지 않는 path 필터.
### Track B — 단계 분리
1. 재사용 워크플로(`workflow_call`) + composite action 으로 setup 블록 공통화
2. release 워크플로 8개 → 1개. 릴리스 태그 네임스페이스 분열 버그도 여기서 해소
3. PR 단계는 경로 필터로 변경 모듈만 실행
4. 게이트를 위 표대로 재배치. 문서 검증류는 2단계로 내린다
5. `ci-gate-matrix.yml` 이 실제로 release 차단을 강제하도록 연결
(이 파일은 죽은 문서가 아니다 — `ci-quality-gates.yml:88` 이 런타임 파싱한다)
6. 도달 불가 Gradle 태스크 39개 정리 (grpc 워크플로가 0개인 것이 주원인)
### Track C — 컨테이너 릴리스 (축소된 범위)
지금 없는 것: 이미지 빌드·푸시. `src/Dockerfile` 은 있으나
`build-push-action` / `bootBuildImage` / `jib` 사용처가 0건이다.
`*-release.yml` 8개는 테스트 실행 + evidence 업로드로 끝난다 — 이름만 릴리스다.
1. 3단계 릴리스 워크플로에 이미지 빌드 + 레지스트리 푸시 추가 (기본 ghcr.io)
2. 태그 규칙 확정 (semver + git sha)
3. SBOM 생성, 이미지 스캔
4. **CI 는 배포하지 않는다.** `kubectl apply` 를 CI 에 넣지 않는다 — GitOps 원칙.
현재 그런 코드가 없으므로 걷어낼 것도 없다.
이후(별도 작업): GitOps repo, 매니페스트, ArgoCD Application, image tag bump 연결.
## 검증
- Track A 완료 시: 변경한 YAML 전수 파싱, 아키텍처 테스트가 실제로 실행되는지 확인
- Track B 완료 시: `verifyCleanArchitectureDependencies`, 아키텍처 테스트, 워크플로 파싱
- 각 Track 종료 시 무엇을 실행했고 무엇을 실행하지 못했는지 명시한다.
실행하지 못한 검증은 "실행하지 않음"이라고 적는다.
## 감사 산출물
- 루브릭: `scratchpad/gradle-audit/RUBRIC.md`
- finding 전체(243KB, `file:line` 근거): `scratchpad/gradle-audit/findings/R1~R8.md`
- 등급 분포: A=25 B=36 C=17 D=31 E=24 · 정리 시 3,368줄 감소 추정
- 아키텍처 위반 0건 (`modules.json` 전수 대조, messaging/grpc 격리 확인)
## 진행 기록
### Track A — 완료 (2026-09-16)
커밋 `2a8d34f` docs / `e345191` fix(ci) / `1535481` refactor(build,src).
E등급 19건 처리. 깨끗한 worktree 체크아웃에서 검증:
`./gradlew help` 통과, 아키텍처 게이트 20개 클래스 174 tests 실패 0 스킵 0,
`-p build-logic test` 통과, `verify-gradle-wrapper.sh` PASS,
`verify-gate-matrix.sh` OK.
감사 범위 밖이었으나 main 이 실제로 깨져 있던 것 두 건도 함께 고쳤다:
`src/gradle/libs.versions.toml``app-bootstrap config/*.yml` 15개가
git 에 없어 깨끗한 체크아웃에서 빌드가 불가능했다.
**절차 교훈**: 커밋을 4번 시도해 전부 되돌렸다. 원인은 깨끗한 체크아웃 검증을
커밋 ** 에 한 것. 이후로는 워킹트리를 커밋하지 않고 후보 커밋 객체로 만들어
새 체크아웃에서 검증한 뒤에만 커밋한다.
### 감사 findings 중 서브에이전트가 반박해 기각한 것
리뷰 결과를 그대로 집행하지 않는다. 수정 담당이 근거를 들어 반박한 건은 기각한다.
- `runtimeClasspathManifest` "출력 미사용" — 거짓. `RuntimeMembershipClasspathAgreementTest`
읽고 `app-bootstrap/build.gradle:415``test dependsOn` 이 걸려 있다
- JUnit 태그 3중 재설정 "충돌" — Gradle 9.0.0 에서 `useJUnitPlatform{}` 은 누적된다(실측)
- `persistence-jpa:226` outbound→inbound — 클래스패스가 아닌 태스크 엣지라 게이트 범위 밖
- `ca.api-surface` "소비자 없음" — 거짓. `ci-gate-matrix.yml:90-100`
`verify-gate-matrix.sh` 가 소비한다. 유지하고 정규식 렌더러만 javac 파싱으로 교체
- `ca.dependency-policy` "의존성 잠금이 대체한다" — 거짓. 락파일은 무엇이 있는지를
기록할 뿐 무엇이 없어야 하는지를 막지 않고, `--write-locks` 는 추가를 조용히 수용한다
- `ca.runtime-membership``moduleRegistryRepositoryRoot` "미사용" — 거짓.
`RuntimeMembershipFunctionalTest.java:123` 이 쓴다
### 아키텍처 게이트가 실제로 뭘 검사하는지 측정 (2026-09-16)
`allowEmptyShould(true)` 가 96곳에 있어 "규칙이 클래스 0개를 검사하고 초록으로
통과하는 것 아니냐"를 의심했다. 추측 대신 측정했다 — 스크래치 worktree 에서
96곳을 전부 `false` 로 뒤집고 아키텍처 스위트를 돌렸다.
**174개 중 3개만 실패했다.** 93곳의 억제는 실제로 클래스를 검사하는 규칙에
방어적으로 붙어 있었다. 비어 있는 3개는 전부 "이 구조를 추가하면 이 규칙을
지켜라" 형태의 선행 가드이고, 템플릿이라 아직 해당 구조가 없다:
- `AGGREGATE_ROOT_SETTERS_ARE_NOT_PUBLIC``set*` 를 가진 `@AggregateRoot` 없음
- 테넌트 스코프 리포지토리 없음
- `CrudRepository` 를 재구현한 프로덕션 타입 없음
결론: 아키텍처 게이트는 속 빈 게이트가 아니다. 의심이 틀렸다.
### HEAD 에서 발견된 실제 실패 2건 (감사 findings 밖)
1. `MessagingCapabilityRegistryContractTest``src/build.gradle` 의 **소스 문자열
6개**를 assert 했다. 결과에 영향 없던 검증 45줄을 지우자 구현이 아니라 테스트가
먼저 깨졌다. 계약 검사로 교체: 모든 스켈레톤이 공용 가드를 통과하는지와,
그 가드가 실제로 throw 하는지만 본다.
2. `MongoModuleBoundaryTest``DO_NOT_INCLUDE_JARS` 때문에 임포트가 0개가 되어
규칙 10개 전부가 "failed to check any classes" 로 실패하고 있었다. 이 레인에서는
모듈 자기 클래스가 jar 로 클래스패스에 올라온다. 옵션 제거로 해결(실험으로 확인).
`importPackages(ROOT)` 가 이미 서드파티를 걸러내므로 옵션은 불필요했다.
## 최종 결과와, 감사 추정이 틀린 부분 (2026-09-16)
감사는 "정리하면 3,368줄 감소"라고 추정했다. **실제로는 빌드·CI 레이어가
1,652줄 늘었다.** 추정이 틀렸고, 그 이유를 적어둔다.
| 영역 | 증감 | 이유 |
| --- | --- | --- |
| `.github/workflows` | 128줄 (파일 30 → 19) | 중복 제거는 실제로 됐다. setup 블록 59회 → composite 1개, 잡 8개 삭제 |
| `.github/ci-gate-matrix.yml` | **+634줄** | 커버리지 8/28 → 28/28 워크플로. 게이트 49 → 107행 |
| `.github/scripts` | +163줄 | 개수 고정 대신 불변식 검증, 워크플로 SHA 잠금 확대 |
| `.github/actions` | +37줄 | composite action 신설 |
| `src/build.gradle` | +276줄 | Checkstyle 배선 + 판단 근거 주석 |
| `src/gradle` | 70줄 | jpa-evidence 274 를 다른 증가가 상쇄 |
| `src/build-logic` | **+701줄** | convention 플러그인 2개·헬퍼 2개 신설(+252) 과 그 테스트(+424) |
| 모듈 `build.gradle` | +3줄 | convention 적용으로 줄었지만 레인 전환이 상쇄 |
**왜 추정이 빗나갔나**
1. **감사 findings 17건이 근거와 함께 기각됐다.** 지울 수 있다고 본 코드가
실제로는 소비자가 있었다. 감사는 읽고 판단했고, 수정 담당은 실행해서
확인했다. 실행이 이겼다.
2. **작동하게 만드는 데는 코드가 든다.** 게이트 매트릭스가 워크플로 8개만
덮고 있던 것을 28개 전부로 넓히면 634줄이 는다. 그 634줄이 "군더더기"가
아니라 그동안 비어 있던 자리다.
3. **convention 플러그인은 총량이 아니라 중복을 줄인다.** build-logic 이
701줄 늘고 모듈 43개가 그만큼 줄지 않은 것은, 플러그인에 테스트(424줄)를
붙였기 때문이다. 테스트 없는 convention 플러그인은 43개 모듈에 동시에
틀릴 수 있는 한 지점이 된다.
**그래서 이 작업의 성과는 "코드가 줄었다"가 아니다.** 안 돌던 게이트가 돌고,
CI 에 단계가 생기고, 깨끗한 체크아웃에서 빌드가 되는 것이다. 줄 수로 보고하면
틀린 보고가 된다.
### 최종 커밋
`2a8d34f` docs · `e345191` CI 게이트 정상화 · `1535481` testkit 이관 + 미추적
필수 파일 · `9bc2e75` CI 단계 분리 + 잔여 114건 · `40ee9f1` 고아 레인 배선 +
카탈로그 이관.
모든 커밋은 워킹트리를 커밋하지 않고 후보 커밋 객체로 만들어 깨끗한 체크아웃에서
검증한 뒤에 올렸고, 커밋된 트리 해시가 검증한 트리와 같은지 매번 대조했다.
@@ -0,0 +1,102 @@
# JPA Evidence Gradle Model Decoupling Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Remove execution-time Gradle `Project`/`Task`/`TaskState` access from JPA evidence generation without changing evidence semantics.
**Architecture:** A shared `JpaEvidenceExecutionService` consumes Gradle task-completion events for non-Test task claims. The JPA evidence plugin snapshots/configures JUnit result directories, provenance, environment/profile values, and dependency versions as typed task inputs. `GenerateJpaEvidenceManifestsTask` becomes a pure evidence assembler over those inputs plus filesystem/exec services.
**Tech Stack:** Java 21, Gradle 9 BuildService + Tooling Events, JUnit 6/TestKit, Jackson 3.
**Spec:** `docs/superpowers/specs/2026-09-17-jpa-evidence-gradle-model-decoupling-design.md`
## Global Constraints
- Preserve readiness-card schema and task names.
- Preserve JUnit XML as test evidence.
- Preserve task-claim meaning: only a successful producer task covers a task claim.
- Preserve evidence grade, blocker, hashing, prerequisite, output, candidate/R2 semantics.
- No execution-time `Project`, `Task`, or `TaskState` access in `GenerateJpaEvidenceManifestsTask`.
- Do not stage, commit, amend, reset, or push existing worktree changes.
---
### Task 1: Task completion evidence service
**Files:**
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaEvidenceExecutionService.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaEvidenceTaskOutcome.java`
- Test: `src/build-tools/src/test/java/dev/caskeleton/buildtools/jpa/JpaEvidenceExecutionServiceTest.java`
**Interfaces:**
- Produces: `JpaEvidenceExecutionService.outcome(String taskPath)` and `completedSuccessfully(String taskPath)`.
- Consumes: Gradle `TaskFinishEvent` via `OperationCompletionListener`.
- [ ] Write tests for success, failure, skipped, and unknown task paths.
- [ ] Verify tests fail because the service/model do not exist.
- [ ] Implement the typed outcome model and thread-safe service.
- [ ] Verify focused tests pass.
### Task 2: Typed JUnit evidence inputs
**Files:**
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaEvidenceTestResultLocator.java`
- Modify: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaEvidenceTaskSupport.java`
- Test: `src/build-tools/src/test/java/dev/caskeleton/buildtools/jpa/JpaEvidenceTestResultLocatorTest.java`
**Interfaces:**
- Consumes: `Map<String, String>` task-path to repository-relative/absolute JUnit XML directory.
- Produces: `JpaGeneratedTestResult read(String taskPath)` without `Project` or `Test`.
- [ ] Write a failing test that creates JUnit XML under a temporary directory and resolves it by task path.
- [ ] Implement file-based result lookup using `JUnitEvidenceReader`.
- [ ] Remove the `readJUnitResult(Project, Test)` helper once no caller remains.
- [ ] Verify focused tests pass.
### Task 3: Generator typed input surface
**Files:**
- Modify: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/GenerateJpaEvidenceManifestsTask.java`
- Test: extend `src/build-tools/src/test/java/dev/caskeleton/buildtools/jpa/JpaEvidencePluginTypeTest.java`
**Interfaces:**
- Add typed properties for profile/CI/artifact/topology/provenance/dependency versions/JUnit result directories.
- Add an internal/service reference to `JpaEvidenceExecutionService`.
- [ ] Add reflection/type tests asserting the new task properties exist and no generator source contains `getProject()` or `Task.getState()` usage.
- [ ] Verify the test fails against the current generator.
- [ ] Add the typed properties and service reference.
- [ ] Replace Project/Task/TaskState/configuration/extra-property reads with typed inputs/service lookups.
- [ ] Verify focused tests pass.
### Task 4: Plugin wiring
**Files:**
- Modify: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaEvidencePlugin.java`
- Modify: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaEvidenceTaskSupport.java`
- Test: add `src/build-tools/src/test/java/dev/caskeleton/buildtools/jpa/JpaEvidencePluginFunctionalTest.java`
**Interfaces:**
- Register shared execution service and task-completion listener.
- Configure generator typed inputs.
- Configure JUnit result-directory mapping for active readiness/support Test tasks.
- Preserve existing `dependsOn` producer graph.
- [ ] Write TestKit fixture asserting typed generator inputs and task wiring.
- [ ] Verify RED.
- [ ] Register/wire the service and all generator properties.
- [ ] Resolve dependency versions and release provenance during configuration/plugin wiring rather than task action.
- [ ] Verify TestKit GREEN.
### Task 5: Regression and Gradle 10-preparation verification
**Files:**
- Modify only if verification exposes a regression.
- [ ] Run `src/build-tools` full `check --warning-mode=fail`.
- [ ] Run root `verifyJpaReadinessRegistry verifyJpaReleaseGateTasks --warning-mode=fail`.
- [ ] Run `:adapter:outbound:persistence-jpa:check --warning-mode=fail`.
- [ ] Run candidate evidence generation with `--warning-mode=all`; verify there is no `Task.project`/execution-time project deprecation from JPA evidence tooling.
- [ ] Run adapter procedural-Groovy scan and confirm no regression.
- [ ] Run `git diff --check`.
- [ ] Record any environment-only Docker/Testcontainers limitation separately from code correctness.
@@ -0,0 +1,46 @@
# JPA Leaf Verification Java Migration Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Remove the remaining procedural JPA leaf verification logic from Groovy while deleting a redundant verifier-of-verifier task.
**Architecture:** Keep the security scenario as a declarative `strictTestLanes.requires(...)` contract and remove `verifyJpaSecurityFixtures` from both the leaf and readiness registry. Move the real PostgreSQL `set_config` source-safety rule into a typed task/verifier owned by the already-applied `ca.jpa-evidence` Java plugin.
**Tech Stack:** Java 21, Gradle 9 binary plugins/tasks, JUnit 6.
**Spec:** `docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md`
## Global Constraints
- Preserve the `verifyJpaSqlConstructionSafety` task name because readiness registry/evidence tooling references it.
- Preserve the PostgreSQL security method selector on `postgresqlSecurityBaselineIntegrationTest`.
- Remove `verifyJpaSecurityFixtures` only together with its readiness-card support-task reference.
- Do not add production dependencies or change JPA runtime behavior.
- Do not stage, commit, amend, or push.
---
### Task 1: Typed SQL construction safety verifier
**Files:**
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaSqlConstructionSafetyVerifier.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaSqlConstructionSafetyResult.java`
- Test: `src/build-tools/src/test/java/dev/caskeleton/buildtools/jpa/JpaSqlConstructionSafetyVerifierTest.java`
- [ ] **Step 1:** Write RED tests for parameterized `set_config`, non-parameterized `set_config`, comments, and nested source paths.
- [ ] **Step 2:** Implement the minimal typed verifier preserving the current line-based rule.
- [ ] **Step 3:** Run the focused verifier tests to GREEN.
### Task 2: Java task ownership and redundant task removal
**Files:**
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/VerifyJpaSqlConstructionSafetyTask.java`
- Modify: `src/build-tools/src/main/java/dev/caskeleton/buildtools/jpa/JpaEvidencePlugin.java`
- Modify: `src/adapter/outbound/persistence-jpa/build.gradle`
- Modify: `src/config/jpa/readiness-cards.yaml`
- [ ] **Step 1:** Register `verifyJpaSqlConstructionSafety` as a typed task from `JpaEvidencePlugin`.
- [ ] **Step 2:** Delete the Groovy implementation of `verifyJpaSqlConstructionSafety`.
- [ ] **Step 3:** Delete `verifyJpaSecurityFixtures` and remove it from the security card support tasks while keeping the `requires(...)` selector.
- [ ] **Step 4:** Verify readiness registry and SQL-safety tasks.
- [ ] **Step 5:** Run `:adapter:outbound:persistence-jpa:check`, build-tools tests, and `git diff --check`.
@@ -0,0 +1,19 @@
# JPA Test Lanes Java Convention Plan
**Goal:** Remove the remaining Map-driven JPA lane factories from Groovy and make lane metadata compile-time checked Java records.
**Architecture:** `ca.jpa-test-lanes` lives in build-logic and configures `ca.strict-test-lane`. Typed record lists own the PostgreSQL readiness and tagged platform lane metadata. The JPA leaf keeps source-set/dependency declarations plus the explicit cross-project task edge.
## Constraints
- Preserve all 14 PostgreSQL readiness task names and selectors.
- Preserve the security method selector in addition to its class selector.
- Preserve five tagged platform lanes and the pool contract lane.
- Preserve UTC JVM args, PostgreSQL evidence image property, and `jpa.matrix.versions` default `16`.
- Keep release orchestration outside the leaf.
- Do not stage, commit, amend, or push.
## Steps
- [ ] RED TestKit contract for registered lanes, typed selector metadata, and property defaults.
- [ ] Implement Java record-backed `JpaTestLanesPlugin` and register `ca.jpa-test-lanes`.
- [ ] Apply plugin in persistence-jpa and remove both Groovy `Map` factories plus pool-lane Groovy configuration.
- [ ] Run focused build-logic tests, JPA check, adapter dynamic-model scan, and broad verification.
@@ -0,0 +1,61 @@
# Mongo Gradle Verification Java Migration Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Remove Mongo JUnit/XML and release-manifest verification algorithms from the Groovy leaf build script and move them into typed Java build tooling without changing task names or verification semantics.
**Architecture:** `ca.mongo-verification` lives in `src/build-tools` because the checks are repository certification tooling, not reusable compilation conventions. The plugin registers the two existing verification task names; normal Java verifier/parser classes own XML/JSON parsing and return typed records, while `persistence-mongo/build.gradle` keeps only plugin/lane/dependency declarations and `check` wiring.
**Tech Stack:** Java 21, Gradle 9 binary plugins/tasks, Jackson 3, JUnit 6, JUnit XML.
**Spec:** `docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md`
## Global Constraints
- Preserve `verifyMongoTestLaneDisjointness` and `verifyMongoReleaseContractLanes` task names and report paths.
- Preserve the existing `test` + `mongoStableContractTest` dependency graph.
- Do not add or resolve new production dependencies in the Mongo leaf.
- Keep Groovy only as declarative build DSL; no JSON/XML parsing or `doLast` verification algorithm remains in the leaf.
- Do not stage, commit, amend, or push; repository policy is human-only commits.
---
### Task 1: Typed Mongo verification core
**Files:**
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/MongoLaneDisjointnessVerifier.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/MongoLaneDisjointnessResult.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/MongoReleaseContract.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/MongoReleaseContractManifestParser.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/MongoReleaseContractLaneVerifier.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/MongoReleaseContractLaneResult.java`
- Test: `src/build-tools/src/test/java/dev/caskeleton/buildtools/mongo/MongoLaneDisjointnessVerifierTest.java`
- Test: `src/build-tools/src/test/java/dev/caskeleton/buildtools/mongo/MongoReleaseContractLaneVerifierTest.java`
**Interfaces:**
- Consumes: Gradle JUnit XML result directories and `config/mongodb/release-contracts.json`.
- Produces: typed result records used by Gradle task classes.
- [ ] **Step 1: Write failing verifier tests** covering disjoint lanes, overlap failure data, manifest filtering to hermetic lanes, missing result XML, and minimum-executed checks.
- [ ] **Step 2: Run** `cd src/build-tools && ../gradlew test --tests 'dev.caskeleton.buildtools.mongo.*' --console=plain` and confirm RED from missing production types.
- [ ] **Step 3: Implement minimal typed records/parsers/verifiers** using fail-closed XML parsing and Jackson 3 JSON tree parsing.
- [ ] **Step 4: Run the focused tests again** and confirm PASS.
### Task 2: Binary plugin/task ownership and leaf cleanup
**Files:**
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/MongoVerificationPlugin.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/VerifyMongoTestLaneDisjointnessTask.java`
- Create: `src/build-tools/src/main/java/dev/caskeleton/buildtools/mongo/VerifyMongoReleaseContractLanesTask.java`
- Modify: `src/build-tools/build.gradle`
- Modify: `src/adapter/outbound/persistence-mongo/build.gradle`
**Interfaces:**
- Consumes: typed verifiers from Task 1.
- Produces: existing task names `verifyMongoTestLaneDisjointness`, `verifyMongoReleaseContractLanes` with unchanged report paths.
- [ ] **Step 1: Register `ca.mongo-verification`** and the two typed tasks in Java.
- [ ] **Step 2: Apply the plugin in the Mongo leaf and delete both Groovy `tasks.register { doLast { ... } }` implementations.**
- [ ] **Step 3: Run** `cd src && ./gradlew :adapter:outbound:persistence-mongo:verifyMongoTestLaneDisjointness :adapter:outbound:persistence-mongo:verifyMongoReleaseContractLanes --console=plain`.
- [ ] **Step 4: Run** `cd src && ./gradlew :adapter:outbound:persistence-mongo:check --console=plain`.
- [ ] **Step 5: Run** `cd src/build-tools && ../gradlew test --console=plain` and `git diff --check`.
@@ -0,0 +1,29 @@
# Redis Topology Lane Java Migration Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Remove the last adapter-level procedural Groovy test lifecycle logic by moving Redis topology qualification into a typed Java convention plugin.
**Architecture:** `ca.redis-topology-lane` belongs in `build-logic`: it configures the default test exclusion, declares the strict `redisTopologyTest` lane, validates mode/properties, forwards topology system properties, and verifies executed required classes/no skips. The Redis leaf keeps only plugin and dependency declarations.
**Tech Stack:** Java 21, Gradle 9, JUnit Platform test tasks.
**Spec:** `docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md`
## Constraints
- Preserve supported modes: standalone, sentinel, cluster, tls.
- Preserve TLS deployment-mode mapping to standalone.
- Preserve required property and required executed-class semantics.
- Preserve fail-closed behavior for unknown mode and skipped topology tests.
- Keep `redisTopologyTest` opt-in; do not add it to normal `check`.
- Do not stage, commit, amend, or push.
### Task 1: Typed topology contract
- [ ] Write failing Java tests for mode validation, required properties, class coverage, and skipped-test rejection.
- [ ] Implement typed contract/result records and get focused tests GREEN.
### Task 2: Java convention plugin
- [ ] Implement `RedisTopologyLanePlugin` using `StrictTestLaneExtension` and a Java `TestListener` tracker.
- [ ] Register `ca.redis-topology-lane` in build-logic.
- [ ] Apply it in cache-redis and remove the Groovy topology lifecycle/configuration block.
- [ ] Verify build-logic tests, Redis unit `check`, task configuration, invalid-mode fail-closed behavior, and `git diff --check`.
@@ -0,0 +1,93 @@
# Messaging Platform Bridge Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Connect the canonical application integration-event publish boundary to the messaging platform without re-encoding bytes or introducing broker-specific ownership in app-bootstrap.
**Architecture:** Add an application-owned publish port, implement it in `adapter/outbound/messaging/platformbridge`, and publish canonical pre-encoded envelopes through `EncodedMessagePublisher`. The bridge preserves identity/routing/evidence and maps platform publish evidence explicitly into application outcomes.
**Tech Stack:** Java 21, Gradle 9, Spring Boot auto-configuration, JUnit 5, AssertJ.
**Spec:** `docs/superpowers/specs/2026-09-18-messaging-platform-bridge-design.md`
## Global Constraints
- Preserve existing uncommitted changes; do not reset, stage, commit, amend, or push.
- Never invent missing canonical identity, timestamp, trace, tenant, schema, or routing values.
- Never bypass `DefaultMessagePublisher` through transport SPI or native Kafka clients.
- Preserve exact `ValidatedIntegrationEvent.envelopeBytes()`.
- Fail closed before send when event or causation identity is not UUIDv7.
- Legacy outbox storage/relay migration is outside this plan.
---
### Task 1: Application-owned canonical publish port
**Files:**
- Create: `application-core/src/main/java/dev/caskeleton/application/messaging/event/IntegrationEventPublishPort.java`
- Test: existing bridge test compile contract
**Interfaces:**
- Produces: `CompletionStage<OutboxPublishOutcome> publish(ValidatedIntegrationEvent event)`
- [x] Create the minimal application-owned interface.
- [x] Run the focused bridge test and verify remaining failures are platform dependencies/adapter implementation, not the port.
### Task 2: Outbound messaging platform API dependency
**Files:**
- Modify: `adapter/outbound/messaging/build.gradle`
- Modify: `adapter/outbound/messaging/gradle.lockfile` through Gradle lock writing
**Interfaces:**
- Consumes: `:messaging:messaging-core-api`, `:messaging:messaging-schema-api`.
- [x] Add only the platform API dependencies required by the bridge.
- [x] Refresh this module's locks.
- [x] Re-run focused bridge test and verify the missing type set is reduced to bridge production code.
### Task 3: Canonical platform bridge
**Files:**
- Create: `adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/platformbridge/PlatformIntegrationEventPublishAdapter.java`
- Test: `adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/platformbridge/PlatformIntegrationEventPublishAdapterTest.java`
**Interfaces:**
- Consumes: `IntegrationEventPublishPort`, `EncodedMessagePublisher`.
- Produces: canonical application-to-platform anti-corruption bridge.
- [x] Implement UUIDv7 parsing that rejects incompatible identity before publisher invocation.
- [x] Map canonical metadata and exact bytes into `MessageEnvelope<EncodedMessage>`.
- [x] Preserve non-first-class evidence in bounded `x-ca-*` headers.
- [x] Map `PublishResult` using transmission evidence.
- [x] Run all three focused bridge tests to GREEN.
### Task 4: Spring ownership while preserving the legacy seam
**Files:**
- Modify: `adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingSettings.java`
- Modify: `adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/autoconfigure/MessagingBridgeRootAutoConfiguration.java`
- Test: focused auto-configuration ownership test
**Interfaces:**
- Consumes: Spring-provided `EncodedMessagePublisher`, explicit `app.messaging.producer-id`.
- Produces: `IntegrationEventPublishPort` bean for canonical events.
- [x] Add explicit `producerId` to the existing `app.messaging` adapter settings.
- [x] Register the canonical bridge only when `app.messaging.producer-id` is explicitly present.
- [x] Keep `KafkaSender` / `KafkaMessageBroker` as a documented transitional dependency of legacy outbox/realtime only.
- [x] Add a Spring test proving producer-id present => one canonical bridge bean, absent => no canonical bridge bean.
- [x] Run outbound messaging tests.
### Task 5: Platform and composition regression verification
**Files:** no new production files unless a test exposes a real defect.
- [x] Run `:messaging:messaging-runtime-core:test`.
- [x] Run `:messaging:messaging-spring-boot-starter:test`.
- [x] Run `:adapter:outbound:messaging:check --warning-mode=fail`.
- [x] Run `:app-bootstrap:architectureTest :app-bootstrap:systemTest --warning-mode=fail`.
- [x] Search for direct app-bootstrap Kafka producer ownership.
- [x] Run `git diff --check`.
- [x] Report any remaining legacy outbox cutover blocker explicitly rather than inventing a migration.
@@ -0,0 +1,105 @@
# Outbox Transport-Only Cutover Implementation Plan
**Goal:** Preserve canonical integration-event bytes and metadata inside the existing legacy `outbox_event` authority, then route canonical claimed rows through the messaging platform while legacy rows keep the current broker path.
**Spec:** `docs/superpowers/specs/2026-09-18-outbox-transport-only-cutover-design.md`
## Constraints
- Preserve all existing uncommitted work. No reset/checkout/stage/commit/amend/push.
- Do not activate or switch to `POLLING_V2`.
- Do not synthesize missing canonical metadata for legacy rows.
- Do not re-encode a persisted canonical envelope.
- One claimed row goes through exactly one publish branch.
- Default configuration remains legacy-compatible and canonical transport is off.
### Task 1 — Split canonical and legacy append ports
- [x] Create `LegacyOutboxAppendPort` with the current `NewOutboxEvent` signature.
- [x] Change `OutboxAppendPort` to accept `ValidatedIntegrationEvent`.
- [x] Move all current raw production consumers and their tests to `LegacyOutboxAppendPort`.
- [x] Make `OutboxStoreAdapter` implement `LegacyOutboxAppendPort` only.
- [x] Run `:application-core:test` and focused sample/outbox compile tests.
### Task 2 — Add additive canonical columns to `outbox_event`
- [x] Add the next PostgreSQL migration after V12.
- [x] Widen event/correlation identifiers as required.
- [x] Add canonical metadata, exact `BYTEA`, hashes/revisions, and all-or-none check constraints.
- [x] Extend `OutboxEventEntity` mappings.
- [x] Update migration history expectations.
- [x] Add real PostgreSQL integration assertions for legacy rows and canonical shape constraints.
### Task 3 — Implement canonical append adapter
- [x] Add `CanonicalOutboxAppendAdapter`.
- [x] Strictly validate UTF-8 compatibility projection.
- [x] Persist every canonical field and exact `envelopeBytes`.
- [x] Preserve old required columns for legacy relay/storage compatibility.
- [x] Add unit tests for byte equality, field mapping and invalid UTF-8.
- [x] Gate bean exposure on `ca-skeleton.outbox.canonical-transport-enabled=true`.
### Task 4 — Split the claimed row model
- [x] Add sealed `ClaimedOutboxEvent`.
- [x] Keep `OutboxEvent` as legacy subtype.
- [x] Add `CanonicalClaimedOutboxEvent` carrying reconstructed `ValidatedIntegrationEvent`.
- [x] Change `OutboxStorePort.claimBatch` to return the sealed type.
- [x] Map all-canonical rows to canonical subtype and all-null rows to legacy subtype.
- [x] Reject partial canonical rows.
- [x] Update legacy relay tests without changing its state-machine semantics.
### Task 5 — Route canonical claims through the platform
- [x] Update `OutboxMessagePublishPort` to accept `ClaimedOutboxEvent`.
- [x] Extend `OutboxMessagePublishAdapter` with canonical `IntegrationEventPublishPort`.
- [x] Legacy subtype uses only `MessageBroker`.
- [x] Canonical subtype uses only the application canonical publish port and exact stored bytes.
- [x] Add focused branch-isolation and outcome tests.
### Task 6 — Add explicit activation and composition validation
- [x] Add `canonicalTransportEnabled` to `OutboxSettings` and `config/outbox.yml`, default false.
- [x] Startup fails when canonical transport is enabled but no `IntegrationEventPublishPort` exists.
- [x] Relay-enabled compatibility deployment still requires the legacy broker until a later zero-legacy-backlog proof.
- [x] Default-off composition keeps the existing legacy path.
- [x] Enabled composition exposes the canonical append/publish path without a second scheduler.
- [x] Update configuration docs/SSOT.
### Task 7 — Regression and architecture verification
- [x] `:application-core:check`
- [x] `:adapter:outbound:persistence-jpa:check`
- [x] focused PostgreSQL migration/outbox integration lane
- [x] `:adapter:outbound:messaging:check`
- [ ] `:sample-portfolio:check` — blocked by pre-existing `JpaLiveEventReplayAdapter` missing `Duration` wiring; the same 3 `SampleApplicationContextTest` failures reproduce on clean HEAD.
- [x] focused sample outbox regression tests (`PosterEventPublisherTest`, `CreateWorkLogOutboxTest`, `WorkLogUseCasesTest`, `WorkLogAuthorizationContractTest`)
- [x] `:app-bootstrap:architectureTest :app-bootstrap:systemTest`
- [x] `verifyCleanArchitectureDependencies`
- [x] `:app-bootstrap:verifyEnvKeys`
- [x] static scans: no platform runtime/Kafka import in canonical bridge
- [x] `git diff --check`
- [x] no separate LLM Wiki branch-note warranted; spec, plan, module README/CLAUDE and configuration reference carry the implementation decision.
## Verification result
Transport-only cutover implementation is complete for this slice.
Passed:
- `:application-core:check`
- `:adapter:outbound:persistence-jpa:check`
- `:adapter:outbound:persistence-jpa:postgresqlMigrationIntegrationTest`
- `:adapter:outbound:messaging:check`
- focused sample outbox regression tests
- `:app-bootstrap:test`
- focused `:app-bootstrap:integrationTest` outbox append + row-lifecycle contracts
- `:app-bootstrap:architectureTest`
- `:app-bootstrap:systemTest`
- `verifyCleanArchitectureDependencies`
- `:app-bootstrap:verifyEnvKeys`
- `git diff --check`
Known unrelated blocker:
- full `:sample-portfolio:check` still fails only the 3 previously documented `SampleApplicationContextTest` cases because `JpaLiveEventReplayAdapter` requires an unbound `Duration` bean. This reproduces on clean HEAD and was not introduced by this cutover.
Publication authority remains `LEGACY_POLLING`; no code path in this slice activates `POLLING_V2`.
@@ -113,7 +113,7 @@ dev.caskeleton.adapter.outbound.notification.core.RoutingNotifier
dev.caskeleton.adapter.outbound.notification.email.google.GoogleEmailNotificationAdapterConfig dev.caskeleton.adapter.outbound.notification.email.google.GoogleEmailNotificationAdapterConfig
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationPlatformSettings dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationPlatformSettings
dev.caskeleton.adapter.outbound.notification.slack.webhook.SlackNotificationAdapterConfig dev.caskeleton.adapter.outbound.notification.slack.webhook.SlackNotificationAdapterConfig
dev.caskeleton.bootstrap.notification.NotificationPlatformSecretsConfig$NotificationSecretsSettings dev.caskeleton.bootstrap.autoconfigure.notification.NotificationPlatformSecretsConfig$NotificationSecretsSettings
``` ```
## Task 5 — default-profile boot ## Task 5 — default-profile boot
@@ -0,0 +1,280 @@
# 검증 표면 축소 설계 — 스켈레톤을 qualification framework에서 되돌리기
날짜: 2026-09-16
근거: 외부 리뷰 "현재 상태를 유지하기 위한 검증이 너무 많고, 그 검증 자체를 다시 검증하는 구조까지 생겼다"
## 0. 리뷰 기준점과 현재 체크아웃의 차이
리뷰는 이 저장소의 **이전 스냅샷**을 보고 작성됐다. 실제 작업 전에 항목별로 재측정했고,
이미 해결된 항목은 "완료"로 확정하고 남은 항목만 작업 대상으로 삼는다.
| 리뷰 주장 | 리뷰가 본 값 | 현재 실측 | 판정 |
| --- | --- | --- | --- |
| `settings.gradle` 183줄 validator | 183줄 | 16줄 (`ca.architecture-registry` 설정 플러그인으로 이전) | 완료 |
| 모듈 수 정확히 18개 강제 | 있음 | 없음 | 완료 |
| runtime composition이 정확히 `app-bootstrap` | 있음 | `runtime_compositions`를 JSON에서 읽음 | 완료 |
| JSON 필드 집합 정확히 일치 | 있음 | `ModuleRegistry.groovy:88,124`에 그대로 있음 | **작업 대상** |
| `sample-portfolio` negative re-entry guard | 있음 | `ModuleRegistry.groovy:215`에 그대로 있음 | **작업 대상** |
| `build-logic` 없음 | 없음 | 존재 (9개 convention plugin) | 부분 완료 |
| version catalog 없음 | 없음 | `gradle/libs.versions.toml` 140줄 | 완료 |
| `adapter/inbound/web/build.gradle` 799줄 OpenAPI | 799줄 | 256줄, codegen 없음 | 완료 |
| leaf `check`가 저장소 전체 검사 | 그랬음 | 루트 `check`로 이미 이전 | 부분 완료 |
| `fileserver-release.yml``kubectl apply` | 있음 | 이미 제거됨 | 완료 |
| `httpclient-release.yml` | 있음 | 파일 자체가 없음 | 해당 없음 |
| `ci-gate-matrix.yml` 282줄 / 37 gate | 282줄 | **1,025줄 / 107 gate** | **작업 대상(악화)** |
| `verify-gate-matrix.sh` | 있음 | 568줄 | **작업 대상** |
| `verify-gradle-wrapper.sh` 740줄 | 740줄 | **799줄** | **작업 대상** |
| `DeveloperExperienceContractTest` 1,100줄 | 1,100줄 | 1,141줄 (CI YAML mutation test 25개) | **작업 대상** |
| `src/build.gradle` 2,469줄 | 2,469줄 | **3,211줄** | **작업 대상(악화)** |
| always-fail Messaging task | 있음 | 9개 그대로 | **작업 대상** |
| 모든 빌드에 Git SHA 강제 | 있음 | 그대로 (`build.gradle:47`) | **작업 대상** |
## 1. 채택하는 판단 기준
리뷰의 핵심 원칙을 이 저장소의 결정 규칙으로 승격한다.
1. **현재 상태(Current State)가 아니라 불변조건(Invariant)을 검증한다.**
"모듈이 N개다", "필드가 정확히 이 집합이다", "문서에 적힌 수가 레지스트리와 같다"는 현재 상태다.
"ID가 중복되지 않는다", "domain이 framework를 참조하지 않는다"는 불변조건이다.
2. **검증기를 검증하지 않는다.** validator를 mutation해서 validator가 실패하는지 보는 task는
스켈레톤의 기본 빌드 정책이 아니다.
3. **자동으로 구성할 수 있는 것은 검증으로 강제하지 않는다.** convention plugin으로 주입한다.
4. **로컬 `check`는 로컬이어야 한다.** leaf의 `check`는 그 leaf만 검사한다.
5. **릴리스 불변조건을 일반 개발 빌드에 강제하지 않는다.**
6. **문서 drift는 빌드 실패 사유가 아니다.** 커스텀 Markdown/Java 파서를 유지하지 않는다.
7. **GitHub Actions = CI + artifact 생산, Argo CD = CD.** CI에 클러스터 배포 자격증명을 넣지 않는다.
8. **Template maintainer용 검증과 Template consumer용 검증을 분리한다.**
이 기준은 기존의 D8 결정("quality 블록을 convention plugin으로 빼지 않는다")을 **대체한다**.
D8의 3번 근거(build-logic이 플러그인 버전을 두 번 선언하게 된다)는 이미 무효다 —
`build-logic/settings.gradle`이 메인 빌드의 `libs.versions.toml`을 읽고 있으므로 버전은 한 곳에 있다.
## 2. 목표 task 계층
```text
:<leaf>:check 컴파일 + 단위 테스트 + spotless + checkstyle + errorprone (그 leaf만)
check (root) 모든 leaf의 check
architectureCheck 의존 방향 · 런타임 멤버십 · application-core 순수성 · Git 미추적 패키지
qualityCheck SpotBugs + FindSecBugs (전 leaf)
configContractCheck :app-bootstrap:verifyEnvKeys
integrationCheck 통합/슬라이스 레인
ci check + architectureCheck + qualityCheck + configContractCheck
releaseCheck ci + 아카이브 위생 + public path snapshot + 릴리스 provenance
```
qualification(JPA readiness, Messaging evidence, notification evidence, transport 등)은
어느 것도 `check` / `ci`에 걸지 않는다. 명시적으로 이름을 불러야 실행된다.
## 3. 변경 목록
### 3.1 삭제
| 대상 | 줄 수 | 이유 |
| --- | ---: | --- |
| `.github/ci-gate-matrix.yml` | 1,025 | Gradle task graph와 workflow graph에 이미 있는 정보의 3중 복제 |
| `.github/scripts/verify-gate-matrix.sh` | 568 | 위 복제본의 정합성 검사기 |
| `.github/scripts/verify-gradle-wrapper.sh` | 799 | workflow 바이트 해시 잠금. 공격자는 해시도 같이 고치면 되고, 개발자는 주석 하나에 해시를 갱신해야 한다 |
| `DeveloperExperienceContractTest`의 wrapper/gate mutation test 25개 | ~700 | 애플리케이션 test suite가 GitHub Actions YAML 파서를 검증 |
| always-fail Messaging skeleton task 9개 | ~85 | 정상 입력으로도 성공할 수 없는 task. TODO를 Gradle API로 만든 것 |
| `verifyReadmeCommands` | 105 | 커스텀 Markdown 명령 파서 |
| `verifyRunbookReferences` | 75 | 커스텀 runbook 식별자 파서 |
| `verifyDocumentedLeafCount` | 78 | 문서에 적힌 leaf 수 = 전형적인 현재 상태 검증 |
| `verifyTestSourceSetRegistry` | 92 | 문서 표 ↔ source set 대조 파서 |
| `verifySpotBugsAnalysisFailureContract` | 58 | 검증기의 검증 |
| `verifyConfigurationPropertiesProcessor` | 90 | build.gradle을 regex로 읽는 검증 → convention으로 대체 |
| `verifyOneTypePerFile` | 8 | 이미 `checkstyleMain` 별칭. 호출자를 `checkstyleMain`으로 바꾸고 이름 폐기 |
| `verifyTrivyignore` | 105 | 빈 registry를 지키는 커스텀 YAML 파서 |
| `verifyQuarantineSunset` | 250 | 빈 registry를 지키는 커스텀 YAML + Java 파서 |
| `blankJavaCommentsAndLiterals` | 95 | 위 두 개만 쓰던 Java 렉서 흉내 |
| `ModuleRegistry`의 필드 집합 정확 일치 · sample-portfolio negative guard | ~25 | 확장 차단 · 삭제된 모듈의 역사가 영구 invariant |
합계 약 4,150줄.
### 3.2 이동
| 대상 | 현 위치 | 새 위치 | 이유 |
| --- | --- | --- | --- |
| java/quality/spring 공통 설정 | `build.gradle``configure(subprojects…)` | `ca.java-conventions` · `ca.quality-conventions` · `ca.java-library` · `ca.spring-library` | 모듈이 자신의 성격을 스스로 선언 |
| `verifyCleanArchitectureDependencies` 외 3개 | `build.gradle` | `ca.architecture` | 아키텍처 규칙을 한 곳에 |
| JPA readiness registry + release gate | `build.gradle` ~610줄 | `gradle/qualification/jpa-qualification.gradle` | 빌드 정책과 certification 분리 |
| Messaging evidence manifest | `build.gradle` ~600줄 | `gradle/qualification/messaging-qualification.gradle` | 동일 |
| `verifyEnvKeys` | 루트 task, 루트 `check` | `:app-bootstrap` 소유, `configContractCheck` | 환경 계약은 composition root의 책임 |
| 모듈 의존 edge 존재/자기참조 검사 | settings 단계(`ModuleRegistry`) | `verifyCleanArchitectureDependencies` | settings에서 죽으면 복구 수단이 없다 |
### 3.3 완화
| 대상 | 현재 | 변경 후 |
| --- | --- | --- |
| Git revision | 없으면 **모든** 빌드가 configuration 단계에서 실패 | 일반 빌드는 `0.0.1-SNAPSHOT`/`unknown`. `releaseCheck`·아카이브 생성에서만 요구 |
| SpotBugs / FindSecBugs | 전 leaf `check` 블로킹 | `qualityCheck` (CI lane). 로컬 `check`에서 제외 |
| `.trivyignore.yaml` | 커스텀 파서가 expiry/reason 강제 | 파일은 유지, 규칙은 문서화 + CODEOWNERS 승인 |
| `flaky-quarantine.yaml` | 커스텀 파서 + 14일 sunset 강제 | 레지스트리 삭제. `@Tag("quarantine")` 제외와 `quarantineTest`는 유지(각 3줄) |
### 3.4 CI
```text
.github/workflows/
├── _reusable-gradle.yml 신규 — checkout + wrapper validation + JDK/캐시 + Gradle 호출
├── ci-quality-gates.yml → 재사용 workflow 호출로 축약
├── dependency-vulnerability.yml 유지 (dependency-review + submission + Trivy)
├── link-check.yml 유지
├── release.yml image/SBOM 생산까지. 클러스터 배포 없음
├── fileserver-certification.yml ← fileserver-release.yml 개명 (CD가 아니라 certification)
└── 나머지 feature qualification 유지, 전부 재사용 workflow 사용
```
wrapper 검증은 `gradle/actions/wrapper-validation`(full SHA 핀)에 맡기고, 재사용 workflow
한 곳에서만 선언한다. full SHA 핀은 리뷰 판단대로 **유지**한다.
### 3.5 CI/CD 경계
```text
GitHub Actions ──► test / scan / image build / SBOM / push ──► GitOps repo manifest ──► Argo CD ──► K8s
```
`docs/ci-cd/boundary.md`로 고정한다. GitHub Actions는 `kubectl apply` / `helm upgrade` /
`argocd app sync`를 하지 않는다. Argo CD auto-sync를 쓰면 CI에 클러스터 자격증명이 필요 없다.
### 3.6 Template maintainer vs consumer
`docs/ci-cd/template-vs-consumer.md`로 구분을 명시한다.
- Template CI: sample 모듈 제거 가능성, optional 모듈 조합 빌드, 레지스트리 확장 가능성
- Consumer CI: 자기 애플리케이션의 test / architecture / security / release
파생 프로젝트가 가져가면 안 되는 workflow와 task를 목록으로 적는다.
## 4. 유지하는 것 (리뷰가 "잘한 것"으로 분류)
`verifyCleanArchitectureDependencies`, dependency locking(STRICT), full SHA action 핀,
dependency-review, Trivy 스캔, path filter 기반 feature CI, nightly 분리, Spotless,
`-Werror`/`-Xlint`, ErrorProne, 재현 가능한 아카이브.
## 5. 검증 방법
- `./gradlew help --offline`로 configuration 성공
- 변경한 leaf마다 `./gradlew <path>:check --offline`
- `./gradlew architectureCheck --offline`
- 워크플로 YAML은 `python3 -c "import yaml…"`로 파싱 확인
- 삭제한 task 이름이 저장소 어디에도 남지 않았는지 `grep`
## 6. 명시적 위험
1. leaf 62개에 `plugins {}` 블록을 추가한다. 적용 순서가 바뀌므로 leaf별 `check`로 확인한다.
2. dependency locking이 STRICT라, 어떤 leaf의 configuration에 의존성이 추가되면 락 파일이 깨진다.
따라서 convention 이동은 **해석되는 의존성 집합을 바꾸지 않는 범위**로 제한한다.
`ca.spring-config`는 이미 processor를 선언한 leaf만 opt-in한다.
3. 삭제하는 task 이름을 참조하는 workflow/문서/테스트를 같은 변경에서 고친다.
---
# 실행 결과 (2026-09-16)
## 실제로 한 것
### Gradle
| 파일 | 전 | 후 |
| --- | ---: | ---: |
| `src/build.gradle` | 3,211줄 | 349줄 |
| `src/settings.gradle` | 16줄 | 16줄 (변경 없음) |
| `src/build-logic/src/main/groovy/` | 9 플러그인 | 16 플러그인 |
루트에서 사라진 것과 간 곳:
| 대상 | 처리 |
| --- | --- |
| `configure(subprojects)` 306줄 | `ca.java-conventions` + `ca.quality-conventions` + `ca.java-library` / `ca.spring-library` / `ca.spring-config` / `ca.jmh-benchmarks` |
| `verifyCleanArchitectureDependencies`, `verifyApplicationCoreDependencyPurity`, `verifyNoIgnoredSourcePackages` | `ca.architecture` (루트 적용) + `architectureCheck` |
| JPA readiness registry + release gate 685줄 | `gradle/qualification/jpa-qualification.gradle` |
| Messaging evidence 300줄 | `gradle/qualification/messaging-qualification.gradle` |
| `verifyEnvKeys` 280줄 | `gradle/config-contract.gradle`, `:app-bootstrap`이 적용, `configContractCheck` |
| always-fail Messaging 태스크 9개 | 삭제 → `docs/roadmap/messaging-r2.md` |
| `verifyReadmeCommands`, `verifyRunbookReferences`, `verifyDocumentedLeafCount`, `verifyTestSourceSetRegistry`, `verifyDocumentationContracts` | 삭제 |
| `verifyConfigurationPropertiesProcessor` + `blankJavaCommentsAndLiterals` | 삭제 → `ca.spring-config` |
| `verifyOneTypePerFile` | 삭제 → 호출자가 `checkstyleMain`을 직접 부른다 |
| `verifyTrivyignore`, `verifyQuarantineSunset` | 삭제. `flaky-quarantine.yaml`도 삭제 |
| `verifySpotBugsAnalysisFailureContract` | 삭제. 검사기 본체(`spotBugsAnalysisFailures`)는 `ca.quality-conventions`로 이동 |
`ModuleRegistry`: 필드 집합 정확 일치 → 필수 필드 존재로 완화. self-dependency / 미지의 id /
sample-portfolio edge 검사는 settings에서 `verifyCleanArchitectureDependencies`로 이동.
Git revision: 없으면 configuration 실패 → `0.0.1-SNAPSHOT` + `sourceRevision='unknown'`.
`verifyReleaseProvenance``releaseCheck`에서만 요구한다.
### CI
| 대상 | 처리 |
| --- | --- |
| `.github/ci-gate-matrix.yml` (1,025줄 / 107 gate) | 삭제 |
| `.github/scripts/verify-gate-matrix.sh` (568줄) | 삭제 |
| `.github/scripts/verify-gradle-wrapper.sh` (799줄) | 삭제 → `gradle/actions/setup-gradle`의 기본 wrapper validation |
| 잡마다 반복되던 wrapper-validation 3줄 블록 59개 | composite action 안으로 이동 |
| `_reusable-gradle.yml` | 신규. 단순 Gradle 잡이 호출 |
| `fileserver-release.yml` | `fileserver-certification.yml`로 개명 |
| `docs/ci-cd/boundary.md`, `docs/ci-cd/template-vs-consumer.md` | 신규 |
### 테스트
| 대상 | 전 | 후 |
| --- | ---: | ---: |
| `DeveloperExperienceContractTest` | 1,141줄 (wrapper mutation 24개) | 379줄 |
| `ConditionalTransportQualificationContractTest` | 860줄 (gate matrix 18개) | 94줄 |
| `MessagingCapabilityRegistryContractTest` | 태스크 존재를 요구 | 태스크 부재를 요구 |
`SampleRemovalSmokeContractTest`의 gate matrix 대조 제거.
## 리뷰와 다르게 결정한 것
1. **`ci-quality-gates.yml``ci.yml`로 개명하지 않았다.** 리뷰 §25는 워크플로 이름 정리를
제안했지만, 같은 절에서 "중요한 것은 workflow 개수가 아니라 공통 setup을 복사하지 않는 것"이라고
했다. 개명은 문서·테스트 20여 곳을 건드리고 얻는 것이 이름뿐이다. `fileserver-release.yml`
개명했다 — 그건 이름이 틀렸기 때문이다(릴리스하지 않는다).
2. **`ca.spring-config`는 opt-in이다.** 리뷰 §14는 convention이 자동으로 processor를 넣는 그림을
보여 주지만, 모든 configuration이 STRICT로 락되어 있어서 지금 선언하지 않은 leaf에 넣으면 락이
깨진다. 지금 선언한 15개 leaf가 명시적으로 적용한다.
3. **`integrationCheck``ci`에 넣지 않았다.** 리뷰 §10의 계층은 `ci = check + architectureCheck
+ integrationCheck`지만, 이 저장소의 통합 레인 상당수는 컨테이너 런타임이 필요하고 이미
`integration-main.yml`(main push)과 nightly로 분리돼 있다. PR 게이트에 Docker를 요구하면 리뷰가
비판한 "로컬에서 돌릴 수 없는 check"가 된다.
4. **`verifyRuntimeModuleMembership`은 단순화하지 않았다.** 리뷰는 "현재보다 중복"이라 봤지만,
현재 구현은 선언된 멤버십이 아니라 **해석된 runtime closure**를 비교한다 — 다른 검증이 답하지
않는 질문이다. 중복이었던 85줄은 이미 이전 작업에서 제거돼 있었다.
5. **`springDependencyManagement`를 1.1.6 → 1.1.7로 올렸다.** 리뷰에 없는 항목이다. build-logic이
같은 플러그인을 적용해야 하는데, 루트는 Spring Boot 플러그인 때문에 이미 1.1.7로 해석하고
있었다(`buildEnvironment``1.1.6 -> 1.1.7`로 표시). 카탈로그가 아무도 해석하지 않는 버전을
적고 있었고, 그대로 두면 convention plugin과 leaf가 서로 다른 버전을 쓰게 된다.
## 검증
```text
./gradlew -p build-logic test BUILD SUCCESSFUL (56 tests)
./gradlew verifyDependencyLocks BUILD SUCCESSFUL (62 leaf, 락 변화 없음)
./gradlew architectureCheck BUILD SUCCESSFUL
./gradlew configContractCheck BUILD SUCCESSFUL
./gradlew ci releaseCheck integrationCheck --dry-run BUILD SUCCESSFUL
./gradlew check --continue :sample-portfolio:test 3건만 실패 (아래)
./gradlew :app-bootstrap:test BUILD SUCCESSFUL
./gradlew :app-bootstrap:functionalTest BUILD SUCCESSFUL
python3 -c "yaml.safe_load(...)" workflow 20개 + composite action 파싱 OK
```
락 파일이 한 줄도 바뀌지 않았다는 것이 convention 이동의 핵심 근거다 — 해석되는 의존성 집합이
그대로라는 뜻이다.
## 이번 작업으로 드러난 기존 결함
셋 다 HEAD(d00c762)에서 동일하게 재현되며, 이번 변경이 원인이 아니다.
1. **`:sample-portfolio:test` 3건 실패.** `JpaLiveEventReplayAdapter``@Repository`인데 생성자
3번째 파라미터 `java.time.Duration`을 만족시킬 빈이 없다. 샘플의 `@ComponentScan("dev.caskeleton")`
이 이 어댑터를 집어오고, `app-bootstrap`은 이 타입을 아예 참조하지 않는다. retention을 어디서
받을지(typed settings)는 설계 결정이라 이번 빌드 리팩터링에서 건드리지 않았다. **미해결.**
2. **`:adapter:outbound:messaging:verifyJsonSchemaRuntimeGraph` — 한 번도 통과한 적 없음.**
`libs.*` 카탈로그 accessor(Provider)를 `String`으로 받는 클로저에 넘겨서 실행 즉시
`MissingMethodException`으로 죽었다. `check`가 이 태스크에 의존했지만 항상 앞선 실패가 먼저
빌드를 멈췄다. 고쳤고, 고치자 진짜 문제가 드러났다 — 카탈로그는 `jackson-core:3.0.2`를 적는데
Jackson BOM은 3.1.5로 해석한다. 버전 고정은 현재 상태 검증이므로 **모듈 존재** 검사로 바꿨다.
3. **`BuildVerificationPurityContractTest` 5건 실패.** public path snapshot의 입력이
`src/.env`에서 커밋된 `config/security.yml`로 옮겨졌는데(`src/.env*`는 gitignore라 CI 체크아웃에
없다) 픽스처는 계속 `.env`를 썼다. 스크립트가 맞고 테스트가 이전 계약을 설명하고 있었다. 고쳤다.
@@ -0,0 +1,150 @@
# JPA Evidence Gradle Model Decoupling Design
## Context
`GenerateJpaEvidenceManifestsTask` currently performs evidence generation after its producer tasks run. Its semantic contract is useful, but the task action reaches back into the live Gradle model through `getProject()`, resolves configurations, locates `Task` instances, reads `Test` report locations, inspects `TaskState`, and reads root extra properties.
Gradle 9 deprecates `Task.project` access at execution time and Gradle 10 will reject it. More importantly, the current task mixes two responsibilities:
1. Gradle configuration/model discovery.
2. Pure evidence assembly from producer results.
The refactor must separate those concerns without weakening evidence claims.
## Goals
- Preserve the current readiness-card and evidence-manifest semantics.
- Remove execution-time `Project`, `Task`, and `TaskState` access from `GenerateJpaEvidenceManifestsTask`.
- Preserve JUnit XML as the source of truth for test execution evidence.
- Preserve successful non-Test task execution as the source of truth for `task-claims` such as architecture/configuration claims.
- Represent generator inputs with typed Gradle properties rather than hidden project lookups.
- Keep producer task names and readiness-card schema unchanged.
- Remain compatible with `--warning-mode=fail` on Gradle 9 and prepare the evidence lane for Gradle 10.
## Non-goals
- Do not redesign the readiness-card schema.
- Do not change evidence grades, prerequisite semantics, content hashing, R1/R2 rules, or output layout.
- Do not introduce marker files into every producer task.
- Do not move release orchestration into the persistence-JPA leaf.
- Do not add new runtime dependencies to application modules.
## Architecture
### 1. Build service owns task completion outcomes
Introduce `JpaEvidenceExecutionService`, a Gradle shared build service implementing `OperationCompletionListener`.
The plugin registers it through `BuildEventsListenerRegistry.onTaskCompletion(...)` so the service receives `TaskFinishEvent` events without the generator querying `TaskState`.
The service stores a thread-safe typed outcome for each task path:
```text
Task path
-> SUCCESS
-> FAILED
-> SKIPPED
```
Only `SUCCESS` satisfies an evidence `task-claim`. Failed or skipped producers do not cover the claim.
The service is build-scoped and contains no `Project` reference.
### 2. Test evidence remains file-based
JUnit evidence already has a durable output: Gradle's JUnit XML result directory. The plugin resolves every readiness/support `Test` task during configuration and supplies a typed mapping:
```text
absolute task path -> JUnit XML result directory
```
The generator reads those directories directly with `JUnitEvidenceReader`; it never locates a `Test` object.
Non-Test support tasks continue to participate in the task graph but do not produce JUnit evidence.
### 3. Configuration-derived values become task inputs
The plugin supplies these inputs before execution:
- evidence profile
- CI job
- artifact location
- topology
- PostgreSQL image
- source revision
- traceable version
- resolved PostgreSQL JDBC version
- resolved Hibernate ORM version
- resolved Flyway version
- repository-relative evidence output location used by the candidate default
- JUnit result-directory mapping
The generator reads only its properties/files plus the execution service.
`releaseProvenance` is the preferred source for revision/version. The existing extra-property compatibility bridge is no longer read by the generator.
### 4. Dependency-version discovery stays in plugin configuration
The JPA evidence plugin owns the Gradle `Configuration` object. It derives the three relevant resolved module versions and writes them into typed task properties before the generator executes.
This keeps dependency-graph access out of the task action. The existing coordinates remain unchanged:
- `org.postgresql:postgresql`
- `org.hibernate.orm:hibernate-core`
- `org.flywaydb:flyway-core`
### 5. Generator becomes an evidence assembler
The generator task action may use:
- its declared Gradle properties/files
- `ExecOperations` for git/docker commands already owned by the task
- `FileSystemOperations`
- `JpaEvidenceExecutionService`
- pure parser/verifier/helper classes
It must not call:
```java
getProject()
Project.findProject(...)
Task.getState()
TaskContainer.findByName(...)
ConfigurationContainer.getByName(...)
ExtraPropertiesExtension.get(...)
```
### 6. Evidence semantics
For a readiness card:
- `evidence.scenarios` are covered only by selectors found in JUnit XML.
- `evidence.task-claims` are covered only when the build service reports the named task completed successfully in the current build.
- `no-skip` remains based on JUnit result counts.
- prerequisite manifest ordering and hashing remain unchanged.
- candidate/R2 blockers remain unchanged.
The primary foundation card still obtains architecture/configuration coverage from successful execution of its declared producer tasks; the mechanism changes from `TaskState` lookup to task-finish events, not the meaning.
## Error handling
- A readiness task expected to produce JUnit evidence but missing from the configured result mapping is a hard failure.
- A configured JUnit result directory that contains no usable result remains subject to the existing JUnit evidence validation.
- A task claim with no successful completion event is simply uncovered and therefore becomes missing required evidence when that claim is required.
- Unsupported evidence profile remains a hard failure.
- Missing immutable image digest/dependency versions retain the existing blocker behavior.
## Testing
1. Unit-test task-event classification in `JpaEvidenceExecutionService`.
2. Unit-test pure JUnit result lookup from configured task-path/directory inputs.
3. TestKit: apply `ca.jpa-evidence` in a fixture and verify the generator task exposes typed inputs without execution-time project lookup.
4. Existing JPA evidence verifier tests must remain green.
5. Run `build-tools:check --warning-mode=fail`.
6. Run `verifyJpaReadinessRegistry verifyJpaReleaseGateTasks --warning-mode=fail`.
7. Run the affected JPA leaf `check`.
8. Run a candidate evidence lane far enough to confirm no `Task.project` deprecation is emitted; environment-dependent Docker/Testcontainers failure may be reported separately from Gradle-model warnings.
## Migration boundary
This change only decouples evidence generation from the live Gradle model. It does not alter the readiness registry, producer tasks, JUnit test suites, manifest schema, release workflow, or evidence verification policy.
@@ -0,0 +1,94 @@
# Messaging Platform Bridge Design
## Goal
Replace the application-specific broker seam with one canonical anti-corruption bridge:
```
application-core IntegrationEventPublishPort
-> adapter/outbound/messaging/platformbridge
-> messaging-schema-api EncodedMessagePublisher
-> messaging-runtime-core DefaultMessagePublisher
-> messaging transport/runtime
```
The bridge must preserve canonical event identity and exact encoded bytes while reusing the platform's destination resolution, authorization, admission, runtime leasing, transport normalization, and observation pipeline.
## Scope
This phase introduces and verifies the canonical bridge. It does **not** migrate the legacy outbox storage/relay rows, because `OutboxEvent` does not retain the schema/order/tenant metadata required to reconstruct `ValidatedIntegrationEvent` without invention.
## Application boundary
Create `IntegrationEventPublishPort` in `application-core`.
Signature:
```java
CompletionStage<OutboxPublishOutcome> publish(ValidatedIntegrationEvent event);
```
The application package depends only on its own canonical event model and application outcome vocabulary.
## Adapter bridge
`PlatformIntegrationEventPublishAdapter` lives under:
```
adapter/outbound/messaging/platformbridge
```
It depends on `EncodedMessagePublisher`, never on a concrete broker client, runtime-core implementation, or transport SPI.
Mapping rules:
- `logicalDestinationId` -> platform `DestinationName`.
- `contractId` -> platform `MessageType`.
- `payloadVersion` -> `SchemaVersion`.
- event and causation identities must parse as UUIDv7; values are preserved exactly. Incompatible identities fail closed before the platform publisher is called.
- `occurredAt` is used for both `producedAt` and `occurredAt` until the application canonical model carries a separate production timestamp. The bridge never invents a new timestamp.
- producer is an explicit constructor/configuration value.
- correlation, partition key, tenant, aggregate order and exact envelope bytes are preserved.
- trace context is explicitly absent (`TraceContext.none()`) until the application model owns canonical trace context.
- exact `envelopeBytes` become `EncodedMessage` bytes; no re-encoding occurs.
- schema/catalog/binding/envelope evidence that has no first-class platform field is preserved as bounded `x-ca-*` headers.
- the schema reference subject is the canonical contract id and version is the canonical payload version.
## Outcome mapping
Mapping is based on completion **and transmission evidence**, not enum name similarity:
- CONFIRMED -> `OutboxPublishOutcome.CONFIRMED`.
- AMBIGUOUS -> `OutboxPublishOutcome.AMBIGUOUS`.
- REJECTED + NOT_TRANSMITTED -> `REJECTED_BEFORE_SEND`.
- REJECTED + any evidence that bytes may have left the process -> `REJECTED_AFTER_BROKER`.
Bridge preparation failures are definite pre-send rejection.
## Platform boundary
`EncodedMessagePublisher` is owned by `messaging-schema-api`, because `EncodedMessage` is owned there and the dependency direction remains acyclic.
`DefaultMessagePublisher` implements both `MessagePublisher` and `EncodedMessagePublisher`. The encoded path skips only codec lookup/encoding; destination resolution, access policy, admission, runtime lease, transport send, deadline handling, result normalization and observation are shared with the normal publish path.
The starter exposes one `DefaultMessagePublisher` singleton, which therefore satisfies both public interfaces.
## Spring ownership
`MessagingBridgeRootAutoConfiguration` owns the bridge bean when an `EncodedMessagePublisher` is present **and** `app.messaging.producer-id` is explicitly configured. Producer identity is never inferred from `spring.application.name` or invented. Application bootstrap must not construct Kafka producer clients or implement broker-specific send behavior.
The existing `KafkaSender` / `KafkaMessageBroker` path remains temporarily for the legacy `OutboxEvent` and realtime publishers, which do not yet carry enough canonical metadata to enter the new bridge without invention. It is explicitly transitional and is removed only with the legacy outbox/realtime cutover. The new canonical bridge never calls it.
## Verification
Required checks:
1. `DefaultMessagePublisherTest`: pre-encoded publish preserves bytes and skips codec while still exercising central pipeline.
2. `PlatformIntegrationEventPublishAdapterTest`: golden mapping, outcome mapping, fail-closed identity behavior.
3. outbound messaging module tests/check.
4. messaging runtime/starter tests.
5. app-bootstrap system test and architecture test after adding the canonical bridge while retaining the documented legacy seam.
6. search proving app-bootstrap has no direct native Kafka sender configuration.
7. dependency/build lock refresh only where dependency ownership changed.
8. `git diff --check`.
@@ -0,0 +1,194 @@
# Outbox Transport-Only Cutover Design
## Status
Approved implementation slice for MSG-015 transport-only cutover.
This design deliberately does **not** activate `POLLING_V2` and does not migrate the publication authority to the v2 delivery tables. The existing `outbox_event` writer/store/claim/status authority remains the only active authority. The change makes that legacy authority capable of carrying a canonical integration event without losing the exact platform envelope.
## Goal
Support both row generations under one legacy relay authority:
```text
business transaction
-> legacy NewOutboxEvent -> legacy row
-> canonical ValidatedIntegrationEvent -> canonical-compatible row
one OutboxStorePort claim authority
-> legacy claimed row -> MessageBroker compatibility path
-> canonical claimed row -> IntegrationEventPublishPort -> messaging platform
```
A row is published through exactly one branch. There is no dual write and no second relay scheduler.
## Application boundaries
### Canonical append
`OutboxAppendPort` becomes the canonical durable append boundary:
```java
void append(ValidatedIntegrationEvent event);
```
### Legacy append
Raw R0 payload append moves to an explicitly named compatibility port:
```java
LegacyOutboxAppendPort
void append(NewOutboxEvent event);
```
Existing sample/durable-operation code that still emits raw `NewOutboxEvent` uses only the legacy port. New canonical code must not call the legacy port.
### Claimed row model
The relay-facing row is a sealed application model:
```text
ClaimedOutboxEvent
|- OutboxEvent // legacy R0 claim model retained for compatibility
`- CanonicalClaimedOutboxEvent // reconstructs one ValidatedIntegrationEvent
```
`OutboxStorePort.claimBatch` returns `List<ClaimedOutboxEvent>`.
Common relay state is exposed by the sealed interface: event id, event type, aggregate id, occurred-at, status and attempt count. The canonical subtype also exposes the exact `ValidatedIntegrationEvent`.
A persisted row with a **partial** canonical metadata set is corrupt and fails closed during mapping. It is never downgraded to the legacy path.
## Storage compatibility projection
The existing PostgreSQL `outbox_event` remains authoritative. Add a forward migration after current legacy V12 that:
- widens `event_id` to `varchar(96)`;
- widens `correlation_id` to `varchar(128)`;
- adds nullable canonical columns to preserve existing rows;
- adds an all-or-none canonical-shape check;
- stores exact canonical envelope bytes in `bytea`;
- keeps the legacy required columns for the rollback window.
Canonical required columns:
```text
contract_id
envelope_version
payload_version
logical_destination
tenant_scope
aggregate_type
aggregate_sequence
event_index
partition_key
envelope_bytes
content_type
schema_set_hash
envelope_sha256
envelope_schema_hash
payload_schema_hash
contract_catalog_revision
destination_binding_revision
```
`causation_id` is optional by the application contract.
Existing legacy columns remain populated for canonical rows with this compatibility projection:
```text
event_id = canonical event id
aggregate_id = canonical aggregate id
event_type = contract id
payload = exact envelope bytes decoded as strict UTF-8
occurred_at = canonical occurred-at
status = PENDING
attempt_count = 0
next_attempt_at = occurred-at
correlation_id = canonical correlation id
idempotency_key = event id
```
The canonical encoder currently emits a UTF-8 JSON envelope. The append adapter verifies strict UTF-8 round-trip before storing the compatibility text. Invalid UTF-8 fails the business transaction; replacement characters are forbidden.
`partitionKeyBytes` is not stored separately because the canonical model already requires it to be exactly the US-ASCII bytes of `partitionKeyText`. The claimed model reconstructs those bytes from the stored canonical text.
## Persistence adapters
`OutboxStoreAdapter` remains the legacy claim/status store and implements `LegacyOutboxAppendPort`, not `OutboxAppendPort`.
A separate `CanonicalOutboxAppendAdapter` implements `OutboxAppendPort`. It participates in the caller's existing write transaction exactly like the legacy adapter and never opens a local transaction.
Both write the same `outbox_event` table; they are alternative semantic inputs, not dual writers for one business fact.
## Activation
Introduce:
```text
ca-skeleton.outbox.canonical-transport-enabled=false
```
Default remains false.
When false:
- existing legacy append/relay behavior is unchanged;
- canonical append bean is not exposed;
- canonical relay routing is not considered an active deployment capability.
When true:
- canonical append bean is exposed;
- startup requires an `IntegrationEventPublishPort`;
- the relay publisher can route canonical claimed rows to that port;
- legacy rows continue through `MessageBroker`;
- while mixed legacy rows may still exist, a relay-enabled deployment still requires the legacy broker. Canonical transport is an additional route, not permission to strand legacy backlog.
The gate is a compatibility/cutover gate only. It does not change DB publication authority and does not activate `POLLING_V2`.
## Publish routing
`OutboxMessagePublishPort` remains the one relay publish port and accepts `ClaimedOutboxEvent`.
Implementation behavior:
- `OutboxEvent` -> existing `OutboxEnvelopeJson` + `MessageBroker`.
- `CanonicalClaimedOutboxEvent` -> exact stored `ValidatedIntegrationEvent` -> `IntegrationEventPublishPort`.
The canonical branch blocks on the returned `CompletionStage` only at this legacy compatibility boundary, because the current legacy relay port is synchronous. The platform result is mapped unchanged into `OutboxPublishOutcome`.
The bridge does not re-encode canonical bytes.
If canonical transport is disabled or the canonical publisher is absent, canonical publication fails closed before broker/platform transmission. Startup validation prevents the normal configured case from reaching that state.
## Outcome policy
The existing legacy relay state machine remains authoritative in this slice:
- CONFIRMED -> mark PUBLISHED.
- AMBIGUOUS -> retryable legacy FAILED flow.
- REJECTED_BEFORE_SEND / REJECTED_AFTER_BROKER -> existing definite-refusal DEAD behavior.
This is intentionally the existing compatibility semantics. The richer v2 per-attempt state machine is a later storage-authority cutover.
## Non-goals
This slice does not:
- switch `OutboxPublicationAuthority` to `POLLING_V2`;
- mutate/reconcile `outbox_event_log_v2` or `outbox_delivery_v2`;
- implement CDC;
- remove `MessageBroker`, `KafkaSender`, `NewOutboxEvent`, `OutboxEvent`, or the legacy scheduler;
- migrate old rows into canonical rows;
- invent tenant, trace, schema or routing metadata for old rows.
## Verification
Required:
1. application port split compiles and old raw producers use `LegacyOutboxAppendPort`;
2. migration integration proves additive columns, exact BYTEA, constraints and legacy compatibility;
3. canonical append adapter round-trips every canonical field and exact bytes;
4. partial canonical row mapping fails closed;
5. legacy row mapping remains unchanged;
6. relay unit test proves canonical row invokes only `IntegrationEventPublishPort`;
7. legacy row invokes only `MessageBroker`;
8. canonical bytes reaching `PlatformIntegrationEventPublishAdapter` are byte-identical;
9. startup rejects canonical transport enabled without `IntegrationEventPublishPort`;
10. default-off composition preserves current behavior;
11. architecture/dependency checks and `git diff --check` pass.
+179
View File
@@ -0,0 +1,179 @@
# 테스트 전략 — 레벨 정의와 소스셋 매핑 (SSOT)
- 기준 일자: 2026-09-07
- 상태: **활성 문서.** 아래 §3 표는 사람이 유지한다. `verifyTestSourceSetRegistry` 가 이 문서의 §3 표와 실제 Gradle 소스셋 선언의
불일치를 빌드 실패로 만든다.
- 근거 리뷰: `docs/reviews/2026-09-07-app-bootstrap-module-code-review.md` (BOOT-014, BOOT-015,
BOOT-016)
## 1. 이 문서가 존재하는 이유
이 저장소는 이미 테스트 레벨 계약을 **기계로** 강제하고 있었다 —
`TestTaxonomyArchitectureTest` 가 contract/architecture 트리의 Testcontainers 의존을 금지하고,
slice 애노테이션 혼합을 막고, fixture 유출을 잡는다. 없던 것은 **사람이 읽을 수 있는 정의**였다.
그 결과 계약은 "패키지 이름"에만 걸려 있었고 "어느 소스셋이 컴파일하는가"에는 걸려 있지 않았다.
Testcontainers 를 쓰는 통합 테스트 9개가 `app-bootstrap/src/test` 안에 있었고, `@Testcontainers` 5개
중 가드가 있는 것은 하나뿐이었다. 즉 `./gradlew :app-bootstrap:test` — 이 저장소가 leaf 별 기본
명령으로 권장하는 바로 그 명령 — 이 Docker 데몬을 요구했다 (BOOT-014).
그래서 규칙을 두 가지 방식으로 동시에 고정한다. 사람은 이 문서를 읽고, 빌드는 §3 표를 읽는다.
## 2. 레벨 정의
레벨은 **이름이 아니라 "어디까지 실제로 붙여서 검증하는가"**로 정의한다. `smoke`, `regression`,
`acceptance` 같은 말은 범위가 아니라 목적이라 레벨이 될 수 없다 — 하나의 E2E 테스트가 동시에
smoke 이고 regression 일 수 있다.
| 레벨 | 무엇을 검증 | 외부 시스템 | 소스셋 |
| --- | --- | --- | --- |
| **unit** | 클래스·함수·도메인 규칙 | 없음 | `src/test` |
| **slice** | 프레임워크 한 계층 | 인메모리/모의 | `src/test` |
| **contract** | 모듈 경계의 형태와 약속 | 없음 | `src/test` |
| **architecture** | 코드 의존 관계, 테스트 분류 자체 | 없음 | `src/test` |
| **integration** | 실제 인프라와의 연결 | 실제 DB/브로커/스토리지 | `src/integrationTest` 또는 leaf 전용 레인 |
| **qualification** | 벤더·프로토콜·배포 형상 | 실제 벤더 런타임 | leaf 전용 레인 |
| **build-qualification** | 빌드·조립 계약 자체 | 없음 (별도 클래스패스) | leaf 전용 레인 |
| **performance** | 지연·처리량 | 실제에 가까움 | leaf 전용 레인 |
### 2.1 소스셋을 나누는 기준은 하나다
> **테스트 코드는 production 패키지 구조를 그대로 미러링한다. 별도 소스셋으로 분리하는 것은
> 실행 환경·의존성·클래스패스가 달라지는 경우뿐이다.**
`unit/`, `service/`, `repository/`, `regression/` 같은 폴더는 만들지 않는다. 서로 다른 분류 축을
한 디렉터리에 섞으면 `UserServiceTest` 가 어디에 속하는지 아무도 답할 수 없게 된다. 이 저장소의
`src/test` 는 이미 production 패키지를 미러링하고 있으며 그 상태를 유지한다.
### 2.2 build-qualification 은 폴더 취향이 아니다
`app-bootstrap` 의 세 레인은 "테스트를 분류하려고" 나눈 것이 아니라 **하나의 소스셋으로 표현할 수
없는 클래스패스 차이** 때문에 존재한다. 합치면 검증 자체가 성립하지 않는다.
- `sampleOffTest``src/test` 와 **같은 소스 파일**을 `sample-portfolio` 없는 클래스패스로 다시
컴파일한다. "샘플을 지워도 템플릿이 성립하는가"의 증명이며, 같은 파일을 두 클래스패스로 컴파일하는
것이 그 정의다.
- `conditionalTransportTest` — GraphQL/gRPC/WebSocket 을 **테스트 전용으로만** 클래스패스에 올린다.
이 의존을 `testImplementation` 으로 옮기면 "기본 클래스패스에는 없다"는 증명 대상 명제가 그 순간
거짓이 된다.
- `functionalTest` — Gradle TestKit 이 별도 Gradle 빌드를 띄운다.
## 3. 소스셋 레지스트리 (기계 검증 대상)
이 표를 읽어 실제 `sourceSets` 선언과 대조하던 `verifyTestSourceSetRegistry` 는 2026-09에 삭제했다
(Markdown 표 파서였고, `<!-- registry:begin -->` 마커가 사라지면 계약이 산문으로 되돌아가는 것을
막으려고 마커 존재 자체까지 검사했다). 레인을 추가하면 이 표도 같이 고친다. 아래 옛 설명은 표를
어떻게 읽어야 하는지에 대한 기준으로 남긴다: 표에 없는 소스셋을
추가하거나 표에 있는 소스셋을 지우면 빌드가 실패한다.
<!-- registry:begin -->
| Gradle 경로 | 소스셋 | 레벨 |
| --- | --- | --- |
| `:adapter:inbound:graphql` | `testFixtures` | fixtures |
| `:adapter:inbound:web` | `jettyCompatTest` | qualification |
| `:adapter:inbound:web` | `nginxProxyTest` | qualification |
| `:adapter:inbound:web` | `testFixtures` | fixtures |
| `:adapter:inbound:web` | `webfluxContractTest` | qualification |
| `:adapter:inbound:websocket` | `jettyWebSocketTest` | qualification |
| `:adapter:inbound:websocket` | `nginxWebSocketTest` | qualification |
| `:adapter:inbound:websocket` | `testFixtures` | fixtures |
| `:adapter:outbound:httpclient` | `httpClientPerformanceTest` | performance |
| `:adapter:outbound:httpclient` | `jmh` | performance |
| `:adapter:outbound:httpclient` | `testFixtures` | fixtures |
| `:adapter:outbound:objectstorage` | `objectStorageAwsQualificationTest` | qualification |
| `:adapter:outbound:objectstorage` | `objectStorageMinioContractTest` | integration |
| `:adapter:outbound:objectstorage` | `objectStorageMinioFaultTest` | integration |
| `:adapter:outbound:persistence-jpa` | `jpaPlatformPerformanceTest` | performance |
| `:adapter:outbound:persistence-jpa` | `postgresqlIntegrationTest` | integration |
| `:adapter:outbound:persistence-jpa` | `testFixtures` | fixtures |
| `:adapter:outbound:persistence-mongo` | `mongoPerformanceTest` | performance |
| `:adapter:outbound:persistence-mongo` | `testFixtures` | fixtures |
| `:app-bootstrap` | `conditionalTransportTest` | build-qualification |
| `:app-bootstrap` | `functionalTest` | build-qualification |
| `:app-bootstrap` | `integrationTest` | integration |
| `:app-bootstrap` | `sampleOffTest` | build-qualification |
| `:messaging:messaging-kafka` | `jmh` | performance |
| `:messaging:messaging-rabbit` | `jmh` | performance |
| `:messaging:messaging-testkit` | `jmh` | performance |
| `:sample-portfolio` | `posterImageMigrationTest` | qualification |
| `:shared-contract` | `edgeRateLimitContractTest` | contract |
<!-- registry:end -->
`src/test` 는 모든 leaf 가 갖는 기본 소스셋이므로 표에 적지 않는다.
## 4. 판단표 — 새 테스트를 어디에 쓰는가
대상 코드가 정해지면 위치와 방식이 기계적으로 결정되어야 한다.
| 대상 | 레벨 | 협력자 | 위치 |
| --- | --- | --- | --- |
| 도메인 엔티티·값 객체 | unit | 없음 | 해당 leaf `src/test` |
| 유스케이스 | unit | 손으로 만든 Fake (Mockito 아님) | `application-core/src/test` |
| 시작 검증기 (`*Validator`) | unit | `MockEnvironment` | `app-bootstrap/src/test` |
| `@Configuration` 조립 | slice | `ApplicationContextRunner` | `app-bootstrap/src/test` |
| 컨트롤러 | slice | `@WebMvcTest` + 모의 유스케이스 | `adapter/inbound/web/src/test` |
| JPA 리포지토리 매핑 | integration | Testcontainers PostgreSQL | `postgresqlIntegrationTest` |
| 아웃박스·멱등성 행 수명주기 | integration | Testcontainers PostgreSQL | `app-bootstrap/src/integrationTest` |
| 브로커 발행/수신 | integration | 실제 브로커 | leaf 전용 레인 |
| 에러 응답 스키마 | contract | 없음 (스냅샷) | `app-bootstrap/src/test/.../contract` |
| 의존 방향·패키지 경계 | architecture | 없음 (ArchUnit) | `app-bootstrap/src/test/.../architecture` |
### 4.1 금지
- `src/test` 안에서 `org.testcontainers` 의존 — `TestTaxonomyArchitectureTest` 가 막는다 (BOOT-014).
- 필요 없는 `@SpringBootTest`. 조립을 검증할 것이 아니면 `ApplicationContextRunner` 나 순수 단위
테스트로 충분하다.
- slice 애노테이션 혼합 (`@WebMvcTest` + `@DataJpaTest`) — Spring 이 지원하지 않는다.
- production 코드가 test fixture 에 의존하는 것.
- 픽스처를 `TestUtil`·`CommonUtil` 같은 이름으로 묶는 것. 역할을 드러내는 이름
(`fixture/`, `fake/`, `container/`, `assertion/`) 을 쓴다.
## 5. 공용 테스트 지원 코드 — `testFixtures`
**표준은 `java-test-fixtures` 하나다 (ADR-BUILD-001).** 공용 테스트 지원 코드는
`src/testFixtures/java` 에 두고, 다른 leaf 는 `testFixtures(project(':x'))` 로 소비한다.
두 관례가 공존하던 상태(BOOT-015)는 해소됐다. `ca.testkit-publisher` 컨벤션 플러그인과 그것을 쓰던
`testkit` 소스셋 5개는 모두 이관됐고, 플러그인 자체도 제거됐다. 이관하면서 드러난 사실 하나는 기록해
둘 값어치가 있다: `testkit*` 구성이 `testImplementation` 을 상속했기 때문에 fixture 들은 각 leaf 가
선언한 모든 테스트 라이브러리를 **말없이** 보고 있었다. `testFixturesImplementation` 으로 옮기면서
그 표면이 드러났고, 다섯 leaf 에서 도합 30개가 넘는 의존을 명시적으로 적어야 했다.
`test` 가 아닌 lane 은 fixture 를 소비한다고 선언해야 한다 — `java-test-fixtures``test`
자동으로 배선한다:
```groovy
strictTestLanes {
sourceSet('postgresqlIntegrationTest') { compilesAgainst 'main', 'testFixtures' }
}
```
디렉터리는 역할을 드러내는 형태를 권고한다 (`fixture/`, `fake/`, `container/`, `assertion/`).
`TestUtil`·`CommonUtil` 같은 무의미한 이름 묶음은 금지한다.
## 6. CI 단계 매핑
폴더만 나누고 CI 에서 한꺼번에 돌리면 분리의 의미가 없다.
```
커밋 / IDE → unit · slice · contract · architecture (`test`)
Pull Request → + integration (integration 레인)
머지 / 스테이징 → + build-qualification (functionalTest, sampleOffTest,
conditionalTransportTest)
야간 / 스케줄 → + qualification · performance
```
`check` 에는 인프라 레인을 붙이지 않는다. 이것은 이 저장소가 이미 따르고 있는 관례이며
(`persistence-jpa``postgresqlIntegrationTest``check` 에 붙어 있지 않다), Docker 없는
환경에서 `check` 가 실패하지 않게 하는 유일한 방법이다.
## 7. LLM 에이전트에게 적용할 때
이 저장소는 에이전트 협업을 전제로 설계되어 있다. 테스트 생성을 맡길 때는 다음 순서를 강제한다.
1. 이 테스트가 §2 의 어느 레벨인지 판정하고 근거를 적는다.
2. §3 표에서 해당 소스셋을 찾는다.
3. 이미 존재하는 fixture 를 먼저 검색한다.
4. 테스트를 작성한다.
5. 판정한 레벨보다 큰 레벨로 작성하지 않았는지 확인한다 (`@SpringBootTest` 를 썼다면 왜 필요한지
설명할 수 있어야 한다).
-27
View File
@@ -1,27 +0,0 @@
# Flaky-test quarantine registry — feature-ci-quality-gates-contract §4 (D7 / D9).
#
# This branch is the flaky-quarantine SSOT. A test that flakes may be tagged with JUnit's built-in
# @Tag("quarantine") so it stops blocking the release gate (src/build.gradle: the main `test` task
# runs excludeTags 'quarantine'; the bucket runs separately via `./gradlew quarantineTest`,
# non-blocking). Quarantine is a TEMPORARY escape, never a parking lot — every quarantined test MUST
# be listed here and MUST leave quarantine within 14 days.
#
# The `verifyQuarantineSunset` Gradle gate (wired into `check`) enforces, on every build:
# - schema — each entry has test / quarantined_since / reason / tracking_issue;
# - sunset — quarantined_since is within 14 days (older → build fails);
# - drift — every @Tag("quarantine") test in src/**/test is registered here (and vice-versa,
# a registered test should carry the tag).
#
# This file lives at the repo ROOT (not docs/, which is gitignored) so it is committed and readable
# by CI — same rationale as .trivyignore.yaml. CODEOWNERS governs merge-time approval of changes.
#
# Schema (one list entry per quarantined test):
#
# quarantined:
# - test: "dev.caskeleton.bootstrap.contract.SomeFlakyContractTest" # FQN, optionally "...#method"
# quarantined_since: "2026-06-20" # ISO date; 14-day sunset
# reason: "intermittent timeout under shared CI load — suspected fixed-port bind race"
# tracking_issue: "https://github.com/<org>/<repo>/issues/123"
#
# The skeleton ships with an EMPTY bucket: no flaky tests are quarantined.
quarantined: []
+9
View File
@@ -4,6 +4,15 @@
# noise that must not enter the image build context, while keeping everything # noise that must not enter the image build context, while keeping everything
# the builder stage needs to resolve dependencies and run bootJar. # 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 -------------------------------------------------------- # ---- Version control --------------------------------------------------------
.git .git
.gitignore .gitignore
+2
View File
@@ -227,6 +227,7 @@ APP_CACHE_REDIS_L1_INVALIDATION_QUEUE_CAPACITY=1024
APP_CACHE_DEFAULT_TTL=300s APP_CACHE_DEFAULT_TTL=300s
APP_CACHE_NEGATIVE_TTL=60s APP_CACHE_NEGATIVE_TTL=60s
APP_MESSAGING_BROKER= APP_MESSAGING_BROKER=
APP_MESSAGING_PRODUCER_ID=
APP_MESSAGING_KAFKA_BROKERS= APP_MESSAGING_KAFKA_BROKERS=
APP_NOTIFICATION_SLACK_PROVIDER= APP_NOTIFICATION_SLACK_PROVIDER=
APP_NOTIFICATION_EMAIL_PROVIDER= APP_NOTIFICATION_EMAIL_PROVIDER=
@@ -301,6 +302,7 @@ APP_GRAPHQL_ENABLED=false
APP_GRAPHQL_DEPLOYMENT_MODE= APP_GRAPHQL_DEPLOYMENT_MODE=
APP_OUTBOX_ENABLED=false APP_OUTBOX_ENABLED=false
APP_OUTBOX_RELAY_ENABLED=false APP_OUTBOX_RELAY_ENABLED=false
APP_OUTBOX_CANONICAL_TRANSPORT_ENABLED=false
APP_NOTIFICATION_PLATFORM_ENABLED=false APP_NOTIFICATION_PLATFORM_ENABLED=false
APP_NOTIFICATION_PLATFORM_MODE=SERVING APP_NOTIFICATION_PLATFORM_MODE=SERVING
# OpenAPI exposure. application-prod.yml pins both false regardless of these. # OpenAPI exposure. application-prod.yml pins both false regardless of these.
+1 -1
View File
@@ -11,7 +11,7 @@ SPRING_PROFILES_ACTIVE=local
# JPA: needs the PostgreSQL service. Flyway owns the schema from dev onward, and local uses the # 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. # same vendor semantics so the two do not diverge.
APP_PERSISTENCE_JPA_ENABLED=true 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_USERNAME=ca_skeleton
APP_DATASOURCE_PASSWORD= APP_DATASOURCE_PASSWORD=
APP_DATASOURCE_DDL_AUTO=validate APP_DATASOURCE_DDL_AUTO=validate
+52 -7
View File
@@ -59,6 +59,27 @@ COPY . .
RUN ./gradlew :app-bootstrap:stageDockerJar --no-daemon -x test \ RUN ./gradlew :app-bootstrap:stageDockerJar --no-daemon -x test \
-PreleaseVersion="${RELEASE_VERSION}" -PgitRevision="${GIT_SHA}" -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 ------------------------------------------------- # ---- Stage 2: runtime image -------------------------------------------------
# JRE-only slim image (D3: no full JDK in production image). # JRE-only slim image (D3: no full JDK in production image).
# Uses eclipse-temurin:21-jre-jammy — the Adoptium-supported JRE variant. # Uses eclipse-temurin:21-jre-jammy — the Adoptium-supported JRE variant.
@@ -130,24 +151,48 @@ VOLUME ["/var/lib/backend/files"]
WORKDIR /app 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 USER app
# ---- Ports ------------------------------------------------------------------ # ---- Ports ------------------------------------------------------------------
# 8080 — application HTTP port # 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 EXPOSE 8080 9001
# ---- Health check ----------------------------------------------------------- # ---- Health check -----------------------------------------------------------
# Targets the actuator readiness probe on the management port (9001). # 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 # The endpoint is real: config/observability.yml sets management.server.port to 9001 and
# (container-side) and will pass once those branches are merged. In this worktree # management.endpoint.health.probes.enabled to true, which is what publishes
# the endpoint may return 404; the container will be UNHEALTHY until merged. # /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 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD wget --no-verbose --tries=1 --spider \ CMD wget --no-verbose --tries=1 --spider \
http://localhost:9001/actuator/health/readiness || exit 1 http://localhost:9001/actuator/health/readiness || exit 1
# ---- Entrypoint ------------------------------------------------------------- # ---- Entrypoint -------------------------------------------------------------
ENTRYPOINT ["java", "-jar", "/app/app.jar"] # The extracted thin JAR, not the uber JAR the builder produced. It carries only application classes
# and a Class-Path pointing at ./lib, so the loader does not open and index a nested-JAR archive on
# every start, and the layout stays AOT-cache/CDS friendly for any later startup work.
ENTRYPOINT ["java", "-jar", "application.jar"]
+17 -3
View File
@@ -63,6 +63,14 @@ COPY . .
RUN ./gradlew :sample-portfolio:stageDockerJar --no-daemon -x test \ RUN ./gradlew :sample-portfolio:stageDockerJar --no-daemon -x test \
-PreleaseVersion="${RELEASE_VERSION}" -PgitRevision="${GIT_SHA}" -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 ------------------------------------------------- # ---- Stage 2: runtime image -------------------------------------------------
# JRE-only slim image (no full JDK in the demo image either). # JRE-only slim image (no full JDK in the demo image either).
FROM eclipse-temurin:21-jre-jammy@sha256:199aebeb3adcde4910695cdebfe782ada38dadb6cc8013159b58d3724451befd AS runtime FROM eclipse-temurin:21-jre-jammy@sha256:199aebeb3adcde4910695cdebfe782ada38dadb6cc8013159b58d3724451befd AS runtime
@@ -108,7 +116,12 @@ RUN groupadd --system --gid 1000 app \
WORKDIR /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 USER app
@@ -125,5 +138,6 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
http://localhost:9001/actuator/health/readiness || exit 1 http://localhost:9001/actuator/health/readiness || exit 1
# ---- Entrypoint ------------------------------------------------------------- # ---- Entrypoint -------------------------------------------------------------
# mainClass (SamplePortfolioApplication) is baked into the bootJar manifest. # mainClass (SamplePortfolioApplication) is baked into the bootJar manifest and survives the
ENTRYPOINT ["java", "-jar", "/app/app.jar"] # extraction into the thin application.jar.
ENTRYPOINT ["java", "-jar", "application.jar"]
+74 -77
View File
@@ -11,40 +11,48 @@
## build.gradle — 빌드 / 검증 게이트 ## build.gradle — 빌드 / 검증 게이트
모든 모듈의 `check` 태스크는 아래 verify 게이트에 의존합니다. 빌드를 통과하려면 검사가 검증은 로컬 피드백과 저장소 전체 검증을 분리합니다. `:<leaf>:check`는 해당 모듈의 컴파일,
모두 green 이어야 합니다. 테스트, Spotless, Checkstyle, Error Prone만 담당합니다. 저장소 전체 정책은 이름이 드러나는
루트 task로 실행하며, CI가 이를 집계합니다.
| 게이트 | 하는 일 | | 게이트 | 하는 일 |
| --- | --- | | --- | --- |
| `verifyCleanArchitectureDependencies` | 모듈 간 의존 방향이 허용된 범위 안에 있는지 검사 | | `architectureCheck` | Clean Architecture 의존 방향, runtime membership, application-core purity, Git에 실리지 않는 source package를 검사 |
| `verifyRuntimeModuleMembership` | registry의 두 composition root membership과 실제 main project dependency가 정확히 일치하는지 검사 | | `qualityCheck` | 모든 leaf의 SpotBugs + FindSecBugs를 실행 |
| `verifyEnvKeys` | `env-keys.yaml``application.yml``src/.env` 가 어긋나지 않는지 검사 | | `configContractCheck` | `:app-bootstrap:verifyEnvKeys`로 환경 설정 계약을 검사 |
| `verifyOneTypePerFile` | 파일당 public 최상위 타입 1개, 파일명 == 타입명인지 검사 | | `qualificationCheck` | Gradle TestKit 기반 build/repository behavior 검증을 실행 |
| `verifyTrivyignore` | `.trivyignore.yaml` 의 Trivy suppression 이 사유·만료일을 갖추고 만료/기한초과가 아닌지 검사 | | `integrationCheck` | 명시적으로 선언된 strict integration lane을 실행. 일부 lane은 container runtime이 필요 |
| `verifyReadmeCommands` | root README의 실행 가능한 Gradle/Compose/Make 명령이 실제 task/file/target과 일치하는지 검사 | | `ci` | 모든 leaf `check` + `architectureCheck` + `qualityCheck` + `configContractCheck` + `qualificationCheck` |
| `releaseCheck` | `ci`에 release provenance/archive 관련 검증을 추가 |
따라서 로컬에서 한 모듈만 수정했다면 `./gradlew :domain-core:check`처럼 focused task를
먼저 사용합니다. Gradle TestKit qualification은 일반 leaf `check``:app-bootstrap:build`
매달지 않고 `qualificationCheck`/`ci`에서 실행합니다.
**2026-09에 삭제한 게이트.** `verifyOneTypePerFile`(Checkstyle의 `OneTopLevelClass`가 같은 규칙을
파싱된 파일에 대해 검사한다), `verifyTrivyignore`·`verifyQuarantineSunset`(빈 레지스트리를 지키는
수백 줄짜리 커스텀 YAML 파서), `verifyReadmeCommands`·`verifyDocumentedLeafCount`·
`verifyRunbookReferences`·`verifyTestSourceSetRegistry`(문서 파서),
`verifyConfigurationPropertiesProcessor`(`ca.spring-config` convention plugin이 대체).
근거는 `docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md`.
### Local bootstrap ### Local bootstrap
`./gradlew bootstrap``bootstrapCompile``bootstrapDependencies` `./gradlew bootstrap``bootstrapCompile``bootstrapDependencies`
`bootstrapMigrateAndStart` `bootstrapSampleContract` `bootstrapSmoke`를 순서대로 실행합니다. `bootstrapMigrateAndStart``bootstrapSmoke`를 순서대로 실행합니다.
DB와 app lifecycle은 저장소 루트의 base/local Compose 조합이 소유하며, app startup Flyway가 DB와 app lifecycle은 저장소 루트의 base/local Compose 조합이 소유하며, app startup Flyway가
끝나 public health endpoint가 준비되어야 다음 단계로 넘어갑니다. `src/.env`는 env 설정의 끝나 public health endpoint가 준비되어야 다음 단계로 넘어갑니다. `src/.env`는 env 설정의
SSOT이고 bootstrap이 별도 env template을 만들지 않습니다. SSOT이고 bootstrap이 별도 env template을 만들지 않습니다.
README command drift는 다음 명령으로 독립 실행할 수 있습니다.
```bash
./gradlew verifyReadmeCommands
```
### Traceable version + dependency locking ### Traceable version + dependency locking
- 모든 project version `<MAJOR>.<MINOR>.<PATCH>+<12자리 git sha>`입니다. base version은 - 추적 가능한 checkout에서는 project version `<MAJOR>.<MINOR>.<PATCH>+<12자리 git sha>`가 됩니다.
`-PreleaseVersion`/`RELEASE_VERSION`, revision은 `-PgitRevision`/`GIT_SHA`/`GITHUB_SHA` 순으로 base version은 `-PreleaseVersion`/`RELEASE_VERSION`, revision은 `-PgitRevision`/`GIT_SHA`/
주입하고, 로컬에서는 현재 Git commit을 읽습니다. `GITHUB_SHA` 순으로 주입하고, 로컬에서는 현재 Git commit을 읽습니다.
- 모든 JAR manifest에 `Implementation-Version``Build-Revision`을 기록합니다. Git metadata도 - Git metadata와 revision property가 모두 없는 일반 source archive build는 configuration 단계에서
revision property도 없는 상태는 traceable artifact를 만들 수 없으므로 configuration 단계에서 실패하지 않고 `<MAJOR>.<MINOR>.<PATCH>-SNAPSHOT`으로 동작합니다. 다만 `releaseCheck`가 실행하는
실패합니다. `verifyReleaseProvenance`는 source revision이 없으면 실패하므로 추적 불가능한 산출물을 release로
취급할 수 없습니다.
- 모든 subproject가 `lockAllConfigurations()` + `LockMode.STRICT`를 사용하고, lock state는 Gradle - 모든 subproject가 `lockAllConfigurations()` + `LockMode.STRICT`를 사용하고, lock state는 Gradle
기본 `<module>/gradle.lockfile`에 둡니다. 이 경로는 Renovate Gradle manager 기본 인식 경로와 기본 `<module>/gradle.lockfile`에 둡니다. 이 경로는 Renovate Gradle manager 기본 인식 경로와
같습니다. 같습니다.
@@ -85,16 +93,16 @@ vendor/build나 container base image까지 byte-for-byte 같음을 주장하지
registry와 ArchUnit 규칙(`CleanArchitectureTest`)을 함께 갱신해야 합니다. settings와 gate는 registry와 ArchUnit 규칙(`CleanArchitectureTest`)을 함께 갱신해야 합니다. settings와 gate는
같은 registry를 읽고, 등록되지 않은 leaf나 허용되지 않은 edge를 fail-closed로 거부합니다. 같은 registry를 읽고, 등록되지 않은 leaf나 허용되지 않은 edge를 fail-closed로 거부합니다.
### `verifyRuntimeModuleMembership` ### `verifyRuntimeModuleRegistry`
- **하는 일.** 같은 registry의 `runtime_compositions`와 각 leaf의 `runtime_memberships`를 읽어 - **하는 일.** `composition_roots`가 가리키는 실제 Gradle `runtimeClasspath`를 resolve해서 그 안의
`app-bootstrap`/`sample-portfolio`의 실제 `api`/`implementation`/`compileOnly`/`runtimeOnly` project dependency가 architecture registry에 등록된 application module인지 확인합니다.
project dependency와 정확히 대조합니다. - **SSOT.** 어떤 module이 runtime에 실리는지는 `app-bootstrap/build.gradle`의 Gradle dependency graph가
- **opt-in의 의미.** membership이 빈 GraphQL/gRPC/WebSocket/Mongo leaf는 독립 빌드 대상이지만 두 결정합니다. registry가 같은 membership 목록을 다시 저장하지 않습니다.
shipped runtime에는 없습니다. app-bootstrap의 `conditionalTransportTest` test-only classpath는 - **변경 규칙.** production edge를 추가하거나 제거할 때는 실제 Gradle dependency와
실제 채택 전에 세 inbound transport를 함께 qualification하기 위한 evidence composition입니다. `allowed_dependencies` 같은 architecture policy만 함께 검토합니다. exact runtime snapshot을 두 군데
- **변경 규칙.** production edge를 추가하거나 제거할 때 `allowed_dependencies`, 맞추는 작업은 없습니다.
`runtime_memberships`, 실제 Gradle dependency를 같은 변경에서 갱신하지 않으면 `check`가 실패합니다. - 기존 `verifyRuntimeModuleMembership` task는 외부 스크립트 호환성을 위한 alias입니다.
세 opt-in inbound transport의 test-only composition, 실제 wire 경계, positive-count/zero-skip 증거는 세 opt-in inbound transport의 test-only composition, 실제 wire 경계, positive-count/zero-skip 증거는
다음 release-blocking aggregate로 실행합니다. 다음 release-blocking aggregate로 실행합니다.
@@ -103,20 +111,24 @@ vendor/build나 container base image까지 byte-for-byte 같음을 주장하지
./gradlew conditionalTransportQualification ./gradlew conditionalTransportQualification
``` ```
### `verifyOneTypePerFile` (code-conventions I6) ### 파일당 public 최상위 타입 1개 (code-conventions I6)
- **하는 일.** `src/main/java` 의 모든 `.java` 파일이 public 최상위 타입을 1개만 갖고, 그 타입 이름이 Checkstyle이 소유합니다 — `OneTopLevelClass``OuterTypeFilename`(`config/checkstyle/checkstyle.xml`).
파일 이름과 같은지 검사합니다 (Google Java Style Guide §3.4.1). `package-info.java`, 각 leaf의 `checkstyleMain`/`checkstyleTest`가 그 leaf의 `check`에서 돕니다.
`module-info.java` 는 예외입니다.
- **근거.** 이 "파일 모양(file-shape)" 규칙은 ArchUnit 으로는 잡을 수 없습니다. ArchUnit 은 컴파일된
bytecode 를 읽기 때문에 "한 파일에 몇 개의 타입이 있었는지", "파일 이름이 무엇이었는지" 같은 소스
파일 레벨 정보를 볼 수 없습니다. 그래서 다른 `verify*` 게이트와 똑같이 기계적으로 강제하려고 소스
파일을 직접 스캔하는 별도 태스크로 만들어 `check` 에 연결했습니다.
### `verifyEnvKeys` `verifyOneTypePerFile`이라는 루트 태스크가 있었고 삭제했습니다. `src/main/java`를 줄 단위 정규식으로
읽었고 세 가지가 틀렸습니다: package-private 최상위 타입이 보이지 않았고(126개 main 소스가 한 번도
매칭되지 않아, 파일 하나에 package-private 타입 다섯 개가 있어도 통과했다), `src/main/java`만 읽었고,
`^public` 앵커 때문에 블록 주석이나 텍스트 블록의 `public`으로 시작하는 줄을 선언으로 셌습니다.
Checkstyle은 파싱된 파일에 같은 질문을 하고, leaf 단위로 돕니다.
- **하는 일.** `docs/registries/env-keys.yaml`, `application.yml`, `src/.env` 세 곳을 lock-step(서로 ### `:app-bootstrap:verifyEnvKeys`
어긋나지 않게) 으로 유지합니다. `env-keys.yaml``APP_` 키의 SSOT 이고, drift 가 생기면 빌드를
- **소유.** app-bootstrap. 이 질문("이 애플리케이션의 배포에 무엇을 줘야 하는가")은 composition
root의 것이고, `./gradlew :domain-core:check`가 알아야 할 사항이 아닙니다. 루트 집계 이름은
`configContractCheck`이고 구현은 `build-logic``ca.config-contract` convention plugin입니다.
- **하는 일.** `docs/registries/env-keys.yaml`, `application.yml`, `src/.env.example` 세 곳을
lock-step(서로 어긋나지 않게) 으로 유지합니다. `env-keys.yaml``APP_` 키의 SSOT 이고, drift 가 생기면 빌드를
실패시킵니다. 실패시킵니다.
- **막으려는 것 3가지.** (1) 필수 env 가 조용히 누락되는 것, (2) 더 이상 쓰지 않는 stale env 키가 - **막으려는 것 3가지.** (1) 필수 env 가 조용히 누락되는 것, (2) 더 이상 쓰지 않는 stale env 키가
`.env` 에 남는 것, (3) 실제로 쓰는 `APP_` 키가 registry 에 등록되지 않고 빠져나가는 것. `.env` 에 남는 것, (3) 실제로 쓰는 `APP_` 키가 registry 에 등록되지 않고 빠져나가는 것.
@@ -152,49 +164,34 @@ vendor/build나 container base image까지 byte-for-byte 같음을 주장하지
checkout 에서 누락되거나 untracked 상태면 즉시 실패합니다. 승인된 변경만 update task로 재생성한 checkout 에서 누락되거나 untracked 상태면 즉시 실패합니다. 승인된 변경만 update task로 재생성한
뒤 보안 리뷰와 함께 커밋합니다. 뒤 보안 리뷰와 함께 커밋합니다.
### `verifyTrivyignore` ### Trivy suppression과 플래키 격리 — 정책은 유지, 파서는 삭제
- **하는 일.** repo 루트 `.trivyignore.yaml` 의 모든 Trivy suppression 항목이 (1) `id`, (2) 비어있지 **Trivy suppression.** repo 루트 `.trivyignore.yaml`이 유일한 suppression 소스이고, 모든 Trivy 호출이
않은 `statement`(사유), (3) 미래이면서 90일 이내인 `expired_at`(만료일) 을 갖추었는지 검사하고, `--ignorefile .trivyignore.yaml`로 명시합니다. 항목은 `id`, 비어 있지 않은 `statement`, 90일 이내의
하나라도 빠지거나 이미 만료됐거나 90일을 초과하면 `./gradlew check` 를 실패시킵니다. 미래 `expired_at`을 갖춰야 합니다. 이 규칙은 그대로이고, 강제하는 주체가 `.github/CODEOWNERS` 리뷰어로
- **막으려는 것.** 2026-05-25 ca-tmpl audit 에서 발견된 "만료일·사유 없는 suppression 을 추가해 바뀌었습니다. `verifyTrivyignore`는 105줄짜리 손으로 쓴 YAML 파서였고 — 들여쓰기 추적, 인라인 스칼라
취약점을 영구히 조용히 우회"하는 구멍입니다. Trivy 는 `expired_at` 이 없으면 **영구 유효**로 처리, 따옴표 제거 — 지키던 파일은 만들어진 이래 계속 비어 있었습니다. 실제 항목이 생기고 그것이
취급하므로(공식 문서), 만료일 누락 자체를 차단해야 합니다. drift하기 시작하면 그때 자동화합니다. 진짜 항목을 상대로, 진짜 YAML 라이브러리로.
- **두 겹의 보완 통제.** 이 게이트는 *필드 검증*(CI), `.github/CODEOWNERS`*merge 승인*(GitHub
네이티브)을 담당합니다. CODEOWNERS 는 "누가 파일을 바꿀 수 있는가"만, 이 게이트는 "필드가 갖춰졌는가"
만 잡으므로 둘은 대체재가 아니라 보완재입니다.
- **결정 — 90일 상한 (프로젝트 선택).** Trivy 문서는 `expired_at` 필드의 *존재*만 보장하고
기간 상한은 권고하지 않습니다. 짧으면 재검토 부담이 늘고, 길면 사실상 영구 ignore 가 되는
trade-off 에서 90일을 기본값으로 두었습니다. fork 는 `src/build.gradle``maxWindowDays`
조정합니다.
- **위치.** suppression 파일은 `docs/` 가 아니라 repo 루트(`.trivyignore.yaml`)에 둡니다 — Trivy 가
스캔 루트에서 자동으로 읽는 커밋 대상 파일이기 때문입니다. 정책 전문(severity·KEV·license·SLA)은
`.github/dependency-vulnerability-policy.md`, CI 배선은 `.github/workflows/dependency-vulnerability.yml`
에 있습니다.
### `verifyQuarantineSunset` + 플래키 격리 **플래키 격리.** 간헐 실패 테스트에 JUnit 기본 `@Tag("quarantine")`를 붙이면 메인 `test`
`excludeTags 'quarantine'`로 제외하므로 merge를 막지 않고, `./gradlew quarantineTest`(비차단)로만
돕니다. 이 두 줄은 유지됩니다.
- **하는 일.** 플래키(간헐 실패) 테스트는 JUnit 기본 `@Tag("quarantine")` 를 붙여 격리합니다. 메인 `flaky-quarantine.yaml` 레지스트리와 `verifyQuarantineSunset`(14일 sunset + drift 검사)은
`test` 태스크는 `excludeTags 'quarantine'` 로 이들을 **릴리스 게이트에서 제외**하므로 플래키 테스트가 삭제했습니다. 250줄짜리 YAML 파서 + Java 렉서(주석과 문자열 리터럴 안의 `@Tag("quarantine")`
merge 를 막지 않습니다. 격리된 테스트는 별도 `./gradlew quarantineTest`(비차단, `ignoreFailures`)로만 걸러내려고 인덱스 보존 렉서를 직접 구현)로 항목이 0개인 레지스트리를 지키고 있었습니다. 순서가
니다. 반대입니다 — 실제로 격리된 테스트가 생기고, 그게 주차장이 되기 시작할 때 도입할 정책입니다.
- **막으려는 것.** 격리가 *영구 주차장* 이 되는 것. `verifyQuarantineSunset`(루트 태스크, `check`
연결)이 매 빌드마다 (1) 레지스트리 스키마(`test`/`quarantined_since`/`reason`/`tracking_issue`),
(2) **14일 sunset**(`quarantined_since` 가 14일을 넘으면 빌드 실패), (3) **drift**(소스에
`@Tag("quarantine")` 가 달렸는데 레지스트리에 없으면 실패)를 검사합니다.
- **결정 — 14일 sunset (프로젝트 선택).** Spotify/Google/MS 사례는 격리 버킷의 정당성만
보이고(Fowler 는 반대), 14일이라는 정량값·자동 강제는 ca-tmpl 절충안입니다(`company-case-study`
강도 — 공식 best practice 아님). fork 는 `src/build.gradle``sunsetDays` 로 조정합니다.
- **위치.** 레지스트리는 `docs/`(gitignore) 가 아니라 repo 루트 `flaky-quarantine.yaml` 에 둡니다 —
CI 가 읽어야 하는 커밋 대상 파일이기 때문입니다(`.trivyignore.yaml` 과 같은 이유). 스켈레톤은 빈
버킷(`quarantined: []`)으로 출고됩니다.
### CI 게이트 배선 ### CI 게이트 배선
- **소유 범위.** 이 계약은 *게이트 배선*(어떤 게이트가 CI 에서 돌고 실패 시 어떻게 릴리스를 막는가)을 - **소유 범위.** 이 계약은 *게이트 배선*(어떤 게이트가 CI 에서 돌고 실패 시 어떻게 릴리스를 막는가)을
소유합니다. 개별 scanner/tool/severity *정책* 은 owner 브랜치가 소유하며, 그 20행 매핑의 in-repo 소유합니다. 개별 scanner/tool/severity *정책* 은 owner 브랜치가 소유하며, 그 20행 매핑의 in-repo
SSOT`.github/ci-gate-matrix.yml` 입니다. `.github/scripts/verify-gate-matrix.sh`(`gate-matrix-lint` SSOT는 Gradle task graph와 GitHub Actions job graph 그 자체입니다.
잡)가 표 ↔ 실제 task/test/job 정합을 매 PR 마다 cross-check 합니다.
`.github/ci-gate-matrix.yml`(1,025줄, 107개 게이트 행)과 `.github/scripts/verify-gate-matrix.sh`
(568줄)는 삭제했습니다. 그 표는 이미 두 그래프에 있는 정보의 세 번째 사본이었고, 검사기는 세 사본을
서로 같게 유지하는 일을 했습니다. 결과적으로 체크 하나를 추가하려면 Gradle · workflow · 표 ·
검사기 기대값 · Java 계약 테스트 다섯 곳을 같이 고쳐야 했습니다.
- **워크플로.** `.github/workflows/ci-quality-gates.yml``release-gate` 잡이 모든 release-blocking - **워크플로.** `.github/workflows/ci-quality-gates.yml``release-gate` 잡이 모든 release-blocking
게이트의 fan-in(단일 required status check)입니다. 플래키 `quarantine` 잡은 의도적으로 `needs` 에서 게이트의 fan-in(단일 required status check)입니다. 플래키 `quarantine` 잡은 의도적으로 `needs` 에서
제외(비차단)됩니다. 위임 게이트(Trivy SCA/이미지 스캔)는 `dependency-vulnerability.yml` 가 소유하며, 제외(비차단)됩니다. 위임 게이트(Trivy SCA/이미지 스캔)는 `dependency-vulnerability.yml` 가 소유하며,
@@ -342,7 +339,7 @@ ca-skeleton:
입력하면 상태와 무관하게 기동을 거부합니다. 입력하면 상태와 무관하게 기동을 거부합니다.
- 다만 `sample-portfolio`의 application YAML에는 legacy facade를 시연하기 위해 15개 키가 남아 - 다만 `sample-portfolio`의 application YAML에는 legacy facade를 시연하기 위해 15개 키가 남아
있습니다. 이 모듈은 fixture/reference consumer이고 production 의존성이 아니며, 그 YAML은 있습니다. 이 모듈은 fixture/reference consumer이고 production 의존성이 아니며, 그 YAML은
`verifyEnvKeys`가 검사하는 세 파일에 포함되지 않습니다. "제거됐다"는 문장이 저장소 전체를 `:app-bootstrap:verifyEnvKeys`가 검사하는 세 파일에 포함되지 않습니다. "제거됐다"는 문장이 저장소 전체를
가리킨다고 읽히지 않도록 범위를 명시합니다. 가리킨다고 읽히지 않도록 범위를 명시합니다.
- legacy JDK facade가 필요한 fork만 canonical composition 밖에서 - legacy JDK facade가 필요한 fork만 canonical composition 밖에서
`OutboundHttpSettings.bindLegacy(Binder)`와 legacy configuration을 명시적으로 import합니다. `OutboundHttpSettings.bindLegacy(Binder)`와 legacy configuration을 명시적으로 import합니다.
+2 -2
View File
@@ -93,7 +93,7 @@ runtimeClasspath 에 Tomcat 을 올리면서, 동시에 같은 artifact 가 `REA
`testCompileClasspath,testRuntimeClasspath` 만). `GraphQlRuntimeTransport` 가 실제 실행 중인 `testCompileClasspath,testRuntimeClasspath` 만). `GraphQlRuntimeTransport` 가 실제 실행 중인
서버를 감지해 `backend.graphql.execution-profile` 과 어긋나면 **부팅을 거부**한다. 서버를 감지해 `backend.graphql.execution-profile` 과 어긋나면 **부팅을 거부**한다.
- `annotationProcessor``spring-boot-configuration-processor``GraphQlPlatformProperties` - `annotationProcessor``spring-boot-configuration-processor``GraphQlPlatformProperties`
`@ConfigurationProperties` 이므로 레포 전역 `verifyConfigurationPropertiesProcessor` 패리티 `@ConfigurationProperties` 이므로 `ca.spring-config` convention plugin이 주는 패리티
게이트가 이 선언을 요구한다. 게이트가 이 선언을 요구한다.
## Forbidden ## Forbidden
@@ -191,7 +191,7 @@ cd src
--console=plain --console=plain
``` ```
플랫폼 테스트 레인(`gradle/graphql-platform-conventions.gradle` 등록). 기본 `test` 플랫폼 테스트 레인(`ca.graphql-platform` convention plugin 등록). 기본 `test`
`quarantine`·`graphql-performance` 태그를 제외한다: `quarantine`·`graphql-performance` 태그를 제외한다:
```bash ```bash
+13 -64
View File
@@ -1,3 +1,10 @@
plugins {
id 'ca.spring-library'
id 'ca.spring-config'
id 'java-test-fixtures'
id 'ca.graphql-platform'
}
// Driving adapter: GraphQL API (skeleton machinery, transport-only). // Driving adapter: GraphQL API (skeleton machinery, transport-only).
// //
// Spring for GraphQL is schema-first: schema files live in src/main/resources/graphql/*.graphqls // Spring for GraphQL is schema-first: schema files live in src/main/resources/graphql/*.graphqls
@@ -17,9 +24,7 @@ description = 'Inbound adapter: GraphQL API (Spring for GraphQL, GraphQL executi
// actor without a credential. A separate test-fixtures artifact keeps them consumable by the tests // actor without a credential. A separate test-fixtures artifact keeps them consumable by the tests
// that want them and out of the jar that runs in production; `verifyGraphQlProductionJar` checks // that want them and out of the jar that runs in production; `verifyGraphQlProductionJar` checks
// the second half rather than trusting it. // the second half rather than trusting it.
apply plugin: 'java-test-fixtures'
apply from: "${rootProject.projectDir}/gradle/graphql-platform-conventions.gradle"
dependencies { dependencies {
implementation project(':shared-contract') implementation project(':shared-contract')
@@ -50,7 +55,6 @@ dependencies {
// declaration an adopter configuring backend.graphql.* gets IDE completion and validation // declaration an adopter configuring backend.graphql.* gets IDE completion and validation
// from the generated metadata rather than from prose. (The prefix is `backend.graphql`; this // from the generated metadata rather than from prose. (The prefix is `backend.graphql`; this
// comment used to say `spring.graphql.platform.*`, which never existed.) // comment used to say `spring.graphql.platform.*`, which never existed.)
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
// A raw request body can only be capped before something decodes it, and on a servlet stack the // A raw request body can only be capped before something decodes it, and on a servlet stack the
// only place that exists is a filter. `compileOnly` is what keeps that from contradicting the // only place that exists is a filter. `compileOnly` is what keeps that from contradicting the
@@ -83,70 +87,15 @@ dependencies {
testImplementation 'io.micrometer:micrometer-core' testImplementation 'io.micrometer:micrometer-core'
} }
registerGraphQlPlatformTestLanes()
registerStrictQualificationTest( extensions.getByName('strictQualification').register(
name: 'graphqlTransportQualificationTest', 'graphqlTransportQualificationTest',
sourceSet: sourceSets.test, sourceSets.test,
requiredClasses: [ [
'dev.caskeleton.adapter.inbound.graphql.GraphqlHttpBoundaryQualificationTest' 'dev.caskeleton.adapter.inbound.graphql.GraphqlHttpBoundaryQualificationTest'
], ],
description: 'Runs exact no-skip GraphQL conditional transport wire evidence.') 'Runs exact no-skip GraphQL conditional transport wire evidence.'
)
// verifyGraphQlProductionJar the production artifact must carry nothing a test wrote.
//
// Moving the testkit into test fixtures is a source-tree decision, and source-tree decisions drift.
// One `implementation` where a `testFixturesImplementation` belonged, one file created in the wrong
// directory, and the contract suites are back inside the jar an adopter deploys where an
// in-memory persisted-operation registry looks like a working bean until a second instance starts,
// and where `testContext(String)` hands out an authenticated actor to anyone who calls it.
//
// So the claim is checked against the jar rather than against the layout that is supposed to
// produce it. Entry names and class names only: this reads the archive index, never the bytecode.
tasks.register('verifyGraphQlProductionJar') {
group = 'verification'
description = 'Fails when the GraphQL production jar contains testkit, fixture or in-memory-only types.'
dependsOn tasks.named('jar')
def jarFile = tasks.named('jar').flatMap { it.archiveFile }
inputs.file(jarFile)
outputs.upToDateWhen { true }
doLast {
Map<String, String> forbidden = [
'/testkit/' : 'contract suites and integration fixtures belong to test fixtures',
'InMemory' : 'an in-memory implementation is a development stand-in, not a shipped default',
'ForTests' : 'a for-tests factory in the production jar is reachable from production code',
'TestContext' : 'a credential-free authenticated context must not ship',
'Fixture' : 'fixtures belong to test fixtures',
]
List<String> violations = []
new java.util.zip.ZipFile(jarFile.get().asFile).withCloseable { archive ->
archive.entries().each { entry ->
if (entry.directory || !entry.name.endsWith('.class')) {
return
}
forbidden.each { marker, reason ->
if (entry.name.contains(marker)) {
violations << "${entry.name}: ${reason}"
}
}
}
}
if (!violations.isEmpty()) {
throw new GradleException(
"The GraphQL production jar contains non-production types:\n " +
violations.sort().join('\n ') +
"\nMove them to src/testFixtures/java, or declare them with " +
"testFixturesImplementation."
)
}
}
}
tasks.named('check') {
dependsOn tasks.named('verifyGraphQlProductionJar')
}
// verifyGraphQlApiSurface every public type this leaf exposes is a committed decision. // verifyGraphQlApiSurface every public type this leaf exposes is a committed decision.
// //
+25 -10
View File
@@ -1,3 +1,11 @@
plugins {
id 'ca.spring-library'
id 'ca.spring-config'
}
String grpcVersion = libs.versions.grpc.get()
String protobufVersion = libs.versions.protobufGrpc.get()
// Driving adapter: gRPC API (skeleton machinery, transport-only). // Driving adapter: gRPC API (skeleton machinery, transport-only).
// //
// A SmartLifecycle bean (GrpcServerRunner) owns the io.grpc Netty server, so this module depends on // A SmartLifecycle bean (GrpcServerRunner) owns the io.grpc Netty server, so this module depends on
@@ -5,10 +13,17 @@
// NO protobuf: there is no `com.google.protobuf` plugin and no `.proto` here health + reflection // 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. // 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 // io.grpc:* / protobuf versions are NOT managed by the Spring Boot BOM, so the grpc-bom +
// catalog, so the grpc-bom + protobuf-bom platforms are imported HERE (module scope) using the root // protobuf-bom platforms are imported HERE (module scope) using the root `ext.grpcVersion` /
// `ext.grpcVersion` / `ext.protobufVersion` SSOT this keeps the strict-locking blast radius to // `ext.protobufVersion` SSOT this keeps the strict-locking blast radius to this module (the shared
// this module (the shared root dependencyManagement block stays io.grpc-free). // 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 { dependencyManagement {
imports { imports {
@@ -29,7 +44,6 @@ dependencies {
implementation "io.grpc:grpc-netty-shaded:${grpcVersion}" implementation "io.grpc:grpc-netty-shaded:${grpcVersion}"
implementation "io.grpc:grpc-services:${grpcVersion}" // health + reflection implementation "io.grpc:grpc-services:${grpcVersion}" // health + reflection
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
// The boot test directly builds generated health/reflection protobuf messages. grpc-services // The boot test directly builds generated health/reflection protobuf messages. grpc-services
// does not expose protobuf-java on its compile API, so keep the narrower test-only declaration. // does not expose protobuf-java on its compile API, so keep the narrower test-only declaration.
@@ -38,11 +52,12 @@ dependencies {
testImplementation "io.grpc:grpc-stub:${grpcVersion}" testImplementation "io.grpc:grpc-stub:${grpcVersion}"
} }
registerStrictQualificationTest( extensions.getByName('strictQualification').register(
name: 'grpcTransportQualificationTest', 'grpcTransportQualificationTest',
sourceSet: sourceSets.test, sourceSets.test,
requiredClasses: [ [
'dev.caskeleton.adapter.inbound.grpc.GrpcSafeActivationTest', 'dev.caskeleton.adapter.inbound.grpc.GrpcSafeActivationTest',
'dev.caskeleton.adapter.inbound.grpc.GrpcP1BoundaryWireTest' 'dev.caskeleton.adapter.inbound.grpc.GrpcP1BoundaryWireTest'
], ],
description: 'Runs exact no-skip gRPC conditional transport wire evidence.') 'Runs exact no-skip gRPC conditional transport wire evidence.'
)
+83 -89
View File
@@ -1,3 +1,11 @@
plugins {
id 'ca.spring-library'
id 'ca.spring-config'
id 'java-test-fixtures'
id 'ca.auxiliary-source-set'
}
// Shared test code as a Gradle test-fixtures variant ADR-BUILD-001.
// The inbound HTTP API execution platform design models itself as 23 Stable Gradle modules under // The inbound HTTP API execution platform design models itself as 23 Stable Gradle modules under
// modules/web. This repository's fail-closed module registry outranks that layout, so those modules // modules/web. This repository's fail-closed module registry outranks that layout, so those modules
// are packages here and WebModuleBoundaryTest enforces the design's module dependency table. The // are packages here and WebModuleBoundaryTest enforces the design's module dependency table. The
@@ -13,15 +21,14 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.session:spring-session-core' implementation 'org.springframework.session:spring-session-core'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' implementation(libs.jackson.databind.nullable) {
implementation('org.openapitools:jackson-databind-nullable:0.2.6') {
exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind' exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind'
} }
// feature-api-contract-baseline D10: OpenAPI producer. springdoc exposes the // feature-api-contract-baseline D10: OpenAPI producer. springdoc exposes the
// running app's machine-readable contract at /v3/api-docs (OAS 3.1, generated // running app's machine-readable contract at /v3/api-docs (OAS 3.1, generated
// never a hand-maintained stale schema). The release-blocking drift gate is // never a hand-maintained stale schema). The release-blocking drift gate is
// owned by feature-contract-verification-test-suite (planned). // owned by feature-contract-verification-test-suite (planned).
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0' implementation libs.springdoc.openapi.starter.webmvc.api
// The two Advanced representations, compile-only on purpose. They were `implementation` first, // The two Advanced representations, compile-only on purpose. They were `implementation` first,
// on the reasoning that a missing backend would surface as a NoClassDefFoundError at the first // on the reasoning that a missing backend would surface as a NoClassDefFoundError at the first
// request that negotiated one. That reasoning was wrong about what the jars do: Spring Boot's // request that negotiated one. That reasoning was wrong about what the jars do: Spring Boot's
@@ -64,14 +71,13 @@ tasks.named('test') {
// The web platform's reusable ArchUnit rules ship in their own source set, consumed by this leaf's // The web platform's reusable ArchUnit rules ship in their own source set, consumed by this leaf's
// tests and by the composition root. A rule pack that only its own fixture tests import is verified // tests and by the composition root. A rule pack that only its own fixture tests import is verified
// as library code and applied to nothing the shape the JPA testkit had to be corrected out of. // as library code and applied to nothing the shape the JPA testkit had to be corrected out of.
strictTestLanes { auxiliarySourceSets {
sourceSet('testkit') { compilesAgainst 'main' }
// The Jetty compatibility lane is its own source set because it needs a different embedded // The Jetty compatibility lane is its own source set because it needs a different embedded
// server on the classpath. Two servers in one source set means Spring Boot picks one and the // server on the classpath. Two servers in one source set means Spring Boot picks one and the
// "Jetty" lane silently runs on Tomcat a compatibility matrix that certifies the same // "Jetty" lane silently runs on Tomcat a compatibility matrix that certifies the same
// container twice. // container twice.
sourceSet('jettyCompatTest') { sourceSet('jettyCompatTest') {
compilesAgainst 'main', 'testkit' compilesAgainst 'main', 'testFixtures'
inherits 'implementation' inherits 'implementation'
} }
// Reactor Netty is the Stable WebFlux server baseline and it cannot share a source set with // Reactor Netty is the Stable WebFlux server baseline and it cannot share a source set with
@@ -85,11 +91,11 @@ strictTestLanes {
// developer's `check` depend on a container runtime, and the usual outcome of that is an // developer's `check` depend on a container runtime, and the usual outcome of that is an
// @Disabled that nobody notices has been there for months. // @Disabled that nobody notices has been there for months.
sourceSet('nginxProxyTest') { sourceSet('nginxProxyTest') {
compilesAgainst 'main', 'testkit' compilesAgainst 'main', 'testFixtures'
inherits 'implementation' inherits 'implementation'
} }
sourceSet('webfluxContractTest') { sourceSet('webfluxContractTest') {
compilesAgainst 'main', 'testkit' compilesAgainst 'main', 'testFixtures'
// Inherits nothing. The default is to extend `testImplementation`, which extends the leaf's // Inherits nothing. The default is to extend `testImplementation`, which extends the leaf's
// own `implementation` and therefore carries spring-boot-starter-web and with Tomcat on // own `implementation` and therefore carries spring-boot-starter-web and with Tomcat on
// the classpath Boot deduces a servlet application, starts a servlet container, and the // the classpath Boot deduces a servlet application, starts a servlet container, and the
@@ -98,16 +104,36 @@ strictTestLanes {
} }
} }
testkitPublisher { // ArchUnit is declared after the testkit source set exists, because `testFixturesImplementation` is
consumedBy 'test'
publishAs 'webTestkit'
}
// ArchUnit is declared after the testkit source set exists, because `testkitImplementation` is
// created by that declaration. // created by that declaration.
dependencies { dependencies {
testkitImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0' // The fixtures speak the application's ports, so they compile against it directly. Under the
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0' // old source set this arrived through testImplementation inheritance.
testFixturesImplementation project(':application-core')
testFixturesImplementation project(':shared-contract')
// Declared rather than inherited: the `testkit` source set extended testImplementation, so the
// fixtures compiled against every test library this leaf declared without stating any of them
// (ADR-BUILD-001). Listing them makes the fixtures' own surface reviewable.
testFixturesImplementation 'jakarta.servlet:jakarta.servlet-api'
testFixturesImplementation 'jakarta.validation:jakarta.validation-api'
testFixturesImplementation 'org.assertj:assertj-core'
testFixturesImplementation 'org.junit.jupiter:junit-jupiter-api'
testFixturesImplementation 'org.springframework:spring-web'
testFixturesImplementation 'org.springframework:spring-webmvc'
testFixturesImplementation 'org.springframework:spring-context'
testFixturesImplementation 'org.springframework:spring-core'
testFixturesImplementation 'org.springframework.boot:spring-boot'
testFixturesImplementation 'org.springframework.boot:spring-boot-test'
testFixturesImplementation 'org.springframework.boot:spring-boot-web-server'
testFixturesImplementation 'org.springframework.boot:spring-boot-autoconfigure'
testFixturesImplementation 'org.springframework.security:spring-security-config'
testFixturesImplementation 'tools.jackson.core:jackson-databind'
testFixturesImplementation 'org.springframework.security:spring-security-core'
testFixturesImplementation 'org.springframework.security:spring-security-web'
testFixturesImplementation 'io.projectreactor:reactor-core'
testFixturesImplementation libs.archunit.junit5
testImplementation libs.archunit.junit5
// Jetty replaces Tomcat for this lane only. The exclusion is what makes the lane mean // Jetty replaces Tomcat for this lane only. The exclusion is what makes the lane mean
// something: with both on the classpath Boot starts Tomcat and the lane certifies nothing. // something: with both on the classpath Boot starts Tomcat and the lane certifies nothing.
@@ -153,77 +179,52 @@ dependencies {
nginxProxyTestRuntimeOnly 'org.junit.platform:junit-platform-launcher' nginxProxyTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
} }
// The lane task. A release compatibility gate that is not wired to a task is a document. strictTestLanes {
tasks.register('webFluxContractTest', Test) { // A release compatibility gate that is not wired to a task is a document.
group = 'verification' lane('webFluxContractTest') {
sourceSet = 'webfluxContractTest'
description = 'Runs the Stable HTTP contract against a real Reactor Netty.' 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' 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'
} }
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false } // 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') {
integration()
sourceSet = 'nginxProxyTest'
description = 'Runs the proxy, prefix and spoofing contract behind a real Nginx.'
jvmArgs '-Duser.timezone=UTC'
}
lane('webJettyCompatTest') {
sourceSet = 'jettyCompatTest'
description = 'Runs the Stable HTTP contract against a real Jetty instead of Tomcat.'
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.'
jvmArgs '-Duser.timezone=UTC' jvmArgs '-Duser.timezone=UTC'
dependsOn 'test', 'webJettyCompatTest', 'webFluxContractTest' 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'
} }
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
}
tasks.register('webJettyCompatTest', Test) { // The Advanced lane. Every capability is off unless a deployment names it, so none of them is
group = 'verification' // exercised by anything a production deployment runs which makes a lane that runs them all the
description = 'Runs the Stable HTTP contract against a real Jetty instead of Tomcat.' // only place a break is noticed before whoever enables it notices.
testClassesDirs = sourceSets.jettyCompatTest.output.classesDirs //
classpath = sourceSets.jettyCompatTest.runtimeClasspath // They also run inside `test`, deliberately. They are ordinary unit tests, and excluding them
useJUnitPlatform() // from the PR gate to make this lane look meaningful would mean the PR gate stopped covering a
failOnNoDiscoveredTests = true // fifth of the leaf.
outputs.upToDateWhen { false } lane('webAdvancedTest') {
tag = 'web-advanced'
description = 'Runs every web Advanced capability contract.'
jvmArgs '-Duser.timezone=UTC' jvmArgs '-Duser.timezone=UTC'
}
} }
strictTestLanes { strictTestLanes {
@@ -240,16 +241,9 @@ strictTestLanes {
lane('webSecurityBoundaryTest') { lane('webSecurityBoundaryTest') {
tag = 'security-boundary' tag = 'security-boundary'
description = 'Runs hermetic JWT/JWKS and CORS filter-boundary contracts with no skips.' description = 'Runs hermetic JWT/JWKS and CORS filter-boundary contracts with no skips.'
customize = { test -> shouldRunAfter 'test'
test.shouldRunAfter test.project.tasks.named('test') jvmArgs '-Duser.timezone=UTC'
test.jvmArgs '-Duser.timezone=UTC' rejectSkipped = true
test.afterSuite { descriptor, result ->
if (descriptor.parent == null && result.skippedTestCount > 0) {
throw new GradleException(
"webSecurityBoundaryTest forbids skipped tests: ${result.skippedTestCount}")
}
}
}
} }
} }
+111 -111
View File
@@ -2,64 +2,64 @@
# Manual edits can break the build and are not advised. # Manual edits can break the build and are not advised.
# This file is expected to be part of source control. # This file is expected to be part of source control.
biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,jettyCompatTestCompileClasspath,nginxProxyTestCompileClasspath,testCompileClasspath,testkitCompileClasspath,webfluxContractTestCompileClasspath biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,jettyCompatTestCompileClasspath,nginxProxyTestCompileClasspath,testCompileClasspath,testkitCompileClasspath,webfluxContractTestCompileClasspath
ch.qos.logback:logback-classic:1.5.38=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath ch.qos.logback:logback-classic:1.5.38=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
ch.qos.logback:logback-core:1.5.38=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath ch.qos.logback:logback-core:1.5.38=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
com.fasterxml.jackson.core:jackson-annotations:2.21=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath com.fasterxml.jackson.core:jackson-annotations:2.21=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
com.fasterxml.jackson.core:jackson-core:2.21.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath com.fasterxml.jackson.core:jackson-core:2.21.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
com.fasterxml.jackson.core:jackson-databind:2.21.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath com.fasterxml.jackson.core:jackson-databind:2.21.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.21.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.21.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
com.fasterxml.jackson:jackson-bom:2.21.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.21.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
com.fasterxml.woodstox:woodstox-core:7.1.1=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath com.fasterxml.woodstox:woodstox-core:7.1.1=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.fasterxml:classmate:1.7.3=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath com.fasterxml:classmate:1.7.3=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
com.github.ben-manes.caffeine:caffeine:3.2.4=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor com.github.ben-manes.caffeine:caffeine:3.2.4=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
com.github.docker-java:docker-java-api:3.7.1=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath com.github.docker-java:docker-java-api:3.7.1=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath
com.github.docker-java:docker-java-transport-zerodep:3.7.1=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.7.1=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath
com.github.docker-java:docker-java-transport:3.7.1=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath com.github.docker-java:docker-java-transport:3.7.1=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
com.github.spotbugs:spotbugs-annotations:4.10.2=spotbugs com.github.spotbugs:spotbugs-annotations:4.10.2=spotbugs
com.github.spotbugs:spotbugs:4.10.2=spotbugs com.github.spotbugs:spotbugs:4.10.2=spotbugs
com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
com.google.auto:auto-common:1.2.2=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor com.google.auto:auto-common:1.2.2=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
com.google.code.findbugs:jsr305:3.0.2=checkstyle,spotbugs com.google.code.findbugs:jsr305:3.0.2=checkstyle,spotbugs
com.google.code.gson:gson:2.13.2=spotbugs com.google.code.gson:gson:2.13.2=spotbugs
com.google.errorprone:error_prone_annotation:2.49.0=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor com.google.errorprone:error_prone_annotation:2.49.0=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.38.0=compileClasspath,jettyCompatTestCompileClasspath,nginxProxyTestCompileClasspath,testCompileClasspath,testkitCompileClasspath,webfluxContractTestCompileClasspath com.google.errorprone:error_prone_annotations:2.38.0=compileClasspath,jettyCompatTestCompileClasspath,nginxProxyTestCompileClasspath,testCompileClasspath,testkitCompileClasspath,webfluxContractTestCompileClasspath
com.google.errorprone:error_prone_annotations:2.41.0=spotbugs 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.47.0=checkstyle
com.google.errorprone:error_prone_annotations:2.49.0=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor com.google.errorprone:error_prone_annotations:2.49.0=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
com.google.errorprone:error_prone_check_api:2.49.0=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor com.google.errorprone:error_prone_check_api:2.49.0=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
com.google.errorprone:error_prone_core:2.49.0=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor com.google.errorprone:error_prone_core:2.49.0=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
com.google.googlejavaformat:google-java-format:1.35.0=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor com.google.googlejavaformat:google-java-format:1.35.0=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
com.google.guava:failureaccess:1.0.3=annotationProcessor,checkstyle,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor com.google.guava:failureaccess:1.0.3=annotationProcessor,checkstyle,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
com.google.guava:guava:33.5.0-jre=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor com.google.guava:guava:33.5.0-jre=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
com.google.guava:guava:33.6.0-jre=checkstyle com.google.guava:guava:33.6.0-jre=checkstyle
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,checkstyle,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,checkstyle,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0=spotbugsPlugins com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0=spotbugsPlugins
com.jayway.jsonpath:json-path:2.10.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath com.jayway.jsonpath:json-path:2.10.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
com.nimbusds:nimbus-jose-jwt:10.4=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath com.nimbusds:nimbus-jose-jwt:10.4=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.puppycrawl.tools:checkstyle:13.5.0=checkstyle com.puppycrawl.tools:checkstyle:13.5.0=checkstyle
com.tngtech.archunit:archunit-junit5-api:1.3.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath com.tngtech.archunit:archunit-junit5-api:1.3.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=jettyCompatTestRuntimeClasspath,nginxProxyTestRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=jettyCompatTestRuntimeClasspath,nginxProxyTestRuntimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine:1.3.0=jettyCompatTestRuntimeClasspath,nginxProxyTestRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath com.tngtech.archunit:archunit-junit5-engine:1.3.0=jettyCompatTestRuntimeClasspath,nginxProxyTestRuntimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
com.tngtech.archunit:archunit-junit5:1.3.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath com.tngtech.archunit:archunit-junit5:1.3.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.tngtech.archunit:archunit:1.3.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath com.tngtech.archunit:archunit:1.3.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
com.vaadin.external.google:android-json:0.0.20131108.vaadin1=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
commons-beanutils:commons-beanutils:1.11.0=checkstyle commons-beanutils:commons-beanutils:1.11.0=checkstyle
commons-codec:commons-codec:1.19.0=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath commons-codec:commons-codec:1.19.0=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath
commons-collections:commons-collections:3.2.2=checkstyle commons-collections:commons-collections:3.2.2=checkstyle
commons-io:commons-io:2.20.0=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath commons-io:commons-io:2.20.0=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath
commons-io:commons-io:2.21.0=spotbugs commons-io:commons-io:2.21.0=spotbugs
commons-logging:commons-logging:1.3.6=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath commons-logging:commons-logging:1.3.6=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
info.picocli:picocli:4.7.7=checkstyle info.picocli:picocli:4.7.7=checkstyle
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
io.micrometer:micrometer-commons:1.16.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath io.micrometer:micrometer-commons:1.16.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
io.micrometer:micrometer-observation:1.16.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath io.micrometer:micrometer-observation:1.16.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
io.netty:netty-buffer:4.2.17.Final=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath io.netty:netty-buffer:4.2.17.Final=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
io.netty:netty-codec-base:4.2.17.Final=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath io.netty:netty-codec-base:4.2.17.Final=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
io.netty:netty-codec-classes-quic:4.2.17.Final=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath io.netty:netty-codec-classes-quic:4.2.17.Final=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
@@ -83,27 +83,27 @@ io.netty:netty-transport-native-unix-common:4.2.17.Final=webfluxContractTestComp
io.netty:netty-transport:4.2.17.Final=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath io.netty:netty-transport:4.2.17.Final=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
io.projectreactor.netty:reactor-netty-core:1.3.7=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath io.projectreactor.netty:reactor-netty-core:1.3.7=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
io.projectreactor.netty:reactor-netty-http:1.3.7=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath io.projectreactor.netty:reactor-netty-http:1.3.7=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
io.projectreactor:reactor-core:3.8.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath io.projectreactor:reactor-core:3.8.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
io.projectreactor:reactor-test:3.8.7=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath io.projectreactor:reactor-test:3.8.7=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
io.swagger.core.v3:swagger-annotations-jakarta:2.2.38=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.38=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.swagger.core.v3:swagger-core-jakarta:2.2.38=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath io.swagger.core.v3:swagger-core-jakarta:2.2.38=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
io.swagger.core.v3:swagger-models-jakarta:2.2.38=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath io.swagger.core.v3:swagger-models-jakarta:2.2.38=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
jakarta.activation:jakarta.activation-api:2.1.4=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.4=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
jakarta.enterprise:jakarta.enterprise.cdi-api:4.1.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath jakarta.enterprise:jakarta.enterprise.cdi-api:4.1.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath
jakarta.enterprise:jakarta.enterprise.lang-model:4.1.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath jakarta.enterprise:jakarta.enterprise.lang-model:4.1.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath
jakarta.inject:jakarta.inject-api:2.0.1=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath jakarta.inject:jakarta.inject-api:2.0.1=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath
jakarta.interceptor:jakarta.interceptor-api:2.2.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath jakarta.interceptor:jakarta.interceptor-api:2.2.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath
jakarta.servlet:jakarta.servlet-api:6.1.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath jakarta.servlet:jakarta.servlet-api:6.1.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestRuntimeClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
jakarta.transaction:jakarta.transaction-api:2.0.1=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath jakarta.transaction:jakarta.transaction-api:2.0.1=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath
jakarta.validation:jakarta.validation-api:3.1.1=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath jakarta.validation:jakarta.validation-api:3.1.1=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
jakarta.websocket:jakarta.websocket-api:2.2.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath jakarta.websocket:jakarta.websocket-api:2.2.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath
jakarta.websocket:jakarta.websocket-client-api:2.2.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath jakarta.websocket:jakarta.websocket-client-api:2.2.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath
jakarta.xml.bind:jakarta.xml.bind-api:4.0.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:4.0.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
javax.inject:javax.inject:1=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor javax.inject:javax.inject:1=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
jaxen:jaxen:2.0.6=spotbugs jaxen:jaxen:2.0.6=spotbugs
net.bytebuddy:byte-buddy-agent:1.17.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath net.bytebuddy:byte-buddy-agent:1.17.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
net.bytebuddy:byte-buddy:1.17.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath net.bytebuddy:byte-buddy:1.17.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
net.java.dev.jna:jna:5.18.1=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath net.java.dev.jna:jna:5.18.1=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath
net.minidev:accessors-smart:2.6.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath net.minidev:accessors-smart:2.6.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
net.minidev:json-smart:2.6.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath net.minidev:json-smart:2.6.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
@@ -111,24 +111,24 @@ net.sf.saxon:Saxon-HE:12.9=checkstyle,spotbugs
org.antlr:antlr4-runtime:4.13.2=checkstyle org.antlr:antlr4-runtime:4.13.2=checkstyle
org.apache.bcel:bcel:6.12.0=spotbugs org.apache.bcel:bcel:6.12.0=spotbugs
org.apache.commons:commons-compress:1.28.0=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath org.apache.commons:commons-compress:1.28.0=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath
org.apache.commons:commons-lang3:3.20.0=checkstyle,compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.apache.commons:commons-lang3:3.20.0=checkstyle,compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,spotbugs,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.commons:commons-text:1.15.0=spotbugs org.apache.commons:commons-text:1.15.0=spotbugs
org.apache.commons:commons-text:1.3=checkstyle org.apache.commons:commons-text:1.3=checkstyle
org.apache.httpcomponents:httpclient:4.5.13=checkstyle org.apache.httpcomponents:httpclient:4.5.13=checkstyle
org.apache.httpcomponents:httpcore:4.4.16=checkstyle org.apache.httpcomponents:httpcore:4.4.16=checkstyle
org.apache.logging.log4j:log4j-api:2.25.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.apache.logging.log4j:log4j-api:2.25.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,spotbugs,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.apache.logging.log4j:log4j-core:2.25.5=spotbugs org.apache.logging.log4j:log4j-core:2.25.5=spotbugs
org.apache.logging.log4j:log4j-to-slf4j:2.25.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.apache.logging.log4j:log4j-to-slf4j:2.25.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.apache.maven.doxia:doxia-core:1.12.0=checkstyle 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-logging-api:1.12.0=checkstyle
org.apache.maven.doxia:doxia-module-xdoc: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.maven.doxia:doxia-sink-api:1.12.0=checkstyle
org.apache.tomcat.embed:tomcat-embed-core:11.0.24=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-core:11.0.24=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-el:11.0.24=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-el:11.0.24=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-websocket:11.0.24=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.apache.tomcat.embed:tomcat-embed-websocket:11.0.24=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.apache.xbean:xbean-reflect:3.7=checkstyle org.apache.xbean:xbean-reflect:3.7=checkstyle
org.apiguardian:apiguardian-api:1.1.2=jettyCompatTestCompileClasspath,nginxProxyTestCompileClasspath,testCompileClasspath,testkitCompileClasspath,webfluxContractTestCompileClasspath org.apiguardian:apiguardian-api:1.1.2=jettyCompatTestCompileClasspath,nginxProxyTestCompileClasspath,testCompileClasspath,testFixturesCompileClasspath,testkitCompileClasspath,webfluxContractTestCompileClasspath
org.assertj:assertj-core:3.27.7=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.assertj:assertj-core:3.27.7=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.awaitility:awaitility:4.3.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.awaitility:awaitility:4.3.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.codehaus.plexus:plexus-classworlds:2.6.0=checkstyle org.codehaus.plexus:plexus-classworlds:2.6.0=checkstyle
org.codehaus.plexus:plexus-component-annotations:2.1.0=checkstyle org.codehaus.plexus:plexus-component-annotations:2.1.0=checkstyle
@@ -167,25 +167,25 @@ org.eclipse.jetty:jetty-session:12.1.12=jettyCompatTestCompileClasspath,jettyCom
org.eclipse.jetty:jetty-util:12.1.12=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath org.eclipse.jetty:jetty-util:12.1.12=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath
org.eclipse.jetty:jetty-xml:12.1.12=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath org.eclipse.jetty:jetty-xml:12.1.12=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath
org.hamcrest:hamcrest:3.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.hamcrest:hamcrest:3.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.hibernate.validator:hibernate-validator:9.0.1.Final=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.hibernate.validator:hibernate-validator:9.0.1.Final=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.javassist:javassist:3.28.0-GA=checkstyle org.javassist:javassist:3.28.0-GA=checkstyle
org.jboss.logging:jboss-logging:3.6.3.Final=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.jboss.logging:jboss-logging:3.6.3.Final=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.jetbrains:annotations:17.0.0=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath org.jetbrains:annotations:17.0.0=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath
org.jspecify:jspecify:1.0.1=annotationProcessor,checkstyle,compileClasspath,jettyCompatTestAnnotationProcessor,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestAnnotationProcessor,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath,testkitAnnotationProcessor,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestAnnotationProcessor,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.jspecify:jspecify:1.0.1=annotationProcessor,checkstyle,compileClasspath,jettyCompatTestAnnotationProcessor,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestAnnotationProcessor,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testFixturesAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitAnnotationProcessor,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestAnnotationProcessor,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:6.0.3=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.junit.jupiter:junit-jupiter-api:6.0.3=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:6.0.3=jettyCompatTestRuntimeClasspath,nginxProxyTestRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath,webfluxContractTestRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:6.0.3=jettyCompatTestRuntimeClasspath,nginxProxyTestRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath,webfluxContractTestRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:6.0.3=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.junit.jupiter:junit-jupiter-params:6.0.3=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.junit.jupiter:junit-jupiter:6.0.3=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.junit.jupiter:junit-jupiter:6.0.3=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.junit.platform:junit-platform-commons:6.0.3=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.junit.platform:junit-platform-commons:6.0.3=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.junit.platform:junit-platform-engine:6.0.3=jettyCompatTestRuntimeClasspath,nginxProxyTestRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath,webfluxContractTestRuntimeClasspath org.junit.platform:junit-platform-engine:6.0.3=jettyCompatTestRuntimeClasspath,nginxProxyTestRuntimeClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath,webfluxContractTestRuntimeClasspath
org.junit.platform:junit-platform-launcher:6.0.3=jettyCompatTestRuntimeClasspath,nginxProxyTestRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath,webfluxContractTestRuntimeClasspath org.junit.platform:junit-platform-launcher:6.0.3=jettyCompatTestRuntimeClasspath,nginxProxyTestRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath,webfluxContractTestRuntimeClasspath
org.junit:junit-bom:6.0.3=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.junit:junit-bom:6.0.3=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.junit:junit-bom:6.1.0=spotbugs org.junit:junit-bom:6.1.0=spotbugs
org.mockito:mockito-core:5.20.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,mockitoAgent,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.mockito:mockito-core:5.20.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,mockitoAgent,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.mockito:mockito-junit-jupiter:5.20.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.mockito:mockito-junit-jupiter:5.20.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.objenesis:objenesis:3.3=jettyCompatTestRuntimeClasspath,nginxProxyTestRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath,webfluxContractTestRuntimeClasspath org.objenesis:objenesis:3.3=jettyCompatTestRuntimeClasspath,nginxProxyTestRuntimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath,webfluxContractTestRuntimeClasspath
org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.opentest4j:opentest4j:1.3.0=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,jettyCompatTestCompileClasspath,nginxProxyTestCompileClasspath,testCompileClasspath,testkitCompileClasspath,webfluxContractTestCompileClasspath org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,jettyCompatTestCompileClasspath,nginxProxyTestCompileClasspath,testCompileClasspath,testkitCompileClasspath,webfluxContractTestCompileClasspath
org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,jettyCompatTestCompileClasspath,nginxProxyTestCompileClasspath,testCompileClasspath,testkitCompileClasspath,webfluxContractTestCompileClasspath org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,jettyCompatTestCompileClasspath,nginxProxyTestCompileClasspath,testCompileClasspath,testkitCompileClasspath,webfluxContractTestCompileClasspath
org.osgi:org.osgi.resource:1.0.0=compileClasspath,jettyCompatTestCompileClasspath,nginxProxyTestCompileClasspath,testCompileClasspath,testkitCompileClasspath,webfluxContractTestCompileClasspath org.osgi:org.osgi.resource:1.0.0=compileClasspath,jettyCompatTestCompileClasspath,nginxProxyTestCompileClasspath,testCompileClasspath,testkitCompileClasspath,webfluxContractTestCompileClasspath
@@ -196,82 +196,82 @@ org.ow2.asm:asm-tree:9.10.1=jettyCompatTestCompileClasspath,jettyCompatTestRunti
org.ow2.asm:asm-util:9.10.1=spotbugs org.ow2.asm:asm-util:9.10.1=spotbugs
org.ow2.asm:asm:9.10.1=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,spotbugs org.ow2.asm:asm:9.10.1=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,spotbugs
org.ow2.asm:asm:9.7.1=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.ow2.asm:asm:9.7.1=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.pcollections:pcollections:4.0.1=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor org.pcollections:pcollections:4.0.1=annotationProcessor,jettyCompatTestAnnotationProcessor,nginxProxyTestAnnotationProcessor,testAnnotationProcessor,testFixturesAnnotationProcessor,testkitAnnotationProcessor,webfluxContractTestAnnotationProcessor
org.reactivestreams:reactive-streams:1.0.4=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.reactivestreams:reactive-streams:1.0.4=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.reflections:reflections:0.10.2=checkstyle org.reflections:reflections:0.10.2=checkstyle
org.rnorth.duct-tape:duct-tape:1.0.8=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath org.rnorth.duct-tape:duct-tape:1.0.8=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath
org.skyscreamer:jsonassert:1.5.3=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.skyscreamer:jsonassert:1.5.3=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.slf4j:jul-to-slf4j:2.0.18=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.18=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.slf4j:slf4j-api:2.0.18=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.slf4j:slf4j-api:2.0.18=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j
org.slf4j:slf4j-simple:2.0.18=checkstyle org.slf4j:slf4j-simple:2.0.18=checkstyle
org.springdoc:springdoc-openapi-starter-common:3.0.0=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springdoc:springdoc-openapi-starter-common:3.0.0=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-autoconfigure:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-autoconfigure:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-configuration-processor:4.0.8=annotationProcessor org.springframework.boot:spring-boot-configuration-processor:4.0.8=annotationProcessor
org.springframework.boot:spring-boot-http-codec:4.0.8=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-http-codec:4.0.8=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-http-converter:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-http-converter:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-jackson:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-jackson:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-jetty:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath org.springframework.boot:spring-boot-jetty:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath
org.springframework.boot:spring-boot-netty:4.0.8=webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-netty:4.0.8=webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-reactor-netty:4.0.8=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-reactor-netty:4.0.8=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-reactor:4.0.8=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-reactor:4.0.8=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-resttestclient:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-resttestclient:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-security-oauth2-resource-server:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-security-oauth2-resource-server:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-security:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-security:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-servlet:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-servlet:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson-test:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-starter-jackson-test:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-starter-jackson:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-starter-jetty-runtime:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath org.springframework.boot:spring-boot-starter-jetty-runtime:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath
org.springframework.boot:spring-boot-starter-jetty:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath org.springframework.boot:spring-boot-starter-jetty:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath
org.springframework.boot:spring-boot-starter-logging:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-starter-logging:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-starter-oauth2-resource-server:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-starter-oauth2-resource-server:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-reactor-netty:4.0.8=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-starter-reactor-netty:4.0.8=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-starter-security:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-starter-security:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-test:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-starter-test:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-starter-tomcat:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-validation:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-starter-validation:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-starter-web:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-starter-web:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-webflux:4.0.8=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-starter-webflux:4.0.8=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc-test:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-starter-webmvc-test:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-starter-webmvc:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-starter:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-starter:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-test-autoconfigure:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-test-autoconfigure:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-test:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-test:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-tomcat:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-tomcat:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-validation:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-validation:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-web-server:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-web-server:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-webflux:4.0.8=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot-webflux:4.0.8=webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.boot:spring-boot-webmvc-test:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-webmvc-test:4.0.8=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot-webmvc:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.boot:spring-boot-webmvc:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.boot:spring-boot:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework.boot:spring-boot:4.0.8=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework.security:spring-security-config:7.0.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.security:spring-security-config:7.0.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.security:spring-security-core:7.0.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.security:spring-security-core:7.0.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.security:spring-security-crypto:7.0.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.security:spring-security-crypto:7.0.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.security:spring-security-oauth2-core:7.0.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.security:spring-security-oauth2-core:7.0.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.security:spring-security-oauth2-jose:7.0.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.security:spring-security-oauth2-jose:7.0.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.security:spring-security-oauth2-resource-server:7.0.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.security:spring-security-oauth2-resource-server:7.0.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.security:spring-security-test:7.0.7=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.security:spring-security-test:7.0.7=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.security:spring-security-web:7.0.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.security:spring-security-web:7.0.7=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework.session:spring-session-core:4.0.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework.session:spring-session-core:4.0.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.springframework:spring-aop:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework:spring-aop:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework:spring-beans:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework:spring-beans:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework:spring-context:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework:spring-context:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework:spring-core:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework:spring-core:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework:spring-expression:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework:spring-expression:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework:spring-test:7.0.9=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework:spring-test:7.0.9=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework:spring-web:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework:spring-web:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework:spring-webflux:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.springframework:spring-webflux:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.springframework:spring-webmvc:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath org.springframework:spring-webmvc:7.0.9=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
org.testcontainers:testcontainers-junit-jupiter:2.0.5=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath org.testcontainers:testcontainers-junit-jupiter:2.0.5=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath
org.testcontainers:testcontainers:2.0.5=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath org.testcontainers:testcontainers:2.0.5=nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath
org.xmlresolver:xmlresolver:5.3.3=checkstyle,spotbugs org.xmlresolver:xmlresolver:5.3.3=checkstyle,spotbugs
org.xmlunit:xmlunit-core:2.10.4=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.xmlunit:xmlunit-core:2.10.4=jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
org.yaml:snakeyaml:2.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath org.yaml:snakeyaml:2.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
tools.jackson.core:jackson-core:3.1.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath tools.jackson.core:jackson-core:3.1.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
tools.jackson.core:jackson-databind:3.1.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath tools.jackson.core:jackson-databind:3.1.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
tools.jackson.dataformat:jackson-dataformat-cbor:3.1.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath tools.jackson.dataformat:jackson-dataformat-cbor:3.1.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
tools.jackson.dataformat:jackson-dataformat-xml:3.1.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath tools.jackson.dataformat:jackson-dataformat-xml:3.1.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
tools.jackson:jackson-bom:3.1.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath tools.jackson:jackson-bom:3.1.5=compileClasspath,jettyCompatTestCompileClasspath,jettyCompatTestRuntimeClasspath,nginxProxyTestCompileClasspath,nginxProxyTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testFixturesCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath,webfluxContractTestCompileClasspath,webfluxContractTestRuntimeClasspath
empty= empty=
@@ -67,7 +67,7 @@ public final class WebBudgetCatalog {
/** The budget for a profile, when it is registered. */ /** The budget for a profile, when it is registered. */
public Optional<WebRequestBudget> find(WebBudgetProfileName name) { public Optional<WebRequestBudget> find(WebBudgetProfileName name) {
return Optional.ofNullable(profiles.get(name == null ? null : name)); return name == null ? Optional.empty() : Optional.ofNullable(profiles.get(name));
} }
/** Every registered profile, for a startup report. */ /** Every registered profile, for a startup report. */
@@ -95,7 +95,7 @@ public final class HmacWebCursorCodec implements WebCursorCodec {
String canonical; String canonical;
try { try {
canonical = new String(DECODER.decode(encodedBody), StandardCharsets.UTF_8); canonical = new String(DECODER.decode(encodedBody), StandardCharsets.UTF_8);
} catch (IllegalArgumentException notBase64) { } catch (IllegalArgumentException malformedEncoding) {
throw new WebCursorException(); throw new WebCursorException();
} }
String[] fields = canonical.split("\\u001f", -1); String[] fields = canonical.split("\\u001f", -1);

Some files were not shown because too many files have changed in this diff Show More