refactor(build,ci): CI를 단계별로 재편하고 감사 잔여 114건 정리
빌드·CI 레이어 전수 감사(133건) 중 Track A 에서 처리한 E등급 19건을 뺀
나머지를 마무리한다. 한 커밋인 이유는 서로 얽혀 있기 때문이다 — 워크플로가
`checkstyleMain` 을 부르려면 루트가 먼저 Checkstyle 을 붙여야 하고, 모듈 43개가
convention 을 적용하려면 build-logic 이 먼저 그 플러그인을 등록해야 한다.
영역별로 쪼개면 중간 커밋이 빌드되지 않는다.
## CI 단계 분리 (워크플로 29파일 3,360줄 → 19파일 2,692줄, 잡 69 → 64)
모듈이 아니라 단계로 자른다. 기존 28개는 `web-*` `jpa-*` 처럼 모듈로 갈려
있어서 같은 일이 파일마다 중복됐다.
- stage 1 `ci-quality-gates.yml` + `pr-adapters.yml`(신규, 잡 단위 경로 필터) — pull_request
- stage 2 `integration-main.yml`(신규) — push:main + 03:00. 문서 게이트는 여기에 둔다
- stage 3 `release.yml`(신규, 릴리스 워크플로 7개 중 5개 흡수) — push: tags v*
setup 블록 59회 복붙 → `.github/actions/setup-gradle-java` 1개(잡당 13줄 → 5줄).
잡 8개 삭제, 각각 대체 잡을 확인했다. `verifyCleanArchitectureDependencies` 실행
횟수가 태그당 9 → 6, PR당 8 → 4 로 줄었다.
## 컨테이너 릴리스 신설
이미지를 만드는 것이 아무것도 없었다. Dockerfile 은 있었지만
build-push-action / bootBuildImage / jib 사용처가 0건이고, `*-release.yml` 8개는
테스트 후 아티팩트만 올렸다 — 이름만 릴리스였다.
Boot 레이어드 추출 + thin-JAR 엔트리포인트로 Dockerfile 을 고치고 릴리스
워크플로에 이미지 빌드·푸시·SBOM·스캔을 넣었다. 로컬 빌드로 검증했다:
레지스트리 content 241MB, 기동 3.7초, uid 1000, 헬스체크 200.
코드만 바뀐 릴리스는 7.68MB 만 재푸시한다(이전이라면 156MB).
CI 는 배포하지 않는다 — 매니페스트와 ArgoCD 는 별도 repo 로 간다.
## 게이트 정리
- gate-matrix 의 개수 고정 해제: `EXPECTED_GATE_COUNT=49` 와 하드코딩된 49개 id
목록을 지우고 불변식으로 대체(필드·enum, 워크플로/잡 실재, id 중복,
`release_blocking: true` 는 실제로 release-gate 의 needs 여야 함).
행을 추가하려면 테스트부터 고쳐야 하던 구조를 풀었다. 커버리지 8/28 → 28/28
- 문서 게이트 4개를 `check` 에서 떼어 `verifyDocumentationContracts` 로 묶고
stage 2 에 배치. 어겨도 런타임은 멀쩡하므로 개발을 막지 않는다
- `verifyOneTypePerFile`(정규식 Java 파싱, 126파일 미탐) → Checkstyle
`OneTopLevelClass` + `OuterTypeFilename`. main 위반 0건, test 의 fixture 29건은
정책을 넓히지 않고 suppressions 에 사유와 함께 명시 제외
- leaf 하나의 `check` 가 끌고 오던 저장소 전역 게이트 18개를 재배치.
`:domain-core:check` 가 13 태스크 11초로 끝난다
- convention 플러그인 2개 신설(`ca.platform-module`, `ca.grpc-platform-module`),
플랫폼 모듈 43개에 적용. 손수 짠 Test 태스크 17개를 `strictTestLanes` 로 전환
(태스크 이름 전부 보존 — CI 가 이름으로 부른다)
- `ca.api-surface` 의 정규식 Java 파서를 javac parse-only 로 교체
(기존 베이스라인 3개와 바이트 동일 확인)
- 죽은 태스크 5개 삭제, `src/gradle` 1,713 → 1,440줄, 모듈 build.gradle
3,072 → 2,977줄
## 검사가 검사를 못 하고 있던 것들
- 11개 계약 테스트가 gitignore 된 `src/.env` 를 요구했다. `.gitignore` 자신이
"examples beside it are the tracked contract, never a real one" 이라고 적어둔
규칙과 어긋난다. 깨끗한 체크아웃에는 그 파일이 없으므로 CI 에서 돌 수 없었다.
추적되는 `.env.example` 로 돌린다
- **`.env.local.example` 이 5432 를 가리키는데 compose 는 5433 을 게시한다.**
이 파일을 복사해 시작하는 신규 개발자는 DB 연결에 실패한다. 이걸 잡으라고
만든 테스트가 추적 안 되는 파일을 읽어서, 이미 설정이 끝난 머신에서만 돌고
정작 처음 받는 사람에겐 아무 검사도 안 하고 있었다. 포트를 고치고 테스트를
추적 파일로 돌렸다
- `MongoModuleBoundaryTest` 의 `DO_NOT_INCLUDE_JARS` 때문에 임포트가 0개가 되어
규칙 10개가 "failed to check any classes" 로 실패 중이었다. 이 레인에서는
모듈 자기 클래스가 jar 로 올라온다. `importPackages(ROOT)` 가 이미 서드파티를
거르므로 옵션은 불필요했다
- `ReleaseManifestTaskExistenceTest` 가 build 파일 텍스트에서 `tasks.register(`
만 찾아, convention 의 `lane('...')` 로 바뀐 태스크를 미등록으로 오판했다
- `ProfileSeparationContractTest` 는 런처가 주입하는 `src/.env` 가 맞는 대상이라
그대로 두되, 파일이 없으면 명시적으로 skip 한다 — "안 돌았다" 가 "통과했다"
로 읽히지 않게
## 검증 (전부 깨끗한 체크아웃에서, 커밋 전에 실행)
`verify-gradle-wrapper.sh` PASS · `verify-gate-matrix.sh` OK(drift 0) ·
워크플로 YAML 전수 파싱 OK · actionlint 지적 0 · `gradlew help` ·
`verifyCleanArchitectureDependencies` · `build-logic test` ·
`:app-bootstrap:test` **1001 tests 실패 0 스킵 5** · `:domain-core:check` ·
`verifyDocumentationContracts`.
## 남은 문제
- 첫 `v*` 태그는 이미지 취약점 스캔에서 실패한다(CRITICAL/HIGH 9건:
ubuntu 베이스 2, tomcat-embed-core 3, amqp-client 3, httpcore5 2).
억제를 넣지 않았다 — 릴리스 1회차를 초록으로 만들려고 임계값을 내리면
게이트가 장식이 된다. 의존성·베이스 갱신이 선행돼야 한다
- `fileserver-v*` / `web-v*` / `websocket-v*` 태그는 이제 아무 run 도 만들지
않는다(배포 단위가 하나라는 결정에 따른 것)
- main push 마다 무거운 레인 3개가 새로 돈다 — 러너 분이 늘어난다
- `ProfileSeparationContractTest` 가 찾아낸 4개 값(cache command-timeout,
cache positive-soft-ttl, idempotency provider, rate-limit command-timeout)이
`.env.example` 과 인라인 기본값 사이에서 갈린다. 런타임 설정 판단이라
건드리지 않았다
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1535481794
commit
9bc2e75fe5
@@ -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
@@ -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
|
||||
|
||||
@@ -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' \
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user