Compare commits

...
7 Commits
Author SHA1 Message Date
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
554 changed files with 20103 additions and 21504 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.
-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: "--no-daemon --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}
+94 -99
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,23 +42,23 @@ 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
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 --no-daemon --stacktrace
# build-logic is an included build: its own suite is not reachable from the root project's
# `check`, so the convention plugins every leaf applies would otherwise ship untested.
- name: Test the build-logic convention plugins
working-directory: src
run: ./gradlew -p build-logic test --no-daemon --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 --no-daemon --stacktrace
@@ -61,80 +67,34 @@ jobs:
run: ./gradlew conditionalTransportQualification --no-daemon --stacktrace run: ./gradlew conditionalTransportQualification --no-daemon --stacktrace
sample-off: sample-off:
runs-on: ubuntu-latest uses: ./.github/workflows/_reusable-gradle.yml
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: ":app-bootstrap:sampleOffTest verifyCleanArchitectureDependencies"
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
run: ./gradlew :app-bootstrap:sampleOffTest verifyCleanArchitectureDependencies --no-daemon --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:
runs-on: ubuntu-latest # Milestone A of the Redis wrapper/typed API plan: policy catalog, typed API parity, permit
steps: # provenance, connection isolation, and the executor guard. There is no real-server lane yet.
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 #
- name: Validate Gradle wrapper # `verifyConfigurationPropertiesProcessor` used to be in this list. It is deleted: the parity it
id: gradle-wrapper-validation # enforced — a leaf declares Spring's configuration processor exactly when it owns
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6 # @ConfigurationProperties — is now what applying `ca.spring-config` means.
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1 # `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: with:
distribution: temurin tasks: >-
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
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
run: >-
./gradlew
:shared-contract:edgeRateLimitContractTest :shared-contract:edgeRateLimitContractTest
:adapter:outbound:cache-redis:check :adapter:outbound:cache-redis:check
:app-bootstrap:verifyEnvKeys
verifyCleanArchitectureDependencies verifyCleanArchitectureDependencies
verifyEnvKeys
verifyPublicPathSnapshot verifyPublicPathSnapshot
verifyConfigurationPropertiesProcessor
--no-daemon --stacktrace
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: >-
@@ -150,43 +110,32 @@ 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: "--no-daemon"
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 - sample-off
- gate-matrix-lint
- redis-sdk - redis-sdk
- jpa-candidate-evidence - jpa-candidate-evidence
if: always() if: always()
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
checks: 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 }} SAMPLE_OFF_RESULT: ${{ needs.sample-off.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 }}
run: | run: |
@@ -194,7 +143,6 @@ jobs:
for result in \ for result in \
"${QUALITY_RESULT}" \ "${QUALITY_RESULT}" \
"${SAMPLE_OFF_RESULT}" \ "${SAMPLE_OFF_RESULT}" \
"${MATRIX_RESULT}" \
"${REDIS_RESULT}" \ "${REDIS_RESULT}" \
"${JPA_CANDIDATE_RESULT}"; do "${JPA_CANDIDATE_RESULT}"; do
if [[ "${result}" != "success" ]]; then if [[ "${result}" != "success" ]]; then
@@ -203,3 +151,50 @@ jobs:
fi fi
done done
echo "release-gate: all current blocking quality jobs succeeded." echo "release-gate: all current blocking quality jobs succeeded."
# `needs` cannot reach another workflow, so a blocking check in another file has to be required
# by result. dependency-vulnerability.yml answers the same pull_request and push-to-main
# triggers as this workflow and trivy-fs carries no `if:` guard, so its check run always exists
# for this SHA — which is what makes it requirable rather than a matter of scheduling luck.
# Only `success` passes: a skipped or cancelled security scan is not a scan.
#
# The release-tag and path-filtered workflows (release.yml, jpa-release.yml,
# fileserver-certification.yml, object-storage-qualification.yml, messaging-certification.yml)
# run on triggers this job does not share, so they cannot be required here without changing
# when they run. That is a stated gap, not a hidden one.
- name: Require the cross-workflow release-blocking checks to have succeeded
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
COMMIT_SHA: ${{ github.sha }}
REQUIRED_CHECKS: trivy-fs
run: |
set -euo pipefail
deadline=$(( $(date +%s) + 1800 ))
for required in ${REQUIRED_CHECKS}; do
while :; do
result="$(
gh api \
"repos/${GH_REPO}/commits/${COMMIT_SHA}/check-runs?check_name=${required}&per_page=100" \
--jq '[.check_runs[]] | sort_by(.started_at) | last
| if . == null then "absent none"
else "\(.status) \(.conclusion // "none")" end'
)"
check_status="${result%% *}"
check_conclusion="${result##* }"
if [[ "${check_status}" == "completed" ]]; then
break
fi
if (( $(date +%s) >= deadline )); then
echo "::error::release-gate: release-blocking check ${required} reported no result for ${COMMIT_SHA} (status ${check_status})"
exit 1
fi
echo "release-gate: waiting for ${required} (status ${check_status})"
sleep 30
done
if [[ "${check_conclusion}" != "success" ]]; then
echo "::error::release-gate: release-blocking check ${required} concluded ${check_conclusion}"
exit 1
fi
echo "release-gate: ${required} concluded success."
done
+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
--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
- 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'
--no-daemon
--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'
--no-daemon
--stacktrace
+4 -48
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
@@ -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: >-
@@ -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:
@@ -110,18 +77,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: 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: >-
+12 -63
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,24 +44,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 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 --no-daemon
--stacktrace --stacktrace
@@ -66,18 +59,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: 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: >-
@@ -91,18 +73,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: Run the servlet and reactive transport contracts - name: Run the servlet and reactive transport contracts
working-directory: src working-directory: src
run: >- run: >-
@@ -116,18 +87,7 @@ 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: >-
@@ -142,18 +102,7 @@ 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: >-
-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
+236
View File
@@ -0,0 +1,236 @@
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
--no-daemon
--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
--no-daemon
--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
--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. 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
--no-daemon
--stacktrace
# The six Docker-backed MongoDB lanes. Until now they ran in no workflow at all: the leaf excludes
# every one of their tags from `test` (build.gradle "Docker-backed lanes are excluded from the
# default unit run"), `check` gains only the hermetic `mongoStableContractTest`, and the only thing
# that named them was scripts/verify-mongodb-platform.sh, which nothing in .github invokes. Six
# lanes that fail closed without Docker, and no machine with Docker was ever asked to run them.
#
# Stage 2 rather than stage 1 because each lane starts real MongoDB containers — mongo:8.0.16,
# mongo:7.0.28 and a Toxiproxy in front of a three-node replica set. That is minutes per lane, and
# the pull-request budget is minutes for the whole gate.
#
# One single-line `./gradlew <task>` step per lane, not one folded command running six, because
# 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 --no-daemon --stacktrace
- name: Three-node failover lane
working-directory: src
run: ./gradlew :adapter:outbound:persistence-mongo:mongoFailoverTest --no-daemon --stacktrace
- name: Migration and backfill restart lane
working-directory: src
run: ./gradlew :adapter:outbound:persistence-mongo:mongoMigrationTest --no-daemon --stacktrace
- name: MongoDB 7.0 compatibility lane
working-directory: src
run: ./gradlew :adapter:outbound:persistence-mongo:mongoCompatibilityTest --no-daemon --stacktrace
- name: Credential, TLS and redaction lane
working-directory: src
run: ./gradlew :adapter:outbound:persistence-mongo:mongoSecurityIntegrationTest --no-daemon --stacktrace
- name: Resource-bound certification lane
working-directory: src
run: ./gradlew :adapter:outbound:persistence-mongo:mongoPerformanceTest --no-daemon --stacktrace
- name: Publish the MongoDB lane reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: mongo-lane-reports
path: src/adapter/outbound/persistence-mongo/build/reports/tests/
if-no-files-found: warn
# The messaging contract evidence DAG. `verifyMessagingContracts` is the root of a chain that ran
# nowhere: it depends on five qualification tasks (application-core, shared-contract,
# sample-portfolio and two in adapter:outbound:messaging), each of which depends on
# `prepareMessagingContractEvidence`; it is finalizedBy
# `validateMessagingContractsEvidenceManifestSchema`; and it depends on
# `validateMessagingJsonSchemaV1EvidenceManifestSchema`, which depends on
# `verifyMessagingJsonSchemaV1`. Strict qualification tasks are registered outside `check` by
# design (ca.strict-qualification.gradle), so none of the seven was reachable from any workflow.
#
# The schema validators are the part that matters. They re-read the manifest bytes the run just
# wrote and validate them against config/messaging/evidence/build-evidence-manifest-v1.schema.json
# — a manifest that claims a qualification nobody executed is exactly the failure they exist to
# catch, and until now nothing executed them either.
#
# Stage 2 rather than stage 1: no containers, but it runs five qualification suites across four
# leaves plus two JavaExec validators, and the tests it re-runs are already inside the PR gate's
# `check`. What this job adds is the evidence manifest, which is a main-branch artifact.
messaging-contract-evidence:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Qualify the messaging contract, catalog, binding and schema evidence
working-directory: src
run: ./gradlew verifyMessagingContracts --no-daemon --stacktrace
- name: Publish the messaging evidence manifest
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: messaging-contract-evidence
path: src/build/messaging-evidence/
if-no-files-found: warn
# app-bootstrap's Testcontainers lane. The leaf gave it a source set of its own precisely so that
# `./gradlew :app-bootstrap:test` would not require a Docker daemon — and the consequence nobody
# closed is that a source set outside `test` is also outside `check`, so the real-PostgreSQL
# outbox and idempotency contracts compiled on every build and executed on none.
bootstrap-integration:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
TESTCONTAINERS_REUSE_ENABLE: "false"
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- uses: ./.github/actions/setup-gradle-java
- name: Run the real-PostgreSQL integration contracts
working-directory: src
run: ./gradlew :app-bootstrap:integrationTest --no-daemon --stacktrace
+5 -13
View File
@@ -19,22 +19,11 @@ jobs:
hibernate8-compatibility: hibernate8-compatibility:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 45 timeout-minutes: 45
continue-on-error: 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: Report Hibernate ORM 8 compatibility - name: Report Hibernate ORM 8 compatibility
id: compatibility-probe
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
@@ -43,12 +32,15 @@ jobs:
--stacktrace --stacktrace
- name: Record what this lane did and did not execute - name: Record what this lane did and did not execute
if: always() if: always()
env:
PROBE_OUTCOME: ${{ steps.compatibility-probe.outcome }}
run: | run: |
mkdir -p compatibility-evidence mkdir -p compatibility-evidence
{ {
echo "target=Hibernate 8" echo "target=Hibernate 8"
echo "target-coordinate=org.hibernate.orm:hibernate-core:8.x" echo "target-coordinate=org.hibernate.orm:hibernate-core:8.x"
echo "status=NOT_EXECUTABLE" echo "status=NOT_EXECUTABLE"
echo "probe-result=${PROBE_OUTCOME}"
echo "reason=Hibernate 8 is not resolvable from this build, so nothing has been compiled or run against it" 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 "what-ran=the current runtime's own policy and lane-definition tests"
echo "sha=${{ github.sha }}" echo "sha=${{ github.sha }}"
+5 -13
View File
@@ -19,22 +19,11 @@ jobs:
jpa4-compatibility: jpa4-compatibility:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 45 timeout-minutes: 45
continue-on-error: 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: Report Jakarta Persistence 4.0 compatibility - name: Report Jakarta Persistence 4.0 compatibility
id: compatibility-probe
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
@@ -43,12 +32,15 @@ jobs:
--stacktrace --stacktrace
- name: Record what this lane did and did not execute - name: Record what this lane did and did not execute
if: always() if: always()
env:
PROBE_OUTCOME: ${{ steps.compatibility-probe.outcome }}
run: | run: |
mkdir -p compatibility-evidence mkdir -p compatibility-evidence
{ {
echo "target=Jakarta Persistence 4" echo "target=Jakarta Persistence 4"
echo "target-coordinate=jakarta.persistence:jakarta.persistence-api:4.x" echo "target-coordinate=jakarta.persistence:jakarta.persistence-api:4.x"
echo "status=NOT_EXECUTABLE" echo "status=NOT_EXECUTABLE"
echo "probe-result=${PROBE_OUTCOME}"
echo "reason=the JPA 4 API is not on any configuration this build resolves, so nothing has been compiled against it" 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 "what-ran=the current runtime's own policy and lane-definition tests"
echo "sha=${{ github.sha }}" echo "sha=${{ github.sha }}"
+5 -13
View File
@@ -25,22 +25,11 @@ jobs:
postgresql19-compatibility: postgresql19-compatibility:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 60 timeout-minutes: 60
continue-on-error: 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: Report PostgreSQL 19 compatibility - name: Report PostgreSQL 19 compatibility
id: compatibility-probe
working-directory: src working-directory: src
run: >- run: >-
./gradlew ./gradlew
@@ -49,12 +38,15 @@ jobs:
--stacktrace --stacktrace
- name: Record what this lane did and did not execute - name: Record what this lane did and did not execute
if: always() if: always()
env:
PROBE_OUTCOME: ${{ steps.compatibility-probe.outcome }}
run: | run: |
mkdir -p compatibility-evidence mkdir -p compatibility-evidence
{ {
echo "target=PostgreSQL 19" echo "target=PostgreSQL 19"
echo "target-coordinate=postgres:19-alpine" echo "target-coordinate=postgres:19-alpine"
echo "status=NOT_EXECUTABLE" echo "status=NOT_EXECUTABLE"
echo "probe-result=${PROBE_OUTCOME}"
echo "reason=no PostgreSQL 19 image is published yet, so no container of that major has ever been started by this lane" 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 "what-ran=the current runtime's own policy and lane-definition tests"
echo "sha=${{ github.sha }}" echo "sha=${{ github.sha }}"
+4 -48
View File
@@ -30,18 +30,7 @@ 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: >-
@@ -56,18 +45,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: Reproduce deadlock, serialization, and commit-ambiguity scenarios - name: Reproduce deadlock, serialization, and commit-ambiguity scenarios
working-directory: src working-directory: src
run: >- run: >-
@@ -81,18 +59,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 query plan and database security suites - name: Run the query plan and database security suites
working-directory: src working-directory: src
run: >- run: >-
@@ -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
-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
+1 -12
View File
@@ -26,18 +26,7 @@ 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: >-
+4 -26
View File
@@ -43,18 +43,7 @@ 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: Run the full JPA release gate on PostgreSQL ${{ matrix.postgresql }} - name: Run the full JPA release gate on PostgreSQL ${{ matrix.postgresql }}
working-directory: src working-directory: src
run: >- run: >-
@@ -126,25 +115,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: 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 verifyCleanArchitectureDependencies
verifyOneTypePerFile checkstyleMain
:app-bootstrap:test --tests '*CleanArchitectureTest' :app-bootstrap:test --tests 'dev.caskeleton.bootstrap.architecture.*'
:adapter:outbound:persistence-jpa:test --tests '*JpaReleaseManifestTest' :adapter:outbound:persistence-jpa:test --tests '*JpaReleaseManifestTest'
--no-daemon --no-daemon
--stacktrace --stacktrace
+4 -12
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,18 +36,7 @@ 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"
+18 -73
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
@@ -90,11 +82,11 @@ jobs:
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:test --tests 'dev.caskeleton.bootstrap.architecture.*' --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
@@ -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.
@@ -153,50 +134,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
@@ -23,18 +34,7 @@ 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: Run non-skipping Poster image migration qualification - name: Run non-skipping Poster image migration qualification
working-directory: src working-directory: src
run: ./gradlew :sample-portfolio:posterImageMigrationTest --no-daemon --stacktrace run: ./gradlew :sample-portfolio:posterImageMigrationTest --no-daemon --stacktrace
@@ -43,18 +43,7 @@ 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: 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 --no-daemon --stacktrace
@@ -64,45 +53,27 @@ 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: 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 --no-daemon --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 --no-daemon --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
--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:
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
--no-daemon
--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
--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:
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
--no-daemon
--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 }}
--no-daemon
--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
--no-daemon
--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 }}
--no-daemon
--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
--no-daemon
--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
+470
View File
@@ -0,0 +1,470 @@
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.*'
--no-daemon
--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
--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
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
--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
# 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: In-process contract lane
working-directory: src
run: ./gradlew :grpc:grpc-testkit:grpcInProcessContractTest --no-daemon --stacktrace
- name: Real Netty transport lane
working-directory: src
run: ./gradlew :grpc:grpc-testkit:grpcNettyContractTest --no-daemon --stacktrace
- name: Fault-injection lane
working-directory: src
run: ./gradlew :grpc:grpc-testkit:grpcFaultTest --no-daemon --stacktrace
- name: Publish the gRPC release evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
with:
name: grpc-release-evidence
path: src/grpc/grpc-testkit/build/reports/tests/
if-no-files-found: warn
# Each 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: 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
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
+29
View File
@@ -122,6 +122,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
@@ -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
+1 -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
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` 고아 레인 배선 +
카탈로그 이관.
모든 커밋은 워킹트리를 커밋하지 않고 후보 커밋 객체로 만들어 깨끗한 체크아웃에서
검증한 뒤에 올렸고, 커밋된 트리 해시가 검증한 트리와 같은지 매번 대조했다.
@@ -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,167 @@
# 검증 표면 축소 설계 — 스켈레톤을 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/문서/테스트를 같은 변경에서 고친다.
+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
+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"]
+48 -57
View File
@@ -18,10 +18,18 @@
| --- | --- | | --- | --- |
| `verifyCleanArchitectureDependencies` | 모듈 간 의존 방향이 허용된 범위 안에 있는지 검사 | | `verifyCleanArchitectureDependencies` | 모듈 간 의존 방향이 허용된 범위 안에 있는지 검사 |
| `verifyRuntimeModuleMembership` | registry의 두 composition root membership과 실제 main project dependency가 정확히 일치하는지 검사 | | `verifyRuntimeModuleMembership` | registry의 두 composition root membership과 실제 main project dependency가 정확히 일치하는지 검사 |
| `verifyEnvKeys` | `env-keys.yaml``application.yml``src/.env` 가 어긋나지 않는지 검사 | | `:app-bootstrap:verifyEnvKeys` | `env-keys.yaml``application.yml``src/.env.example` ↔ 타입 설정 메타데이터가 어긋나지 않는지 검사 |
| `verifyOneTypePerFile` | 파일당 public 최상위 타입 1개, 파일명 == 타입명인지 검사 | | `verifyApplicationCoreDependencyPurity` | application-core의 production 의존이 project-only이고 클래스패스에 프레임워크가 없는지 검사 |
| `verifyTrivyignore` | `.trivyignore.yaml` 의 Trivy suppression 이 사유·만료일을 갖추고 만료/기한초과가 아닌지 검사 | | `verifyNoIgnoredSourcePackages` | Git이 실을 수 없는 Java 소스 파일이 없는지 검사 |
| `verifyReadmeCommands` | root README의 실행 가능한 Gradle/Compose/Make 명령이 실제 task/file/target과 일치하는지 검사 |
`architectureCheck` 하나가 위 네 개를 모두 실행합니다.
**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
@@ -31,12 +39,6 @@ DB와 app lifecycle은 저장소 루트의 base/local Compose 조합이 소유
끝나 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은 - 모든 project version은 `<MAJOR>.<MINOR>.<PATCH>+<12자리 git sha>`입니다. base version은
@@ -103,20 +105,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`이고 정의는 `src/gradle/config-contract.gradle`입니다.
- **하는 일.** `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 +158,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 +333,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합니다.
+1 -1
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
+3 -1
View File
@@ -8,6 +8,9 @@
// spring-graphql / graphql-java versions are managed by the Spring Boot BOM, so no explicit // spring-graphql / graphql-java versions are managed by the Spring Boot BOM, so no explicit
// versions or module-scoped platform imports are needed (unlike the grpc adapter, whose io.grpc // versions or module-scoped platform imports are needed (unlike the grpc adapter, whose io.grpc
// coordinates the BOM does not manage). // coordinates the BOM does not manage).
apply plugin: 'ca.spring-library'
apply plugin: 'ca.spring-config'
description = 'Inbound adapter: GraphQL API (Spring for GraphQL, GraphQL execution platform)' description = 'Inbound adapter: GraphQL API (Spring for GraphQL, GraphQL execution platform)'
// The contract suites, the integration fixtures and the in-memory registries are for the people // The contract suites, the integration fixtures and the in-memory registries are for the people
@@ -50,7 +53,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
+14 -5
View File
@@ -5,10 +5,20 @@
// 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.
apply plugin: 'ca.spring-library'
apply plugin: 'ca.spring-config'
dependencyManagement { dependencyManagement {
imports { imports {
@@ -29,7 +39,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.
+67 -67
View File
@@ -1,3 +1,9 @@
// Shared test code as a Gradle test-fixtures variant ADR-BUILD-001.
apply plugin: 'ca.spring-library'
apply plugin: 'ca.spring-config'
apply plugin: 'java-test-fixtures'
// 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 +19,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
@@ -65,13 +70,12 @@ tasks.named('test') {
// 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 { strictTestLanes {
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 +89,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 +102,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 +177,53 @@ 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 customize = { test -> test.jvmArgs '-Duser.timezone=UTC' }
classpath = sourceSets.webfluxContractTest.runtimeClasspath
useJUnitPlatform()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
} }
// Docker-gated, and it says so rather than skipping. A lane that quietly passes when the container // Docker-gated, and it says so rather than skipping. A lane that quietly passes when the
// runtime is missing is a lane that has been certifying nothing since whenever Docker last broke. // container runtime is missing is a lane that has been certifying nothing since whenever Docker
tasks.register('webNginxProxyTest', Test) { // last broke.
group = 'verification' lane('webNginxProxyTest') {
sourceSet = 'nginxProxyTest'
description = 'Runs the proxy, prefix and spoofing contract behind a real Nginx.' description = 'Runs the proxy, prefix and spoofing contract behind a real Nginx.'
testClassesDirs = sourceSets.nginxProxyTest.output.classesDirs customize = { test -> test.jvmArgs '-Duser.timezone=UTC' }
classpath = sourceSets.nginxProxyTest.runtimeClasspath }
useJUnitPlatform()
failOnNoDiscoveredTests = true lane('webJettyCompatTest') {
outputs.upToDateWhen { false } sourceSet = 'jettyCompatTest'
jvmArgs '-Duser.timezone=UTC' description = 'Runs the Stable HTTP contract against a real Jetty instead of Tomcat.'
customize = { test -> test.jvmArgs '-Duser.timezone=UTC' }
} }
// The cross-stack gate. It depends on every recording lane rather than tolerating a missing one: // 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 // a parity check that compares whatever happens to be present would report agreement across a
// matrix with a hole in it. // matrix with a hole in it.
tasks.register('webCrossStackParityTest', Test) { lane('webCrossStackParityTest') {
group = 'verification' tag = 'web-parity'
description = 'Compares the wire contract recorded by Tomcat, Jetty and Reactor Netty.' description = 'Compares the wire contract recorded by Tomcat, Jetty and Reactor Netty.'
testClassesDirs = sourceSets.test.output.classesDirs customize = { test ->
classpath = sourceSets.test.runtimeClasspath test.jvmArgs '-Duser.timezone=UTC'
useJUnitPlatform { test.dependsOn 'test', 'webJettyCompatTest', 'webFluxContractTest'
includeTags 'web-parity'
} }
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
dependsOn 'test', 'webJettyCompatTest', 'webFluxContractTest'
} }
// The Advanced lane. Every capability is off unless a deployment names it, so none of them is // 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 // 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. // 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 // They also run inside `test`, deliberately. They are ordinary unit tests, and excluding them
// the PR gate to make this lane look meaningful would mean the PR gate stopped covering a fifth of // from the PR gate to make this lane look meaningful would mean the PR gate stopped covering a
// the leaf. // fifth of the leaf.
tasks.register('webAdvancedTest', Test) { lane('webAdvancedTest') {
group = 'verification' tag = 'web-advanced'
description = 'Runs every web Advanced capability contract.' description = 'Runs every web Advanced capability contract.'
testClassesDirs = sourceSets.test.output.classesDirs customize = { test -> test.jvmArgs '-Duser.timezone=UTC' }
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform {
includeTags 'web-advanced'
} }
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
}
tasks.register('webJettyCompatTest', Test) {
group = 'verification'
description = 'Runs the Stable HTTP contract against a real Jetty instead of Tomcat.'
testClassesDirs = sourceSets.jettyCompatTest.output.classesDirs
classpath = sourceSets.jettyCompatTest.runtimeClasspath
useJUnitPlatform()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
} }
strictTestLanes { strictTestLanes {
+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=
@@ -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