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

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

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

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

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

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

## 컨테이너 릴리스 신설

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

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

## 게이트 정리

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

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

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

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

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

## 남은 문제

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-16 18:27:15 +09:00
co-authored by Claude Opus 5
parent 1535481794
commit 9bc2e75fe5
132 changed files with 4002 additions and 3083 deletions
@@ -0,0 +1,37 @@
name: Set up Java and the Gradle cache
description: >-
Installs the repository's pinned Temurin JDK and restores the Gradle cache keyed on this
repository's build files. Every Gradle job used to carry this block verbatim, so the JDK patch
level and the cache key lived in fifty-nine places and could drift in any one of them.
# Deliberately NOT in this action: `actions/checkout` and the Gradle wrapper validation step.
#
# Neither can move here, and the reasons are different:
#
# * checkout — a `./.github/actions/...` reference is resolved from the checked-out working
# copy, so the action file does not exist until checkout has already run. A composite action
# cannot contain the step that makes itself readable.
# * wrapper validation — .github/scripts/verify-gradle-wrapper.sh reads each workflow job and
# requires 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
# is the guard: it is what makes "this job validated the wrapper before running it" checkable
# from the workflow file alone. Hiding the step behind an action would also break the guarded
# `if: ${{ always() && steps.gradle-wrapper-validation.outcome == 'success' }}` form the same
# script enforces, because a composite action's step ids are not visible to its caller — the
# condition would silently evaluate to false and skip the step it was protecting.
#
# So a Gradle job is four lines of preamble (checkout, the three-line validation step) plus one
# line for this action, instead of thirteen.
runs:
using: composite
steps:
- 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
+592 -70
View File
@@ -1,6 +1,16 @@
# 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.
# Every CI control in this checkout: one row per job in .github/workflows, plus the delegated-pending
# rows for controls that are real and that no workflow runs.
#
# The rows are grouped by STAGE, because that is how the workflows are now split. Stage 1 blocks a
# merge, stage 2 reports on main after the merge, stage 3 stops a release. A control's stage is the
# honest form of "does this gate development", and it is what release_blocking below records.
#
# Image signing and provenance attestation are still absent. So is any join between the image build
# in release.yml and the two evidence workflows that answer the same release tag from their own files
# (jpa-release.yml, fileserver-release.yml); that join is registered delegated-pending rather than
# left unstated. .github/scripts/verify-gate-matrix.sh checks every row against the repository and is
# itself the gate-matrix-lint job below; it pins no gate count, so registering a new control is
# adding a row here and nothing else.
#
# Fields:
# release_blocking: true, false, or conditional
@@ -9,7 +19,23 @@
# 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
#
# release_blocking is verified, not declared:
# true a release gate waits on this job — it is ci-quality-gates.yml::release-gate itself,
# one of that job's `needs:`, a name in its REQUIRED_CHECKS, or a job in a workflow
# that only runs on a release tag. The lint fails a `true` row that is none of those.
# conditional the control is real and gates something, but no release gate can require it: it runs
# behind a path filter, a schedule, a manual dispatch, or an input, so its check run
# does not exist for every commit a release gate sees.
# false advisory. A failure here is a signal, not a stop.
gates:
# =================================================================================
# STAGE 1 — pull request. These block a merge.
# =================================================================================
#
# ci-quality-gates.yml — the repository-wide gate. No path filter: it runs on every pull request
# and every push to main, which is what makes the leaf test suites, the architecture dependency
# gate and the Checkstyle ruleset below cover a diff without any per-adapter job repeating them.
- id: format-lint
release_blocking: true
mechanism: gradle-plugin-task
@@ -45,13 +71,6 @@ gates:
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
@@ -80,13 +99,6 @@ gates:
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
@@ -101,6 +113,7 @@ gates:
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
@@ -118,6 +131,7 @@ gates:
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
@@ -135,17 +149,16 @@ gates:
workflow: ci-quality-gates.yml
job: release-gate
execution: job
#
# Checkstyle, not the retired regex. verifyOneTypePerFile parsed Java with
# `^public\s+...` line by line; OneTopLevelClass and OuterTypeFilename ask the same two questions
# against a parsed file and see the package-private top-level types the regex could not. The task
# still exists in src/build.gradle as an aggregate of every leaf's checkstyleMain, but no workflow
# names it any more, so this row names what actually runs: checkstyleMain, inside `check`.
- 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
mechanism: gradle-plugin-task
ref: checkstyle@checkstyleMain
workflow: ci-quality-gates.yml
job: quality-gates
execution: check
@@ -177,6 +190,7 @@ gates:
workflow: ci-quality-gates.yml
job: quality-gates
execution: explicit
#
# Points at the inventory guard rather than at one suite. The 87 architecture rules used to be
# 74-in-one-class plus a scattered remainder, so naming CleanArchitectureTest.java named most of
# them and silently omitted the rest; after the split (BOOT-012) it would have named a fifth of
@@ -218,13 +232,6 @@ gates:
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
@@ -239,6 +246,130 @@ gates:
workflow: ci-quality-gates.yml
job: quarantine
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
#
# pr-adapters.yml — the lanes `check` cannot reach: a second servlet container, a real Nginx,
# Reactor Netty, one PostgreSQL major per job, one HTTP transport per job. Each job is behind a
# per-job path filter computed from the pull request's diff, so no release gate can wait on one.
- id: httpclient-contract-stable-contract
release_blocking: conditional
mechanism: workflow-job
ref: httpclient-stable-contract
workflow: pr-adapters.yml
job: httpclient-stable-contract
execution: job
- id: httpclient-security-and-compatibility
release_blocking: conditional
mechanism: workflow-job
ref: httpclient-security-and-compatibility
workflow: pr-adapters.yml
job: httpclient-security-and-compatibility
execution: job
- id: jpa-postgresql-contract
release_blocking: conditional
mechanism: workflow-job
ref: jpa-postgresql-contract
workflow: pr-adapters.yml
job: jpa-postgresql-contract
execution: job
- id: jpa-migration-smoke
release_blocking: conditional
mechanism: workflow-job
ref: jpa-migration-smoke
workflow: pr-adapters.yml
job: jpa-migration-smoke
execution: job
- id: web-cross-stack-parity
release_blocking: conditional
mechanism: workflow-job
ref: web-cross-stack-parity
workflow: pr-adapters.yml
job: web-cross-stack-parity
execution: job
- id: web-nginx-proxy-contract
release_blocking: conditional
mechanism: workflow-job
ref: web-nginx-proxy-contract
workflow: pr-adapters.yml
job: web-nginx-proxy-contract
execution: job
- id: websocket-container-matrix
release_blocking: conditional
mechanism: workflow-job
ref: websocket-container-matrix
workflow: pr-adapters.yml
job: websocket-container-matrix
execution: job
- id: websocket-nginx-contract
release_blocking: conditional
mechanism: workflow-job
ref: websocket-nginx-contract
workflow: pr-adapters.yml
job: websocket-nginx-contract
execution: job
#
# The job every lane in that file reads. It fails closed on an unreadable diff rather than
# reporting that nothing changed, because a filter that answers false on a broken comparison
# turns off every gate behind it and reports green.
- id: pr-adapter-change-filter
release_blocking: conditional
mechanism: workflow-job
ref: changes
workflow: pr-adapters.yml
job: changes
execution: job
#
# fileserver-pr.yml — path-filtered pull-request gates. They stay in their own file because
# FileserverDocumentationCoverageTest requires every job id docs/fileserver/support-matrix.md
# names to be defined in a .github/workflows/fileserver-*.yml.
#
# Path-filtered pull-request gates. They are the only automated check of the fileserver's
# ext4 and HTTP contracts, but they start only when the filtered paths change, so no release
# gate can wait on them.
- id: fileserver-unit-and-architecture
release_blocking: conditional
mechanism: workflow-job
ref: fileserver-unit-and-architecture
workflow: fileserver-pr.yml
job: fileserver-unit-and-architecture
execution: job
- id: fileserver-local-ext4-contract
release_blocking: conditional
mechanism: workflow-job
ref: fileserver-local-ext4-contract
workflow: fileserver-pr.yml
job: fileserver-local-ext4-contract
execution: job
- id: fileserver-http-contract
release_blocking: conditional
mechanism: workflow-job
ref: fileserver-http-contract
workflow: fileserver-pr.yml
job: fileserver-http-contract
execution: job
- id: fileserver-security-suite
release_blocking: conditional
mechanism: workflow-job
ref: fileserver-security-suite
workflow: fileserver-pr.yml
job: fileserver-security-suite
execution: job
- id: fileserver-bounded-memory
release_blocking: conditional
mechanism: workflow-job
ref: fileserver-bounded-memory
workflow: fileserver-pr.yml
job: fileserver-bounded-memory
execution: job
#
# dependency-vulnerability.yml — supply chain. trivy-fs is the one cross-workflow check
# ci-quality-gates.yml::release-gate requires by name through REQUIRED_CHECKS.
- id: dependency-review
release_blocking: conditional
mechanism: workflow-job
@@ -260,6 +391,8 @@ gates:
workflow: dependency-vulnerability.yml
job: trivy-fs
execution: job
#
# link-check.yml — committed documentation links, path-filtered.
- id: documentation-links
release_blocking: conditional
mechanism: workflow-job
@@ -267,15 +400,148 @@ gates:
workflow: link-check.yml
job: lychee
execution: job
# =================================================================================
# STAGE 2 — merged state. These do not block a merge; the merge already happened.
# =================================================================================
#
# integration-main.yml — push to main, nightly, or dispatch. The documentation-drift gates that
# left `check`, and the lanes that need a machine which is not simultaneously compiling.
- id: documented-leaf-count
release_blocking: false
mechanism: gradle-custom-task
ref: verifyDocumentedLeafCount
workflow: integration-main.yml
job: documentation-contracts
execution: job
- id: runbook-reference-drift
release_blocking: false
mechanism: gradle-custom-task
ref: verifyRunbookReferences
workflow: integration-main.yml
job: documentation-contracts
execution: job
- id: readme-command-drift
release_blocking: false
mechanism: gradle-custom-task
ref: verifyReadmeCommands
workflow: integration-main.yml
job: documentation-contracts
execution: job
#
# Scheduled. httpclient-nightly-http3-experimental is continue-on-error on purpose: HTTP/3 is
# an opt-in experiment (-Phttp3.tests.enabled) and a red experiment must not bury a real
# nightly regression. Registered false so the matrix says that out loud instead of leaving a
# job that cannot fail unrecorded.
- id: httpclient-nightly-fault-injection
release_blocking: false
mechanism: workflow-job
ref: httpclient-fault-injection
workflow: integration-main.yml
job: httpclient-fault-injection
execution: job
- id: httpclient-nightly-performance
release_blocking: false
mechanism: workflow-job
ref: httpclient-performance
workflow: integration-main.yml
job: httpclient-performance
execution: job
- id: httpclient-nightly-http3-experimental
release_blocking: false
mechanism: workflow-job
ref: httpclient-http3-experimental
workflow: integration-main.yml
job: httpclient-http3-experimental
execution: job
#
# Scheduled load, abuse and shutdown lane.
- id: web-load-abuse-and-shutdown
release_blocking: false
mechanism: workflow-job
ref: web-load-abuse-and-shutdown
workflow: integration-main.yml
job: web-load-abuse-and-shutdown
execution: job
#
# The four documentation-drift gates, as one task. They were `dependsOn` of the root `check`
# and are not any more: a README sentence about a renamed task should not fail a
# compile-and-test run. This job is what keeps that a demotion rather than a deletion — with
# no workflow invoking the aggregate, all four would run nowhere. release_blocking: false is
# the demotion stated as a fact the lint can check.
- id: documentation-contracts
release_blocking: false
mechanism: gradle-custom-task
ref: verifyDocumentationContracts
workflow: integration-main.yml
job: documentation-contracts
execution: explicit
#
# Ran inside verifyDocumentationContracts above rather than named in the job, which is why
# this row is execution: job. It had no row at all before.
- id: test-source-set-registry
release_blocking: false
mechanism: gradle-custom-task
ref: verifyTestSourceSetRegistry
workflow: integration-main.yml
job: documentation-contracts
execution: job
#
# notification-platform.yml — `pr` is path-filtered and also runs on push to main; nightly-chaos
# is schedule/dispatch only. The release-blocking notification controls are the verifyNotification*
# rows above, which run inside check on every pull request.
#
# `pr` is path-filtered (and runs on push to main); nightly-chaos is schedule/dispatch only.
# The release-blocking notification controls are the verifyNotification* rows above, which run
# inside check on every pull request.
- id: notification-platform-pr
release_blocking: conditional
mechanism: workflow-job
ref: pr
workflow: notification-platform.yml
job: pr
execution: job
- id: notification-platform-nightly-chaos
release_blocking: false
mechanism: workflow-job
ref: nightly-chaos
workflow: notification-platform.yml
job: nightly-chaos
execution: job
#
# messaging-certification.yml — path-filtered pull request plus a weekly schedule.
#
# 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: conditional
mechanism: gradle-custom-task
ref: verifyMessagingCertificationEvidence
workflow: messaging-certification.yml
job: broker-certification
execution: explicit
#
# object-storage-qualification.yml — pull request, weekly schedule, and two protected dispatch
# inputs for the AWS sandbox lane.
#
# These three ran under `release_blocking: true` while nothing waited on them. object-storage-
# qualification.yml has no push-to-main trigger and messaging-certification.yml is behind a path
# filter, so neither produces a check run for every commit ci-quality-gates.yml::release-gate
# judges; requiring them there would hang on the commits where they never start. They are
# conditional — real controls on their own trigger — until someone decides to widen that trigger,
# which is a CI-minutes decision about MinIO and Kafka containers, not a wiring oversight.
- id: object-storage-minio-managed-contract
release_blocking: true
release_blocking: conditional
mechanism: gradle-custom-task
ref: objectStorageMinioContractTest
workflow: object-storage-qualification.yml
job: minio-managed-contract
execution: explicit
- id: poster-image-migration
release_blocking: true
release_blocking: conditional
mechanism: gradle-custom-task
ref: posterImageMigrationTest
workflow: object-storage-qualification.yml
@@ -295,13 +561,9 @@ gates:
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
#
# redis-sdk-topology.yml — path-filtered pull request (standalone only) and the nightly matrix.
#
# 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
@@ -313,41 +575,163 @@ gates:
workflow: redis-sdk-topology.yml
job: topology-evidence
execution: job
#
# The lane that topology-evidence needs. Same trigger as the evidence row above it.
- id: redis-sdk-topology-lanes
release_blocking: conditional
mechanism: workflow-job
ref: lanes
workflow: redis-sdk-topology.yml
job: lanes
execution: job
#
# fileserver-nightly.yml — scheduled only. NFS ambiguity, process-kill and lease behaviour need
# hours and real filesystems; a failure is a signal to act on, not a stop on a release that did not
# cause it.
#
# Scheduled only. NFS ambiguity, process-kill and lease behaviour need hours and real
# filesystems; a failure is a signal to act on, not a stop on a release that did not cause it.
- id: fileserver-nfs-ambiguity
release_blocking: false
mechanism: workflow-job
ref: fileserver-nfs-ambiguity
workflow: fileserver-nightly.yml
job: fileserver-nfs-ambiguity
execution: job
- id: fileserver-process-kill-matrix
release_blocking: false
mechanism: workflow-job
ref: fileserver-process-kill-matrix
workflow: fileserver-nightly.yml
job: fileserver-process-kill-matrix
execution: job
- id: fileserver-large-file-performance
release_blocking: false
mechanism: workflow-job
ref: fileserver-large-file-performance
workflow: fileserver-nightly.yml
job: fileserver-large-file-performance
execution: job
- id: fileserver-multi-instance-lease
release_blocking: false
mechanism: workflow-job
ref: fileserver-multi-instance-lease
workflow: fileserver-nightly.yml
job: fileserver-multi-instance-lease
execution: job
#
# jpa-nightly.yml — the middle of the PostgreSQL matrix and the suites too slow or too
# Docker-heavy for a pull request.
#
# Scheduled JPA matrix, failure injection, query-plan/security and pool pressure.
- id: jpa-full-matrix
release_blocking: false
mechanism: workflow-job
ref: jpa-full-matrix
workflow: jpa-nightly.yml
job: jpa-full-matrix
execution: job
- id: jpa-failure-injection
release_blocking: false
mechanism: workflow-job
ref: jpa-failure-injection
workflow: jpa-nightly.yml
job: jpa-failure-injection
execution: job
- id: jpa-query-plan-and-security
release_blocking: false
mechanism: workflow-job
ref: jpa-query-plan-and-security
workflow: jpa-nightly.yml
job: jpa-query-plan-and-security
execution: job
- id: jpa-pool-pressure
release_blocking: false
mechanism: workflow-job
ref: jpa-pool-pressure
workflow: jpa-nightly.yml
job: jpa-pool-pressure
execution: job
#
# jpa-next-*.yml — weekly early-warning lanes against unreleased upstream versions. They exist to
# find out before the upgrade, so they block nothing.
#
# Weekly early-warning lanes against unreleased upstream versions. They exist to find out
# before the upgrade, so they block nothing.
- id: hibernate8-compatibility
release_blocking: false
mechanism: workflow-job
ref: hibernate8-compatibility
workflow: jpa-next-hibernate8.yml
job: hibernate8-compatibility
execution: job
- id: jpa4-compatibility
release_blocking: false
mechanism: workflow-job
ref: jpa4-compatibility
workflow: jpa-next-jpa4.yml
job: jpa4-compatibility
execution: job
- id: postgresql19-compatibility
release_blocking: false
mechanism: workflow-job
ref: postgresql19-compatibility
workflow: jpa-next-postgresql19.yml
job: postgresql19-compatibility
execution: job
#
# jpa-r2-evidence.yml — dispatch-only production-profile manifest run.
- 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
# =================================================================================
# STAGE 3 — release tag. These stop a release.
# =================================================================================
#
# release.yml — one workflow, one deployable unit. Evidence jobs first, then app-image-release,
# which `needs:` all of them: while the image build lived in its own workflow it could publish
# while a sibling suite was still running or already red.
- id: httpclient-stable-contract
release_blocking: true
mechanism: gradle-custom-task
ref: httpClientStableContractTest
workflow: httpclient-release.yml
job: release-gate
workflow: release.yml
job: httpclient-release-gate
execution: explicit
- id: httpclient-security-suite
release_blocking: true
mechanism: gradle-custom-task
ref: httpClientSecurityTest
workflow: httpclient-release.yml
job: release-gate
workflow: release.yml
job: httpclient-release-gate
execution: explicit
- id: httpclient-fault-injection
release_blocking: true
mechanism: gradle-custom-task
ref: httpClientFailureInjectionTest
workflow: httpclient-release.yml
job: release-gate
workflow: release.yml
job: httpclient-release-gate
execution: explicit
- id: httpclient-performance-certification
release_blocking: true
mechanism: gradle-custom-task
ref: httpClientPerformanceTest
workflow: httpclient-release.yml
job: release-gate
workflow: release.yml
job: httpclient-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
workflow: release.yml
job: httpclient-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
@@ -359,39 +743,177 @@ gates:
release_blocking: conditional
mechanism: delegated-pending
ref: spring62-runtime-lane
workflow: httpclient-release.yml
job: release-gate
workflow: release.yml
job: httpclient-release-gate
execution: job
- id: httpclient-spring70-compatibility
release_blocking: true
mechanism: gradle-custom-task
ref: spring70CompatibilityTest
workflow: httpclient-release.yml
job: release-gate
workflow: release.yml
job: httpclient-release-gate
execution: explicit
- id: httpclient-documentation-drift
release_blocking: true
mechanism: workflow-job
ref: httpclient-documentation
workflow: httpclient-release.yml
workflow: 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
workflow: release.yml
job: httpclient-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
#
# Tag-triggered (`v*`, `web-v*`). The Stable web release gate.
- id: web-stable-release-gate
release_blocking: true
mechanism: gradle-custom-task
ref: verifyMessagingCertificationEvidence
workflow: messaging-certification.yml
job: broker-certification
execution: explicit
mechanism: workflow-job
ref: web-stable-release-gate
workflow: release.yml
job: web-stable-release-gate
execution: job
#
# Tag-triggered (`v*`, `websocket-v*`). The Stable websocket release gate.
- id: websocket-stable-release-gate
release_blocking: true
mechanism: workflow-job
ref: websocket-stable-release-gate
workflow: release.yml
job: websocket-stable-release-gate
execution: job
#
# Tag-triggered (`v*`). The only job in this repository that produces a deployable artifact.
#
# One row, not three, because one job is one control: the job builds the app-bootstrap image,
# generates its CycloneDX SBOM, refuses to publish on a CRITICAL or HIGH finding, and only then
# pushes the semver and sha- tags and records the digest. The scan is a step inside that control
# rather than a control of its own, which is the same shape filesystem-vulnerability-scan uses for
# the trivy-fs job. `release_blocking: true` holds because the workflow runs only for a release
# tag, so the job failing fails that release run.
- id: container-image-release
release_blocking: true
mechanism: workflow-job
ref: app-image-release
workflow: release.yml
job: app-image-release
execution: job
#
# The join this repository cannot express, stated rather than implied.
#
# Most of it is closed now. app-image-release lives in release.yml and `needs:` the four evidence
# jobs in that file, so the image cannot be built, scanned or pushed past a failed web, websocket,
# httpclient or architecture gate. `needs:` still reaches only inside one workflow file, and two
# evidence workflows are still outside it: jpa-release.yml and fileserver-release.yml both answer
# the same `v*` tag and neither can be waited on from here.
#
# Those two are not in release.yml for a mechanical reason rather than a design one —
# JpaReleaseRenderingTest reads `.github/workflows/jpa-release.yml` by that exact path, and
# FileserverDocumentationCoverageTest requires the job ids docs/fileserver/support-matrix.md names
# to be defined in a `.github/workflows/fileserver-*.yml`. Folding them in means changing a test
# and a document in src/ and docs/ in the same commit, which is a decision with owners rather than
# a wiring oversight.
#
# Until then the remaining control is a human one: the GitOps repository promotes a digest whose
# tag's jpa-release and fileserver-release runs somebody has looked at, not a digest that merely
# exists. Registered here so that sentence lives somewhere a lint can point at, the way
# fileserver-pvc-cluster-certification does for the storage claim.
- id: container-release-evidence-join
release_blocking: conditional
mechanism: delegated-pending
ref: container-release-evidence-join
workflow: release.yml
job: app-image-release
execution: job
#
# verifyCleanArchitectureDependencies, verifyPublicPathSnapshot, verifyEnvKeys and the
# bootstrap architecture suite, once per release. The four release workflows this replaced ran
# the dependency gate six times and the architecture suite four times for one tag, on separate
# runners, against one commit.
- id: release-architecture-and-surface
release_blocking: true
mechanism: workflow-job
ref: architecture-and-surface
workflow: release.yml
job: architecture-and-surface
execution: job
#
# jpa-release.yml — tag-triggered (`v*`). jpa-release-promotion needs jpa-release-gate, so the
# promotion cannot run past a failed gate. Its own file because JpaReleaseRenderingTest reads that
# exact path and holds its matrix to src/config/jpa/release-registry.json.
#
# Tag-triggered (`v*`). jpa-release-promotion needs jpa-release-gate, so the promotion cannot
# run past a failed gate.
- id: jpa-release-gate
release_blocking: true
mechanism: workflow-job
ref: jpa-release-gate
workflow: jpa-release.yml
job: jpa-release-gate
execution: job
- id: jpa-release-promotion
release_blocking: true
mechanism: workflow-job
ref: jpa-release-promotion
workflow: jpa-release.yml
job: jpa-release-promotion
execution: job
- id: jpa-architecture-and-docs
release_blocking: true
mechanism: workflow-job
ref: jpa-architecture-and-docs
workflow: jpa-release.yml
job: jpa-architecture-and-docs
execution: job
#
# fileserver-release.yml — tag-triggered (`v*`). Its own file for the same reason fileserver-pr.yml
# is.
#
# Tag-triggered: `v*` runs this workflow and a failing job fails that
# release. fileserver-pvc-certification checks the manifest only — the cluster half is the
# delegated-pending row below it.
- id: fileserver-full-verification
release_blocking: true
mechanism: workflow-job
ref: fileserver-full-verification
workflow: fileserver-release.yml
job: fileserver-full-verification
execution: job
- id: fileserver-documentation-gate
release_blocking: true
mechanism: workflow-job
ref: fileserver-documentation-gate
workflow: fileserver-release.yml
job: fileserver-documentation-gate
execution: job
- id: fileserver-pvc-certification
release_blocking: true
mechanism: workflow-job
ref: fileserver-pvc-certification
workflow: fileserver-release.yml
job: fileserver-pvc-certification
execution: job
#
# The other half of the PVC claim, and the half no runner can produce. An operator
# applies infra/fileserver/kubernetes/pvc-certification-job.yaml to a real cluster and
# records the result in docs/fileserver/storage-certification.md. The workflow step
# that pretended to do this in CI reported success whenever the cluster secret was
# absent, which is every checkout of this template; it is gone, and the claim is
# tracked here instead of being green for nothing.
- id: fileserver-pvc-cluster-certification
release_blocking: conditional
mechanism: delegated-pending
ref: fileserver-pvc-cluster-lane
workflow: fileserver-release.yml
job: fileserver-pvc-certification
execution: job
- id: fileserver-sensitive-telemetry-scan
release_blocking: true
mechanism: workflow-job
ref: fileserver-sensitive-telemetry-scan
workflow: fileserver-release.yml
job: fileserver-sensitive-telemetry-scan
execution: job
+134 -14
View File
@@ -24,18 +24,23 @@ 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
# There is deliberately no expected gate count here. A hand-edited integer made the matrix
# un-editable: no control could be registered without editing the guard whose purpose was to stop
# the matrix changing, and the guard caught nothing a per-row rule does not already catch — a row
# whose task, workflow or job does not exist fails below regardless of how many rows there are.
# What replaces it is the per-row invariant set: required fields, valid enums, a workflow and job
# that exist, a registered and actually-executed mechanism, unique ids, and the release-blocking
# rule below. Those hold at any count.
#
# The one property the count did carry is kept explicitly: a matrix with no gates at all is drift,
# not a clean run.
# The release gate every pull request and push to main passes through. Named rather than inferred:
# `release_blocking: true` is checked against what this job waits on, so the field means something a
# machine can verify instead of being an enum nobody reads.
readonly RELEASE_GATE_WORKFLOW='ci-quality-gates.yml'
readonly RELEASE_GATE_JOB='release-gate'
if [[ ! -f "${MATRIX}" ]]; then
printf '::error::gate-matrix-lint: missing %s\n' "${MATRIX}" >&2
@@ -315,6 +320,115 @@ job_runs_gradle_task() {
return 1
}
# A workflow that only runs for a release tag. Its jobs need no separate release gate: the workflow
# run *is* the release, so a failing job fails it. Detected from the `on:` block rather than from a
# filename, because "release" in a filename is a naming convention and `on: push: tags:` is not.
workflow_is_release_tag_triggered() {
local workflow_file="$1"
[[ -f "${workflow_file}" ]] || return 1
awk '
/^on:[[:space:]]*$/ { in_on=1; next }
/^[^[:space:]#]/ { in_on=0 }
in_on && /^[[:space:]]+tags:/ { found=1 }
END { exit found ? 0 : 1 }
' "${workflow_file}"
}
# Jobs the release gate actually waits on: its `needs:` inside its own workflow, plus the job names
# in REQUIRED_CHECKS, which is how it requires a check run produced by a different workflow.
RELEASE_GATE_NEEDS=""
RELEASE_GATE_REQUIRED_CHECKS=""
load_release_gate_requirements() {
[[ -n "${RELEASE_GATE_NEEDS}" ]] && return 0
RELEASE_GATE_NEEDS="<none>"
RELEASE_GATE_REQUIRED_CHECKS="<none>"
local workflow_file="${REPO_ROOT}/.github/workflows/${RELEASE_GATE_WORKFLOW}"
[[ -f "${workflow_file}" ]] || return 0
grep -Eqs -- "^[[:space:]]{2}${RELEASE_GATE_JOB}:[[:space:]]*$" "${workflow_file}" || return 0
local entry kind value
local -a needs=()
local -a checks=()
while IFS= read -r entry; do
[[ "${entry}" =~ ^(need|check)\ [A-Za-z0-9_-]+$ ]] || continue
kind="${entry%% *}"
value="${entry#* }"
if [[ "${kind}" == "need" ]]; then
needs+=("${value}")
else
checks+=("${value}")
fi
done < <(
job_body "${workflow_file}" "${RELEASE_GATE_JOB}" | awk '
/^[[:space:]]+needs:[[:space:]]*\[/ {
value=$0
sub(/^[[:space:]]+needs:[[:space:]]*\[/, "", value)
sub(/\].*$/, "", value)
count=split(value, parts, /[[:space:]]*,[[:space:]]*/)
for (index_value = 1; index_value <= count; index_value++) {
gsub(/[[:space:]]/, "", parts[index_value])
if (parts[index_value] != "") { print "need " parts[index_value] }
}
next
}
/^[[:space:]]+needs:[[:space:]]*[A-Za-z0-9_-]+[[:space:]]*$/ {
value=$0
sub(/^[[:space:]]+needs:[[:space:]]*/, "", value)
sub(/[[:space:]]+$/, "", value)
print "need " value
next
}
/^[[:space:]]+needs:[[:space:]]*$/ { in_needs=1; next }
in_needs && /^[[:space:]]+-[[:space:]]+/ {
value=$0
sub(/^[[:space:]]+-[[:space:]]+/, "", value)
sub(/[[:space:]]+$/, "", value)
print "need " value
next
}
in_needs { in_needs=0 }
/^[[:space:]]+REQUIRED_CHECKS:[[:space:]]*/ {
value=$0
sub(/^[[:space:]]+REQUIRED_CHECKS:[[:space:]]*/, "", value)
count=split(value, entries, /[[:space:]]+/)
for (index_value = 1; index_value <= count; index_value++) {
if (entries[index_value] != "") { print "check " entries[index_value] }
}
}
'
)
(( ${#needs[@]} > 0 )) && RELEASE_GATE_NEEDS="$(printf '%s\n' "${needs[@]}" | sort -u)"
(( ${#checks[@]} > 0 )) && RELEASE_GATE_REQUIRED_CHECKS="$(printf '%s\n' "${checks[@]}" | sort -u)"
return 0
}
# `release_blocking: true` used to be read by nothing but an enum test, so a gate could claim to
# block a release that no job anywhere waited on — filesystem-vulnerability-scan was red while
# release-gate was green and nothing in the repository joined the two. A gate earns `true` by being
# required on a path a release actually takes:
# - it is the release gate job itself, or one of that job's `needs:` in the same workflow;
# - its job name is listed in the release gate's REQUIRED_CHECKS (the cross-workflow hook);
# - its workflow only runs for a release tag, so the job failing fails that release run.
# A control that is real but reachable by none of those is `conditional`, which is the honest value
# and is what the enum is for.
gate_is_enforced_by_a_release_gate() {
local gate_workflow="$1"
local gate_job="$2"
load_release_gate_requirements
if [[ "${gate_workflow}" == "${RELEASE_GATE_WORKFLOW}" ]]; then
if [[ "${gate_job}" == "${RELEASE_GATE_JOB}" ]]; then
return 0
fi
if printf '%s\n' "${RELEASE_GATE_NEEDS}" | grep -qxF -- "${gate_job}"; then
return 0
fi
fi
if printf '%s\n' "${RELEASE_GATE_REQUIRED_CHECKS}" | grep -qxF -- "${gate_job}"; then
return 0
fi
workflow_is_release_tag_triggered "${REPO_ROOT}/.github/workflows/${gate_workflow}"
}
while IFS=$'\t' read -r id blocking mechanism ref workflow job execution; do
[[ -z "${id}" ]] && continue
total=$((total + 1))
@@ -347,6 +461,12 @@ while IFS=$'\t' read -r id blocking mechanism ref workflow job execution; do
continue
fi
if [[ "${blocking}" == "true" ]] \
&& ! gate_is_enforced_by_a_release_gate "${workflow}" "${job}"; then
failures+=("gate '${id}' is release_blocking: true but no release gate requires job '${job}' in '${workflow}'")
continue
fi
case "${mechanism}" in
gradle-custom-task)
if [[ ! "${ref}" =~ ^[A-Za-z_][A-Za-z0-9_-]*$ ]]; then
@@ -432,8 +552,8 @@ while IFS=$'\t' read -r id blocking mechanism ref workflow job execution; do
verified=$((verified + 1))
done <<< "${records}"
if (( total != EXPECTED_GATE_COUNT )); then
failures+=("matrix has ${total} gates; expected ${EXPECTED_GATE_COUNT}")
if (( total == 0 )); then
failures+=("matrix declares no gates")
fi
printf 'gate-matrix-lint: %d gates, %d verified, %d delegated-pending\n' \
+98 -55
View File
@@ -7,43 +7,50 @@ readonly EXPECTED_WRAPPER_JAR_SHA256='76805e32c009c0cf0dd5d206bddc9fb22ea42e84db
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):
# Lock update procedure (only after intentional review of the complete .github diff):
# find .github/workflows -mindepth 1 -maxdepth 1 \
# \( -name '*.yml' -o -name '*.yaml' \) ! -type f -print # must print nothing
# \( -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.
# | LC_ALL=C sort -z | xargs -0 sha256sum # EXPECTED_WORKFLOW_LOCK
# find .github/actions -mindepth 2 -maxdepth 2 \
# \( -name 'action.yml' -o -name 'action.yaml' \) ! -type f -print # must print nothing
# find .github/actions -mindepth 2 -maxdepth 2 -type f \
# \( -name 'action.yml' -o -name 'action.yaml' \) -print0 \
# | LC_ALL=C sort -z | xargs -0 sha256sum # EXPECTED_COMPOSITE_ACTION_LOCK
# Replace an entire sorted array in the same reviewed change. Never refresh a single digest merely
# to make this verifier pass.
#
# Composite actions are locked alongside the workflows, and for the same reason. A job's Java
# toolchain and Gradle cache configuration used to be written out in every workflow that needed it,
# so the pinned actions/setup-java commit sat inside the locked bytes fifty-nine times over.
# .github/actions/setup-gradle-java/action.yml now holds the single copy: leaving it out of this
# lock would mean one unreviewed edit could change what every Gradle job in the repository installs
# and runs, while this verifier still said PASS. The two arrays are compared separately so that a
# drifting action does not shift every workflow's expected position and bury the real message.
readonly EXPECTED_WORKFLOW_LOCK=(
'eb85872c4b2f59b3d80b4d558a200f47c2e1fdde97c1a1781dfc26fa3771b8a8 .github/workflows/ci-quality-gates.yml'
'59de260a70c2c0a0d686d97035a189dc0567395977dfa18758f1a2d89d15a00d .github/workflows/dependency-vulnerability.yml'
'1b3220c922f954500f727c6a799b24e4962915845b9248e8e496e5050e829f28 .github/workflows/fileserver-nightly.yml'
'1c6f72c3914e3fb99e6dd93274ded1c784014ed6ff6cea2df8575e73ff46345a .github/workflows/fileserver-pr.yml'
'86a240c4ce7d0d293616e30de30ed77bcfdc700fedb8916f083eda9567099096 .github/workflows/fileserver-release.yml'
'06c762637998ef394da6cbec907e318e024f8119ccdec7b286f142e677e6d748 .github/workflows/httpclient-contract.yml'
'823bc346e58a58b2c0814cd1e3e55ec90d360c138419ec3d8f05deb59c62c7eb .github/workflows/httpclient-nightly.yml'
'ad84000efc438ee7439517b8f85819e62b13dab0aa4f94066c2905060f3bb581 .github/workflows/httpclient-release.yml'
'13a284e11d7ea66b70707b038a7c28495cbf88e66454a88ec2caf8e8f95550ab .github/workflows/jpa-next-hibernate8.yml'
'cf45357dc27c1d6e7fa4462c76f716bc44352fd8d59cc6829708a2c345f8ffda .github/workflows/jpa-next-jpa4.yml'
'b0efe44efd94b3a10f86e3a63278d8946d0fc65d5f011bb3759bd74c31d5f6e7 .github/workflows/jpa-next-postgresql19.yml'
'21e065880ef5d4c4ff973f52d8107ef08398ebaf9518ec6b2fd82d49c5d822c6 .github/workflows/jpa-nightly.yml'
'0a1e8e71fa8940c1517a3410e79c7bdf7d52134686a09fbf25bdcf14ab77f9f0 .github/workflows/jpa-pr.yml'
'59cb3a0ffc687a15eefe96bc5e3a70d42be78e1cc85d2e7f7880dac6124ca4c7 .github/workflows/jpa-r2-evidence.yml'
'fb9bd883106004ad1bb2471ce5ce885ca68cc9db9bf5e6ee1f6ac3c0ccc54087 .github/workflows/jpa-release.yml'
'444bb0da12f631fa20f492d3dc37e93b762d144640e4f86b81b7bdd3d4c81312 .github/workflows/ci-quality-gates.yml'
'e7f355c7eb81a72e0f1d2892843621bf11384ca2a4bf36f1daf3900b82ae46e7 .github/workflows/dependency-vulnerability.yml'
'2fa9c8081df1679c1feb9aa101aff47d7d2c24995c155aff6d1e4799eaad8f21 .github/workflows/fileserver-nightly.yml'
'1686b7b637611c8cd5eb87b2cc759f5cd2c6b878154363fc336c16b93c635ada .github/workflows/fileserver-pr.yml'
'b47932200c9ac9db57070b43bc70c40c89c152e9235d7a1325baab407df215e9 .github/workflows/fileserver-release.yml'
'3dc5a3e989043725133a1bbc90636c261406fafaf0158672323ae29dda95c5dd .github/workflows/integration-main.yml'
'4345d5cfb5a139a11cf3647c58fff61ab08397ace186919cdc7a769cdfc4d4b7 .github/workflows/jpa-next-hibernate8.yml'
'726b3d91603a2529205d1d5568253b57d85fcbb9d10d3efe182491c9da744d78 .github/workflows/jpa-next-jpa4.yml'
'3c073a928dfb266051a1a52f4d66bf6d6903b9dbd2cdb6459fab661228f27e88 .github/workflows/jpa-next-postgresql19.yml'
'c098946cfa7ba9c2959a6f8217f20af1ced28a45f22d088bc7ee4df661d45e84 .github/workflows/jpa-nightly.yml'
'b73314359be3391f8b569bb2ea0a5757927c4bbbd42d84c242e0e15e494320cd .github/workflows/jpa-r2-evidence.yml'
'43c565aa2709bc4d72cfcedf56816c6442bb63a23cc1db011e425ae0181d0bcd .github/workflows/jpa-release.yml'
'5be7e931db749029d89787da042d6d7cf8e683d60698bd8a2993c29db26355fb .github/workflows/link-check.yml'
'8adafc59a2d87a6c65ef94b4726d7d036ac81b150ed3d301578308e6f9a3523f .github/workflows/messaging-certification.yml'
'34a918d48426d11a0598ef3ba36ba5e6b88150705a3baafbe8b3f36d0b0e25b7 .github/workflows/notification-platform.yml'
'9f00644e2d6835981c9041a8c37bf144dffde50d0ca78b781dc7558ab0e85c26 .github/workflows/object-storage-qualification.yml'
'd8c099119df05308ffc1343569124e80c6abfad6823581ba9d9b6e187884692b .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'
'21e35b5cfdd7878b74a2dd8efdac20732dfba261529da8d066ec627251aa73f7 .github/workflows/web-release.yml'
'f37b2b2598687679a3fb0ae9ea2b50cd5d84a64de7e5852f38a3e5b93bf76e4d .github/workflows/websocket-advanced-nightly.yml'
'5643fe9c9d27d9e6f5ac30a731e77a962b68bed2961566e2e64cdb3991ef2350 .github/workflows/websocket-pr.yml'
'32232df17da7ae3d2257eda3953eaeb1a774752ecbecb6fb15b921702cf7c434 .github/workflows/websocket-release.yml'
'62a852157481e89c778c0498067a7443bde22bf421995ade8714a89e4eca347c .github/workflows/messaging-certification.yml'
'ee9f247297559077c7766f7f0f8b5e39538b496922f6b2cc2621aa04593f320a .github/workflows/notification-platform.yml'
'e685bc846108503ee2cf1e06b6cec040174d49348bd205400f891828f24dda68 .github/workflows/object-storage-qualification.yml'
'67ef53adb80551629a482e2610a0753dd0fadf85f523e985c4693354df543748 .github/workflows/pr-adapters.yml'
'376a71f7a2b9990e1e96937ad3dd46a33f266cc742ca499b208bc909897b67f3 .github/workflows/redis-sdk-topology.yml'
'3f1ff34053bb455587ab9f03305331310b4df4aa4a49bddf875b6969c9afaa05 .github/workflows/release.yml'
)
readonly EXPECTED_COMPOSITE_ACTION_LOCK=(
'7ec6591f26a1bd76658c55472e16b195b80db2c4792b429efda5a0dcbde61a45 .github/actions/setup-gradle-java/action.yml'
)
readonly EXPECTED_WRAPPER_PROPERTIES=(
'distributionBase=GRADLE_USER_HOME'
@@ -71,6 +78,10 @@ readonly REPOSITORY_ROOT=$1
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"
# Not asserted to exist here, deliberately. The structural and wrapper-validation diagnostics below
# are what a reader needs first; a missing composite action surfaces as a lock mismatch at the end,
# which is still fail-closed.
readonly ACTIONS_DIRECTORY="${REPOSITORY_ROOT}/.github/actions"
[[ -f "${WRAPPER_PROPERTIES}" ]] || fail "missing wrapper properties: ${WRAPPER_PROPERTIES}"
[[ -f "${WRAPPER_JAR}" ]] || fail "missing wrapper JAR: ${WRAPPER_JAR}"
@@ -85,34 +96,66 @@ readonly actual_wrapper_jar_sha256=$(sha256sum "${WRAPPER_JAR}" | awk '{print $1
|| 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}')
# One digest line per locked file, in the same LC_ALL=C order the update procedure prints. A symlink
# or a non-regular file is reported as such rather than followed: a workflow replaced by a link to
# another workflow is exactly the substitution this lock exists to catch.
collect_actual_lock() {
local locked_file locked_file_relative locked_file_sha256
while IFS= read -r -d '' locked_file; do
locked_file_relative=${locked_file#"${REPOSITORY_ROOT}"/}
if [[ -L "${locked_file}" || ! -f "${locked_file}" ]]; then
locked_file_sha256='<invalid-file-type>'
else
locked_file_sha256=$(sha256sum -- "${locked_file}" | awk '{print $1}')
fi
printf '%s %s\n' "${locked_file_sha256}" "${locked_file_relative}"
done
}
# Compared position by position rather than as a set, so an added, removed, renamed or reordered
# entry is a mismatch and the message names both sides.
compare_lock() {
local label=$1
shift
local -a expected=("$@")
local entry_count=${#expected[@]}
if ((${#actual_lock[@]} > entry_count)); then
entry_count=${#actual_lock[@]}
fi
actual_workflow_lock+=("${locked_workflow_sha256} ${locked_workflow_relative}")
done < <(
local index expected_entry actual_entry
for ((index = 0; index < entry_count; index++)); do
expected_entry=${expected[index]-<missing>}
actual_entry=${actual_lock[index]-<missing>}
if [[ "${actual_entry}" != "${expected_entry}" ]]; then
printf 'gradle-wrapper-contract: %s lock mismatch: expected %q; actual %q\n' \
"${label}" "${expected_entry}" "${actual_entry}" >&2
workflow_lock_valid=0
fi
done
}
mapfile -t actual_lock < <(
find "${WORKFLOWS_DIRECTORY}" -mindepth 1 -maxdepth 1 \
\( -name '*.yml' -o -name '*.yaml' \) -print0 \
| LC_ALL=C sort -z
| LC_ALL=C sort -z \
| collect_actual_lock
)
compare_lock 'workflow' ${EXPECTED_WORKFLOW_LOCK[@]+"${EXPECTED_WORKFLOW_LOCK[@]}"}
workflow_lock_entry_count=${#EXPECTED_WORKFLOW_LOCK[@]}
if ((${#actual_workflow_lock[@]} > workflow_lock_entry_count)); then
workflow_lock_entry_count=${#actual_workflow_lock[@]}
# A missing .github/actions directory yields an empty list, which mismatches every expected entry.
# That is the fail-closed answer: a composite action every Gradle job uses cannot be absent.
actual_lock=()
if [[ -d "${ACTIONS_DIRECTORY}" ]]; then
mapfile -t actual_lock < <(
find "${ACTIONS_DIRECTORY}" -mindepth 2 -maxdepth 2 \
\( -name 'action.yml' -o -name 'action.yaml' \) -print0 \
| LC_ALL=C sort -z \
| collect_actual_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
compare_lock 'composite action' \
${EXPECTED_COMPOSITE_ACTION_LOCK[@]+"${EXPECTED_COMPOSITE_ACTION_LOCK[@]}"}
workflow_count=0
gradle_job_count=0
@@ -751,6 +794,6 @@ done < <(find "${WORKFLOWS_DIRECTORY}" -type f \( -name '*.yml' -o -name '*.yaml
((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'
|| fail 'workflow lock mismatch: the workflow or composite-action set or bytes differ from the reviewed embedded manifest'
printf 'gradle-wrapper-contract: PASS\n'
+10 -49
View File
@@ -36,15 +36,7 @@ jobs:
echo "::error::${snapshot} exists locally but is not committed."
exit 1
fi
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Check quality, public paths, and dependency locks
working-directory: src
run: ./gradlew check verifyPublicPathSnapshot verifyDependencyLocks --warning-mode=fail --no-daemon --stacktrace
@@ -74,15 +66,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Verify the application without the sample fixture
working-directory: src
run: ./gradlew :app-bootstrap:sampleOffTest verifyCleanArchitectureDependencies --no-daemon --stacktrace
@@ -101,15 +85,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
# 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.
@@ -133,15 +109,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Produce zero-skip JPA candidate manifests
working-directory: src
run: >-
@@ -166,15 +134,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Run quarantined tests as an advisory signal
working-directory: src
run: ./gradlew quarantineTest --no-daemon
@@ -226,10 +186,11 @@ jobs:
# That is what makes it requirable by result rather than by scheduling luck. Only `success`
# passes: a skipped or cancelled security scan is not a scan.
#
# The other release_blocking gates outside this file (object-storage-qualification.yml,
# httpclient-release.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 left as a
# stated gap rather than a silently different one.
# The other release_blocking gates outside this file run on triggers this job does not share
# and so cannot be required here without changing when they run: release.yml, jpa-release.yml
# and fileserver-release.yml answer a release tag, and object-storage-qualification.yml and
# messaging-certification.yml answer a path filter or a schedule. That is left as a stated gap
# rather than a silently different one.
- name: Require the cross-workflow release-blocking checks to have succeeded
env:
GH_TOKEN: ${{ github.token }}
+5 -10
View File
@@ -38,15 +38,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Submit the resolved Gradle dependency graph
uses: gradle/actions/dependency-submission@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # gradle/actions@v4.4.4
with:
@@ -179,7 +171,10 @@ jobs:
trivy-kev.json | sort -u > found-cves.txt
jq -r '.vulnerabilities[]?.cveID | select(type == "string")' \
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
echo "::error::CISA KEV-listed vulnerability found regardless of CVSS:"
printf '%s\n' "${hits}"
+4 -36
View File
@@ -27,15 +27,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Start the NFSv4 certification environment
run: docker compose -f infra/fileserver/nfs/compose.yml up -d --wait
- name: Run the network-filesystem ambiguity suite
@@ -57,15 +49,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Run the crash matrix and reconciliation suites
working-directory: src
run: >-
@@ -84,15 +68,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Run the large-file and slow-client suites under a constrained heap
working-directory: src
env:
@@ -113,15 +89,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Prove no run commits bytes from a stale lease
working-directory: src
run: >-
+8 -45
View File
@@ -27,6 +27,9 @@ on:
- 'infra/fileserver/kubernetes/**'
- 'infra/fileserver/nfs/**'
- '.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:
contents: read
@@ -44,15 +47,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Run the fileserver application and architecture suites
working-directory: src
run: >-
@@ -70,15 +65,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Certify the local content store against the shared contract
working-directory: src
run: >-
@@ -95,15 +82,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Run the servlet and reactive transport contracts
working-directory: src
run: >-
@@ -120,15 +99,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Run the path, filename, range, and problem-detail hardening suite
working-directory: src
run: >-
@@ -146,15 +117,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Prove transfer cost does not scale with file size
working-directory: src
run: >-
+36 -51
View File
@@ -2,8 +2,25 @@ 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.
#
# 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:
@@ -22,15 +39,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Run the architecture-wide dependency and module verification
working-directory: src
run: >-
@@ -56,15 +65,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Prove every support claim maps to a job and every endpoint is documented
working-directory: src
run: >-
@@ -81,10 +82,22 @@ jobs:
- 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.
# 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
# fileserver-pvc-cluster-certification (delegated-pending) in .github/ci-gate-matrix.yml, so
# the absence is a tracked control rather than a green check.
- name: Check the certification manifest still says what the claim depends on
run: |
set -euo pipefail
@@ -96,26 +109,6 @@ jobs:
# 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
@@ -125,15 +118,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Prove telemetry carries no filename, path, or raw identifier
working-directory: src
run: >-
-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 'dev.caskeleton.bootstrap.architecture.*'
--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
+156
View File
@@ -0,0 +1,156 @@
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 documentation-drift gates. They used to be `dependsOn` of the root `check`, so a README
# sentence about a renamed task failed a compile-and-test run and the fix was to edit a document
# before unrelated code could build. src/build.gradle now aggregates them as
# `verifyDocumentationContracts` and leaves them out of `check`. That demotion is only half a
# change: a gate nothing invokes has not been demoted, it has been deleted. This job is the other
# half, and it is the reason the four gates still run at all.
#
# 2. 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.
#
# 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
jobs:
# verifyReadmeCommands, verifyDocumentedLeafCount, verifyRunbookReferences and
# verifyTestSourceSetRegistry, as one task. Named as the aggregate rather than as four steps so
# that adding a fifth documentation gate is a build-file edit and not a workflow edit — and so
# that the demotion out of `check` has exactly one consumer to point at.
documentation-contracts:
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: ./.github/actions/setup-gradle-java
- name: Verify the documentation contracts
working-directory: src
run: ./gradlew verifyDocumentationContracts --no-daemon --stacktrace
# 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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: ./.github/actions/setup-gradle-java
- name: Run the 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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- 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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- 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
# in .github/ci-gate-matrix.yml as release_blocking: false 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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- 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
+1 -9
View File
@@ -24,15 +24,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Report Hibernate ORM 8 compatibility
id: compatibility-probe
working-directory: src
+1 -9
View File
@@ -24,15 +24,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Report Jakarta Persistence 4.0 compatibility
id: compatibility-probe
working-directory: src
+1 -9
View File
@@ -30,15 +30,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Report PostgreSQL 19 compatibility
id: compatibility-probe
working-directory: src
+4 -36
View File
@@ -33,15 +33,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Certify the platform against PostgreSQL ${{ matrix.postgresql }}
working-directory: src
run: >-
@@ -59,15 +51,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Reproduce deadlock, serialization, and commit-ambiguity scenarios
working-directory: src
run: >-
@@ -84,15 +68,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Run the query plan and database security suites
working-directory: src
run: >-
@@ -110,15 +86,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Verify pool saturation and REQUIRES_NEW connection behaviour
working-directory: src
# 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 'dev.caskeleton.bootstrap.architecture.*'
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 -9
View File
@@ -29,15 +29,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Verify the production-profile JPA R2 manifest DAG
working-directory: src
run: >-
+3 -19
View File
@@ -46,15 +46,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Run the full JPA release gate on PostgreSQL ${{ matrix.postgresql }}
working-directory: src
run: >-
@@ -129,21 +121,13 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Verify architecture boundaries and the support matrix
working-directory: src
run: >-
./gradlew
verifyCleanArchitectureDependencies
verifyOneTypePerFile
checkstyleMain
:app-bootstrap:test --tests 'dev.caskeleton.bootstrap.architecture.*'
:adapter:outbound:persistence-jpa:test --tests '*JpaReleaseManifestTest'
--no-daemon
@@ -17,6 +17,9 @@ on:
paths:
- "src/messaging/**"
- ".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:
- cron: "41 4 * * 3"
workflow_dispatch:
@@ -36,15 +39,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Certify the Kafka adapter against a real broker
working-directory: src
# GITHUB_SHA is read by the lane and written into every evidence line, because "certified"
+16 -65
View File
@@ -31,6 +31,9 @@ on:
- 'docs/notification/**'
- 'infra/notification/**'
- '.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:
branches: [ main ]
schedule:
@@ -55,15 +58,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Compile and format check
working-directory: src
run: ./gradlew :application-core:compileJava :adapter:outbound:notification:compileJava --console=plain
@@ -116,15 +111,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
# 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 —
# so the job name was the only place those three properties existed.
@@ -153,50 +140,14 @@ jobs:
fi
done
provider-sandbox:
name: provider sandbox smoke (secret-protected, non-blocking)
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 30
environment: notification-provider-sandbox
# 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
# two echo statements, which is what let five channels be graded Stable on nothing.
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: 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
# There is no provider-sandbox job. It ran only on workflow_dispatch and could not succeed by
# any path: with no credentials its first step exit 1-ed, and with credentials the only test it
# ran was ProviderSandboxSmokeTest, whose body is an unconditional fail() saying a real sandbox
# call is not implemented. Its credential check read secrets.NOTIFICATION_SANDBOX_CREDENTIALS,
# which nothing in this repository consumes — the test reads NOTIFICATION_SANDBOX_ENABLED — so
# any non-empty string satisfied it and was then dropped.
#
# The unimplemented state is still stated in two places that do not depend on a workflow:
# ProviderSandboxSmokeTest itself, and the unsatisfied provider-wire-qualified claim in
# docs/notification/evidence-manifest.json, which verifyNotificationEvidence enforces inside
# check. When a real sandbox call is implemented, the job comes back with it.
@@ -37,15 +37,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Run non-skipping Poster image migration qualification
working-directory: src
run: ./gradlew :sample-portfolio:posterImageMigrationTest --no-daemon --stacktrace
@@ -57,15 +49,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Run exact-release MinIO managed contract
working-directory: src
run: ./gradlew :adapter:outbound:objectstorage:objectStorageMinioContractTest --no-daemon --stacktrace
@@ -78,15 +62,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Run digest-pinned MinIO and Toxiproxy fault contract
working-directory: src
run: ./gradlew :adapter:outbound:objectstorage:objectStorageMinioFaultTest --no-daemon --stacktrace
@@ -109,15 +85,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Run protected AWS common-subset qualification
working-directory: src
run: ./gradlew :adapter:outbound:objectstorage:objectStorageAwsQualificationTest --no-daemon --stacktrace
+313
View File
@@ -0,0 +1,313 @@
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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- 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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: ./.github/actions/setup-gradle-java
- name: Run the 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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: ./.github/actions/setup-gradle-java
- name: Run the 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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: ./.github/actions/setup-gradle-java
- name: Run the 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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- 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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: ./.github/actions/setup-gradle-java
- name: Run the 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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- 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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: ./.github/actions/setup-gradle-java
- name: Run the migration upgrade smoke scenarios
working-directory: src
run: >-
./gradlew
:adapter:outbound:persistence-jpa:jpaPlatformMigrationTest
--no-daemon
--stacktrace
+4 -9
View File
@@ -34,6 +34,9 @@ on:
- "src/adapter/outbound/cache-redis/**"
- "infra/redis-sdk/**"
- ".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:
# 02:30 UTC daily. Nightly, not hourly: the matrix starts real servers.
- cron: "30 2 * * *"
@@ -116,15 +119,7 @@ jobs:
- 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
- uses: ./.github/actions/setup-gradle-java
- name: Start the topology
env:
REDIS_VERSION: ${{ matrix.redis_version }}
+445
View File
@@ -0,0 +1,445 @@
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-release.yml — FileserverDocumentationCoverageTest requires every job id named in
# docs/fileserver/support-matrix.md to be defined in a `.github/workflows/fileserver-*.yml`.
# 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, which is what the
# `container-release-evidence-join` row in .github/ci-gate-matrix.yml records.
#
# 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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: ./.github/actions/setup-gradle-java
- name: Verify architecture boundaries and the published surfaces
working-directory: src
run: >-
./gradlew
verifyCleanArchitectureDependencies
verifyPublicPathSnapshot
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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: ./.github/actions/setup-gradle-java
- name: Run 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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: ./.github/actions/setup-gradle-java
- name: Run 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
# 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. 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
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- 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
- 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.
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
# 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 and verifyTrivyignore enforces that each entry carries a rationale and an expiry.
# 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
-68
View File
@@ -1,68 +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:
# 'v*' is this repository's release tag, and jpa-release, httpclient-release and
# web-advanced-release already fire on it. While this workflow answered only to
# 'web-v*', tagging 'v1.2.3' ran the Advanced gate and skipped this Stable one, so a
# release could choose which gate it cleared. Both patterns are listed: the namespaced
# tag keeps working for a component-only release, and the repository tag can no longer
# bypass the gate.
- 'v*'
- '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 'dev.caskeleton.bootstrap.architecture.*'
--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
-63
View File
@@ -1,63 +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:
# 'v*' is this repository's release tag, and jpa-release, httpclient-release and
# web-advanced-release already fire on it. While this workflow answered only to
# 'websocket-v*', tagging 'v1.2.3' ran the Advanced gate and skipped this Stable one, so a
# release could choose which gate it cleared. Both patterns are listed: the namespaced
# tag keeps working for a component-only release, and the repository tag can no longer
# bypass the gate.
- 'v*'
- '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 'dev.caskeleton.bootstrap.architecture.*'
--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