refactor: 각 어댑터터별 리펙토링 진행
This commit is contained in:
@@ -45,6 +45,96 @@ gates:
|
|||||||
workflow: ci-quality-gates.yml
|
workflow: ci-quality-gates.yml
|
||||||
job: quality-gates
|
job: quality-gates
|
||||||
execution: check
|
execution: check
|
||||||
|
- id: documented-leaf-count
|
||||||
|
release_blocking: true
|
||||||
|
mechanism: gradle-custom-task
|
||||||
|
ref: verifyDocumentedLeafCount
|
||||||
|
workflow: ci-quality-gates.yml
|
||||||
|
job: quality-gates
|
||||||
|
execution: check
|
||||||
|
- id: declared-dependency-absence
|
||||||
|
release_blocking: true
|
||||||
|
mechanism: gradle-custom-task
|
||||||
|
ref: verifyDependencyPolicy
|
||||||
|
workflow: ci-quality-gates.yml
|
||||||
|
job: quality-gates
|
||||||
|
execution: check
|
||||||
|
- id: notification-api-surface
|
||||||
|
release_blocking: true
|
||||||
|
mechanism: gradle-custom-task
|
||||||
|
ref: verifyNotificationApiSurface
|
||||||
|
workflow: ci-quality-gates.yml
|
||||||
|
job: quality-gates
|
||||||
|
execution: check
|
||||||
|
- id: notification-configuration-contract
|
||||||
|
release_blocking: true
|
||||||
|
mechanism: gradle-custom-task
|
||||||
|
ref: verifyNotificationConfiguration
|
||||||
|
workflow: ci-quality-gates.yml
|
||||||
|
job: quality-gates
|
||||||
|
execution: check
|
||||||
|
- id: notification-support-grade-evidence
|
||||||
|
release_blocking: true
|
||||||
|
mechanism: gradle-custom-task
|
||||||
|
ref: verifyNotificationEvidence
|
||||||
|
workflow: ci-quality-gates.yml
|
||||||
|
job: quality-gates
|
||||||
|
execution: check
|
||||||
|
- id: runbook-reference-drift
|
||||||
|
release_blocking: true
|
||||||
|
mechanism: gradle-custom-task
|
||||||
|
ref: verifyRunbookReferences
|
||||||
|
workflow: ci-quality-gates.yml
|
||||||
|
job: quality-gates
|
||||||
|
execution: check
|
||||||
|
- id: graphql-api-surface
|
||||||
|
release_blocking: true
|
||||||
|
mechanism: gradle-custom-task
|
||||||
|
ref: verifyGraphQlApiSurface
|
||||||
|
workflow: ci-quality-gates.yml
|
||||||
|
job: quality-gates
|
||||||
|
execution: check
|
||||||
|
- id: mongo-api-surface
|
||||||
|
release_blocking: true
|
||||||
|
mechanism: gradle-custom-task
|
||||||
|
ref: verifyMongoApiSurface
|
||||||
|
workflow: ci-quality-gates.yml
|
||||||
|
job: quality-gates
|
||||||
|
execution: check
|
||||||
|
# The strongest evidence this repository produces, and CI does not run it. Fifteen Compose lanes
|
||||||
|
# start real PostgreSQL, MongoDB, Kafka, MinIO, Mailpit and Keycloak, take a real client-credentials
|
||||||
|
# JWT, and prove things no in-JVM test can: that all-off boots with no external resource, that the
|
||||||
|
# notification handoff delivers exactly once across a restart on the same volume, that the startup
|
||||||
|
# log is silent. It runs from a developer's machine via scripts/run-compose-runtime-smoke.sh and
|
||||||
|
# from nowhere else — no workflow invokes it, so nothing re-runs it on a pull request.
|
||||||
|
#
|
||||||
|
# Registered delegated-pending so the gap is a tracked absence rather than an unstated one.
|
||||||
|
# Executing it in CI needs a Docker-capable runner and a decision about the minutes fifteen
|
||||||
|
# container lanes cost, which is an infrastructure choice rather than a wiring oversight.
|
||||||
|
- id: runtime-smoke-matrix
|
||||||
|
release_blocking: conditional
|
||||||
|
mechanism: delegated-pending
|
||||||
|
ref: runtime-smoke-matrix-lane
|
||||||
|
workflow: ci-quality-gates.yml
|
||||||
|
job: release-gate
|
||||||
|
execution: job
|
||||||
|
# `conditional-transport-qualification` above is the registered GraphQL control, and it is a
|
||||||
|
# boundary test: a @SpringBootTest over a nested test application with in-memory Basic Auth. Its own
|
||||||
|
# javadoc says so — "the nested application deliberately owns only test authentication and CORS
|
||||||
|
# policy". That is a legitimate transport-boundary proof and it is not release evidence for the
|
||||||
|
# security posture, which is the distinction the Definition of Done draws.
|
||||||
|
#
|
||||||
|
# The real proof exists: the local-graphql Compose lane obtains a Keycloak client-credentials token
|
||||||
|
# and posts it to /graphql on the running bootJar, asserting that anonymous and malformed
|
||||||
|
# credentials are refused and the authenticated query answers. It is part of the runtime smoke
|
||||||
|
# matrix above, so it inherits that control's pending status rather than having none of its own.
|
||||||
|
- id: graphql-runtime-jwt
|
||||||
|
release_blocking: conditional
|
||||||
|
mechanism: delegated-pending
|
||||||
|
ref: graphql-runtime-jwt-lane
|
||||||
|
workflow: ci-quality-gates.yml
|
||||||
|
job: release-gate
|
||||||
|
execution: job
|
||||||
- id: one-type-per-file
|
- id: one-type-per-file
|
||||||
release_blocking: true
|
release_blocking: true
|
||||||
mechanism: gradle-custom-task
|
mechanism: gradle-custom-task
|
||||||
@@ -287,3 +377,15 @@ gates:
|
|||||||
workflow: httpclient-release.yml
|
workflow: httpclient-release.yml
|
||||||
job: release-gate
|
job: release-gate
|
||||||
execution: explicit
|
execution: explicit
|
||||||
|
# The messaging platform's only claim that needs a real broker to be true. The gate is the
|
||||||
|
# evidence check rather than the lane, and it depends on the lane: passing means both that every
|
||||||
|
# fault scenario produced the outcome the shared contract fixes and that the committed manifest is
|
||||||
|
# what this run wrote. Before it existed, `CertifiedEvidence` was a hand-authored list and
|
||||||
|
# "certified against a live broker" was a sentence a developer could type.
|
||||||
|
- id: messaging-broker-certification
|
||||||
|
release_blocking: true
|
||||||
|
mechanism: gradle-custom-task
|
||||||
|
ref: verifyMessagingCertificationEvidence
|
||||||
|
workflow: messaging-certification.yml
|
||||||
|
job: broker-certification
|
||||||
|
execution: explicit
|
||||||
|
|||||||
@@ -27,8 +27,15 @@ 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
|
# 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,
|
# 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*
|
# 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.
|
# claim, distinct from the API-surface scan that was standing in for it. 40 after the Gradle
|
||||||
readonly EXPECTED_GATE_COUNT=38
|
# convention wave registered documented-leaf-count and declared-dependency-absence, then 46 after
|
||||||
|
# the final qualification wave registered the four notification/runbook gates that existed but ran
|
||||||
|
# nowhere and the two API-surface gates the convention had already wired into check. 48 once the
|
||||||
|
# Compose runtime matrix and the GraphQL runtime JWT claim were registered as delegated-pending —
|
||||||
|
# both are real and neither runs in CI. 49 once the messaging broker certification lane registered
|
||||||
|
# its evidence gate — the first control in this repository whose subject is not "did the tests pass"
|
||||||
|
# but "is the committed evidence what the run produced".
|
||||||
|
readonly EXPECTED_GATE_COUNT=49
|
||||||
|
|
||||||
if [[ ! -f "${MATRIX}" ]]; then
|
if [[ ! -f "${MATRIX}" ]]; then
|
||||||
printf '::error::gate-matrix-lint: missing %s\n' "${MATRIX}" >&2
|
printf '::error::gate-matrix-lint: missing %s\n' "${MATRIX}" >&2
|
||||||
@@ -135,6 +142,25 @@ gradle_custom_task_is_registered_in_build_file() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# A lane declared through the `ca.strict-test-lane` convention. The convention exists because the
|
||||||
|
# five lines every lane used to repeat were copied per lane and per leaf, and two copies had
|
||||||
|
# already lost `failOnNoDiscoveredTests`; registering through it is still registering, so this lint
|
||||||
|
# has to recognise the declaration or it reports every converted lane as missing.
|
||||||
|
if grep -qsE -- "lane\\(['\"]${task_name}['\"]\\)" "${build_file}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# An API surface gate declared through the `ca.api-surface` convention, which derives every task
|
||||||
|
# name from one label so a leaf cannot verify one surface while telling the reader about another.
|
||||||
|
# The name is computed, so there is no literal `tasks.register('verifyMongoApiSurface')` anywhere;
|
||||||
|
# what the build file says is `apiSurface { label = 'Mongo' }`.
|
||||||
|
if [[ "${task_name}" =~ ^verify(.+)ApiSurface$ ]]; then
|
||||||
|
local surface_label="${BASH_REMATCH[1]}"
|
||||||
|
if grep -qsE -- "label[[:space:]]*=[[:space:]]*['\"]${surface_label}['\"]" "${build_file}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
awk -v required_task="${task_name}" '
|
awk -v required_task="${task_name}" '
|
||||||
index($0, "registerStrictQualificationTest(") > 0 { inside_registration=1 }
|
index($0, "registerStrictQualificationTest(") > 0 { inside_registration=1 }
|
||||||
inside_registration && /^[[:space:]]*name:[[:space:]]*/ {
|
inside_registration && /^[[:space:]]*name:[[:space:]]*/ {
|
||||||
@@ -159,14 +185,64 @@ gradle_custom_task_is_registered_in_build_file() {
|
|||||||
' "${build_file}"
|
' "${build_file}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Every `dependsOn ... named('x')` in the build, collected once.
|
||||||
|
#
|
||||||
|
# This used to be one recursive grep per gate. That was affordable at 38 gates and stopped being so
|
||||||
|
# at 48: the whole lint crossed the ten-second budget its own contract test asserts, and the first
|
||||||
|
# symptom was that test failing rather than anything about gate coverage. One pass, then membership
|
||||||
|
# tests against the result.
|
||||||
|
CHECK_WIRING_CACHE=""
|
||||||
|
load_check_wiring() {
|
||||||
|
[[ -n "${CHECK_WIRING_CACHE}" ]] && return 0
|
||||||
|
CHECK_WIRING_CACHE="$(grep -RhoE -- "dependsOn[^\n]*named\((['\"])[A-Za-z0-9_.-]+\1\)" \
|
||||||
|
"${REPO_ROOT}/src" --include='build.gradle' --include='ca.*.gradle' 2>/dev/null \
|
||||||
|
| grep -oE "(['\"])[A-Za-z0-9_.-]+\1" | tr -d "\"'" | sort -u)"
|
||||||
|
# A build with no such wiring at all would leave this empty and make every membership test pass by
|
||||||
|
# vacuity, so an empty result is a marker rather than an answer.
|
||||||
|
[[ -z "${CHECK_WIRING_CACHE}" ]] && CHECK_WIRING_CACHE="<none>"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
gradle_custom_task_wired_into_check() {
|
||||||
|
local task_name="$1"
|
||||||
|
load_check_wiring
|
||||||
|
if printf '%s\n' "${CHECK_WIRING_CACHE}" | grep -qxF -- "${task_name}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# `ca.api-surface` wires check as `dependsOn tasks.named(verifyName())`, where verifyName() is
|
||||||
|
# derived from the leaf's label. The declaration that makes the gate real is the label, so that is
|
||||||
|
# what proves the wiring — the convention has exactly one check wiring and it is unconditional.
|
||||||
|
if [[ "${task_name}" =~ ^verify(.+)ApiSurface$ ]]; then
|
||||||
|
local surface_label="${BASH_REMATCH[1]}"
|
||||||
|
if grep -RqsE -- "label[[:space:]]*=[[:space:]]*['\"]${surface_label}['\"]" "${REPO_ROOT}/src" \
|
||||||
|
--include='build.gradle' \
|
||||||
|
&& grep -qsE -- "dependsOn tasks\.named\(verifyName\(\)\)" \
|
||||||
|
"${REPO_ROOT}/src/build-logic/src/main/groovy/ca.api-surface.gradle"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# The build files, found once rather than once per gate. Same reason as the wiring cache above: the
|
||||||
|
# per-gate `find` was a fixed cost multiplied by a number that grew.
|
||||||
|
GRADLE_FILE_CACHE=""
|
||||||
|
load_gradle_files() {
|
||||||
|
[[ -n "${GRADLE_FILE_CACHE}" ]] && return 0
|
||||||
|
GRADLE_FILE_CACHE="$(find "${REPO_ROOT}/src" -type f -name '*.gradle' | sort)"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
gradle_custom_task_is_registered() {
|
gradle_custom_task_is_registered() {
|
||||||
local task_name="$1"
|
local task_name="$1"
|
||||||
local build_file
|
local build_file
|
||||||
while IFS= read -r -d '' build_file; do
|
load_gradle_files
|
||||||
|
while IFS= read -r build_file; do
|
||||||
|
[[ -z "${build_file}" ]] && continue
|
||||||
if gradle_custom_task_is_registered_in_build_file "${task_name}" "${build_file}"; then
|
if gradle_custom_task_is_registered_in_build_file "${task_name}" "${build_file}"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
done < <(find "${REPO_ROOT}/src" -type f -name '*.gradle' -print0)
|
done <<< "${GRADLE_FILE_CACHE}"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,9 +401,16 @@ while IFS=$'\t' read -r id blocking mechanism ref workflow job execution; do
|
|||||||
failures+=("gate '${id}' expects Gradle check in job '${job}'")
|
failures+=("gate '${id}' expects Gradle check in job '${job}'")
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
# Build files *and* convention plugins. A gate can now be wired into check from an included
|
||||||
|
# build's convention rather than from a leaf's build.gradle, and a lint that only reads
|
||||||
|
# build.gradle would call such a gate unwired while it runs on every leaf — a false failure
|
||||||
|
# that teaches the next author to delete the matrix row instead of trusting it.
|
||||||
|
#
|
||||||
|
# A convention that derives the task name from a label wires check by that derived name, so
|
||||||
|
# there is no literal to grep for either; `gradle_custom_task_wired_into_check` handles both
|
||||||
|
# the literal and the derived form.
|
||||||
if [[ "${mechanism}" == "gradle-custom-task" ]] \
|
if [[ "${mechanism}" == "gradle-custom-task" ]] \
|
||||||
&& ! grep -RqsE -- "dependsOn.*named\\(['\"]${ref}['\"]\\)" "${REPO_ROOT}/src" \
|
&& ! gradle_custom_task_wired_into_check "${ref}"; then
|
||||||
--include='build.gradle'; then
|
|
||||||
failures+=("gate '${id}' task '${ref}' exists but is not wired into Gradle check")
|
failures+=("gate '${id}' task '${ref}' exists but is not wired into Gradle check")
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ readonly EXPECTED_WORKFLOW_LOCK=(
|
|||||||
'3be84c9f15fa3b2ac5a085f8d725ec6d05e7007ae0b433da9e79b3bf340d57ea .github/workflows/jpa-next-hibernate8.yml'
|
'3be84c9f15fa3b2ac5a085f8d725ec6d05e7007ae0b433da9e79b3bf340d57ea .github/workflows/jpa-next-hibernate8.yml'
|
||||||
'a2b74bfb3af12d6d03cd2ea8a5e48490dd131afb89b79694d498c5798387ac53 .github/workflows/jpa-next-jpa4.yml'
|
'a2b74bfb3af12d6d03cd2ea8a5e48490dd131afb89b79694d498c5798387ac53 .github/workflows/jpa-next-jpa4.yml'
|
||||||
'cd955ef4af895df477896dad9577810f010b2beea8570b09b008f9e94e928bd0 .github/workflows/jpa-next-postgresql19.yml'
|
'cd955ef4af895df477896dad9577810f010b2beea8570b09b008f9e94e928bd0 .github/workflows/jpa-next-postgresql19.yml'
|
||||||
'b56b548a867b74eaeccb42e7df4f4e52cf7ce657ab27f91e2c8d7ea9944d64af .github/workflows/jpa-nightly.yml'
|
'21e065880ef5d4c4ff973f52d8107ef08398ebaf9518ec6b2fd82d49c5d822c6 .github/workflows/jpa-nightly.yml'
|
||||||
'04851f44ba94533bfbc8fabe2b3a2b408726a9996e86ed3864986d1499d16b50 .github/workflows/jpa-pr.yml'
|
'04851f44ba94533bfbc8fabe2b3a2b408726a9996e86ed3864986d1499d16b50 .github/workflows/jpa-pr.yml'
|
||||||
'59cb3a0ffc687a15eefe96bc5e3a70d42be78e1cc85d2e7f7880dac6124ca4c7 .github/workflows/jpa-r2-evidence.yml'
|
'59cb3a0ffc687a15eefe96bc5e3a70d42be78e1cc85d2e7f7880dac6124ca4c7 .github/workflows/jpa-r2-evidence.yml'
|
||||||
'4748f2ba0a0b77dc1a858ebcfa7db6e41627d97843df5f0aa978bc2facccaad2 .github/workflows/jpa-release.yml'
|
'cf4f80134197dd6d7dc177f0d21294a6b9ffe8709be67d05089f7ff0ce6c9429 .github/workflows/jpa-release.yml'
|
||||||
'5be7e931db749029d89787da042d6d7cf8e683d60698bd8a2993c29db26355fb .github/workflows/link-check.yml'
|
'5be7e931db749029d89787da042d6d7cf8e683d60698bd8a2993c29db26355fb .github/workflows/link-check.yml'
|
||||||
|
'8adafc59a2d87a6c65ef94b4726d7d036ac81b150ed3d301578308e6f9a3523f .github/workflows/messaging-certification.yml'
|
||||||
'4e4ccfa267ecd63b9369803d49f2dbdb2fa899517ad4cf23ab11d29104557a91 .github/workflows/notification-platform.yml'
|
'4e4ccfa267ecd63b9369803d49f2dbdb2fa899517ad4cf23ab11d29104557a91 .github/workflows/notification-platform.yml'
|
||||||
'64245586cd5936f1a5647b57f2cd9acd316f96fd75f713b1890decb812e7d5fe .github/workflows/object-storage-qualification.yml'
|
'64245586cd5936f1a5647b57f2cd9acd316f96fd75f713b1890decb812e7d5fe .github/workflows/object-storage-qualification.yml'
|
||||||
'cbc104ea486c746229895e804e3be7716e056a02cce0588c537bce9f442f8b38 .github/workflows/redis-sdk-topology.yml'
|
'cbc104ea486c746229895e804e3be7716e056a02cce0588c537bce9f442f8b38 .github/workflows/redis-sdk-topology.yml'
|
||||||
|
|||||||
@@ -119,10 +119,14 @@ jobs:
|
|||||||
src/**/*.gradle
|
src/**/*.gradle
|
||||||
src/**/gradle-wrapper.properties
|
src/**/gradle-wrapper.properties
|
||||||
src/**/gradle.lockfile
|
src/**/gradle.lockfile
|
||||||
- name: Measure pool saturation and REQUIRES_NEW pressure
|
- name: Verify pool saturation and REQUIRES_NEW connection behaviour
|
||||||
working-directory: src
|
working-directory: src
|
||||||
# Machine-dependent bounds are reported rather than asserted unless explicitly enabled, so a
|
# A behaviour contract, not a measurement. This step used to switch assertions off with an
|
||||||
# noisy shared runner does not produce a red build that means nothing.
|
# explicit property and call the result a certification, so the only threshold it ever
|
||||||
|
# asserted was that thresholds were not being asserted. What
|
||||||
|
# it checks now — that REQUIRES_NEW needs two connections per concurrent thread, that a
|
||||||
|
# saturated pool reports its pending count, that a caller waits rather than proceeding
|
||||||
|
# without a connection — is true on any runner, so there is nothing to switch off.
|
||||||
run: >-
|
run: >-
|
||||||
./gradlew
|
./gradlew
|
||||||
:adapter:outbound:persistence-jpa:jpaPlatformPoolContractTest
|
:adapter:outbound:persistence-jpa:jpaPlatformPoolContractTest
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
name: jpa-release
|
name: jpa-release
|
||||||
|
|
||||||
# The release gate. Every item in docs/jpa/support-matrix.md's gate table has a job or an assertion
|
# The release gate. src/config/jpa/release-registry.json is the source: every gate it declares has a
|
||||||
# here, and JpaReleaseManifest parses that document so a gate removed from the docs fails the build
|
# job or an assertion here, JpaReleaseRenderingTest holds this file's matrix and promotion lists to
|
||||||
|
# the registry's Stable majors, and verifyJpaReleaseGateTasks resolves each gate's task against the
|
||||||
|
# real Gradle graph. So a gate removed from the registry, or a major demoted in it, fails the build
|
||||||
# rather than quietly ceasing to be checked.
|
# rather than quietly ceasing to be checked.
|
||||||
|
#
|
||||||
|
# The matrix below is therefore not free to drift: editing it without editing the registry fails the
|
||||||
|
# unit lane.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# The messaging platform's broker certification lane.
|
||||||
|
#
|
||||||
|
# Separate from ci-quality-gates.yml because it needs a container runtime and several minutes of it.
|
||||||
|
# The lane deliberately carries no Docker guard: every other container suite in the messaging tree
|
||||||
|
# skips with a stated reason when Docker is absent, and a certification lane that skipped would
|
||||||
|
# report success for a broker nobody started — which is the exact claim the evidence exists to rule
|
||||||
|
# out.
|
||||||
|
#
|
||||||
|
# The job runs the evidence gate rather than the lane, and the gate depends on the lane. What it
|
||||||
|
# proves is not only that the scenarios pass but that the committed manifest
|
||||||
|
# (messaging-testkit/src/main/resources/messaging/broker-certification-evidence.jsonl) is what this
|
||||||
|
# run produced, so "certified against a live broker" cannot be restored by editing a file.
|
||||||
|
name: messaging-certification
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "src/messaging/**"
|
||||||
|
- ".github/workflows/messaging-certification.yml"
|
||||||
|
schedule:
|
||||||
|
- cron: "41 4 * * 3"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Reuse would hand one scenario the broker another scenario had already faulted.
|
||||||
|
TESTCONTAINERS_REUSE_ENABLE: "false"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
broker-certification:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||||
|
- name: Validate Gradle wrapper
|
||||||
|
id: gradle-wrapper-validation
|
||||||
|
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||||
|
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: "21.0.11+10"
|
||||||
|
cache: gradle
|
||||||
|
cache-dependency-path: |
|
||||||
|
src/**/*.gradle
|
||||||
|
src/**/gradle-wrapper.properties
|
||||||
|
src/**/gradle.lockfile
|
||||||
|
- name: 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"
|
||||||
|
# is a claim about one source tree.
|
||||||
|
run: ./gradlew :messaging:messaging-kafka:verifyMessagingCertificationEvidence --no-daemon --stacktrace
|
||||||
|
- name: Publish the certification evidence
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
|
||||||
|
with:
|
||||||
|
name: messaging-broker-certification-evidence
|
||||||
|
path: src/messaging/messaging-kafka/build/messaging-certification/
|
||||||
|
if-no-files-found: warn
|
||||||
@@ -56,9 +56,20 @@ build when a policy document states a leaf count that the registry does not agre
|
|||||||
| `adapter:outbound:persistence-*` | JPA/PostgreSQL and MongoDB persistence adapters | application/domain/shared contracts as registered |
|
| `adapter:outbound:persistence-*` | JPA/PostgreSQL and MongoDB persistence adapters | application/domain/shared contracts as registered |
|
||||||
| `adapter:outbound:*` | support, messaging, cache, notification, storage, file, HTTP client, identifier capabilities | application/domain/shared and registered support edge |
|
| `adapter:outbound:*` | support, messaging, cache, notification, storage, file, HTTP client, identifier capabilities | application/domain/shared and registered support edge |
|
||||||
| `shared-contract` | Skeleton-wide operational contracts | Java stdlib only |
|
| `shared-contract` | Skeleton-wide operational contracts | Java stdlib only |
|
||||||
|
| `messaging:*` | Vendored messaging platform: a product with its own API, SPI, adapters and composition boundary, not a layer of this application | `messaging:*` only — it depends on no `domain-core`, `application-core`, or `shared-contract` type |
|
||||||
| `sample-portfolio` | Fixture/reference consumer | registered runtime leaves; never a production dependency |
|
| `sample-portfolio` | Fixture/reference consumer | registered runtime leaves; never a production dependency |
|
||||||
| `app-bootstrap` | Spring Boot entrypoint and composition root | registered runtime leaves |
|
| `app-bootstrap` | Spring Boot entrypoint and composition root | registered runtime leaves |
|
||||||
|
|
||||||
|
The `messaging:*` family is the one entry that is not a Clean Architecture layer, and it is listed so
|
||||||
|
that the exception is stated rather than inferred from a directory. It is a vendored library — its
|
||||||
|
own `*-api` leaves are its ports, its broker leaves are its adapters, its starter is its composition
|
||||||
|
root — and the messaging module review (`docs/reviews/2026-08-14-messaging-module-code-review.md`
|
||||||
|
MSG-023 §6.2) chose that layout deliberately over folding it into `adapter:outbound:*`. This
|
||||||
|
application is supposed to reach it the way it reaches any library: through an application-owned port
|
||||||
|
satisfied by an anti-corruption bridge in `adapter:outbound:messaging`. That bridge does not exist
|
||||||
|
yet (MSG-015), so today the composition root wires the starter directly; `src/messaging/CLAUDE.md`
|
||||||
|
holds the detail.
|
||||||
|
|
||||||
Never infer an individual leaf's Gradle path, allowed dependency, or test command from this table.
|
Never infer an individual leaf's Gradle path, allowed dependency, or test command from this table.
|
||||||
Read its `gradle_path`, `allowed_dependencies`, and `runtime_memberships` from
|
Read its `gradle_path`, `allowed_dependencies`, and `runtime_memberships` from
|
||||||
`src/config/architecture/modules.json`; derive the focused test from that Gradle path.
|
`src/config/architecture/modules.json`; derive the focused test from that Gradle path.
|
||||||
|
|||||||
@@ -325,6 +325,37 @@ services:
|
|||||||
- caskeleton-infra
|
- caskeleton-infra
|
||||||
restart: "no"
|
restart: "no"
|
||||||
|
|
||||||
|
# The GraphQL transport, as a request. auth-smoke proves a token can be obtained and that public
|
||||||
|
# health answers; this proves /graphql is guarded and that an authenticated query executes.
|
||||||
|
graphql-smoke:
|
||||||
|
profiles:
|
||||||
|
- local-graphql
|
||||||
|
- all-adapters
|
||||||
|
image: curlimages/curl:8.10.1
|
||||||
|
depends_on:
|
||||||
|
keycloak:
|
||||||
|
condition: service_healthy
|
||||||
|
# uid 0 for the mounted 0600 client secret, same as auth-smoke.
|
||||||
|
user: "0:0"
|
||||||
|
entrypoint: ["/bin/sh", "/opt/graphql-smoke/graphql-smoke.sh"]
|
||||||
|
environment:
|
||||||
|
APP_BASE_URL: "http://app:8080"
|
||||||
|
KEYCLOAK_ISSUER: "http://keycloak:8080/realms/ca-skeleton"
|
||||||
|
KEYCLOAK_CLIENT_ID: "ca-skeleton-api"
|
||||||
|
# Spring for GraphQL serves its own endpoint through a router function rather than an
|
||||||
|
# annotated controller, so the presentation base-path prefix does not apply to it.
|
||||||
|
GRAPHQL_PATH: "${GRAPHQL_PATH:-/graphql}"
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ./infra/graphql/smoke
|
||||||
|
target: /opt/graphql-smoke
|
||||||
|
read_only: true
|
||||||
|
secrets:
|
||||||
|
- keycloak-graphql-smoke-client-secret
|
||||||
|
networks:
|
||||||
|
- caskeleton-infra
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
# ---- One-shot smoke clients --------------------------------------------------
|
# ---- One-shot smoke clients --------------------------------------------------
|
||||||
# Never `up --wait` targets. Each is run with `run --rm` and must exit zero; a missing, skipped or
|
# Never `up --wait` targets. Each is run with `run --rm` and must exit zero; a missing, skipped or
|
||||||
# non-zero one fails its lane rather than being treated as "not applicable".
|
# non-zero one fails its lane rather than being treated as "not applicable".
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# ADR-GQL-001 — GraphQL context stays inbound; object authorization moves to application-core; the persisted-operation store stays an inbound SPI
|
||||||
|
|
||||||
|
- Status: Accepted
|
||||||
|
- Date: 2026-08-24
|
||||||
|
- Review: `docs/reviews/2026-08-14-graphql-module-code-review.md` GQL-026
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The GraphQL leaf's own documentation described three things crossing its boundary: a
|
||||||
|
`GraphQlRequestContext` with a deadline propagated into application, JPA, Mongo and the HTTP client;
|
||||||
|
object authorization decided inside the transport; and a persisted-operation registry implemented by
|
||||||
|
an external durable store.
|
||||||
|
|
||||||
|
Two of those invert the dependency direction. If `application-core` or an outbound adapter
|
||||||
|
implements a type that lives in `adapter:inbound:graphql`, the registry edge that says inbound
|
||||||
|
depends on application is satisfied while the real compile-time dependency runs the other way.
|
||||||
|
|
||||||
|
The third is a business rule in the wrong layer: whether an actor may see an object is a decision
|
||||||
|
about the domain, and GraphQL is one of four transports this skeleton ships.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Three different answers, because the three problems are not the same problem.
|
||||||
|
|
||||||
|
**GraphQL context stays inbound-local.** It is mapped explicitly onto application command fields —
|
||||||
|
actor, tenant, deadline — rather than travelling as a type. Nothing outside the leaf references
|
||||||
|
`GraphQlRequestContext`, and the boundary test is that grep returns nothing outside it.
|
||||||
|
|
||||||
|
**Object authorization moves to `application-core`.** `ObjectAccessPolicy`, `ObjectAccessRequest`
|
||||||
|
and `ObjectAccessDecision` are transport-neutral and live with the other application policies;
|
||||||
|
`ApplicationObjectAuthorization` in the GraphQL leaf is the bridge that calls them. This is the one
|
||||||
|
of the three that was a real layering defect, and it is fixed rather than documented.
|
||||||
|
|
||||||
|
**The persisted-operation store stays an inbound-owned SPI.** `GraphQlPersistedOperationRegistry`
|
||||||
|
remains in `advanced/persisted`, and no leaf outside GraphQL implements it.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
The third decision is the one that needs defending, because it leaves the reported risk in place
|
||||||
|
rather than removing it.
|
||||||
|
|
||||||
|
The risk is conditional: the direction inverts only when something outside the leaf implements the
|
||||||
|
interface. Nothing does. The template ships an in-memory registry and no durable one, because it
|
||||||
|
ships no persisted-operation store at all.
|
||||||
|
|
||||||
|
The alternative was to introduce a generic operational key-value store port owned by a neutral
|
||||||
|
contract holder, with the GraphQL adapter owning only the key and value mapping. That port would
|
||||||
|
have exactly one interface, zero implementations and one speculative consumer — a new abstraction
|
||||||
|
whose shape is guessed from a requirement nobody has stated. This repository has spent a full
|
||||||
|
remediation pass deleting controls that existed and were reached by nothing, and inventing a port
|
||||||
|
for a store that does not exist is how the next one of those gets written.
|
||||||
|
|
||||||
|
So the decision is to leave the SPI where it is and to move it when a durable store is actually
|
||||||
|
built. Moving it then is a rename across one leaf and one new adapter, which is cheaper than
|
||||||
|
carrying a wrong abstraction until then. What must not happen in the meantime is an outbound leaf
|
||||||
|
implementing the inbound interface, because that is the moment the direction actually inverts, and
|
||||||
|
it would happen in a commit whose diff looks like an implementation rather than a layering change.
|
||||||
|
|
||||||
|
The composition root wires these and owns no business or storage policy of its own.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
`verifyCleanArchitectureDependencies` and `modules.json` hold the leaf's edges to
|
||||||
|
`domain-core`, `application-core` and `shared-contract`. `ObjectAccessPolicyTest` covers the
|
||||||
|
application-side policy and `ApplicationObjectAuthorizationTest` the bridge.
|
||||||
|
|
||||||
|
The condition this ADR turns on — that nothing outside the GraphQL leaf implements the
|
||||||
|
persisted-operation SPI — is a claim about the whole repository, so it is checked at the
|
||||||
|
composition root rather than inside the leaf, next to the other GraphQL boundary rules in
|
||||||
|
`app-bootstrap`'s architecture suite.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# ADR-JPA-006 — `audit` is the canonical technical audit model; `auditing` stays a frozen candidate
|
||||||
|
|
||||||
|
- Status: Accepted
|
||||||
|
- Date: 2026-08-24
|
||||||
|
- Review: `docs/reviews/2026-08-14-jpa-module-code-review.md` JPA-022
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Two complete technical-audit mechanisms live in this leaf and they disagree about the schema.
|
||||||
|
|
||||||
|
`audit/AuditableEntity` stamps `created_at`/`created_by`/`updated_at`/`updated_by` with an actor
|
||||||
|
column of length 256, captured through explicit `initializeAudit`/`applyModification` calls and an
|
||||||
|
`AuditContextPort`. `auditing/AuditMetadata` is a Spring Data embeddable that stamps
|
||||||
|
`created_*`/`modified_*` with an actor column of length 64, captured by `@CreatedDate` and friends
|
||||||
|
through an `AuditorAware`.
|
||||||
|
|
||||||
|
Only the first is real: it is what the sample entities extend and what the migrations were written
|
||||||
|
for. `JpaAuditingConfiguration` is not a Spring `@Configuration`, and nothing in production
|
||||||
|
constructs any of the three `auditing` types.
|
||||||
|
|
||||||
|
The review asked for one canonical model with a migration or activation decision. The failure mode
|
||||||
|
it was protecting against is specific: an author of a new entity picks whichever package they find
|
||||||
|
first, and column names, actor lengths and capture lifecycles then diverge per table.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
`audit/AuditableEntity` is canonical. `auditing` stays in the tree as a candidate and is excluded
|
||||||
|
from the Stable capability report.
|
||||||
|
|
||||||
|
The candidate is not deleted and not promoted. Deleting it would discard a working Spring Data
|
||||||
|
integration that a deployment preferring declarative auditing would want. Promoting it would mean
|
||||||
|
either renaming `modified_*` to `updated_*` and widening the actor column — a schema migration of
|
||||||
|
every audited table to gain nothing a caller asked for — or moving the sample entities onto
|
||||||
|
`modified_*`, which is the same migration in the other direction.
|
||||||
|
|
||||||
|
Neither is worth doing now. What the divergence actually needed was not consolidation but a rule
|
||||||
|
that an entity cannot straddle the two, and that rule is cheaper than either migration.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Two audit mechanisms remain readable in one leaf, and a reader has to be told which one is live.
|
||||||
|
That cost is paid in this document, in the package javadoc and in a test whose name says so.
|
||||||
|
|
||||||
|
Two failure modes stay silent unless they are asserted, so both are:
|
||||||
|
|
||||||
|
- The candidate acquires a stereotype and starts stamping in every deployment that has this module
|
||||||
|
on the classpath, including the ones whose tables have no `modified_*` columns — where the result
|
||||||
|
is a failed startup rather than a feature.
|
||||||
|
- Somebody "harmonises" the two by editing one side's column names, at which point the schema a
|
||||||
|
deployed table was migrated for and the schema its entity expects diverge with no migration
|
||||||
|
between them.
|
||||||
|
|
||||||
|
If the candidate is ever promoted, it is promoted atomically: forward migration, sample conversion,
|
||||||
|
`AuditContextPort → AuditorAware` and `Clock → DateTimeProvider` bridges land together, and this
|
||||||
|
ADR is superseded rather than amended.
|
||||||
|
|
||||||
|
Bulk and native updates stamp nothing under either mechanism. That is a property of JPA, not of the
|
||||||
|
choice made here, so it is enforced separately rather than assumed away.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
`JpaAuditMechanismRule.entitiesUseExactlyOneAuditMechanism` and
|
||||||
|
`bulkUpdatesOfAuditedEntitiesStampAudit`, run against the real production graph by
|
||||||
|
`JpaProductionArchitectureTest` at the composition root — not against fixtures, which is how the
|
||||||
|
earlier version of this rule pack passed while applying to nothing. `AuditingCandidateStatusTest`
|
||||||
|
asserts the candidate carries no composing stereotype and that the two column sets stay distinct.
|
||||||
|
`JpaAuditMechanismRuleTest` exercises the rules' own negative cases.
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
# split into capability artifacts.
|
# split into capability artifacts.
|
||||||
# Update only after review with:
|
# Update only after review with:
|
||||||
# ./gradlew :adapter:inbound:graphql:updateGraphQlApiSurface -PapproveGraphQlApiSurfaceChange
|
# ./gradlew :adapter:inbound:graphql:updateGraphQlApiSurface -PapproveGraphQlApiSurfaceChange
|
||||||
# types: 395
|
# types: 398
|
||||||
dev.caskeleton.adapter.inbound.graphql.HealthGraphqlController
|
dev.caskeleton.adapter.inbound.graphql.HealthGraphqlController
|
||||||
dev.caskeleton.adapter.inbound.graphql.advanced.admin.GraphQlAdminPrincipal
|
dev.caskeleton.adapter.inbound.graphql.advanced.admin.GraphQlAdminPrincipal
|
||||||
dev.caskeleton.adapter.inbound.graphql.advanced.admin.GraphQlPersistedOperationAdminAuthorization
|
dev.caskeleton.adapter.inbound.graphql.advanced.admin.GraphQlPersistedOperationAdminAuthorization
|
||||||
@@ -179,6 +179,7 @@ dev.caskeleton.adapter.inbound.graphql.compat.GraphQlSchemaUsage
|
|||||||
dev.caskeleton.adapter.inbound.graphql.context.ActorRef
|
dev.caskeleton.adapter.inbound.graphql.context.ActorRef
|
||||||
dev.caskeleton.adapter.inbound.graphql.context.GraphQlCommandAttribution
|
dev.caskeleton.adapter.inbound.graphql.context.GraphQlCommandAttribution
|
||||||
dev.caskeleton.adapter.inbound.graphql.context.GraphQlDeadline
|
dev.caskeleton.adapter.inbound.graphql.context.GraphQlDeadline
|
||||||
|
dev.caskeleton.adapter.inbound.graphql.context.GraphQlIdentityFingerprinter
|
||||||
dev.caskeleton.adapter.inbound.graphql.context.GraphQlRequestContext
|
dev.caskeleton.adapter.inbound.graphql.context.GraphQlRequestContext
|
||||||
dev.caskeleton.adapter.inbound.graphql.context.TenantContext
|
dev.caskeleton.adapter.inbound.graphql.context.TenantContext
|
||||||
dev.caskeleton.adapter.inbound.graphql.cost.GraphQlComplexityCalculator
|
dev.caskeleton.adapter.inbound.graphql.cost.GraphQlComplexityCalculator
|
||||||
@@ -358,6 +359,7 @@ dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlPlatformRejectionMapper
|
|||||||
dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlPlatformWebInterceptor
|
dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlPlatformWebInterceptor
|
||||||
dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlPreparsedDocumentAdapter
|
dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlPreparsedDocumentAdapter
|
||||||
dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlPrincipalResolver
|
dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlPrincipalResolver
|
||||||
|
dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlRequestObservationConventionAdapter
|
||||||
dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlWireErrorMapper
|
dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlWireErrorMapper
|
||||||
dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlWireErrors
|
dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlWireErrors
|
||||||
dev.caskeleton.adapter.inbound.graphql.runtime.servlet.GraphQlRequestBodyLimitFilter
|
dev.caskeleton.adapter.inbound.graphql.runtime.servlet.GraphQlRequestBodyLimitFilter
|
||||||
@@ -387,6 +389,7 @@ dev.caskeleton.adapter.inbound.graphql.schema.GraphQlSchemaHash
|
|||||||
dev.caskeleton.adapter.inbound.graphql.schema.GraphQlSchemaMappingException
|
dev.caskeleton.adapter.inbound.graphql.schema.GraphQlSchemaMappingException
|
||||||
dev.caskeleton.adapter.inbound.graphql.schema.GraphQlSchemaOwnership
|
dev.caskeleton.adapter.inbound.graphql.schema.GraphQlSchemaOwnership
|
||||||
dev.caskeleton.adapter.inbound.graphql.schema.GraphQlSchemaResource
|
dev.caskeleton.adapter.inbound.graphql.schema.GraphQlSchemaResource
|
||||||
|
dev.caskeleton.adapter.inbound.graphql.security.ApplicationObjectAuthorization
|
||||||
dev.caskeleton.adapter.inbound.graphql.security.GraphQlAuthenticatedPrincipal
|
dev.caskeleton.adapter.inbound.graphql.security.GraphQlAuthenticatedPrincipal
|
||||||
dev.caskeleton.adapter.inbound.graphql.security.GraphQlAuthenticationContextFactory
|
dev.caskeleton.adapter.inbound.graphql.security.GraphQlAuthenticationContextFactory
|
||||||
dev.caskeleton.adapter.inbound.graphql.security.GraphQlAuthenticationException
|
dev.caskeleton.adapter.inbound.graphql.security.GraphQlAuthenticationException
|
||||||
|
|||||||
@@ -0,0 +1,340 @@
|
|||||||
|
# JPA persistence leaf public API surface — every public top-level type in src/main/java.
|
||||||
|
# A public type in a single-jar leaf is reachable from every adopter's code, so
|
||||||
|
# additions are reviewed rather than discovered. `api` is the intended external
|
||||||
|
# surface; the rest is implementation that has not been moved under an internal
|
||||||
|
# root yet.
|
||||||
|
# Update only after review with:
|
||||||
|
# ./gradlew :adapter:outbound:persistence-jpa:updateJpaApiSurface -PapproveJpaApiSurfaceChange
|
||||||
|
# types: 332
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationName
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.capability.CapabilitySupport
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.capability.JpaCapability
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.capability.SupportLevel
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.CheckConstraintViolationException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.ConnectionUnavailableException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintCode
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintViolationDetails
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.DataCorruptionException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.DeadlockDetectedException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.FailureCategory
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.ForeignKeyViolationException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.JpaEntityNotFoundException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.JpaFailureContext
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.JpaPersistenceException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.NotNullConstraintViolationException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.OptimisticConflictException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.PessimisticLockTimeoutException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.QueryTimeoutException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.SchemaMismatchException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.SerializationFailureException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.SqlExceptionSqlStateResolver
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.SqlStateResolver
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.TransactionCompletionUnknownException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.TransactionTimeoutException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.UniqueConstraintViolationException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.error.VendorFailureTranslator
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.query.CursorCodec
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.query.CursorPayloadCodec
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.query.KeysetPageRequest
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.query.KeysetSlice
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.query.NoopQueryObservation
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.query.QueryName
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.query.QueryObservation
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.query.QueryScope
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.query.SignedJsonCursorCodec
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.query.SortDirection
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.transaction.IsolationLevel
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.transaction.JitterMode
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.transaction.JpaRetryPolicy
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.transaction.JpaTransactionExecutor
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.transaction.PropagationMode
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryDecision
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryDisposition
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryEventListener
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryProfile
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionAttempt
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionCompletionEvidence
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionProfile
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.audit.AuditContextPort
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.audit.AuditableEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.audit.DomainContextAuditContextPort
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.auditing.AuditMetadata
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.auditing.JpaAuditingConfiguration
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.auditing.JpaAuditorProvider
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.cache.CacheConcurrencyStrategy
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.cache.CacheRegionCatalog
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.cache.HibernateCacheGuard
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.cache.HibernateCachePolicy
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.cache.HibernateCacheSettings
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.config.JpaAdapterComponentsConfig
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.config.PersistenceJpaConfig
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.config.PersistenceVendorSettings
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.envers.EntityRevision
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.envers.EnversConfigurationGuard
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.envers.EnversHistoryPolicy
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.envers.EnversHistoryReader
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.envers.EnversRevisionMetadata
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.envers.HibernateEnversHistoryReader
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.ExperimentalFeature
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.ExperimentalFeatureGate
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.database.TenantDataSourceLifecycle
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.database.TenantDataSourceRegistry
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.database.TenantEntityManagerFactoryRegistry
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.database.TenantPoolBudget
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.next.CompatibilityLane
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.next.ExperimentalPromotionGate
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.next.HibernateCompatibilityPolicy
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.next.PromotionDecision
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.next.PromotionEvidence
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.replica.ConsistencyAwareDataSourceRouter
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.replica.ConsistencyToken
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.replica.ReadConsistency
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.replica.ReplicaLagMonitor
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.replica.ReplicaRoutingDecision
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.replica.ReplicaTarget
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.replica.TransactionContext
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.rls.RlsAdminBypassToken
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.rls.RlsPolicyVerifier
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.rls.RlsTenantSessionBinder
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.schema.SchemaMultiTenantConnectionProvider
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.schema.SchemaTenantMigrationOrchestrator
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.schema.SchemaTenantRegistry
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.schema.TenantMigrationStatus
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.tenant.TenantAwareRepositoryGuard
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.tenant.TenantContext
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.tenant.TenantEntityListenerGuard
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.experimental.tenant.TenantId
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.failure.PersistenceExceptionTranslator
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.failure.SqlStateErrorMapping
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.failure.StandardSqlStateErrorMapping
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.FileserverJpaPersistenceConfig
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.FileserverSchemaActivation
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.JpaCleanupQueue
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.JpaContentReferenceLedger
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.JpaFileMetadataStore
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.JpaFileQuotaService
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.JpaQuotaCommitGateway
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.JpaQuotaReclaimGateway
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.JpaRecoveryQueue
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.JpaStagingUploadLocator
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.JpaUploadSessionStore
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.entity.CleanupItemEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.entity.FileEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.entity.QuotaReservationEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.entity.RecoveryItemEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.entity.UploadSessionEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.entity.VerificationResultEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileTransitionRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverCleanupRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverQuotaRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverRecoveryRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.JpaFileRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.JpaUploadSessionRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.UploadLeaseRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.h2.H2IdempotencyClaimRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.h2.H2LocalTimeoutConfigurer
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.h2.H2OutboxClaimRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.h2.H2PersistenceConfig
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.h2.H2SqlStateErrorMapping
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.HibernateProviderPolicy
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.HibernateStatisticsCollector
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.HibernateStatisticsSnapshot
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.JdbcBatchCounter
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.NamedStatementInspector
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.QueryNameContext
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.batch.BatchExecutionResult
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.batch.HibernateBatchConfigurationGuard
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.batch.HibernateJpaBatchExecutor
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.batch.JpaBatchExecutor
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.batch.JpaBatchProfile
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.batch.JpaBatchProfileRegistry
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.bulk.AffectedRowsExpectation
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.bulk.BulkDmlExecutor
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.bulk.BulkDmlResult
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.bulk.BulkOperationName
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.bulk.HibernateBulkDmlExecutor
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.stateless.HibernateStatelessSessionRunner
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.stateless.StatelessRowCapExceededException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.stateless.StatelessSessionRunner
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.stateless.StatelessWorkName
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.hibernate.stateless.StatelessWorkResult
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.idempotency.IdempotencyClaimRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.idempotency.IdempotencyReaper
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.idempotency.IdempotencyRecordJpaRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.idempotency.IdempotencyResponseObjectStore
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.idempotency.IdempotencyStoreAdapter
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.idempotency.entity.IdempotencyRecordEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.idempotency.mapper.IdempotencyRecordEntityMapper
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.lock.DistributedLockPersistenceConfig
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.lock.LockRegistryDistributedLockAdapter
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.lock.LockSettings
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.migration.ConcurrentIndexMigrationInspector
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.migration.FailedConcurrentIndexRecovery
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.migration.FlywaySchemaPolicy
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.migration.FlywayValidationGate
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.migration.MigrationResource
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.migration.NonTransactionalMigrationPolicy
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.migration.SchemaManagementMode
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.migration.SchemaVersionSnapshot
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.NotificationJpaPersistenceConfig
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.NotificationSchemaActivation
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.NotificationSchemaStream
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.configuration.NotificationJpaPersistenceFacade
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.crypto.DirectAeadNotificationPayloadCrypto
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.crypto.NotificationCiphertext
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.crypto.NotificationCryptoException
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.crypto.NotificationHmacDigester
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.crypto.NotificationKeyMaterialHandle
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.crypto.NotificationKeyMaterialProvider
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.AdminAuditEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.AdminAuditJpaRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.ConsentEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.ConsentJpaRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.ContactPointEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.ContactPointJpaRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.DeduplicationClaimEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.DeduplicationClaimJpaRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.DeliveryAttemptEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.DeliveryAttemptJpaRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.JdbcNotificationServingState
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.JdbcReconciliationJobStore
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.JpaAdminOperationStore
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.JpaContactPointStore
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.JpaDeliveryAttemptStore
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.JpaNotificationRequestStore
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.JpaNotificationSideEffectStore
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.JpaPolicyStores
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.JpaProviderEventLedger
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.JpaRecipientDeliveryStore
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.JpaRecipientLeaseStore
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.JpaSuppressionStore
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.JpaTemplateRegistry
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.NotificationRecordMapper
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.NotificationRequestEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.NotificationRequestJpaRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.PreferenceEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.PreferenceJpaRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.ProviderEventEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.ProviderEventJpaRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.RecipientClaimSql
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.RecipientDeliveryEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.RecipientDeliveryJpaRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.SuppressionEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.SuppressionJpaRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.TemplateVersionEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.TemplateVersionJpaRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.TenantBoundRepositoryGuard
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.inbox.InboxCommitEventPublisher
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.inbox.InboxItemEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.inbox.InboxItemJpaRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.inbox.InboxOutboxRecordFactory
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.notification.platform.inbox.JpaNotificationInbox
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.observation.JpaMetricTags
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.observation.JpaRetryObservation
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.observation.JpaTransactionObservation
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.observation.LowCardinality
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.observation.MicrometerQueryObservation
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.observation.SqlDiagnosticRedactor
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.outbox.OutboxClaimRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.outbox.OutboxEventJpaRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.outbox.OutboxReaper
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.outbox.OutboxStoreAdapter
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.outbox.entity.OutboxEventEntity
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlIdempotencyClaimRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlLocalTimeoutConfigurer
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlOutboxClaimRepository
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlPersistenceConfig
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlSqlStateErrorMapping
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.array.PostgreSqlArraySupport
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.constraint.PostgreSqlConstraintCatalog
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.constraint.PostgreSqlConstraintViolationTranslator
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.copy.BoundedCopyInputStream
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.copy.CopyAdminCapability
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.copy.CopyFormat
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.copy.CopyLimits
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.copy.CopyOperationName
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.copy.CopyResult
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.copy.PostgreSqlCopyLoader
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.copy.RegisteredCopyStatement
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.copy.RegisteredPostgreSqlCopyLoader
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.error.ConstraintCatalog
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.error.PostgreSqlExceptionTranslator
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.error.PostgreSqlFailureClassifier
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.error.PostgreSqlServerErrorFields
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.error.PostgreSqlState
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.idempotency.PostgreSqlOwnerSafeIdempotencyStore
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.inbox.PostgreSqlSameStoreInboxAdapter
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.json.JsonDocument
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.json.JsonDocumentCodec
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.json.JsonPathName
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.json.PostgreSqlJsonQuerySupport
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.lock.LockWaitObservation
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.lock.PostgreSqlLockExceptionTranslator
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.lock.PostgreSqlLockOptions
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.lock.PostgreSqlWorkClaimExecutor
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.lock.WorkClaim
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.lock.WorkClaimExecutor
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.lock.WorkQueueDefinition
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.lock.WorkQueueName
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.outbox.PostgreSqlImmutableOutboxAppendAdapter
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.outbox.PostgreSqlPollingDeliveryAdapter
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.range.PgRange
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.range.PgRangeCodec
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.range.PgRangeJdbcType
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.range.PostgreSqlRangeQuerySupport
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.write.NativeWriteName
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.write.PostgreSqlUpsertExecutor
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.write.RegisteredPostgreSqlUpsertExecutor
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.write.RegisteredUpsertStatement
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.write.UpsertConflictTarget
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.write.UpsertResult
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.postgresql.write.WriteDisposition
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.querydsl.PredicatePolicy
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.querydsl.QueryPage
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.querydsl.QuerydslJpaSupport
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.security.DatabasePrivilegeReport
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.security.DatabaseRolePolicy
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.security.PostgreSqlRuntimeRoleVerifier
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.security.SearchPathPolicy
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.EntityGraphCatalog
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.EntityManagerAccess
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.FetchPlanApplier
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.FetchPlanName
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.JpaKeysetQuerySupport
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.JpaRepositoryFragmentSupport
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.JpaStreamExecutor
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.JpaStreamScope
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.KeysetPredicateBuilder
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.KeysetSliceAssembler
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.KeysetTerm
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.RegisteredQuery
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.SafeSortField
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.SafeSortMapper
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.SafeSortRegistry
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.ScrollPolicy
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.springdata.SpecificationPolicy
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.BackoffCalculator
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.CommitFailureClassifier
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.CompletionUnknownRecord
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.CompletionUnknownRecorder
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.DefaultJpaRetryPolicy
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.EffectiveTransactionTimeouts
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.EvidenceAwareJpaTransactionManager
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.FullTransactionRetryCoordinator
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.JpaTransactionConfig
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.JpaTransactionSettings
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.OptimisticConflictTranslator
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.PersistenceFailureTranslatorChain
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.RetryBudget
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.RetrySleeper
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.SpringJpaTransactionExecutor
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.SpringTransactionPort
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.ThreadRetrySleeper
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionDefinitionMapper
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionEvidenceContext
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionEvidenceFrame
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionEvidenceScope
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionLocalTimeoutConfigurer
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionProfileRegistry
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionStartBudget
|
||||||
|
dev.caskeleton.adapter.outbound.persistence.transaction.UnknownOperation
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
# root yet.
|
# root yet.
|
||||||
# Update only after review with:
|
# Update only after review with:
|
||||||
# ./gradlew :adapter:outbound:persistence-mongo:updateMongoApiSurface -PapproveMongoApiSurfaceChange
|
# ./gradlew :adapter:outbound:persistence-mongo:updateMongoApiSurface -PapproveMongoApiSurfaceChange
|
||||||
# types: 343
|
# types: 346
|
||||||
dev.caskeleton.adapter.outbound.mongo.MongoOptInAutoConfigurationImportFilter
|
dev.caskeleton.adapter.outbound.mongo.MongoOptInAutoConfigurationImportFilter
|
||||||
dev.caskeleton.adapter.outbound.mongo.MongoPersistenceConfig
|
dev.caskeleton.adapter.outbound.mongo.MongoPersistenceConfig
|
||||||
dev.caskeleton.adapter.outbound.mongo.MongoPersistenceSettings
|
dev.caskeleton.adapter.outbound.mongo.MongoPersistenceSettings
|
||||||
@@ -146,8 +146,6 @@ dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoPlatformAutoConfigurati
|
|||||||
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoPlatformHealthIndicator
|
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoPlatformHealthIndicator
|
||||||
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoPlatformSettings
|
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoPlatformSettings
|
||||||
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoProfileProperties
|
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoProfileProperties
|
||||||
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoStableReleaseEvidence
|
|
||||||
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoStableReleaseGate
|
|
||||||
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoStartupValidator
|
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoStartupValidator
|
||||||
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoTopologyProbe
|
dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoTopologyProbe
|
||||||
dev.caskeleton.adapter.outbound.mongo.changestream.MongoChangeEventIdentity
|
dev.caskeleton.adapter.outbound.mongo.changestream.MongoChangeEventIdentity
|
||||||
@@ -158,6 +156,10 @@ dev.caskeleton.adapter.outbound.mongo.changestream.MongoClusterTime
|
|||||||
dev.caskeleton.adapter.outbound.mongo.changestream.MongoResumeCheckpoint
|
dev.caskeleton.adapter.outbound.mongo.changestream.MongoResumeCheckpoint
|
||||||
dev.caskeleton.adapter.outbound.mongo.changestream.MongoResumeCheckpointStore
|
dev.caskeleton.adapter.outbound.mongo.changestream.MongoResumeCheckpointStore
|
||||||
dev.caskeleton.adapter.outbound.mongo.changestream.MongoResumePosition
|
dev.caskeleton.adapter.outbound.mongo.changestream.MongoResumePosition
|
||||||
|
dev.caskeleton.adapter.outbound.mongo.changestream.MongoResumeTokenCodec
|
||||||
|
dev.caskeleton.adapter.outbound.mongo.changestream.consumer.MongoChangeStreamSource
|
||||||
|
dev.caskeleton.adapter.outbound.mongo.changestream.consumer.ReactiveMongoChangeStreamConsumer
|
||||||
|
dev.caskeleton.adapter.outbound.mongo.changestream.consumer.SpringReactiveChangeStreamSource
|
||||||
dev.caskeleton.adapter.outbound.mongo.changestream.projector.MongoChangeClaim
|
dev.caskeleton.adapter.outbound.mongo.changestream.projector.MongoChangeClaim
|
||||||
dev.caskeleton.adapter.outbound.mongo.changestream.projector.MongoChangeDeduplicationStore
|
dev.caskeleton.adapter.outbound.mongo.changestream.projector.MongoChangeDeduplicationStore
|
||||||
dev.caskeleton.adapter.outbound.mongo.changestream.projector.MongoChangeProjectionResult
|
dev.caskeleton.adapter.outbound.mongo.changestream.projector.MongoChangeProjectionResult
|
||||||
@@ -199,6 +201,7 @@ dev.caskeleton.adapter.outbound.mongo.imperative.atomic.AtomicUpdateResult
|
|||||||
dev.caskeleton.adapter.outbound.mongo.imperative.atomic.MongoAtomicOperations
|
dev.caskeleton.adapter.outbound.mongo.imperative.atomic.MongoAtomicOperations
|
||||||
dev.caskeleton.adapter.outbound.mongo.imperative.atomic.MongoAtomicOperationsTemplate
|
dev.caskeleton.adapter.outbound.mongo.imperative.atomic.MongoAtomicOperationsTemplate
|
||||||
dev.caskeleton.adapter.outbound.mongo.imperative.atomic.MongoAtomicPolicy
|
dev.caskeleton.adapter.outbound.mongo.imperative.atomic.MongoAtomicPolicy
|
||||||
|
dev.caskeleton.adapter.outbound.mongo.imperative.atomic.MongoAtomicPolicyRegistry
|
||||||
dev.caskeleton.adapter.outbound.mongo.imperative.atomic.MongoUpdateOperator
|
dev.caskeleton.adapter.outbound.mongo.imperative.atomic.MongoUpdateOperator
|
||||||
dev.caskeleton.adapter.outbound.mongo.imperative.atomic.ReturnDocumentMode
|
dev.caskeleton.adapter.outbound.mongo.imperative.atomic.ReturnDocumentMode
|
||||||
dev.caskeleton.adapter.outbound.mongo.imperative.bulk.MongoBulkExecutor
|
dev.caskeleton.adapter.outbound.mongo.imperative.bulk.MongoBulkExecutor
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ in a fail-closed contract (`verifyJpaReadinessRegistry` in `src/build.gradle`).
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `test` | `src/test` — hermetic unit lane, `./gradlew :adapter:outbound:persistence-jpa:test` |
|
| `test` | `src/test` — hermetic unit lane, `./gradlew :adapter:outbound:persistence-jpa:test` |
|
||||||
| `contractTest`, `integrationTest`, `migrationTest`, `failureTest`, `compatibilityTest` | `src/postgresqlIntegrationTest` — real PostgreSQL containers; selected by the `jpaPlatform*` Gradle tasks |
|
| `contractTest`, `integrationTest`, `migrationTest`, `failureTest`, `compatibilityTest` | `src/postgresqlIntegrationTest` — real PostgreSQL containers; selected by the `jpaPlatform*` Gradle tasks |
|
||||||
| `performanceTest` | `src/jpaPlatformPerformanceTest` — machine-dependent bounds, never part of `check` |
|
| `performanceTest` | `src/jpaPlatformPerformanceTest` — pool and `REQUIRES_NEW` connection behaviour, run by `jpaPlatformPoolContractTest`; never part of `check`. The source set keeps the plan's name; the lane asserts behaviour rather than measuring, and no numeric performance bound is claimed anywhere from it. |
|
||||||
|
|
||||||
Docker-dependent lanes fail closed rather than skipping, matching the existing
|
Docker-dependent lanes fail closed rather than skipping, matching the existing
|
||||||
`PostgreSqlReadinessSupport.assertDockerAvailable()` convention in this leaf.
|
`PostgreSqlReadinessSupport.assertDockerAvailable()` convention in this leaf.
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ major changed nothing so long as the string survived somewhere in the document.
|
|||||||
declares a support level per major as a field, each gate names the Gradle task that produces its
|
declares a support level per major as a field, each gate names the Gradle task that produces its
|
||||||
evidence, and this document describes what the registry says.
|
evidence, and this document describes what the registry says.
|
||||||
|
|
||||||
|
Being a rendering used to be a claim rather than a mechanism: the tables below were still typed by
|
||||||
|
hand, so a major demoted in the registry stayed Stable here and kept its full release job.
|
||||||
|
`JpaReleaseRenderingTest` now compares the database table, the gate table and `jpa-release.yml`'s
|
||||||
|
matrix and promotion lists to the registry, and `verifyJpaReleaseGateTasks` resolves every gate's
|
||||||
|
task against the real Gradle task graph. Edit the registry; these tables follow, or the build fails.
|
||||||
|
|
||||||
|
Two renderings stayed outside that comparison until they were added to it. `jpa-nightly.yml` runs
|
||||||
|
its own matrix and nothing checked it, so a demotion corrected the release lane and left the nightly
|
||||||
|
lane certifying the major. And an Experimental major's "compatibility lane only" named no file: the
|
||||||
|
lane existed, but the registry, this document and the release workflow could each be read end to end
|
||||||
|
without establishing that, so a reader looking for it concluded there was none. An Experimental major
|
||||||
|
now has to be recorded as the target of a lane in `.github/workflows`, and a Stable lane may not run
|
||||||
|
it.
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
| Database | Support | Evidence |
|
| Database | Support | Evidence |
|
||||||
@@ -17,7 +31,7 @@ evidence, and this document describes what the registry says.
|
|||||||
| PostgreSQL 16 | Stable | full contract suite, release lane (own matrix job) |
|
| PostgreSQL 16 | Stable | full contract suite, release lane (own matrix job) |
|
||||||
| PostgreSQL 17 | Stable | full contract suite, release lane (own matrix job) |
|
| PostgreSQL 17 | Stable | full contract suite, release lane (own matrix job) |
|
||||||
| PostgreSQL 18 | Stable | full contract suite, release lane (own matrix job) |
|
| PostgreSQL 18 | Stable | full contract suite, release lane (own matrix job) |
|
||||||
| PostgreSQL 19 | Experimental | compatibility lane only; promotion requires an ADR |
|
| PostgreSQL 19 | Experimental | [`jpa-next-postgresql19.yml`](../../.github/workflows/jpa-next-postgresql19.yml) — `NOT_EXECUTABLE`: no `postgres:19-alpine` is published, so no container of that major has been started; promotion requires an ADR |
|
||||||
| H2 | Local convenience | **never** evidence of PostgreSQL behaviour |
|
| H2 | Local convenience | **never** evidence of PostgreSQL behaviour |
|
||||||
|
|
||||||
Each major gets its **own release job**, because for a while it did not. The release lane passed
|
Each major gets its **own release job**, because for a while it did not. The release lane passed
|
||||||
@@ -82,6 +96,8 @@ the difference visible instead of asserting a constant against itself. See
|
|||||||
| PostgreSQL `COPY` | Admin (J4) |
|
| PostgreSQL `COPY` | Admin (J4) |
|
||||||
| Hibernate second-level cache | Advanced |
|
| Hibernate second-level cache | Advanced |
|
||||||
| Hibernate Envers | Advanced |
|
| Hibernate Envers | Advanced |
|
||||||
|
| Technical auditing — `audit/AuditableEntity` | Stable (canonical) |
|
||||||
|
| Technical auditing — `auditing/AuditMetadata` | Candidate, not composed |
|
||||||
| Multi-tenancy (column, RLS, schema, database) | Experimental |
|
| Multi-tenancy (column, RLS, schema, database) | Experimental |
|
||||||
| Consistency-aware read replica | Experimental |
|
| Consistency-aware read replica | Experimental |
|
||||||
|
|
||||||
@@ -98,6 +114,26 @@ Each row is a way the platform could pass its tests and still be wrong in produc
|
|||||||
| `runtime-role-no-ddl` | gate | the application's own credential being able to alter or drop schema objects |
|
| `runtime-role-no-ddl` | gate | the application's own credential being able to alter or drop schema objects |
|
||||||
| `collection-fetch-pagination` | gate | a paged collection fetch silently reading the whole table and paginating in memory |
|
| `collection-fetch-pagination` | gate | a paged collection fetch silently reading the whole table and paginating in memory |
|
||||||
|
|
||||||
|
### The two audit mechanisms
|
||||||
|
|
||||||
|
`audit/AuditableEntity` is the canonical one: `created_*`/`updated_*`, a 256-character actor,
|
||||||
|
stamped explicitly by the repository adapter. It is what the sample entities extend and what the
|
||||||
|
migrations were written for.
|
||||||
|
|
||||||
|
`auditing/AuditMetadata` is a second, complete mechanism with different column names
|
||||||
|
(`modified_*`), a different actor length (64) and a different capture lifecycle (Spring Data
|
||||||
|
listeners). Nothing embeds it and nothing composes `JpaAuditingConfiguration`, which is why it is
|
||||||
|
listed as a candidate rather than as a capability: promoting it means choosing between reshaping it
|
||||||
|
to the canonical columns and writing a forward migration for the new ones, and that choice has not
|
||||||
|
been made. Until it is, an entity picks one mechanism or none — enforced on the production graph by
|
||||||
|
`JpaAuditMechanismRule.entitiesUseExactlyOneAuditMechanism`.
|
||||||
|
|
||||||
|
Neither mechanism reaches a bulk or native update. Both stamp on an ordinary save — one in the
|
||||||
|
adapter, one on a managed entity's lifecycle — so a statement that goes straight to the database
|
||||||
|
leaves the audit columns showing the previous save. A bulk update of an audited entity must
|
||||||
|
therefore set the audit column in the statement, which
|
||||||
|
`JpaAuditMechanismRule.bulkUpdatesOfAuditedEntitiesStampAudit` checks over the production graph.
|
||||||
|
|
||||||
## Explicitly unsupported
|
## Explicitly unsupported
|
||||||
|
|
||||||
- Reactive JPA. JPA is a blocking specification; a reactive facade over it moves the blocking call
|
- Reactive JPA. JPA is a blocking specification; a reactive facade over it moves the blocking call
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
> either of the old prefixes now fails startup with a message naming the key — see
|
> either of the old prefixes now fails startup with a message naming the key — see
|
||||||
> `MessagingPrefixMigrationValidator`.
|
> `MessagingPrefixMigrationValidator`.
|
||||||
|
|
||||||
|
> **이 페이지는 실행된다.** 아래 YAML 블록은 `MessagingConfigurationBindingTest`가 이 파일에서 직접
|
||||||
|
> 읽어 컨텍스트에 올린다. 문서가 설명하는 모양이 곧 바인딩되는 모양이라는 뜻이고, 문서를 고치면서
|
||||||
|
> 코드를 고치지 않으면 테스트가 깨진다. 이전 판은 destination·broker·security 세 섹션을 설명했지만
|
||||||
|
> 어떤 binder도 그것을 읽지 않았다 — 문서대로 설정한 배포는 아무것도 바뀌지 않았고 아무 말도 듣지
|
||||||
|
> 못했다 (MSG-008).
|
||||||
|
|
||||||
## Destination profile
|
## Destination profile
|
||||||
|
|
||||||
@@ -36,7 +41,7 @@ app:
|
|||||||
idempotent: true
|
idempotent: true
|
||||||
consumer:
|
consumer:
|
||||||
group: order-projection
|
group: order-projection
|
||||||
concurrency: 6
|
concurrency: 1 # DESTINATION 순서를 요구하면 1이어야 한다
|
||||||
max-in-flight-per-ordering-unit: 1
|
max-in-flight-per-ordering-unit: 1
|
||||||
prefetch: 16
|
prefetch: 16
|
||||||
handler-timeout: 30s
|
handler-timeout: 30s
|
||||||
@@ -57,10 +62,21 @@ app:
|
|||||||
max-bytes: 1048576
|
max-bytes: 1048576
|
||||||
claim-check-threshold-bytes: 1048576
|
claim-check-threshold-bytes: 1048576
|
||||||
key-resolver-configured: true
|
key-resolver-configured: true
|
||||||
production: true
|
production: false
|
||||||
topology-auto-create: false
|
topology-auto-create: false
|
||||||
|
order-events-dlq:
|
||||||
|
broker: kafka-primary
|
||||||
|
kind: WORK_QUEUE
|
||||||
|
physical:
|
||||||
|
topic: order.events.v1.dlt
|
||||||
|
schema:
|
||||||
|
message-types: [order.created]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`dlq.destination`이 가리키는 destination도 선언되어야 한다. 선언되지 않은 이름은 부팅 실패이며,
|
||||||
|
메시지가 갈 곳 없는 DLQ 설정이 조용히 통과하지 않는다. `retry.destination`과 `dlq.destination`이
|
||||||
|
섞여 만드는 순환(A의 retry가 B로, B의 dlq가 A로)도 하나의 그래프로 검사되어 경로와 함께 거절된다.
|
||||||
|
|
||||||
## 기본값
|
## 기본값
|
||||||
|
|
||||||
| 설정 | 기본값 | 근거 |
|
| 설정 | 기본값 | 근거 |
|
||||||
@@ -81,8 +97,17 @@ app:
|
|||||||
| Outbox polling | 500ms | |
|
| Outbox polling | 500ms | |
|
||||||
| metric dimension 상한 | 200 | cardinality 폭발 방지 |
|
| metric dimension 상한 | 200 | cardinality 폭발 방지 |
|
||||||
|
|
||||||
|
`schema.codec`은 `application/json`, `schema.compatibility`는 `BACKWARD_TRANSITIVE`,
|
||||||
|
`guarantees.delivery`는 `AT_LEAST_ONCE`, `retry.mode`는 `NONE`이 기본값이다. 자동 retry가 기본으로
|
||||||
|
꺼져 있는 이유는 순서를 흐트러뜨리거나 비멱등 side effect를 두 번 실행하는 retry가 눈에 보이는
|
||||||
|
실패보다 나쁘기 때문이다.
|
||||||
|
|
||||||
## Broker profile
|
## Broker profile
|
||||||
|
|
||||||
|
브로커는 `app.messaging.brokers` 아래에 한 번만 기술한다. `type`이 어느 계열의 설정이 적용되는지
|
||||||
|
결정하며, 다른 계열의 키(Kafka 항목의 `prefetch` 같은)는 무시되지 않고 부팅 실패로 거절된다 —
|
||||||
|
무시하면 그 줄을 쓴 사람은 무언가가 적용됐다고 믿게 된다.
|
||||||
|
|
||||||
### Kafka
|
### Kafka
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -92,15 +117,16 @@ app:
|
|||||||
kafka-primary:
|
kafka-primary:
|
||||||
type: kafka
|
type: kafka
|
||||||
stable: true
|
stable: true
|
||||||
production: true
|
production: false
|
||||||
bootstrap-servers: [broker-1:9093, broker-2:9093]
|
bootstrap-servers: [broker-1:9093, broker-2:9093]
|
||||||
enable-idempotence: true # stable에서 필수
|
enable-idempotence: true # stable에서 필수
|
||||||
acks: all # stable에서 필수
|
acks: all # stable에서 필수
|
||||||
max-in-flight-requests-per-connection: 5 # 최대 5
|
max-in-flight-requests-per-connection: 5 # 최대 5
|
||||||
delivery-timeout: 30s
|
delivery-timeout: 30s
|
||||||
enable-auto-commit: false # 항상 금지
|
enable-auto-commit: false # 항상 금지
|
||||||
tls-enabled: true # production 필수
|
consumer-group: order-projection
|
||||||
authentication-enabled: true # production 필수
|
tls-enabled: false # production이면 필수
|
||||||
|
authentication-enabled: false # production이면 필수
|
||||||
```
|
```
|
||||||
|
|
||||||
### RabbitMQ
|
### RabbitMQ
|
||||||
@@ -112,7 +138,7 @@ app:
|
|||||||
rabbit-primary:
|
rabbit-primary:
|
||||||
type: rabbitmq
|
type: rabbitmq
|
||||||
stable: true
|
stable: true
|
||||||
production: true
|
production: false
|
||||||
addresses: [rabbit-1:5671]
|
addresses: [rabbit-1:5671]
|
||||||
publisher-confirms: true # stable에서 필수
|
publisher-confirms: true # stable에서 필수
|
||||||
publisher-returns: true # stable에서 필수
|
publisher-returns: true # stable에서 필수
|
||||||
@@ -121,10 +147,14 @@ app:
|
|||||||
auto-ack: false # 항상 금지
|
auto-ack: false # 항상 금지
|
||||||
prefetch: 16
|
prefetch: 16
|
||||||
quorum-queues: true # durable work queue 필수
|
quorum-queues: true # durable work queue 필수
|
||||||
tls-enabled: true
|
tls-enabled: false
|
||||||
authentication-enabled: true
|
authentication-enabled: false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`production: true`인 브로커는 `tls-enabled`와 `authentication-enabled`가 모두 참이어야 하고,
|
||||||
|
그렇지 않으면 `KafkaProfileValidator` / `RabbitProfileValidator`가 부팅을 거절한다. 위 예시가
|
||||||
|
`production: false`인 것은 이 페이지가 그대로 실행되는 fixture이기 때문이며, 실 배포는 셋 다 참이다.
|
||||||
|
|
||||||
## 보안
|
## 보안
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -142,6 +172,14 @@ app:
|
|||||||
administrable: []
|
administrable: []
|
||||||
```
|
```
|
||||||
|
|
||||||
|
키는 `app.messaging.brokers`에 선언된 브로커 이름과 같아야 한다. `tls-enabled`와 `production`은
|
||||||
|
브로커 쪽에만 있고 여기에 중복되지 않는다 — 하나의 브로커가 두 곳에서 기술되면 두 값이 어긋나는
|
||||||
|
날이 오고, 어느 쪽이 이기는지는 아무도 모른다.
|
||||||
|
|
||||||
|
`credential-id`는 이름일 뿐이고 자격 증명 자체가 아니다. 실제 재료는 `CredentialProvider`가
|
||||||
|
연결 시점에 해석하므로, 설정 덤프나 힙 덤프에서 나오는 것은 이름뿐이다. producer와 consumer는
|
||||||
|
서로 다른 `credential-id`를 써야 하며, 같으면 부팅에 실패한다.
|
||||||
|
|
||||||
## Experimental / Optional
|
## Experimental / Optional
|
||||||
|
|
||||||
기본값은 전부 `false`다.
|
기본값은 전부 `false`다.
|
||||||
@@ -168,3 +206,18 @@ app:
|
|||||||
```
|
```
|
||||||
|
|
||||||
`per-destination-limit > global-limit`이면 global limit이 limit이 아니게 되므로 부팅에 실패한다.
|
`per-destination-limit > global-limit`이면 global limit이 limit이 아니게 되므로 부팅에 실패한다.
|
||||||
|
|
||||||
|
## 바인딩되지 않는 키
|
||||||
|
|
||||||
|
섹션은 바인딩되는데 그 안의 키 하나가 오타인 경우는 접두사 오타와 달리 조용하다 — 섹션은 붙고,
|
||||||
|
플랫폼은 뜨고, 바꾸러 온 그 설정만 적용되지 않는다. `MessagingConfigurationKeyValidator`가
|
||||||
|
`app.messaging.destinations|brokers|security` 아래의 모든 키를 settings 레코드에서 파생한 목록과
|
||||||
|
대조하고, 없는 키는 그 키 이름을 담아 부팅을 거절한다.
|
||||||
|
|
||||||
|
허용 키 목록은 이 문서가 아니라 레코드에서 나온다. 문서에 목록을 적으면 필드가 추가된 날 그
|
||||||
|
목록이 틀리고, 오타를 잡으라고 만든 검사가 정상 필드를 거절하게 된다.
|
||||||
|
|
||||||
|
환경변수(`APP_MESSAGING_...`)는 이 검사의 대상이 아니다. `APP_MESSAGING_DESTINATIONS_ORDER_EVENTS_
|
||||||
|
CONSUMER_PREFETCH`에서 entry 이름과 leaf를 가르는 밑줄은 둘 안에 있는 밑줄과 구별되지 않으므로,
|
||||||
|
되돌려 쪼개려면 추측해야 한다. 여기서의 추측은 정상 배포를 거절하는 쪽으로 틀리며, 그것은 배포
|
||||||
|
매니페스트에 손으로 적어야 하는 변수에서 오타 하나를 놓치는 것보다 나쁘다.
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
플랫폼이 **무엇을 보장하는지**와 **무엇을 보장하지 않는지**를 브로커별로 고정한다.
|
플랫폼이 **무엇을 보장하는지**와 **무엇을 보장하지 않는지**를 브로커별로 고정한다.
|
||||||
여기 없는 조합은 지원되지 않는다.
|
여기 없는 조합은 지원되지 않는다.
|
||||||
|
|
||||||
|
> **등급은 증거를 따른다.** `CompatibilityMatrix.Entry.hasLiveBrokerCertification()`은 선언된
|
||||||
|
> boolean이 아니라 `CertifiedEvidence`가 가진 레인 증거에서 파생된다. RabbitMQ가 Stable에서 내려온
|
||||||
|
> 이유가 이것이다 — 어댑터는 공유 contract 7개를 통과하고 `RabbitBrokerIT`가 실 컨테이너에서 정상
|
||||||
|
> 경로를 돌리지만, 이 저장소의 Stable 기준인 **장애 시나리오 증거**가 하나도 없다. 레인이 생겨
|
||||||
|
> 증거를 내면 등급은 코드 수정 없이 따라 올라간다.
|
||||||
|
|
||||||
> **인증 근거.** 이 표의 버전은 이 저장소의 컨테이너 레인이 실제로 실행한 이미지다. 이전 판은
|
> **인증 근거.** 이 표의 버전은 이 저장소의 컨테이너 레인이 실제로 실행한 이미지다. 이전 판은
|
||||||
> Kafka 4.2/4.3을 선언했지만 fixture는 `apache/kafka:4.1.0`, lockfile client는 4.1.1이었다 — 표와
|
> Kafka 4.2/4.3을 선언했지만 fixture는 `apache/kafka:4.1.0`, lockfile client는 4.1.1이었다 — 표와
|
||||||
> 코드 상수가 서로 일치했을 뿐 어느 쪽도 실행된 적이 없었다. 장애 시나리오 커버리지도 마찬가지로
|
> 코드 상수가 서로 일치했을 뿐 어느 쪽도 실행된 적이 없었다. 장애 시나리오 커버리지도 마찬가지로
|
||||||
@@ -25,7 +31,7 @@
|
|||||||
| 브로커 | 등급 | 인증 기준 | Stable 기능 | 제한 |
|
| 브로커 | 등급 | 인증 기준 | Stable 기능 | 제한 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| Kafka | Stable | 4.1.x | producer idempotence, consumer group, batch, pause/resume, replay, transaction capability | Share Group은 Experimental |
|
| Kafka | Stable | 4.1.x | producer idempotence, consumer group, batch, pause/resume, replay, transaction capability | Share Group은 Experimental |
|
||||||
| RabbitMQ | Stable | 4.3.x | exchange/routing, publisher confirm, mandatory return, manual ACK, quorum queue, retry queue, DLQ | stream 및 특수 plugin 미지원 |
|
| RabbitMQ | Experimental | 4.3.x | exchange/routing, publisher confirm, mandatory return, manual ACK, quorum queue, retry queue, DLQ | 장애 시나리오 레인 미실행 — 증거 없음. stream 및 특수 plugin 미지원 |
|
||||||
| Pulsar | Experimental | 4.0 LTS + 4.2 | typed publish/consume, Shared, Key_Shared, schema | transaction 미승격, 기본 비활성 |
|
| Pulsar | Experimental | 4.0 LTS + 4.2 | typed publish/consume, Shared, Key_Shared, schema | transaction 미승격, 기본 비활성 |
|
||||||
| NATS JetStream | Experimental | 2.14.x | stream, durable consumer, explicit ACK, dedupe, replay | native DLQ 없음(플랫폼이 대행), 기본 비활성 |
|
| NATS JetStream | Experimental | 2.14.x | stream, durable consumer, explicit ACK, dedupe, replay | native DLQ 없음(플랫폼이 대행), 기본 비활성 |
|
||||||
| Artemis/JMS | Extension | 범위 밖 | adapter SPI만 | 별도 ADR + Contract Suite 통과 필요 |
|
| Artemis/JMS | Extension | 범위 밖 | adapter SPI만 | 별도 ADR + Contract Suite 통과 필요 |
|
||||||
@@ -86,11 +92,15 @@ Kafka와 RabbitMQ가 동일한 7개 테스트를 변경 없이 통과한다. 결
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `KafkaBrokerIT` | `acks=all`이 실제 replication 증거를 만든다 / 잘못된 토픽은 `REJECTED` / 발행-소비 왕복에서 identity 보존 및 contiguous commit |
|
| `KafkaBrokerIT` | `acks=all`이 실제 replication 증거를 만든다 / 잘못된 토픽은 `REJECTED` / 발행-소비 왕복에서 identity 보존 및 contiguous commit |
|
||||||
| `KafkaAmbiguityChaosIT` | 브로커를 `docker pause`로 멈춘 상태의 publish가 **`AMBIGUOUS`** 로 보고된다 (broker acceptance 없음, confirmation level `NONE`, 비-retryable) |
|
| `KafkaAmbiguityChaosIT` | 브로커를 `docker pause`로 멈춘 상태의 publish가 **`AMBIGUOUS`** 로 보고된다 (broker acceptance 없음, confirmation level `NONE`, 비-retryable) |
|
||||||
|
| `KafkaBrokerCertificationIT` | 인증 레인. Toxiproxy를 broker 앞에 두고 connection cut / confirm 유실 / 지연 / settlement 유실을 각각 주입하고, 통과한 시나리오마다 `BrokerCertificationEvidence` 한 줄을 manifest에 쓴다 |
|
||||||
| `RabbitBrokerIT` | exchange가 confirm했는데 어떤 큐에도 바인딩되지 않은 publish가 **`REJECTED` + `UNROUTABLE`** 로 보고된다 |
|
| `RabbitBrokerIT` | exchange가 confirm했는데 어떤 큐에도 바인딩되지 않은 publish가 **`REJECTED` + `UNROUTABLE`** 로 보고된다 |
|
||||||
| `OutboxPostgresIT` | 롤백된 트랜잭션은 발행 가능한 행을 남기지 않는다 / `SKIP LOCKED` lease가 두 relay를 분리한다 / ambiguous 행이 같은 `messageId`로 재클레임된다 |
|
| `OutboxPostgresIT` | 롤백된 트랜잭션은 발행 가능한 행을 남기지 않는다 / `SKIP LOCKED` lease가 두 relay를 분리한다 / ambiguous 행이 같은 `messageId`로 재클레임된다 |
|
||||||
| `InboxPostgresIT` | 재전달이 side effect를 두 번 적용하지 않는다 / 롤백은 예약도 되돌린다 |
|
| `InboxPostgresIT` | 재전달이 side effect를 두 번 적용하지 않는다 / 롤백은 예약도 되돌린다 |
|
||||||
|
|
||||||
Docker가 없으면 `DockerAvailability` 가드로 skip되며, 이 표의 항목은 그때 **검증되지 않은 것**으로 취급한다.
|
Docker가 없으면 `DockerAvailability` 가드로 skip되며, 이 표의 항목은 그때 **검증되지 않은 것**으로 취급한다.
|
||||||
|
`KafkaBrokerCertificationIT`만 예외다 — 인증 레인은 가드를 달지 않고 Docker가 없으면 실패한다. skip하는
|
||||||
|
레인은 아무도 켜지 않은 브로커에 대해 성공을 보고하기 때문이다. 그래서 이 레인은 `test`에서 태그로
|
||||||
|
제외되고 `messagingCertificationTest`로만 실행된다.
|
||||||
|
|
||||||
### 3. 장애 시나리오 커버리지 (`BrokerFailureMatrix`)
|
### 3. 장애 시나리오 커버리지 (`BrokerFailureMatrix`)
|
||||||
|
|
||||||
@@ -109,6 +119,18 @@ Docker가 없으면 `DockerAvailability` 가드로 skip되며, 이 표의 항목
|
|||||||
커버해야 하고, Experimental 어댑터는 `LIVE_BROKER` 커버리지를 주장할 수 없다. 커버리지는 *능력*이 아니라
|
커버해야 하고, Experimental 어댑터는 `LIVE_BROKER` 커버리지를 주장할 수 없다. 커버리지는 *능력*이 아니라
|
||||||
*무엇을 실제로 돌렸는지*의 기록이다.
|
*무엇을 실제로 돌렸는지*의 기록이다.
|
||||||
|
|
||||||
|
**증거의 출처.** `CertifiedEvidence`는 더 이상 손으로 쓴 목록이 아니라
|
||||||
|
`messaging-testkit/src/main/resources/messaging/broker-certification-evidence.jsonl`을 읽는다. 그 파일은
|
||||||
|
`messagingCertificationTest` 레인이 실제 Kafka 컨테이너에 장애를 주입하며 만들어낸 출력이고,
|
||||||
|
`verifyMessagingCertificationEvidence`가 커밋된 manifest와 이번 실행의 출력을 대조해 다르면 빌드를
|
||||||
|
실패시킨다. 즉 **manifest를 손으로 고치면 게이트가 깨지고, 레인을 돌리면 manifest가 다시 쓰인다.**
|
||||||
|
|
||||||
|
오늘 Kafka가 가진 증거는 `connection-cut-after-write` · `confirm-timeout` · `high-latency` ·
|
||||||
|
`settlement-lost` 네 개다. `connection-refused`는 남은 gap이며 그 이유가 있다 — Kafka producer는 연결
|
||||||
|
존재 여부를 알기 전에 레코드를 버퍼에 넣으므로, 연결 거부는 전송에 대해 아무것도 증명하지 못하는
|
||||||
|
delivery timeout으로 나타난다. 이를 `REJECTED`로 보고하는 것은 이 플랫폼이 금지한 추측이므로,
|
||||||
|
시나리오는 `CertifiedEvidence.knownGaps`가 이름으로 들고 있는 미커버 항목으로 남는다.
|
||||||
|
|
||||||
### 실 브로커가 실제로 잡아낸 결함
|
### 실 브로커가 실제로 잡아낸 결함
|
||||||
|
|
||||||
이 스위트들은 장식이 아니다. 작성 과정에서 결정적 테스트가 통과하는데 실 인프라에서 실패한
|
이 스위트들은 장식이 아니다. 작성 과정에서 결정적 테스트가 통과하는데 실 인프라에서 실패한
|
||||||
|
|||||||
@@ -26,8 +26,12 @@ dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.Notification
|
|||||||
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationPlatformMode
|
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationPlatformMode
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationPlatformSettings
|
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationPlatformSettings
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationProviderAssembly
|
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationProviderAssembly
|
||||||
|
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationSecretRequirements
|
||||||
|
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationSmtpProviderConfig
|
||||||
|
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationSmtpSettings
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.ProviderRuntimeAssembler
|
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.ProviderRuntimeAssembler
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.ProviderType
|
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.ProviderType
|
||||||
|
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.SmtpProviderRuntimeAssembler
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.dispatch.AttemptPermit
|
dev.caskeleton.adapter.outbound.notification.platform.dispatch.AttemptPermit
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.dispatch.CapabilityReconciliationGateway
|
dev.caskeleton.adapter.outbound.notification.platform.dispatch.CapabilityReconciliationGateway
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.dispatch.ConfiguredProfileCatalog
|
dev.caskeleton.adapter.outbound.notification.platform.dispatch.ConfiguredProfileCatalog
|
||||||
@@ -56,6 +60,7 @@ dev.caskeleton.adapter.outbound.notification.platform.observation.LoggingNotific
|
|||||||
dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationHealthReporter
|
dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationHealthReporter
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationHealthSnapshot
|
dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationHealthSnapshot
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationServingThresholds
|
dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationServingThresholds
|
||||||
|
dev.caskeleton.adapter.outbound.notification.platform.provider.EmailAttachments
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.provider.ProviderResults
|
dev.caskeleton.adapter.outbound.notification.platform.provider.ProviderResults
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.provider.UnconfiguredAttachmentResolver
|
dev.caskeleton.adapter.outbound.notification.platform.provider.UnconfiguredAttachmentResolver
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.provider.apns.ApnsFailureClassifier
|
dev.caskeleton.adapter.outbound.notification.platform.provider.apns.ApnsFailureClassifier
|
||||||
@@ -89,6 +94,7 @@ dev.caskeleton.adapter.outbound.notification.platform.provider.ses.SesRequestMap
|
|||||||
dev.caskeleton.adapter.outbound.notification.platform.provider.ses.SesSuppressionUpdater
|
dev.caskeleton.adapter.outbound.notification.platform.provider.ses.SesSuppressionUpdater
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.provider.ses.SnsCertificateProvider
|
dev.caskeleton.adapter.outbound.notification.platform.provider.ses.SnsCertificateProvider
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.provider.ses.SnsSignatureVerifier
|
dev.caskeleton.adapter.outbound.notification.platform.provider.ses.SnsSignatureVerifier
|
||||||
|
dev.caskeleton.adapter.outbound.notification.platform.provider.smtp.JavaMailSenderSmtpDispatch
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.provider.smtp.SmtpDispatch
|
dev.caskeleton.adapter.outbound.notification.platform.provider.smtp.SmtpDispatch
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.provider.smtp.SmtpDispatchException
|
dev.caskeleton.adapter.outbound.notification.platform.provider.smtp.SmtpDispatchException
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.provider.smtp.SmtpFailureClassifier
|
dev.caskeleton.adapter.outbound.notification.platform.provider.smtp.SmtpFailureClassifier
|
||||||
@@ -122,6 +128,7 @@ dev.caskeleton.adapter.outbound.notification.platform.reactor.ReactorContextBrid
|
|||||||
dev.caskeleton.adapter.outbound.notification.platform.reactor.ReactorNotificationOrchestrator
|
dev.caskeleton.adapter.outbound.notification.platform.reactor.ReactorNotificationOrchestrator
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.security.AesGcmCallbackPayloadProtection
|
dev.caskeleton.adapter.outbound.notification.platform.security.AesGcmCallbackPayloadProtection
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.security.AesGcmContactPointProtector
|
dev.caskeleton.adapter.outbound.notification.platform.security.AesGcmContactPointProtector
|
||||||
|
dev.caskeleton.adapter.outbound.notification.platform.security.AesGcmNotificationPayloadProtection
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.security.CredentialGeneration
|
dev.caskeleton.adapter.outbound.notification.platform.security.CredentialGeneration
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.security.HmacProviderRequestIdHasher
|
dev.caskeleton.adapter.outbound.notification.platform.security.HmacProviderRequestIdHasher
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.security.ProviderCredentialManager
|
dev.caskeleton.adapter.outbound.notification.platform.security.ProviderCredentialManager
|
||||||
@@ -137,6 +144,7 @@ dev.caskeleton.adapter.outbound.notification.platform.template.NotificationTempl
|
|||||||
dev.caskeleton.adapter.outbound.notification.platform.template.PlaceholderTemplateEngine
|
dev.caskeleton.adapter.outbound.notification.platform.template.PlaceholderTemplateEngine
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.template.Sha256MessageDigestAdapter
|
dev.caskeleton.adapter.outbound.notification.platform.template.Sha256MessageDigestAdapter
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.template.TemplateSlotMode
|
dev.caskeleton.adapter.outbound.notification.platform.template.TemplateSlotMode
|
||||||
|
dev.caskeleton.adapter.outbound.notification.platform.template.TemplateSlotPolicy
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.template.ThymeleafNotificationRenderer
|
dev.caskeleton.adapter.outbound.notification.platform.template.ThymeleafNotificationRenderer
|
||||||
dev.caskeleton.adapter.outbound.notification.platform.template.ThymeleafStringTemplateEngine
|
dev.caskeleton.adapter.outbound.notification.platform.template.ThymeleafStringTemplateEngine
|
||||||
dev.caskeleton.adapter.outbound.notification.provider.AttemptCorrelationId
|
dev.caskeleton.adapter.outbound.notification.provider.AttemptCorrelationId
|
||||||
@@ -384,6 +392,7 @@ dev.caskeleton.application.notification.platform.callback.ProviderEventProjector
|
|||||||
dev.caskeleton.application.notification.platform.callback.ProviderEventRecord
|
dev.caskeleton.application.notification.platform.callback.ProviderEventRecord
|
||||||
dev.caskeleton.application.notification.platform.callback.ProviderEventRecordId
|
dev.caskeleton.application.notification.platform.callback.ProviderEventRecordId
|
||||||
dev.caskeleton.application.notification.platform.callback.ProviderEventSource
|
dev.caskeleton.application.notification.platform.callback.ProviderEventSource
|
||||||
|
dev.caskeleton.application.notification.platform.callback.ProviderRequestIdHash
|
||||||
dev.caskeleton.application.notification.platform.callback.StandardDeliveryProjector
|
dev.caskeleton.application.notification.platform.callback.StandardDeliveryProjector
|
||||||
dev.caskeleton.application.notification.platform.callback.SuppressionFacts
|
dev.caskeleton.application.notification.platform.callback.SuppressionFacts
|
||||||
dev.caskeleton.application.notification.platform.callback.VerifiedCallback
|
dev.caskeleton.application.notification.platform.callback.VerifiedCallback
|
||||||
@@ -400,6 +409,7 @@ dev.caskeleton.application.notification.platform.contact.LegacyFcmRegistrationTo
|
|||||||
dev.caskeleton.application.notification.platform.contact.MobilePushTarget
|
dev.caskeleton.application.notification.platform.contact.MobilePushTarget
|
||||||
dev.caskeleton.application.notification.platform.contact.PhoneNumber
|
dev.caskeleton.application.notification.platform.contact.PhoneNumber
|
||||||
dev.caskeleton.application.notification.platform.contact.WebPushSubscriptionValue
|
dev.caskeleton.application.notification.platform.contact.WebPushSubscriptionValue
|
||||||
|
dev.caskeleton.application.notification.platform.dispatch.AcceptNotificationApplicationUseCase
|
||||||
dev.caskeleton.application.notification.platform.dispatch.ApplicationReceiptServiceImpl
|
dev.caskeleton.application.notification.platform.dispatch.ApplicationReceiptServiceImpl
|
||||||
dev.caskeleton.application.notification.platform.dispatch.AttachmentIntegrityGuard
|
dev.caskeleton.application.notification.platform.dispatch.AttachmentIntegrityGuard
|
||||||
dev.caskeleton.application.notification.platform.dispatch.CancelNotificationApplicationUseCase
|
dev.caskeleton.application.notification.platform.dispatch.CancelNotificationApplicationUseCase
|
||||||
@@ -430,6 +440,7 @@ dev.caskeleton.application.notification.platform.dispatch.PolicyRoutePlanner
|
|||||||
dev.caskeleton.application.notification.platform.dispatch.ProviderDispatchGatewayPort
|
dev.caskeleton.application.notification.platform.dispatch.ProviderDispatchGatewayPort
|
||||||
dev.caskeleton.application.notification.platform.dispatch.ProviderProfileCatalogPort
|
dev.caskeleton.application.notification.platform.dispatch.ProviderProfileCatalogPort
|
||||||
dev.caskeleton.application.notification.platform.dispatch.ProviderRequestIdHasherPort
|
dev.caskeleton.application.notification.platform.dispatch.ProviderRequestIdHasherPort
|
||||||
|
dev.caskeleton.application.notification.platform.dispatch.PublishNotificationTemplateApplicationUseCase
|
||||||
dev.caskeleton.application.notification.platform.dispatch.RecipientDeliveryRecord
|
dev.caskeleton.application.notification.platform.dispatch.RecipientDeliveryRecord
|
||||||
dev.caskeleton.application.notification.platform.dispatch.RecipientDeliveryStorePort
|
dev.caskeleton.application.notification.platform.dispatch.RecipientDeliveryStorePort
|
||||||
dev.caskeleton.application.notification.platform.dispatch.RecipientLease
|
dev.caskeleton.application.notification.platform.dispatch.RecipientLease
|
||||||
@@ -499,12 +510,16 @@ dev.caskeleton.application.notification.platform.policy.SuppressionReason
|
|||||||
dev.caskeleton.application.notification.platform.policy.SuppressionScope
|
dev.caskeleton.application.notification.platform.policy.SuppressionScope
|
||||||
dev.caskeleton.application.notification.platform.policy.SuppressionSource
|
dev.caskeleton.application.notification.platform.policy.SuppressionSource
|
||||||
dev.caskeleton.application.notification.platform.policy.SuppressionStorePort
|
dev.caskeleton.application.notification.platform.policy.SuppressionStorePort
|
||||||
|
dev.caskeleton.application.notification.platform.port.in.AcceptNotificationCommand
|
||||||
|
dev.caskeleton.application.notification.platform.port.in.AcceptNotificationUseCase
|
||||||
dev.caskeleton.application.notification.platform.port.in.CancelNotificationCommand
|
dev.caskeleton.application.notification.platform.port.in.CancelNotificationCommand
|
||||||
dev.caskeleton.application.notification.platform.port.in.CancelNotificationUseCase
|
dev.caskeleton.application.notification.platform.port.in.CancelNotificationUseCase
|
||||||
dev.caskeleton.application.notification.platform.port.in.GetNotificationQuery
|
dev.caskeleton.application.notification.platform.port.in.GetNotificationQuery
|
||||||
dev.caskeleton.application.notification.platform.port.in.GetNotificationUseCase
|
dev.caskeleton.application.notification.platform.port.in.GetNotificationUseCase
|
||||||
dev.caskeleton.application.notification.platform.port.in.IngestProviderCallbackCommand
|
dev.caskeleton.application.notification.platform.port.in.IngestProviderCallbackCommand
|
||||||
dev.caskeleton.application.notification.platform.port.in.IngestProviderCallbackUseCase
|
dev.caskeleton.application.notification.platform.port.in.IngestProviderCallbackUseCase
|
||||||
|
dev.caskeleton.application.notification.platform.port.in.PublishNotificationTemplateCommand
|
||||||
|
dev.caskeleton.application.notification.platform.port.in.PublishNotificationTemplateUseCase
|
||||||
dev.caskeleton.application.notification.platform.port.in.ScheduleNotificationCommand
|
dev.caskeleton.application.notification.platform.port.in.ScheduleNotificationCommand
|
||||||
dev.caskeleton.application.notification.platform.port.in.ScheduleNotificationUseCase
|
dev.caskeleton.application.notification.platform.port.in.ScheduleNotificationUseCase
|
||||||
dev.caskeleton.application.notification.platform.port.in.SubmitNotificationCommand
|
dev.caskeleton.application.notification.platform.port.in.SubmitNotificationCommand
|
||||||
@@ -539,6 +554,8 @@ dev.caskeleton.application.notification.platform.push.ReceiptKind
|
|||||||
dev.caskeleton.application.notification.platform.push.ReceiptResult
|
dev.caskeleton.application.notification.platform.push.ReceiptResult
|
||||||
dev.caskeleton.application.notification.platform.security.AccessContext
|
dev.caskeleton.application.notification.platform.security.AccessContext
|
||||||
dev.caskeleton.application.notification.platform.security.ContactPointProtector
|
dev.caskeleton.application.notification.platform.security.ContactPointProtector
|
||||||
|
dev.caskeleton.application.notification.platform.security.NotificationPayloadProtection
|
||||||
|
dev.caskeleton.application.notification.platform.security.NotificationPayloadUnreadableException
|
||||||
dev.caskeleton.application.notification.platform.security.NotificationRedactor
|
dev.caskeleton.application.notification.platform.security.NotificationRedactor
|
||||||
dev.caskeleton.application.notification.platform.security.ProtectedContactPoint
|
dev.caskeleton.application.notification.platform.security.ProtectedContactPoint
|
||||||
dev.caskeleton.application.notification.platform.security.SafeDiagnosticContext
|
dev.caskeleton.application.notification.platform.security.SafeDiagnosticContext
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ still waiting on gets claimed by a second worker, and the recipient receives the
|
|||||||
| `callbacks.enabled` | `APP_NOTIFICATION_PLATFORM_CALLBACKS_ENABLED` | `false` | boolean |
|
| `callbacks.enabled` | `APP_NOTIFICATION_PLATFORM_CALLBACKS_ENABLED` | `false` | boolean |
|
||||||
| `callbacks.max-body-bytes` | `APP_NOTIFICATION_PLATFORM_CALLBACK_MAX_BODY_BYTES` | `65508` | 1..65508 |
|
| `callbacks.max-body-bytes` | `APP_NOTIFICATION_PLATFORM_CALLBACK_MAX_BODY_BYTES` | `65508` | 1..65508 |
|
||||||
| `callbacks.replay-skew` | `APP_NOTIFICATION_PLATFORM_CALLBACK_REPLAY_SKEW` | `5m` | positive |
|
| `callbacks.replay-skew` | `APP_NOTIFICATION_PLATFORM_CALLBACK_REPLAY_SKEW` | `5m` | positive |
|
||||||
|
| `callbacks.trusted-proxies` | `APP_NOTIFICATION_PLATFORM_CALLBACK_TRUSTED_PROXIES` | *(empty)* | CSV of peer addresses |
|
||||||
|
|
||||||
|
여러 provider가 요청 URL에 서명하므로, 그 URL을 잘못 재구성하면 정상 webhook이 전부 서명 실패가 된다.
|
||||||
|
`trusted-proxies`가 비어 있으면 forwarded 헤더를 **믿지 않고** 컨테이너가 관측한 값을 쓴다. 무조건 믿으면
|
||||||
|
아무 호출자나 자기 서명이 검증될 URL을 고를 수 있어 서명 자체가 무의미해진다. 로드밸런서 뒤에 있는 배포는
|
||||||
|
그 peer를 명시한다.
|
||||||
|
|
||||||
65508 is not a round number by accident: it is the ciphertext column's 65536 bytes minus the AES-GCM
|
65508 is not a round number by accident: it is the ciphertext column's 65536 bytes minus the AES-GCM
|
||||||
nonce and tag. A larger configured value would pass every check above the database and fail the
|
nonce and tag. A larger configured value would pass every check above the database and fail the
|
||||||
@@ -69,6 +75,43 @@ them, because the keys are deployment-chosen; supply them as YAML or as
|
|||||||
A profile pins provider type, environment, credential profile, timeouts, concurrency and rate limit.
|
A profile pins provider type, environment, credential profile, timeouts, concurrency and rate limit.
|
||||||
Sender identity and credential profile are separate concerns.
|
Sender identity and credential profile are separate concerns.
|
||||||
|
|
||||||
|
## SMTP relay
|
||||||
|
|
||||||
|
The one provider profile the template ships, off. A deployment that wants the common case sets
|
||||||
|
`APP_NOTIFICATION_PLATFORM_SMTP_ENABLED=true` and the relay address; one that wants a different
|
||||||
|
profile id or a second family declares it in its own YAML instead.
|
||||||
|
|
||||||
|
The profile and the relay are separate tables below because they answer different questions. The
|
||||||
|
profile says *which* provider serves EMAIL and under what limits; the relay says *what the transport
|
||||||
|
is*. Host, port and credentials are not here at all — they stay `spring.mail.*`, because Spring
|
||||||
|
already owns them and a second spelling would be a second thing to keep in step.
|
||||||
|
|
||||||
|
| Property | Environment variable | Default | Bound |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `providers.smtp.enabled` | `APP_NOTIFICATION_PLATFORM_SMTP_ENABLED` | `false` | boolean |
|
||||||
|
| `providers.smtp.primary-for-channel` | `APP_NOTIFICATION_PLATFORM_SMTP_PRIMARY` | `true` | boolean; exactly one primary per channel |
|
||||||
|
| `providers.smtp.environment` | `APP_NOTIFICATION_PLATFORM_SMTP_ENVIRONMENT` | `local` | required when enabled |
|
||||||
|
| `providers.smtp.credential-profile` | `APP_NOTIFICATION_PLATFORM_SMTP_CREDENTIAL_PROFILE` | `default` | resolved through `SecretMaterialProvider`, never inline material |
|
||||||
|
| `providers.smtp.timeout` | `APP_NOTIFICATION_PLATFORM_SMTP_TIMEOUT` | `10s` | positive, finite |
|
||||||
|
| `providers.smtp.max-concurrency` | `APP_NOTIFICATION_PLATFORM_SMTP_MAX_CONCURRENCY` | `4` | positive |
|
||||||
|
| `providers.smtp.rate-per-second` | `APP_NOTIFICATION_PLATFORM_SMTP_RATE_PER_SECOND` | `10` | positive |
|
||||||
|
|
||||||
|
| Property | Environment variable | Default | Bound |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `smtp.tls-mode` | `APP_NOTIFICATION_PLATFORM_SMTP_TLS_MODE` | `STARTTLS_REQUIRED` | `STARTTLS_REQUIRED` or `IMPLICIT_TLS` |
|
||||||
|
| `smtp.sender-identity` | `APP_NOTIFICATION_PLATFORM_SMTP_SENDER_IDENTITY` | `no-reply@example.invalid` | address |
|
||||||
|
| `smtp.connect-timeout` | `APP_NOTIFICATION_PLATFORM_SMTP_CONNECT_TIMEOUT` | `5s` | positive, finite |
|
||||||
|
| `smtp.read-timeout` | `APP_NOTIFICATION_PLATFORM_SMTP_READ_TIMEOUT` | `10s` | positive, finite |
|
||||||
|
| `smtp.write-timeout` | `APP_NOTIFICATION_PLATFORM_SMTP_WRITE_TIMEOUT` | `10s` | positive, finite |
|
||||||
|
| `smtp.max-concurrency` | `APP_NOTIFICATION_PLATFORM_SMTP_DISPATCH_CONCURRENCY` | `4` | positive |
|
||||||
|
|
||||||
|
The TLS mode enum has no plaintext member. An unencrypted relay is refused by construction rather
|
||||||
|
than by a validator somebody has to remember to run.
|
||||||
|
|
||||||
|
The default sender is an RFC 2606 reserved domain that resolves nowhere, so a deployment that forgot
|
||||||
|
to set one produces a traceable bounce instead of mail apparently sent from an address it does not
|
||||||
|
own.
|
||||||
|
|
||||||
## Startup failures
|
## Startup failures
|
||||||
|
|
||||||
Startup fails rather than degrading when:
|
Startup fails rather than degrading when:
|
||||||
@@ -91,6 +134,33 @@ All key material arrives through `SecretMaterialProvider`. Nothing is read from
|
|||||||
committed file, or from a plaintext log. Contact point encryption and lookup HMAC keys must be
|
committed file, or from a plaintext log. Contact point encryption and lookup HMAC keys must be
|
||||||
distinct, and the encryption key must be exactly 256 bits.
|
distinct, and the encryption key must be exactly 256 bits.
|
||||||
|
|
||||||
|
Eight purposes, eight keys. Each is base64 of at least 32 bytes and each must differ from every
|
||||||
|
other; the platform decodes them at startup and refuses to boot if one is blank, short or shared. A
|
||||||
|
blank value used to be skipped, which meant the platform started without the key and found out on
|
||||||
|
the first contact point — in production, on a recipient's notification.
|
||||||
|
|
||||||
|
Every default below is **unset**, deliberately. Supply the values out of band, per environment. Do
|
||||||
|
not write one into this table, into `application.yml`, into an `.env` file that is tracked, or into
|
||||||
|
any example: a value that appears in the repository is a value that has been disclosed.
|
||||||
|
|
||||||
|
| Purpose | Key material | Active key id |
|
||||||
|
|---|---|---|
|
||||||
|
| Contact point encryption | `APP_NOTIFICATION_PLATFORM_CONTACT_ENCRYPTION_KEY` | `APP_NOTIFICATION_PLATFORM_CONTACT_ENCRYPTION_KEY_ID` |
|
||||||
|
| Contact point lookup HMAC | `APP_NOTIFICATION_PLATFORM_CONTACT_LOOKUP_HMAC_KEY` | `APP_NOTIFICATION_PLATFORM_CONTACT_LOOKUP_HMAC_KEY_ID` |
|
||||||
|
| Callback signing | `APP_NOTIFICATION_PLATFORM_CALLBACK_SIGNING_KEY` | `APP_NOTIFICATION_PLATFORM_CALLBACK_SIGNING_KEY_ID` |
|
||||||
|
| Callback fingerprint HMAC | `APP_NOTIFICATION_PLATFORM_CALLBACK_FINGERPRINT_HMAC_KEY` | `APP_NOTIFICATION_PLATFORM_CALLBACK_FINGERPRINT_HMAC_KEY_ID` |
|
||||||
|
| Provider credential encryption | `APP_NOTIFICATION_PLATFORM_PROVIDER_CREDENTIAL_KEY` | `APP_NOTIFICATION_PLATFORM_PROVIDER_CREDENTIAL_KEY_ID` |
|
||||||
|
| Provider request lookup HMAC | `APP_NOTIFICATION_PLATFORM_PROVIDER_REQUEST_LOOKUP_HMAC_KEY` | `APP_NOTIFICATION_PLATFORM_PROVIDER_REQUEST_LOOKUP_HMAC_KEY_ID` |
|
||||||
|
| Payload encryption | `APP_NOTIFICATION_PLATFORM_PAYLOAD_ENCRYPTION_KEY` | `APP_NOTIFICATION_PLATFORM_PAYLOAD_ENCRYPTION_KEY_ID` |
|
||||||
|
| Web Push VAPID signing | `APP_NOTIFICATION_PLATFORM_VAPID_SIGNING_KEY` | `APP_NOTIFICATION_PLATFORM_VAPID_SIGNING_KEY_ID` |
|
||||||
|
|
||||||
|
A key id is not secret — an id identifies key material without revealing it — but it is required,
|
||||||
|
and it has no default on purpose. A constant id makes a rotation indistinguishable from the key it
|
||||||
|
replaced, so nothing could decrypt what was written before it. Change the id in the same deployment
|
||||||
|
that changes the material, and keep the superseded key readable under its old id until the data it
|
||||||
|
wrote has been re-encrypted. The rotation sequence is in
|
||||||
|
[at-rest-threat-model.md](at-rest-threat-model.md).
|
||||||
|
|
||||||
## Readiness
|
## Readiness
|
||||||
|
|
||||||
The platform contributes a `notifications` actuator endpoint and a health indicator. It reports DOWN
|
The platform contributes a `notifications` actuator endpoint and a health indicator. It reports DOWN
|
||||||
|
|||||||
@@ -1672,9 +1672,12 @@ env_keys:
|
|||||||
required_test: idempotency-contract:ttl-applied
|
required_test: idempotency-contract:ttl-applied
|
||||||
|
|
||||||
- name: APP_IDEMPOTENCY_PROVIDER
|
- name: APP_IDEMPOTENCY_PROVIDER
|
||||||
|
# postgresql selects the owner-safe V2 store on the primary data source. It had no value here
|
||||||
|
# while the store, its schema stream and its integration suite all existed, so the capability
|
||||||
|
# could only be reached by constructing it in a test.
|
||||||
type: enum
|
type: enum
|
||||||
default: jdbc
|
default: jdbc
|
||||||
allowed_values: [disabled, jdbc, redis]
|
allowed_values: [disabled, jdbc, redis, postgresql]
|
||||||
classification: public-config
|
classification: public-config
|
||||||
required: false
|
required: false
|
||||||
reload_policy: restart-only
|
reload_policy: restart-only
|
||||||
@@ -4970,6 +4973,21 @@ env_keys:
|
|||||||
compatibility_impact: behavior-change
|
compatibility_impact: behavior-change
|
||||||
required_test: adapter-contract:notification-callback-body-bound
|
required_test: adapter-contract:notification-callback-body-bound
|
||||||
|
|
||||||
|
- name: APP_NOTIFICATION_PLATFORM_CALLBACK_TRUSTED_PROXIES
|
||||||
|
# source: NTF-001 — peers whose forwarded headers may be believed when reconstructing the URL a
|
||||||
|
# provider signed. Empty means the resolver uses what the container observed; honouring
|
||||||
|
# forwarded headers unconditionally would let any caller pick the URL its signature is checked
|
||||||
|
# against, which defeats the signature.
|
||||||
|
type: csv
|
||||||
|
default: ""
|
||||||
|
allowed_values: null
|
||||||
|
classification: security-relevant
|
||||||
|
required: false
|
||||||
|
reload_policy: restart-only
|
||||||
|
owner_branch: worktree-notification-platform
|
||||||
|
validation: none
|
||||||
|
compatibility_impact: behavior-change
|
||||||
|
required_test: adapter-contract:notification-callback-url-resolution
|
||||||
- name: APP_NOTIFICATION_PLATFORM_CALLBACK_REPLAY_SKEW
|
- name: APP_NOTIFICATION_PLATFORM_CALLBACK_REPLAY_SKEW
|
||||||
# source: NTF-025 — how far a callback timestamp may differ from local time before it is treated as a replay
|
# source: NTF-025 — how far a callback timestamp may differ from local time before it is treated as a replay
|
||||||
type: duration
|
type: duration
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# P1 remediation status — the five module reviews
|
||||||
|
|
||||||
|
**Reviews:** `docs/reviews/2026-08-14-{jpa,graphql,messaging,mongodb,notification}-module-code-review.md`
|
||||||
|
**Baseline:** the P0 pass was already complete when this pass began; this file records the P1 pass,
|
||||||
|
which is complete — all 31 findings closed, two of them by establishing that the review's own
|
||||||
|
accepted outcome was already met rather than by writing code.
|
||||||
|
**Verified at:** repo-wide `test`, `spotlessCheck`, `verifyCleanArchitectureDependencies`,
|
||||||
|
`verifyEnvKeys`, `verifyPublicPathSnapshot` and the root `CleanArchitectureTest` all green.
|
||||||
|
|
||||||
|
This file exists because the status was previously carried only in conversation and had to be
|
||||||
|
reconstructed. A finding's row is the claim; the evidence column is where the claim is falsifiable.
|
||||||
|
|
||||||
|
## The recurring defect
|
||||||
|
|
||||||
|
Nearly every P1 in these five reviews is one shape: **a control that exists, passes its own tests,
|
||||||
|
and is reached by nothing.** Not a wrong algorithm — an unreachable one. The tests passed because
|
||||||
|
they constructed the class directly; the capability was absent because no configuration could.
|
||||||
|
|
||||||
|
Examples closed in this pass: the Mongo typed-update path (`MongoBulkExecutor` and
|
||||||
|
`MongoAtomicOperationsTemplate` were constructed by nothing), the entire Mongo change-stream
|
||||||
|
capability (no production code opened a stream at all), `GraphQlBatchLoaderRegistrar.register` (no
|
||||||
|
caller, so the wrong-key refusal never ran on an executing query), `PublishOptions.timeout()` (read
|
||||||
|
by nobody on the real publish path), `markExhausted` (no caller, so a row that spent its budget
|
||||||
|
stayed `AMBIGUOUS` forever), `OutboxMessagePublishPort.publishForOutcome` (no caller, so every
|
||||||
|
ambiguous publish collapsed into an exception), the outbox relay itself (no bean ran a pass), and
|
||||||
|
`BackpressureController` (a limiter the publish path never consulted, reporting `globalInFlight: 0`
|
||||||
|
under any load).
|
||||||
|
|
||||||
|
The lesson worth keeping: **a passing unit test is not evidence a capability exists.** The
|
||||||
|
reachability question — what constructs this, and on which request path — has to be asked
|
||||||
|
separately, and several of the tests added in this pass exist only to ask it.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
| Finding | Verdict | Evidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| JPA-005 | closed already | roll-up in `NotificationRequestStatusPolicy`; port is tenant-scoped; ArchUnit `PERSISTENCE_DOES_NOT_DEPEND_ON_APPLICATION_SERVICES` |
|
||||||
|
| JPA-006 | closed already | `PersistenceJpaRootAutoConfiguration` is in `AutoConfiguration.imports` and imports the real runtime config |
|
||||||
|
| JPA-007 | closed under item 6 | the review offers two accepted outcomes: full integration, or the interim state under item 6's two conditions. Both hold and were verified in code — `FullTransactionRetryCoordinator:89-93` resolves the policy per call from the calling profile, and `DefaultJpaRetryPolicy:61-64` returns on `!failure.retryable()` before consulting the category allowlist |
|
||||||
|
| JPA-008 | closed | nothing registered a `VendorFailureTranslator`, so every executor ran `withoutCatalogs()` and a 40001 never reached the retry classifier |
|
||||||
|
| JPA-026 | closed | a rehydrated callback event had no matcher once `attempt_id` was null; hash fallback + write-back added |
|
||||||
|
| JPA-028 | closed | the reaper query had no caller, uploads had no terminal state, cleanup decided from a lease it had read rather than claiming |
|
||||||
|
| JPA-029 | closed already | tuple cutoff implemented; the signal is documented best-effort by decision |
|
||||||
|
| GQL-004 | not a defect | every evidence bullet false at HEAD; no WebFlux dependency exists, transport disagreement fails startup |
|
||||||
|
| GQL-011 | closed | unkeyed truncated SHA-256 over actor/tenant replaced with a keyed, rotating HMAC; no default key |
|
||||||
|
| GQL-015 | closed | schema extensions were invisible to the comparator, so a field removed by `extend type` produced no change at all |
|
||||||
|
| GQL-016 | closed | the batch executor returned the loader's map verbatim, so the wrong-key refusal and missing-key policy never ran |
|
||||||
|
| GQL-017 | closed | the blocking bridge was opt-in and null by default, so a reactive runtime ran blocking chunks on the event loop |
|
||||||
|
| MNG-010 | closed | the guardrail was a `Set<String>` asserted against itself; now an ArchUnit rule over the real production graph at the composition root |
|
||||||
|
| MNG-012 | closed | a failed abort or close on a committed transaction was discarded by a closing brace |
|
||||||
|
| MNG-018 | closed | TLS and auth were asserted against a settings object; four TLS cases now run against real servers |
|
||||||
|
| MNG-024 | closed | the reactive binder carried read preference and write concern only, so reactive writes skipped auditing and callbacks |
|
||||||
|
| MNG-026 | closed | both typed-update paths were unreachable, and the bulk executor could be built with no policy at all |
|
||||||
|
| MNG-028 | closed | no production code opened a change stream; the consumer now owns load → resume → stream → project → checkpoint |
|
||||||
|
| MSG-006 | closed | `markExhausted` had no caller and the scheduler's backoff was never written; a relay worker now runs passes |
|
||||||
|
| MSG-008 | closed | validators were beans nothing injected, and the documented configuration bound nowhere; destination/broker/security sections now bind under `app.messaging` and the reference document is executed by a test |
|
||||||
|
| MSG-010 | closed | `BackpressureController` deleted as an unreachable duplicate; its one unique capability moved into the gate that is called |
|
||||||
|
| MSG-012 | closed | header values accepted CR/LF/NUL, identifiers were bounded in chars not bytes, `traceparent` was any string, denylists matched exact spellings only |
|
||||||
|
| MSG-014 | closed | `hasLiveBrokerCertification` is derived from recorded evidence rather than declared, and the evidence is now a manifest a fault lane wrote against a real broker rather than a hand-authored list |
|
||||||
|
| MSG-015 | semantic half closed | the outcome-aware publish path is wired; the anti-corruption bridge needs a `modules.json` edge and is an architecture decision |
|
||||||
|
| MSG-016 | closed | no reserved name existed for tenant, so every consumed message was rebuilt with none; the canonical metadata is now columns, and the CDC event key moved off `destination`, which had put every message on a topic onto one partition |
|
||||||
|
| MSG-017 | closed | the timeout is an absolute deadline; contradictory `PublishResult` combinations are unrepresentable; `brokerHints` removed |
|
||||||
|
| NTF-015 | closed | webhook signing was one shared secret for every subscription; SES silently dropped attachments and now sends them as raw MIME |
|
||||||
|
| NTF-016 | closed | retired keys were forced to one purpose so a provider-credential drain failed; required purposes now follow enabled capabilities |
|
||||||
|
| NTF-019 | closed already | split inbound ports carry capabilities; four ArchUnit rules with negative fixtures close the gate |
|
||||||
|
|
||||||
|
## Found while closing, not in any review
|
||||||
|
|
||||||
|
`SmtpMimeMessageFactory` handed JavaMail the resolver's one-shot stream. JavaMail reads an
|
||||||
|
attachment twice — once to choose the part's transfer encoding, once to write it — so the second
|
||||||
|
read returned nothing and the message went out announcing a filename and carrying no bytes, with
|
||||||
|
the attempt recorded as accepted. Every existing test asserted on the outcome of the send rather
|
||||||
|
than on what was sent, which is why a bug that emptied every attachment on the one provider family
|
||||||
|
this platform can actually assemble survived a full review pass.
|
||||||
|
|
||||||
|
`SmtpAttachmentBodyTest` now reads the attachment back off the serialised message the way a
|
||||||
|
receiving client would. It was confirmed to fail against the original code and pass against the
|
||||||
|
fix, because a regression test nobody has watched fail is a regression test of unknown shape.
|
||||||
|
|
||||||
|
## Observations that are not open P1 items
|
||||||
|
|
||||||
|
Both were checked against the reviews rather than assumed, because "looks unfinished" and "is an open
|
||||||
|
finding" are different claims.
|
||||||
|
|
||||||
|
**The GraphQL cursor key gate.** `GraphQlPlatformStartupValidator` refuses to start a production
|
||||||
|
deployment without `backend.graphql.cursor.key-ids`, and nothing signs a cursor with it:
|
||||||
|
`GraphQlConnectionAssembler` and `HmacGraphQlCursorCodec` have no consumer anywhere in this
|
||||||
|
repository, because the template ships no paginating resolver. This is not GQL-010, which is about
|
||||||
|
the codec's framing, rotation and scope and is implemented — versioned framing, the codec choosing
|
||||||
|
the active key rather than the caller, v1 decode kept only for migration, tenant scope bound. It
|
||||||
|
belongs to the `modelled` grading the leaf's own `GraphQlPolicyRequestPathTest` already documents in
|
||||||
|
as many words. Declaring beans for it would create the unreachable-control defect this pass exists
|
||||||
|
to close, and the present behaviour fails closed, which is the safe direction. Left as it is, on
|
||||||
|
purpose.
|
||||||
|
|
||||||
|
## Product work, not remediation
|
||||||
|
|
||||||
|
**Notification provider transports.** Only SMTP has a `ProviderRuntimeAssembler`.
|
||||||
|
`NotificationProviderAssembly` refuses to start a profile whose family has no assembler, naming the
|
||||||
|
transport as a seam rather than an implementation — which is the honest fail-closed behaviour, not a
|
||||||
|
defect. Building SES, Twilio, FCM, APNs and WebPush transports is product work.
|
||||||
@@ -25,7 +25,9 @@ status: stub
|
|||||||
|
|
||||||
### Step 1 — 확인
|
### Step 1 — 확인
|
||||||
1. ERROR log에서 `OUTBOX_PUBLISH_FAILED` 라인 확인: `event_type`, `event_id`, `correlation_id`, `attempt_count` 추출
|
1. ERROR log에서 `OUTBOX_PUBLISH_FAILED` 라인 확인: `event_type`, `event_id`, `correlation_id`, `attempt_count` 추출
|
||||||
2. broker 상태 확인: `APP_MESSAGING_BROKER` 값(공백이면 messaging 비활성)과 broker endpoint 가용성
|
2. broker 상태 확인: `APP_MESSAGING_BROKER` 값과 broker endpoint 가용성. 이 키는 활성화 스위치가
|
||||||
|
아니라 **선택자**다 — messaging을 끄는 것은 `APP_MESSAGING_ENABLED=false`이고, 이 값을 비운다고
|
||||||
|
messaging이 꺼지지는 않는다.
|
||||||
- `APP_MESSAGING_BROKER`가 공백인 채로 relay가 켜져 있으면 **애플리케이션이 기동하지 않는다**
|
- `APP_MESSAGING_BROKER`가 공백인 채로 relay가 켜져 있으면 **애플리케이션이 기동하지 않는다**
|
||||||
(`OutboxRelayBrokerRequirementValidator`, MSG-024). 이 조합에서는 publish가 전부
|
(`OutboxRelayBrokerRequirementValidator`, MSG-024). 이 조합에서는 publish가 전부
|
||||||
`AdapterDisabledException`으로 실패하며 PENDING row가 DEAD까지 소진되기 때문이다.
|
`AdapterDisabledException`으로 실패하며 PENDING row가 DEAD까지 소진되기 때문이다.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
|||||||
|
$ ./gradlew verifyCleanArchitectureDependencies verifyEnvKeys verifyRuntimeModuleMembership verifyPublicPathSnapshot verifyDocumentedLeafCount --console=plain
|
||||||
|
run-at: 2026-08-20T01:59:14Z
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :verifyCleanArchitectureDependencies
|
||||||
|
> Task :shared-contract:compileJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processResources UP-TO-DATE
|
||||||
|
> Task :shared-contract:classes UP-TO-DATE
|
||||||
|
> Task :shared-contract:jar UP-TO-DATE
|
||||||
|
> Task :application-core:compileJava UP-TO-DATE
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes UP-TO-DATE
|
||||||
|
> Task :application-core:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:support:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:compileJava UP-TO-DATE
|
||||||
|
|
||||||
|
> Task :verifyEnvKeys
|
||||||
|
verifyEnvKeys: OK — 341 env keys, 6 required placeholders covered, 225 application APP_ references registered, 61 typed properties registered, 338 rows with a consumer or a deprecation.
|
||||||
|
|
||||||
|
> Task :verifyRuntimeModuleMembership
|
||||||
|
verifyRuntimeModuleMembership: 2 runtime composition(s) match the registry
|
||||||
|
|
||||||
|
> Task :verifyPublicPathSnapshot
|
||||||
|
verifyPublicPathSnapshot: OK — committed public paths are unchanged.
|
||||||
|
|
||||||
|
> Task :verifyDocumentedLeafCount
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 8s
|
||||||
|
21 actionable tasks: 5 executed, 16 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
exit=0
|
||||||
@@ -0,0 +1,901 @@
|
|||||||
|
$ ./gradlew check --warning-mode=fail --no-daemon --console=plain (re-run after the SpotBugs fix)
|
||||||
|
run-at: 2026-08-20T01:53:16Z
|
||||||
|
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
|
||||||
|
Daemon will be stopped at the end of the build
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :verifyCleanArchitectureDependencies
|
||||||
|
|
||||||
|
> Task :verifyConfigurationPropertiesProcessor
|
||||||
|
verifyConfigurationPropertiesProcessor: OK — all 44 registered leaves have exact configuration-processor parity.
|
||||||
|
|
||||||
|
> Task :verifyDocumentedLeafCount
|
||||||
|
> Task :shared-contract:compileJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processResources UP-TO-DATE
|
||||||
|
> Task :shared-contract:classes UP-TO-DATE
|
||||||
|
> Task :shared-contract:jar UP-TO-DATE
|
||||||
|
> Task :application-core:compileJava UP-TO-DATE
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes UP-TO-DATE
|
||||||
|
> Task :application-core:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:support:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:compileJava UP-TO-DATE
|
||||||
|
|
||||||
|
> Task :verifyEnvKeys
|
||||||
|
verifyEnvKeys: OK — 341 env keys, 6 required placeholders covered, 225 application APP_ references registered, 61 typed properties registered, 338 rows with a consumer or a deprecation.
|
||||||
|
|
||||||
|
> Task :verifyJpaReadinessRegistryContract
|
||||||
|
verifyJpaReadinessRegistryContract: OK — unknown card, duplicate task, missing prerequisite, cycle, duplicate migration ownership, missing selected task, and malformed evidence ownership all fail closed.
|
||||||
|
|
||||||
|
> Task :verifyJpaReadinessRegistry
|
||||||
|
verifyJpaReadinessRegistry: OK — 17 exact cards, 9 owned migration streams, acyclic prerequisites, unique tasks/locations/history tables, and selected task existence verified.
|
||||||
|
|
||||||
|
> Task :verifyNoIgnoredSourcePackages
|
||||||
|
verifyNoIgnoredSourcePackages: OK — 5164 Java sources are all committable.
|
||||||
|
|
||||||
|
> Task :verifyNoStaleTraceableJars
|
||||||
|
verifyNoStaleTraceableJars: OK — no stale traceable JARs in build/libs.
|
||||||
|
|
||||||
|
> Task :verifyNotificationApiSurface
|
||||||
|
verifyNotificationApiSurface: OK — 586 public types, unchanged.
|
||||||
|
|
||||||
|
> Task :verifyNotificationConfiguration
|
||||||
|
verifyNotificationConfiguration: OK — 42 platform settings, bound, documented and registered.
|
||||||
|
|
||||||
|
> Task :verifyNotificationEvidence
|
||||||
|
verifyNotificationEvidence: OK — 4 claims proven, every grade in support-matrix.md is backed.
|
||||||
|
|
||||||
|
> Task :verifyOneTypePerFile
|
||||||
|
verifyOneTypePerFile: OK — one public top-level type per file, names match.
|
||||||
|
|
||||||
|
> Task :verifyQuarantineSunset
|
||||||
|
verifyQuarantineSunset: OK — 0 registered, 0 tagged (14-day sunset enforced).
|
||||||
|
|
||||||
|
> Task :verifyReadmeCommands
|
||||||
|
verifyReadmeCommands: OK — executable commands in /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/README.md resolve.
|
||||||
|
|
||||||
|
> Task :verifyRunbookReferences
|
||||||
|
|
||||||
|
> Task :verifyRuntimeModuleMembership
|
||||||
|
verifyRuntimeModuleMembership: 2 runtime composition(s) match the registry
|
||||||
|
|
||||||
|
> Task :verifySpotBugsAnalysisFailureContract
|
||||||
|
verifySpotBugsAnalysisFailureContract: OK — clean and advisory bug-only reports pass; missing classes and analysis errors fail closed.
|
||||||
|
|
||||||
|
> Task :verifyTrivyignore
|
||||||
|
verifyTrivyignore: OK — 0 suppression(s) validated (reason + bounded, non-expired expiry).
|
||||||
|
|
||||||
|
> Task :domain-core:compileJava UP-TO-DATE
|
||||||
|
> Task :domain-core:processResources NO-SOURCE
|
||||||
|
> Task :domain-core:classes UP-TO-DATE
|
||||||
|
> Task :domain-core:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:jar UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:web:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:fileserver:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:httpclient:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
|
||||||
|
> Task :adapter:outbound:identifier:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:identifier:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:notification:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:jar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:compileJava UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:processResources UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:grpc:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:jar UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:websocket:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:jar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:compileConditionalTransportTestJava UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:processConditionalTransportTestResources NO-SOURCE
|
||||||
|
> Task :app-bootstrap:conditionalTransportTestClasses UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:checkstyleConditionalTransportTest UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:compileFunctionalTestJava UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:processFunctionalTestResources NO-SOURCE
|
||||||
|
> Task :app-bootstrap:functionalTestClasses UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:checkstyleFunctionalTest UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitJar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:compileSampleOffTestJava UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:processSampleOffTestResources UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:sampleOffTestClasses UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:checkstyleSampleOffTest UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:compileJava UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:processResources UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:classes UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:jar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:compileTestJava UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:processTestResources UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:testClasses UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:runtimeClasspathManifest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-runtime:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-claim-check:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-cloudevents:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-core-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-observability:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-policy:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-rabbit:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-reliability-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-runtime-core:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-json:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-security:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:processResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-transport-spi:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:objectstorage:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:objectstorage:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:objectstorage:jar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:test UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:functionalTest UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:spotbugsConditionalTransportTest UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:spotbugsFunctionalTest UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:spotbugsSampleOffTest UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:spotlessJava UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:check
|
||||||
|
|
||||||
|
> Task :verifyApplicationCoreDependencyPurity
|
||||||
|
verifyApplicationCoreDependencyPurity: OK — application-core production declarations are project-only and application classpaths contain no Spring/logging/metrics frameworks.
|
||||||
|
|
||||||
|
> Task :application-core:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :application-core:compileTestJava UP-TO-DATE
|
||||||
|
> Task :application-core:processTestResources NO-SOURCE
|
||||||
|
> Task :application-core:testClasses UP-TO-DATE
|
||||||
|
> Task :application-core:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :application-core:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :application-core:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :application-core:spotlessJava UP-TO-DATE
|
||||||
|
> Task :application-core:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :application-core:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :application-core:test UP-TO-DATE
|
||||||
|
> Task :application-core:check
|
||||||
|
> Task :domain-core:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :domain-core:compileTestJava NO-SOURCE
|
||||||
|
> Task :domain-core:processTestResources NO-SOURCE
|
||||||
|
> Task :domain-core:testClasses UP-TO-DATE
|
||||||
|
> Task :domain-core:checkstyleTest NO-SOURCE
|
||||||
|
> Task :domain-core:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :domain-core:spotbugsTest NO-SOURCE
|
||||||
|
> Task :domain-core:spotlessJava UP-TO-DATE
|
||||||
|
> Task :domain-core:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :domain-core:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :domain-core:test NO-SOURCE
|
||||||
|
> Task :domain-core:check
|
||||||
|
> Task :sample-portfolio:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:compileTestJava UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:processTestResources UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:testClasses UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:compilePosterImageMigrationTestJava UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:processPosterImageMigrationTestResources NO-SOURCE
|
||||||
|
> Task :sample-portfolio:posterImageMigrationTestClasses UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:checkstylePosterImageMigrationTest UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:spotbugsPosterImageMigrationTest UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:spotlessJava UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:test UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:check
|
||||||
|
> Task :shared-contract:compileEdgeRateLimitContractTestJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processEdgeRateLimitContractTestResources NO-SOURCE
|
||||||
|
> Task :shared-contract:edgeRateLimitContractTestClasses UP-TO-DATE
|
||||||
|
> Task :shared-contract:checkstyleEdgeRateLimitContractTest UP-TO-DATE
|
||||||
|
> Task :shared-contract:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :shared-contract:compileTestJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processTestResources NO-SOURCE
|
||||||
|
> Task :shared-contract:testClasses UP-TO-DATE
|
||||||
|
> Task :shared-contract:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :shared-contract:spotbugsEdgeRateLimitContractTest UP-TO-DATE
|
||||||
|
> Task :shared-contract:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :shared-contract:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :shared-contract:spotlessJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :shared-contract:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :shared-contract:test UP-TO-DATE
|
||||||
|
> Task :shared-contract:check
|
||||||
|
> Task :messaging:messaging-admin-api:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-api:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:check
|
||||||
|
> Task :messaging:messaging-admin-runtime:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-runtime:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:check
|
||||||
|
> Task :messaging:messaging-claim-check:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-claim-check:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:check
|
||||||
|
> Task :messaging:messaging-cloudevents:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-cloudevents:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:check
|
||||||
|
> Task :messaging:messaging-core-api:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-core-api:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:check
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:processResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:check
|
||||||
|
> Task :messaging:messaging-kafka:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:compileJmhJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:processJmhResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka:jmhClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:checkstyleJmh SKIPPED
|
||||||
|
> Task :messaging:messaging-kafka:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:spotbugsJmh SKIPPED
|
||||||
|
> Task :messaging:messaging-kafka:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:check
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:check
|
||||||
|
> Task :messaging:messaging-nats-experimental:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-nats-experimental:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-nats-experimental:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:check
|
||||||
|
> Task :messaging:messaging-observability:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-observability:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:check
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:check
|
||||||
|
> Task :messaging:messaging-policy:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-policy:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:check
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:check
|
||||||
|
> Task :messaging:messaging-rabbit:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-rabbit:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:compileJmhJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:processJmhResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-rabbit:jmhClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:checkstyleJmh SKIPPED
|
||||||
|
> Task :messaging:messaging-rabbit:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:spotbugsJmh SKIPPED
|
||||||
|
> Task :messaging:messaging-rabbit:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:check
|
||||||
|
> Task :messaging:messaging-reliability-api:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:compileTestJava NO-SOURCE
|
||||||
|
> Task :messaging:messaging-reliability-api:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-reliability-api:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:checkstyleTest NO-SOURCE
|
||||||
|
> Task :messaging:messaging-reliability-api:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:spotbugsTest NO-SOURCE
|
||||||
|
> Task :messaging:messaging-reliability-api:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:test NO-SOURCE
|
||||||
|
> Task :messaging:messaging-reliability-api:check
|
||||||
|
> Task :messaging:messaging-runtime-core:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-runtime-core:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:check
|
||||||
|
> Task :messaging:messaging-schema-api:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-api:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:check
|
||||||
|
> Task :messaging:messaging-schema-avro:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-avro:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:processTestResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:check
|
||||||
|
> Task :messaging:messaging-schema-json:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-json:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:check
|
||||||
|
> Task :messaging:messaging-schema-protobuf:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:check
|
||||||
|
> Task :messaging:messaging-security:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-security:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:check
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:processTestResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:check
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:check
|
||||||
|
> Task :messaging:messaging-testkit:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-testkit:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:compileJmhJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:processJmhResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-testkit:jmhClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:checkstyleJmh SKIPPED
|
||||||
|
> Task :messaging:messaging-testkit:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:spotbugsJmh SKIPPED
|
||||||
|
> Task :messaging:messaging-testkit:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:check
|
||||||
|
> Task :messaging:messaging-transport-spi:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-transport-spi:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:spotlessJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:check
|
||||||
|
> Task :adapter:inbound:graphql:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:compileTestFixturesJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:processTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:processTestFixturesResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:graphql:testFixturesClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:checkstyleTestFixtures UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:testFixturesJar UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:spotbugsTestFixtures UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:spotlessJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:test UP-TO-DATE
|
||||||
|
|
||||||
|
> Task :adapter:inbound:graphql:verifyGraphQlApiSurface
|
||||||
|
verifyGraphQlApiSurface: OK — the committed public API surface is unchanged.
|
||||||
|
|
||||||
|
> Task :adapter:inbound:graphql:verifyGraphQlProductionJar UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:grpc:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:spotlessJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:test UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:check
|
||||||
|
> Task :adapter:inbound:web:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:web:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:spotlessJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:test UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:checkstyleTest
|
||||||
|
> Task :adapter:inbound:web:webSecurityBoundaryTest
|
||||||
|
> Task :adapter:inbound:graphql:check
|
||||||
|
> Task :adapter:inbound:web:check
|
||||||
|
> Task :adapter:inbound:websocket:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:websocket:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:spotlessJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:test UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:check
|
||||||
|
> Task :adapter:outbound:cache-redis:checkstyleMain UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:processTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:checkstyleTest UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:spotbugsMain UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:spotbugsTest UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:spotlessJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:spotlessJavaCheck UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:spotlessCheck UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:check
|
||||||
|
> Task :adapter:outbound:fileserver:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:fileserver:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:checkstyleMain
|
||||||
|
> Task :adapter:outbound:fileserver:spotlessJava
|
||||||
|
> Task :adapter:outbound:fileserver:spotlessJavaCheck
|
||||||
|
> Task :adapter:outbound:fileserver:spotlessCheck
|
||||||
|
> Task :adapter:outbound:fileserver:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:httpclient:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:checkstyleTest
|
||||||
|
> Task :adapter:outbound:httpclient:compileHttpClientPerformanceTestJava
|
||||||
|
> Task :adapter:outbound:httpclient:processHttpClientPerformanceTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:httpclient:httpClientPerformanceTestClasses
|
||||||
|
> Task :adapter:outbound:httpclient:checkstyleHttpClientPerformanceTest
|
||||||
|
> Task :adapter:outbound:fileserver:spotbugsMain
|
||||||
|
> Task :adapter:outbound:fileserver:spotbugsTest
|
||||||
|
> Task :adapter:outbound:httpclient:compileJmhJava
|
||||||
|
> Task :adapter:outbound:httpclient:processJmhResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:httpclient:jmhClasses
|
||||||
|
> Task :adapter:outbound:httpclient:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:httpclient:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:checkstyleJmh
|
||||||
|
|
||||||
|
> Task :adapter:outbound:httpclient:httpClientBlockHoundTest
|
||||||
|
OpenJDK 64-Bit Server VM warning: Option AllowRedefinitionToAddDeleteMethods was deprecated in version 13.0 and will likely be removed in a future release.
|
||||||
|
|
||||||
|
> Task :adapter:outbound:httpclient:checkstyleMain
|
||||||
|
> Task :adapter:outbound:httpclient:checkstyleTestkit
|
||||||
|
> Task :adapter:outbound:httpclient:checkstyleTest
|
||||||
|
> Task :adapter:outbound:httpclient:httpClientSecurityTest
|
||||||
|
> Task :adapter:outbound:httpclient:httpClientStableContractTest
|
||||||
|
> Task :adapter:outbound:httpclient:spotbugsJmh SKIPPED
|
||||||
|
> Task :adapter:outbound:httpclient:spotlessJava
|
||||||
|
> Task :adapter:outbound:httpclient:spotlessJavaCheck
|
||||||
|
> Task :adapter:outbound:httpclient:spotlessCheck
|
||||||
|
> Task :adapter:outbound:httpclient:spring62ApiSurfaceScan
|
||||||
|
> Task :adapter:outbound:httpclient:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:compileTestGroovy UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:identifier:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:spotlessJava
|
||||||
|
> Task :adapter:outbound:httpclient:spotbugsHttpClientPerformanceTest
|
||||||
|
> Task :adapter:outbound:identifier:spotlessJavaCheck
|
||||||
|
> Task :adapter:outbound:identifier:spotlessCheck
|
||||||
|
> Task :adapter:outbound:identifier:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:processTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:checkstyleTest
|
||||||
|
> Task :adapter:outbound:identifier:checkstyleMain
|
||||||
|
> Task :adapter:outbound:messaging:spotlessJava
|
||||||
|
> Task :adapter:outbound:messaging:spotlessJavaCheck
|
||||||
|
> Task :adapter:outbound:messaging:spotlessCheck
|
||||||
|
> Task :adapter:outbound:messaging:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:verifyJsonSchemaRuntimeGraph
|
||||||
|
> Task :adapter:outbound:notification:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:processTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:check
|
||||||
|
> Task :adapter:outbound:messaging:checkstyleTest
|
||||||
|
> Task :adapter:outbound:messaging:checkstyleMain
|
||||||
|
> Task :adapter:outbound:httpclient:spotbugsMain
|
||||||
|
> Task :adapter:outbound:httpclient:spotbugsTest
|
||||||
|
> Task :adapter:outbound:httpclient:spotbugsTestkit
|
||||||
|
> Task :adapter:outbound:notification:checkstyleTest
|
||||||
|
> Task :adapter:outbound:notification:spotlessJava
|
||||||
|
> Task :adapter:outbound:notification:spotlessJavaCheck
|
||||||
|
> Task :adapter:outbound:notification:spotlessCheck
|
||||||
|
> Task :adapter:outbound:notification:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:verifyDependencyPolicy
|
||||||
|
> Task :adapter:outbound:objectstorage:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:objectstorage:processTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:objectstorage:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:checkstyleMain
|
||||||
|
> Task :adapter:outbound:objectstorage:compileObjectStorageAwsQualificationTestJava
|
||||||
|
> Task :adapter:outbound:objectstorage:processObjectStorageAwsQualificationTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:objectstorage:objectStorageAwsQualificationTestClasses
|
||||||
|
> Task :adapter:outbound:objectstorage:checkstyleObjectStorageAwsQualificationTest
|
||||||
|
> Task :adapter:outbound:objectstorage:checkstyleMain
|
||||||
|
> Task :adapter:outbound:identifier:spotbugsMain
|
||||||
|
> Task :adapter:outbound:identifier:spotbugsTest
|
||||||
|
> Task :adapter:outbound:messaging:spotbugsMain
|
||||||
|
> Task :adapter:outbound:messaging:spotbugsTest
|
||||||
|
> Task :adapter:outbound:notification:spotbugsMain
|
||||||
|
> Task :adapter:outbound:notification:spotbugsTest
|
||||||
|
> Task :adapter:outbound:objectstorage:compileObjectStorageMinioContractTestJava
|
||||||
|
> Task :adapter:outbound:objectstorage:processObjectStorageMinioContractTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:objectstorage:objectStorageMinioContractTestClasses
|
||||||
|
> Task :adapter:outbound:objectstorage:checkstyleObjectStorageMinioContractTest
|
||||||
|
> Task :adapter:outbound:objectstorage:compileObjectStorageMinioFaultTestJava
|
||||||
|
> Task :adapter:outbound:objectstorage:processObjectStorageMinioFaultTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:objectstorage:objectStorageMinioFaultTestClasses
|
||||||
|
> Task :adapter:outbound:objectstorage:checkstyleObjectStorageMinioFaultTest
|
||||||
|
> Task :adapter:outbound:identifier:check
|
||||||
|
> Task :adapter:outbound:objectstorage:checkstyleTest
|
||||||
|
> Task :adapter:outbound:objectstorage:spotlessJava
|
||||||
|
> Task :adapter:outbound:objectstorage:spotlessJavaCheck
|
||||||
|
> Task :adapter:outbound:objectstorage:spotlessCheck
|
||||||
|
> Task :adapter:outbound:objectstorage:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJpaPlatformPerformanceTestJava
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processJpaPlatformPerformanceTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformPerformanceTestClasses
|
||||||
|
> Task :adapter:outbound:persistence-jpa:checkstyleJpaPlatformPerformanceTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:checkstyleMain
|
||||||
|
> Task :adapter:outbound:objectstorage:spotbugsMain
|
||||||
|
> Task :adapter:outbound:objectstorage:spotbugsObjectStorageAwsQualificationTest
|
||||||
|
> Task :adapter:outbound:objectstorage:spotbugsObjectStorageMinioContractTest
|
||||||
|
> Task :adapter:outbound:objectstorage:spotbugsObjectStorageMinioFaultTest
|
||||||
|
> Task :adapter:outbound:objectstorage:spotbugsTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources
|
||||||
|
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:check
|
||||||
|
> Task :adapter:outbound:persistence-jpa:checkstyleTestkit
|
||||||
|
> Task :adapter:outbound:persistence-jpa:checkstyleTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:checkstylePostgresqlIntegrationTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:spotlessJava
|
||||||
|
> Task :adapter:outbound:persistence-jpa:spotlessJavaCheck
|
||||||
|
> Task :adapter:outbound:persistence-jpa:spotlessCheck
|
||||||
|
> Task :adapter:outbound:persistence-jpa:test UP-TO-DATE
|
||||||
|
|
||||||
|
> Task :adapter:outbound:persistence-jpa:verifyJpaEvidenceHarnessContract
|
||||||
|
verifyJpaEvidenceHarnessContract: OK — skip, dirty/local R2, and content mutation fail closed.
|
||||||
|
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileMongoPerformanceTestJava
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processMongoPerformanceTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoPerformanceTestClasses
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:check
|
||||||
|
> Task :adapter:outbound:persistence-mongo:checkstyleMongoPerformanceTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:spotbugsJpaPlatformPerformanceTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:spotbugsMain
|
||||||
|
> Task :adapter:outbound:persistence-mongo:checkstyleTestkit
|
||||||
|
> Task :adapter:outbound:persistence-jpa:spotbugsPostgresqlIntegrationTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:spotbugsTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:spotbugsTestkit
|
||||||
|
> Task :adapter:outbound:persistence-mongo:checkstyleMain
|
||||||
|
> Task :adapter:outbound:persistence-mongo:checkstyleTest
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoStableContractTest
|
||||||
|
> Task :adapter:outbound:persistence-mongo:spotlessJava
|
||||||
|
> Task :adapter:outbound:persistence-mongo:spotlessJavaCheck
|
||||||
|
> Task :adapter:outbound:persistence-mongo:spotlessCheck
|
||||||
|
> Task :adapter:outbound:persistence-mongo:test UP-TO-DATE
|
||||||
|
|
||||||
|
> Task :adapter:outbound:persistence-mongo:verifyMongoApiSurface
|
||||||
|
verifyMongoApiSurface: OK — the committed public API surface is unchanged.
|
||||||
|
|
||||||
|
> Task :adapter:outbound:persistence-mongo:verifyMongoReleaseContractLanes
|
||||||
|
> Task :adapter:outbound:httpclient:check
|
||||||
|
> Task :adapter:outbound:objectstorage:check
|
||||||
|
> Task :adapter:outbound:persistence-mongo:verifyMongoTestLaneDisjointness
|
||||||
|
> Task :adapter:outbound:support:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:support:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:spotlessJava
|
||||||
|
> Task :adapter:outbound:support:spotlessJavaCheck
|
||||||
|
> Task :adapter:outbound:support:spotlessCheck
|
||||||
|
> Task :adapter:outbound:support:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:check
|
||||||
|
> Task :adapter:outbound:support:checkstyleMain
|
||||||
|
> Task :adapter:outbound:support:checkstyleTest
|
||||||
|
> Task :adapter:outbound:persistence-mongo:spotbugsMongoPerformanceTest
|
||||||
|
> Task :adapter:outbound:persistence-mongo:spotbugsMain
|
||||||
|
> Task :adapter:outbound:persistence-mongo:spotbugsTest
|
||||||
|
> Task :adapter:outbound:persistence-mongo:spotbugsTestkit
|
||||||
|
> Task :adapter:outbound:support:spotbugsTest
|
||||||
|
> Task :adapter:outbound:support:spotbugsMain
|
||||||
|
> Task :adapter:outbound:support:check
|
||||||
|
> Task :adapter:outbound:persistence-mongo:check
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 5m 42s
|
||||||
|
547 actionable tasks: 117 executed, 430 up-to-date
|
||||||
|
exit=0
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
$ ./gradlew clean compileJava compileTestJava --warning-mode=fail --no-daemon --console=plain
|
||||||
|
run-at: 2026-08-20T01:18:16Z
|
||||||
|
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
|
||||||
|
Daemon will be stopped at the end of the build
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:clean
|
||||||
|
> Task :application-core:clean
|
||||||
|
> Task :domain-core:clean
|
||||||
|
> Task :sample-portfolio:clean
|
||||||
|
> Task :shared-contract:clean
|
||||||
|
> Task :messaging:messaging-admin-api:clean
|
||||||
|
> Task :messaging:messaging-admin-runtime:clean
|
||||||
|
> Task :messaging:messaging-claim-check:clean
|
||||||
|
> Task :messaging:messaging-cloudevents:clean
|
||||||
|
> Task :messaging:messaging-core-api:clean
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:clean
|
||||||
|
> Task :messaging:messaging-kafka:clean
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:clean
|
||||||
|
> Task :messaging:messaging-nats-experimental:clean
|
||||||
|
> Task :messaging:messaging-observability:clean
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:clean
|
||||||
|
> Task :messaging:messaging-policy:clean
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:clean
|
||||||
|
> Task :messaging:messaging-rabbit:clean
|
||||||
|
> Task :messaging:messaging-reliability-api:clean
|
||||||
|
> Task :messaging:messaging-runtime-core:clean
|
||||||
|
> Task :messaging:messaging-schema-api:clean
|
||||||
|
> Task :messaging:messaging-schema-avro:clean
|
||||||
|
> Task :messaging:messaging-schema-json:clean
|
||||||
|
> Task :messaging:messaging-schema-protobuf:clean
|
||||||
|
> Task :messaging:messaging-security:clean
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:clean
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:clean
|
||||||
|
> Task :messaging:messaging-testkit:clean
|
||||||
|
> Task :messaging:messaging-transport-spi:clean
|
||||||
|
> Task :adapter:inbound:graphql:clean
|
||||||
|
> Task :adapter:inbound:grpc:clean
|
||||||
|
> Task :adapter:inbound:web:clean
|
||||||
|
> Task :adapter:inbound:websocket:clean
|
||||||
|
> Task :adapter:outbound:cache-redis:clean
|
||||||
|
> Task :adapter:outbound:fileserver:clean
|
||||||
|
> Task :adapter:outbound:httpclient:clean
|
||||||
|
> Task :adapter:outbound:identifier:clean
|
||||||
|
> Task :adapter:outbound:messaging:clean
|
||||||
|
> Task :adapter:outbound:notification:clean
|
||||||
|
> Task :adapter:outbound:objectstorage:clean
|
||||||
|
> Task :adapter:outbound:persistence-jpa:clean
|
||||||
|
> Task :adapter:outbound:persistence-mongo:clean
|
||||||
|
> Task :adapter:outbound:support:clean
|
||||||
|
> Task :shared-contract:compileJava
|
||||||
|
> Task :shared-contract:processResources
|
||||||
|
> Task :shared-contract:classes
|
||||||
|
> Task :shared-contract:jar
|
||||||
|
> Task :application-core:compileJava
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes
|
||||||
|
> Task :application-core:jar
|
||||||
|
> Task :domain-core:compileJava
|
||||||
|
> Task :domain-core:processResources NO-SOURCE
|
||||||
|
> Task :domain-core:classes
|
||||||
|
> Task :domain-core:jar
|
||||||
|
> Task :messaging:messaging-core-api:compileJava
|
||||||
|
> Task :messaging:messaging-observability:compileJava
|
||||||
|
> Task :messaging:messaging-schema-api:compileJava
|
||||||
|
> Task :messaging:messaging-policy:compileJava
|
||||||
|
> Task :messaging:messaging-reliability-api:compileJava
|
||||||
|
> Task :messaging:messaging-security:compileJava
|
||||||
|
> Task :messaging:messaging-admin-api:compileJava
|
||||||
|
> Task :messaging:messaging-transport-spi:compileJava
|
||||||
|
> Task :messaging:messaging-admin-runtime:compileJava
|
||||||
|
> Task :messaging:messaging-claim-check:compileJava
|
||||||
|
> Task :messaging:messaging-cloudevents:compileJava
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava
|
||||||
|
> Task :messaging:messaging-kafka:compileJava
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava
|
||||||
|
> Task :messaging:messaging-rabbit:compileJava
|
||||||
|
> Task :messaging:messaging-runtime-core:compileJava
|
||||||
|
> Task :messaging:messaging-schema-json:compileJava
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:compileJava
|
||||||
|
> Task :adapter:inbound:graphql:compileJava
|
||||||
|
> Task :adapter:inbound:graphql:processResources
|
||||||
|
> Task :adapter:inbound:graphql:classes
|
||||||
|
> Task :adapter:inbound:graphql:jar
|
||||||
|
> Task :adapter:inbound:web:compileJava
|
||||||
|
> Task :adapter:inbound:web:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:web:classes
|
||||||
|
> Task :adapter:inbound:web:jar
|
||||||
|
> Task :adapter:outbound:support:compileJava
|
||||||
|
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:support:classes
|
||||||
|
> Task :adapter:outbound:support:jar
|
||||||
|
> Task :adapter:outbound:cache-redis:compileJava
|
||||||
|
> Task :adapter:outbound:cache-redis:processResources
|
||||||
|
> Task :adapter:outbound:cache-redis:classes
|
||||||
|
> Task :adapter:outbound:cache-redis:jar
|
||||||
|
> Task :adapter:outbound:fileserver:compileJava
|
||||||
|
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:fileserver:classes
|
||||||
|
> Task :adapter:outbound:fileserver:jar
|
||||||
|
> Task :adapter:outbound:httpclient:compileJava
|
||||||
|
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:httpclient:classes
|
||||||
|
> Task :adapter:outbound:httpclient:jar
|
||||||
|
> Task :adapter:outbound:identifier:compileJava
|
||||||
|
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
|
||||||
|
> Task :adapter:outbound:identifier:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:identifier:classes
|
||||||
|
> Task :adapter:outbound:identifier:jar
|
||||||
|
> Task :adapter:outbound:messaging:compileJava
|
||||||
|
> Task :adapter:outbound:messaging:processResources
|
||||||
|
> Task :adapter:outbound:messaging:classes
|
||||||
|
> Task :adapter:outbound:messaging:jar
|
||||||
|
> Task :adapter:outbound:notification:compileJava
|
||||||
|
> Task :adapter:outbound:notification:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:notification:classes
|
||||||
|
> Task :adapter:outbound:notification:jar
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJava
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processResources
|
||||||
|
> Task :adapter:outbound:persistence-jpa:classes
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jar
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileJava
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processResources
|
||||||
|
> Task :adapter:outbound:persistence-mongo:classes
|
||||||
|
> Task :adapter:outbound:persistence-mongo:jar
|
||||||
|
> Task :app-bootstrap:compileJava
|
||||||
|
> Task :sample-portfolio:compileJava
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:compileJava
|
||||||
|
> Task :messaging:messaging-nats-experimental:compileJava
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:compileJava
|
||||||
|
> Task :messaging:messaging-schema-avro:compileJava
|
||||||
|
> Task :messaging:messaging-schema-protobuf:compileJava
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:compileJava
|
||||||
|
> Task :messaging:messaging-testkit:compileJava
|
||||||
|
> Task :adapter:inbound:grpc:compileJava
|
||||||
|
> Task :adapter:inbound:websocket:compileJava
|
||||||
|
> Task :adapter:outbound:objectstorage:compileJava
|
||||||
|
> Task :app-bootstrap:processResources
|
||||||
|
> Task :app-bootstrap:classes
|
||||||
|
> Task :sample-portfolio:processResources
|
||||||
|
> Task :sample-portfolio:classes
|
||||||
|
> Task :sample-portfolio:jar
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitClasses
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitJar
|
||||||
|
> Task :app-bootstrap:compileTestJava
|
||||||
|
> Task :application-core:compileTestJava
|
||||||
|
> Task :domain-core:compileTestJava NO-SOURCE
|
||||||
|
> Task :sample-portfolio:compileTestJava
|
||||||
|
> Task :shared-contract:compileTestJava
|
||||||
|
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-api:classes
|
||||||
|
> Task :messaging:messaging-admin-api:compileTestJava
|
||||||
|
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-runtime:classes
|
||||||
|
> Task :messaging:messaging-admin-runtime:compileTestJava
|
||||||
|
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-claim-check:classes
|
||||||
|
> Task :messaging:messaging-claim-check:compileTestJava
|
||||||
|
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-cloudevents:classes
|
||||||
|
> Task :messaging:messaging-cloudevents:compileTestJava
|
||||||
|
> Task :messaging:messaging-core-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-core-api:classes
|
||||||
|
> Task :messaging:messaging-core-api:compileTestJava
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:classes
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:compileTestJava
|
||||||
|
> Task :messaging:messaging-kafka:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka:classes
|
||||||
|
> Task :messaging:messaging-kafka:compileTestJava
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:classes
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:compileTestJava
|
||||||
|
> Task :messaging:messaging-nats-experimental:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-nats-experimental:classes
|
||||||
|
> Task :messaging:messaging-nats-experimental:compileTestJava
|
||||||
|
> Task :messaging:messaging-observability:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-observability:classes
|
||||||
|
> Task :messaging:messaging-observability:compileTestJava
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:classes
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:compileTestJava
|
||||||
|
> Task :messaging:messaging-policy:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-policy:classes
|
||||||
|
> Task :messaging:messaging-policy:compileTestJava
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:classes
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:compileTestJava
|
||||||
|
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-rabbit:classes
|
||||||
|
> Task :messaging:messaging-rabbit:compileTestJava
|
||||||
|
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-reliability-api:classes
|
||||||
|
> Task :messaging:messaging-reliability-api:compileTestJava NO-SOURCE
|
||||||
|
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-runtime-core:classes
|
||||||
|
> Task :messaging:messaging-runtime-core:compileTestJava
|
||||||
|
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-api:classes
|
||||||
|
> Task :messaging:messaging-schema-api:compileTestJava
|
||||||
|
> Task :messaging:messaging-schema-avro:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-avro:classes
|
||||||
|
> Task :messaging:messaging-schema-avro:compileTestJava
|
||||||
|
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-json:classes
|
||||||
|
> Task :messaging:messaging-schema-json:compileTestJava
|
||||||
|
> Task :messaging:messaging-schema-protobuf:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:classes
|
||||||
|
> Task :messaging:messaging-schema-protobuf:compileTestJava
|
||||||
|
> Task :messaging:messaging-security:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-security:classes
|
||||||
|
> Task :messaging:messaging-security:compileTestJava
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:processResources
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:classes
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:compileTestJava
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:classes
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:compileTestJava
|
||||||
|
> Task :messaging:messaging-testkit:processResources
|
||||||
|
> Task :messaging:messaging-testkit:classes
|
||||||
|
> Task :messaging:messaging-testkit:compileTestJava
|
||||||
|
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-transport-spi:classes
|
||||||
|
> Task :messaging:messaging-transport-spi:compileTestJava
|
||||||
|
> Task :adapter:inbound:graphql:compileTestFixturesJava
|
||||||
|
> Task :adapter:inbound:graphql:compileTestJava
|
||||||
|
> Task :adapter:inbound:grpc:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:grpc:classes
|
||||||
|
> Task :adapter:inbound:grpc:compileTestJava
|
||||||
|
> Task :adapter:inbound:web:compileTestJava
|
||||||
|
> Task :adapter:inbound:websocket:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:websocket:classes
|
||||||
|
> Task :adapter:inbound:websocket:compileTestJava
|
||||||
|
> Task :adapter:outbound:cache-redis:compileTestJava
|
||||||
|
> Task :adapter:outbound:fileserver:compileTestJava
|
||||||
|
> Task :adapter:outbound:httpclient:compileTestkitJava
|
||||||
|
> Task :adapter:outbound:httpclient:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:httpclient:testkitClasses
|
||||||
|
> Task :adapter:outbound:httpclient:compileTestJava
|
||||||
|
> Task :adapter:outbound:identifier:compileTestJava
|
||||||
|
> Task :adapter:outbound:messaging:compileTestJava
|
||||||
|
> Task :adapter:outbound:notification:compileTestJava
|
||||||
|
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:objectstorage:classes
|
||||||
|
> Task :adapter:outbound:objectstorage:compileTestJava
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestJava
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestkitJava
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testkitClasses
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestJava
|
||||||
|
> Task :adapter:outbound:support:compileTestJava
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 4m 48s
|
||||||
|
170 actionable tasks: 162 executed, 8 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
exit=0
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
$ ./gradlew test --warning-mode=fail --no-daemon --console=plain
|
||||||
|
run-at: 2026-08-20T01:33:26Z
|
||||||
|
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
|
||||||
|
Daemon will be stopped at the end of the build
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :shared-contract:compileJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processResources UP-TO-DATE
|
||||||
|
> Task :shared-contract:classes UP-TO-DATE
|
||||||
|
> Task :shared-contract:jar UP-TO-DATE
|
||||||
|
> Task :application-core:compileJava UP-TO-DATE
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes UP-TO-DATE
|
||||||
|
> Task :application-core:jar UP-TO-DATE
|
||||||
|
> Task :domain-core:compileJava UP-TO-DATE
|
||||||
|
> Task :domain-core:processResources NO-SOURCE
|
||||||
|
> Task :domain-core:classes UP-TO-DATE
|
||||||
|
> Task :domain-core:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:jar UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:web:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:support:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:fileserver:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:httpclient:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
|
||||||
|
> Task :adapter:outbound:identifier:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:identifier:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:notification:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:jar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:compileJava UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:processResources UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:classes UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:compileJava UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:processResources UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:classes UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitJar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:compileTestJava UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:runtimeClasspathManifest UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:processTestResources UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-runtime:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-claim-check:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-cloudevents:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-core-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-observability:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-policy:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-rabbit:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-reliability-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-runtime-core:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-json:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-security:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:processResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-transport-spi:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:objectstorage:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:objectstorage:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:objectstorage:jar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:test UP-TO-DATE
|
||||||
|
> Task :application-core:compileTestJava UP-TO-DATE
|
||||||
|
> Task :application-core:processTestResources NO-SOURCE
|
||||||
|
> Task :application-core:testClasses UP-TO-DATE
|
||||||
|
> Task :application-core:test UP-TO-DATE
|
||||||
|
> Task :domain-core:compileTestJava NO-SOURCE
|
||||||
|
> Task :domain-core:processTestResources NO-SOURCE
|
||||||
|
> Task :domain-core:testClasses UP-TO-DATE
|
||||||
|
> Task :domain-core:test NO-SOURCE
|
||||||
|
> Task :sample-portfolio:compileTestJava UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:processTestResources UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:testClasses UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:test UP-TO-DATE
|
||||||
|
> Task :shared-contract:compileTestJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processTestResources NO-SOURCE
|
||||||
|
> Task :shared-contract:testClasses UP-TO-DATE
|
||||||
|
> Task :shared-contract:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-api:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-runtime:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-claim-check:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-cloudevents:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-core-api:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:processResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka-share-experimental:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-nats-experimental:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-nats-experimental:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-nats-experimental:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-observability:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-policy:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-pulsar-experimental:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-rabbit:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:compileTestJava NO-SOURCE
|
||||||
|
> Task :messaging:messaging-reliability-api:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-reliability-api:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:test NO-SOURCE
|
||||||
|
> Task :messaging:messaging-runtime-core:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-runtime-core:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-api:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-avro:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:processTestResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-avro:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-json:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-protobuf:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-security:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:processTestResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-cloud-stream-bridge:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-testkit:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:test UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-transport-spi:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:test UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:compileTestFixturesJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:processTestFixturesResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:graphql:testFixturesClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:testFixturesJar UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:processTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:test UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:grpc:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:grpc:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:test UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:web:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:test UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:websocket:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:websocket:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:processTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:fileserver:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:httpclient:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:httpclient:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:compileTestGroovy UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:identifier:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:processTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:processTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:test
|
||||||
|
> Task :adapter:outbound:objectstorage:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:objectstorage:processTestResources
|
||||||
|
> Task :adapter:outbound:objectstorage:testClasses
|
||||||
|
> Task :adapter:outbound:objectstorage:test
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:test
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:test
|
||||||
|
> Task :adapter:outbound:support:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:support:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:test
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 2m 38s
|
||||||
|
200 actionable tasks: 6 executed, 194 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
exit=0
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Task 1 — the Wave 0 red set is empty, and the lanes are removed
|
||||||
|
|
||||||
|
## Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src
|
||||||
|
./gradlew wave0RedReport --console=plain --no-daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
`BUILD SUCCESSFUL in 43s` — but that says nothing on its own, and this is the important part of the
|
||||||
|
evidence rather than a caveat to it. Both `wave0Red` lanes set `ignoreFailures = true`, because they
|
||||||
|
were reports rather than gates: their job was to answer "what is still red from the baseline?", not
|
||||||
|
to fail a build. A green exit code from a report is not a claim about the tests.
|
||||||
|
|
||||||
|
## What the results actually say
|
||||||
|
|
||||||
|
```
|
||||||
|
app-bootstrap/build/test-results/wave0Red: 0 classes, 0 tests, 0 failures, 0 skipped
|
||||||
|
messaging/messaging-observability/build/test-results/wave0Red: 0 classes, 0 tests, 0 failures, 0 skipped
|
||||||
|
```
|
||||||
|
|
||||||
|
Zero tests, because no test carries the tag any more:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rc 'wave0-red' $(git ls-files '*.java') # no match in any tracked Java source
|
||||||
|
```
|
||||||
|
|
||||||
|
The three Wave 0 characterizations — the full-`test` scanner failure, the five-switch-off bean and
|
||||||
|
resource inventory, and the local/dev boot and Compose merge reproductions — became ordinary tests as
|
||||||
|
the waves that fixed them landed, and their `@Tag("wave0-red")` markers came off with them. The red
|
||||||
|
set is empty by the only measure that matters: there is nothing left tagged.
|
||||||
|
|
||||||
|
## Why the lanes are deleted rather than kept
|
||||||
|
|
||||||
|
Two reasons, and the second is the one that generalises.
|
||||||
|
|
||||||
|
1. The plan says so, and its reason holds: a permanent lane for an empty set is a lane that stops
|
||||||
|
being read.
|
||||||
|
2. These lanes are the exact shape the Wave 5 lane convention exists to refuse. A tag filter that
|
||||||
|
matches nothing does not fail — `failOnNoDiscoveredTests` applies to discovery and a tag excludes
|
||||||
|
after discovery — so the lane runs, executes zero tests, and reports success. Here that is
|
||||||
|
harmless, because the lanes are reports and their emptiness is the answer. But leaving two
|
||||||
|
hand-rolled lanes in that shape, outside the convention that would have refused them, is leaving
|
||||||
|
a template for the next lane somebody copies.
|
||||||
|
|
||||||
|
Removed:
|
||||||
|
|
||||||
|
- `src/build.gradle` — the `wave0RedReport` aggregate
|
||||||
|
- `src/app-bootstrap/build.gradle` — the `wave0Red` lane and the `test { excludeTags 'wave0-red' }`
|
||||||
|
- `src/messaging/messaging-observability/build.gradle` — the same pair
|
||||||
|
|
||||||
|
The `excludeTags` removal matters as much as the lane removal: it was what kept tagged
|
||||||
|
characterizations out of the ordinary suite. With no tagged test left it is inert, and leaving it
|
||||||
|
would silently exclude any future test that reused the tag.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
$ ./gradlew wave0RedReport --console=plain --no-daemon
|
||||||
|
run-at: 2026-08-20 (Wave 6 execution pass)
|
||||||
|
|
||||||
|
FAILURE: Build failed with an exception.
|
||||||
|
|
||||||
|
* What went wrong:
|
||||||
|
Task 'wave0RedReport' not found in root project 'ca-skeleton' and its subprojects.
|
||||||
|
|
||||||
|
exit=1
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
READ THIS BEFORE TREATING THE LINE ABOVE AS A REGRESSION.
|
||||||
|
|
||||||
|
This is the expected end state, not a failure. Task 1's checkbox is "confirm the report is empty,
|
||||||
|
*then delete the lanes and the aggregate*". The deletion happened, so the task it names no longer
|
||||||
|
exists and the command can no longer run. `grep -rn wave0Red src --include=*.gradle` returns
|
||||||
|
nothing, which is the same fact from the other direction.
|
||||||
|
|
||||||
|
The substantive evidence — the empty red set measured while the lanes still existed — is in
|
||||||
|
`task1-wave0-red-set.md` beside this file: 0 classes / 0 tests / 0 failures in both lanes, with the
|
||||||
|
note that those lanes ran `ignoreFailures = true` and so a green exit code from them was never the
|
||||||
|
claim.
|
||||||
|
|
||||||
|
Provenance note: this file previously held the raw log of that earlier successful run. It was
|
||||||
|
overwritten during the Wave 6 execution pass by re-running the command under the same filename
|
||||||
|
without checking what was already there. The original log is not recoverable (this directory is
|
||||||
|
untracked). What was lost is the raw transcript; what the checkbox depends on survives in the
|
||||||
|
`.md` above, which was written from it.
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
# Task 2 — Wave 2 Task B5, the three ghost release lanes
|
||||||
|
|
||||||
|
Wave 2 offered two outcomes for `mongoShardedTest`, `mongoAtlasTest` and `mongoKmsTest`: implement
|
||||||
|
them, or demote them and stop describing an unrunnable gate. **(b) Demote was taken**, and it is
|
||||||
|
reflected in all three places the plan names.
|
||||||
|
|
||||||
|
## `src/config/mongodb/release-contracts.json`
|
||||||
|
|
||||||
|
All three moved out of the Stable blocking set into `experimental_contracts`, each carrying
|
||||||
|
`"blocking": false`, `"promotion": "experimental"` and a `not_promoted_reason` that states the
|
||||||
|
mechanism rather than an intention:
|
||||||
|
|
||||||
|
> A manifest entry pointing at an unregistered task does not fail; it is simply never run, and the
|
||||||
|
> release reports green for a capability nobody qualified. Demoted rather than implemented so the
|
||||||
|
> green means what it says.
|
||||||
|
|
||||||
|
That is the correct reading. A release manifest naming a task that no build file registers is not a
|
||||||
|
failing gate — it is an absent one, and absence is indistinguishable from success in a report that
|
||||||
|
counts failures.
|
||||||
|
|
||||||
|
## `docs/mongodb/advanced/sharding.md`
|
||||||
|
|
||||||
|
The gate row now reads:
|
||||||
|
|
||||||
|
> **Not promoted.** No `mongoShardedTest` lane is registered, and a sharded cluster is not an
|
||||||
|
> environment this repository stands up. Listed under `experimental_contracts` in
|
||||||
|
> `src/config/mongodb/release-contracts.json`; promoting it needs the lane, its required class, and
|
||||||
|
> protected-environment evidence to exist first.
|
||||||
|
|
||||||
|
## `scripts/verify-mongodb-advanced.sh`
|
||||||
|
|
||||||
|
Setting `MONGODB_SHARDED_URI` is now an explicit error rather than an invocation of a task that does
|
||||||
|
not exist:
|
||||||
|
|
||||||
|
```
|
||||||
|
sharded topology is experimental and has no registered lane;
|
||||||
|
MONGODB_SHARDED_URI was set but mongoShardedTest does not exist.
|
||||||
|
See experimental_contracts in src/config/mongodb/release-contracts.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
The distinction the script draws is worth keeping: an operator who exported the URI expected a
|
||||||
|
qualification to run, so silence would be worse than failure. With the URI unset it records missing
|
||||||
|
evidence instead, which is a different statement from a pass.
|
||||||
|
|
||||||
|
## The arbiter
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src
|
||||||
|
./gradlew :app-bootstrap:test --tests '*ReleaseManifestTaskExistenceTest' --console=plain
|
||||||
|
```
|
||||||
|
|
||||||
|
`BUILD SUCCESSFUL` — 1 test, 0 failures, 0 skipped. The manifest names no blocking task that the
|
||||||
|
build does not register.
|
||||||
|
|
||||||
|
## Registered Mongo lanes, for the record
|
||||||
|
|
||||||
|
`mongoStableContractTest`, `mongoReplicaSetTest`, `mongoFailoverTest`, `mongoMigrationTest`,
|
||||||
|
`mongoCompatibilityTest`, `mongoSecurityIntegrationTest`, `mongoPerformanceTest` — seven, and none of
|
||||||
|
the three demoted names among them, which is the state the demotion describes.
|
||||||
+181
@@ -0,0 +1,181 @@
|
|||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :shared-contract:compileJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processResources UP-TO-DATE
|
||||||
|
> Task :shared-contract:classes UP-TO-DATE
|
||||||
|
> Task :shared-contract:jar UP-TO-DATE
|
||||||
|
> Task :application-core:compileJava UP-TO-DATE
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes UP-TO-DATE
|
||||||
|
> Task :application-core:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:support:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:processTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:notification:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:processTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:test UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:jar UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:compileTestFixturesJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:processTestFixturesResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:graphql:testFixturesClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:testFixturesJar UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:processTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:test UP-TO-DATE
|
||||||
|
2026-08-19T04:59:21.954Z INFO 432501 --- [ionShutdownHook] o.s.boot.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
|
||||||
|
2026-08-19T04:59:21.963Z INFO 432501 --- [tomcat-shutdown] o.s.boot.tomcat.GracefulShutdown : Graceful shutdown complete
|
||||||
|
2026-08-19T04:59:21.978Z INFO 432501 --- [ionShutdownHook] o.s.boot.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
|
||||||
|
2026-08-19T04:59:21.980Z INFO 432501 --- [tomcat-shutdown] o.s.boot.tomcat.GracefulShutdown : Graceful shutdown complete
|
||||||
|
> Task :adapter:inbound:graphql:graphqlStableTest
|
||||||
|
|
||||||
|
> Task :verifyRuntimeModuleMembership
|
||||||
|
verifyRuntimeModuleMembership: 2 runtime composition(s) match the registry
|
||||||
|
|
||||||
|
> Task :domain-core:compileJava UP-TO-DATE
|
||||||
|
> Task :domain-core:processResources NO-SOURCE
|
||||||
|
> Task :domain-core:classes UP-TO-DATE
|
||||||
|
> Task :domain-core:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:web:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:fileserver:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:httpclient:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
|
||||||
|
> Task :adapter:outbound:identifier:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:identifier:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:jar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:compileJava UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:processResources UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:grpc:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:jar UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:websocket:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:jar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:compileConditionalTransportTestJava UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:processConditionalTransportTestResources NO-SOURCE
|
||||||
|
> Task :app-bootstrap:conditionalTransportTestClasses UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:conditionalTransportCompositionTestRequiredClasses
|
||||||
|
> Task :app-bootstrap:conditionalTransportCompositionTest
|
||||||
|
|
||||||
|
> Task :app-bootstrap:conditionalTransportCompositionTestEvidence
|
||||||
|
conditionalTransportCompositionTest: 3 tests, 0 skipped
|
||||||
|
|
||||||
|
> Task :adapter:inbound:graphql:graphqlTransportQualificationTestRequiredClasses
|
||||||
|
2026-08-19T04:59:43.275Z INFO 433478 --- [ionShutdownHook] o.s.boot.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
|
||||||
|
2026-08-19T04:59:43.288Z INFO 433478 --- [tomcat-shutdown] o.s.boot.tomcat.GracefulShutdown : Graceful shutdown complete
|
||||||
|
> Task :adapter:inbound:graphql:graphqlTransportQualificationTest
|
||||||
|
|
||||||
|
> Task :adapter:inbound:graphql:graphqlTransportQualificationTestEvidence
|
||||||
|
graphqlTransportQualificationTest: 8 tests, 0 skipped
|
||||||
|
|
||||||
|
> Task :adapter:inbound:grpc:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:grpc:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:grpcTransportQualificationTestRequiredClasses
|
||||||
|
> Task :adapter:inbound:grpc:grpcTransportQualificationTest
|
||||||
|
|
||||||
|
> Task :adapter:inbound:grpc:grpcTransportQualificationTestEvidence
|
||||||
|
grpcTransportQualificationTest: 15 tests, 0 skipped
|
||||||
|
|
||||||
|
> Task :adapter:inbound:websocket:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:websocket:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:websocketTransportQualificationTestRequiredClasses
|
||||||
|
2026-08-19T05:00:00.518Z INFO 437360 --- [ionShutdownHook] o.s.boot.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
|
||||||
|
2026-08-19T05:00:00.523Z INFO 437360 --- [tomcat-shutdown] o.s.boot.tomcat.GracefulShutdown : Graceful shutdown complete
|
||||||
|
> Task :adapter:inbound:websocket:websocketTransportQualificationTest
|
||||||
|
|
||||||
|
> Task :adapter:inbound:websocket:websocketTransportQualificationTestEvidence
|
||||||
|
websocketTransportQualificationTest: 5 tests, 0 skipped
|
||||||
|
|
||||||
|
> Task :conditionalTransportQualification
|
||||||
|
conditional-transport-graphql: 8 tests, 0 skipped
|
||||||
|
conditional-transport-grpc: 15 tests, 0 skipped
|
||||||
|
conditional-transport-websocket: 5 tests, 0 skipped
|
||||||
|
conditional-transport-composition: 3 tests, 0 skipped
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 1m 8s
|
||||||
|
101 actionable tasks: 15 executed, 86 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
EXIT=0
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
# NOT RUN — the GraphQL bootJar JWT qualification
|
||||||
|
|
||||||
|
Recorded as not-run with its reason, per this wave's rule that a gate which could not run is never
|
||||||
|
reported as passing.
|
||||||
|
|
||||||
|
## What was attempted
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src
|
||||||
|
./gradlew :app-bootstrap:graphqlRuntimeQualification --console=plain
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
* What went wrong:
|
||||||
|
Cannot locate tasks that match ':app-bootstrap:graphqlRuntimeQualification' as task
|
||||||
|
'graphqlRuntimeQualification' not found in project ':app-bootstrap'.
|
||||||
|
exit=1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why it cannot run
|
||||||
|
|
||||||
|
The task does not exist, and neither does the source set the spec names for it.
|
||||||
|
`app-bootstrap/src/` holds `main`, `test`, `functionalTest`, `conditionalTransportTest` and
|
||||||
|
`sampleOffTest` — there is no `graphqlRuntimeQualificationTest`.
|
||||||
|
|
||||||
|
The spec (`…-five-adapter-runtime-remediation-review-design.md`, GraphQL section) fixes the
|
||||||
|
canonical task as `:app-bootstrap:graphqlRuntimeQualification`, depending on `bootJar`, running the
|
||||||
|
produced jar as a child process, taking a client-credentials token from a Keycloak container that
|
||||||
|
imported the same tracked realm artifact, and calling real HTTP `/graphql`. None of that machinery
|
||||||
|
was built.
|
||||||
|
|
||||||
|
## What exists instead, and why it does not substitute
|
||||||
|
|
||||||
|
| Artefact | What it actually proves | Why it is not release evidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `GraphqlHttpBoundaryQualificationTest` | `/graphql` and `/graphiql` answer over HTTP in a `@SpringBootTest` | authenticates with `withBasicAuth(USERNAME, PASSWORD)` and `httpBasic(Customizer.withDefaults())` — test-only Basic auth, not the JWT decoder a deployment runs |
|
||||||
|
| `ConditionalTransportCompositionContractTest` | the GraphQL types load and the leaf's runtime membership matches the registry | `assertThatCodeLoads(typeName)` is class existence; it makes no request and sees no token |
|
||||||
|
|
||||||
|
The spec anticipates exactly these two and rules both out by name: the boundary test "may remain a
|
||||||
|
module contract test but is not aggregated into release evidence", and class existence is named as
|
||||||
|
the thing the qualification exists to replace.
|
||||||
|
|
||||||
|
## Consequence for the Definition of Done
|
||||||
|
|
||||||
|
Spec §13 item — *"GraphQL blocking qualification runs the bootJar JWT composition exactly once and
|
||||||
|
uses neither class-existence nor test-only Basic Auth as release evidence"* — **cannot be ticked**.
|
||||||
|
It is the one item of the twenty-four in that state.
|
||||||
|
|
||||||
|
## This was already declared, not discovered
|
||||||
|
|
||||||
|
`.github/ci-gate-matrix.yml` registers the gate as `mechanism: delegated-pending` with the comment
|
||||||
|
that it "inherits that control's pending status rather than having none of its own", and
|
||||||
|
`verify-gate-matrix.sh` reports `49 gates, 45 verified, 4 delegated-pending`. The gap is recorded in
|
||||||
|
the repository's own control plane; this file is the Wave 6 confirmation of it, not a new finding.
|
||||||
|
|
||||||
|
## What closing it would take
|
||||||
|
|
||||||
|
A new `graphqlRuntimeQualificationTest` source set, a Gradle lane depending on `bootJar`, a child
|
||||||
|
process launcher for the jar, a Keycloak container importing `infra/keycloak/realms/
|
||||||
|
ca-skeleton-realm.json`, and the required-class / zero-discovery / stale-XML refusals the spec lists.
|
||||||
|
That is new capability, which this wave explicitly does not add.
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
$ ./gradlew :app-bootstrap:graphqlRuntimeQualification --console=plain
|
||||||
|
run-at: 2026-08-20T07:20:21Z
|
||||||
|
Mem: 30Gi 18Gi 1.4Gi 688Mi 12Gi 12Gi
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
|
||||||
|
[Incubating] Problems report is available at: file:///home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/build/reports/problems/problems-report.html
|
||||||
|
|
||||||
|
FAILURE: Build failed with an exception.
|
||||||
|
|
||||||
|
* What went wrong:
|
||||||
|
Cannot locate tasks that match ':app-bootstrap:graphqlRuntimeQualification' as task 'graphqlRuntimeQualification' not found in project ':app-bootstrap'.
|
||||||
|
|
||||||
|
* Try:
|
||||||
|
> Run gradlew tasks to get a list of available tasks.
|
||||||
|
> For more on name expansion, please refer to https://docs.gradle.org/9.0.0/userguide/command_line_interface.html#sec:name_abbreviation in the Gradle documentation.
|
||||||
|
> Run with --stacktrace option to get the stack trace.
|
||||||
|
> Run with --info or --debug option to get more log output.
|
||||||
|
> Run with --scan to generate a Build Scan (Powered by Develocity).
|
||||||
|
> Get more help at https://help.gradle.org.
|
||||||
|
|
||||||
|
BUILD FAILED in 2s
|
||||||
|
8 actionable tasks: 8 up-to-date
|
||||||
|
exit=1
|
||||||
+162
@@ -0,0 +1,162 @@
|
|||||||
|
$ ./gradlew :adapter:inbound:graphql:graphqlStableTest conditionalTransportQualification --console=plain
|
||||||
|
run-at: 2026-08-20T07:18:59Z
|
||||||
|
total used free shared buff/cache available
|
||||||
|
Mem: 30Gi 17Gi 2.0Gi 687Mi 12Gi 13Gi
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :shared-contract:compileJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processResources UP-TO-DATE
|
||||||
|
> Task :shared-contract:classes UP-TO-DATE
|
||||||
|
> Task :shared-contract:jar UP-TO-DATE
|
||||||
|
> Task :application-core:compileJava UP-TO-DATE
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes UP-TO-DATE
|
||||||
|
> Task :application-core:jar UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:jar UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:compileTestFixturesJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:processTestFixturesResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:graphql:testFixturesClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:testFixturesJar UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:processTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:testClasses UP-TO-DATE
|
||||||
|
2026-08-20T07:19:40.310Z INFO 3669807 --- [ionShutdownHook] o.s.boot.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
|
||||||
|
2026-08-20T07:19:40.317Z INFO 3669807 --- [tomcat-shutdown] o.s.boot.tomcat.GracefulShutdown : Graceful shutdown complete
|
||||||
|
2026-08-20T07:19:40.345Z INFO 3669807 --- [ionShutdownHook] o.s.boot.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
|
||||||
|
2026-08-20T07:19:40.347Z INFO 3669807 --- [tomcat-shutdown] o.s.boot.tomcat.GracefulShutdown : Graceful shutdown complete
|
||||||
|
> Task :adapter:inbound:graphql:graphqlStableTest
|
||||||
|
|
||||||
|
> Task :verifyRuntimeModuleMembership
|
||||||
|
verifyRuntimeModuleMembership: 2 runtime composition(s) match the registry
|
||||||
|
|
||||||
|
> Task :domain-core:compileJava UP-TO-DATE
|
||||||
|
> Task :domain-core:processResources NO-SOURCE
|
||||||
|
> Task :domain-core:classes UP-TO-DATE
|
||||||
|
> Task :domain-core:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:web:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:support:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:fileserver:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:httpclient:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
|
||||||
|
> Task :adapter:outbound:identifier:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:identifier:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:notification:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:jar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:compileJava UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:processResources UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:grpc:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:jar UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:websocket:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:jar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:compileConditionalTransportTestJava UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:processConditionalTransportTestResources NO-SOURCE
|
||||||
|
> Task :app-bootstrap:conditionalTransportTestClasses UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:conditionalTransportCompositionTestRequiredClasses
|
||||||
|
> Task :app-bootstrap:conditionalTransportCompositionTest
|
||||||
|
|
||||||
|
> Task :app-bootstrap:conditionalTransportCompositionTestEvidence
|
||||||
|
conditionalTransportCompositionTest: 3 tests, 0 skipped
|
||||||
|
|
||||||
|
> Task :adapter:inbound:graphql:graphqlTransportQualificationTestRequiredClasses
|
||||||
|
2026-08-20T07:19:56.193Z INFO 3670728 --- [ionShutdownHook] o.s.boot.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
|
||||||
|
2026-08-20T07:19:56.198Z INFO 3670728 --- [tomcat-shutdown] o.s.boot.tomcat.GracefulShutdown : Graceful shutdown complete
|
||||||
|
> Task :adapter:inbound:graphql:graphqlTransportQualificationTest
|
||||||
|
|
||||||
|
> Task :adapter:inbound:graphql:graphqlTransportQualificationTestEvidence
|
||||||
|
graphqlTransportQualificationTest: 8 tests, 0 skipped
|
||||||
|
|
||||||
|
> Task :adapter:inbound:grpc:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:grpc:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:grpc:grpcTransportQualificationTestRequiredClasses
|
||||||
|
> Task :adapter:inbound:grpc:grpcTransportQualificationTest
|
||||||
|
|
||||||
|
> Task :adapter:inbound:grpc:grpcTransportQualificationTestEvidence
|
||||||
|
grpcTransportQualificationTest: 15 tests, 0 skipped
|
||||||
|
|
||||||
|
> Task :adapter:inbound:websocket:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:websocket:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:websocket:websocketTransportQualificationTestRequiredClasses
|
||||||
|
2026-08-20T07:20:10.449Z INFO 3671395 --- [ionShutdownHook] o.s.boot.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
|
||||||
|
2026-08-20T07:20:10.454Z INFO 3671395 --- [tomcat-shutdown] o.s.boot.tomcat.GracefulShutdown : Graceful shutdown complete
|
||||||
|
> Task :adapter:inbound:websocket:websocketTransportQualificationTest
|
||||||
|
|
||||||
|
> Task :adapter:inbound:websocket:websocketTransportQualificationTestEvidence
|
||||||
|
websocketTransportQualificationTest: 5 tests, 0 skipped
|
||||||
|
|
||||||
|
> Task :conditionalTransportQualification
|
||||||
|
conditional-transport-graphql: 8 tests, 0 skipped
|
||||||
|
conditional-transport-grpc: 15 tests, 0 skipped
|
||||||
|
conditional-transport-websocket: 5 tests, 0 skipped
|
||||||
|
conditional-transport-composition: 3 tests, 0 skipped
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 1m 11s
|
||||||
|
88 actionable tasks: 15 executed, 73 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
exit=0
|
||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
===== PostgreSQL 16 =====
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :shared-contract:compileJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processResources UP-TO-DATE
|
||||||
|
> Task :shared-contract:classes UP-TO-DATE
|
||||||
|
> Task :shared-contract:jar UP-TO-DATE
|
||||||
|
> Task :application-core:compileJava UP-TO-DATE
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes UP-TO-DATE
|
||||||
|
> Task :application-core:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformFailureTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformMigrationTest
|
||||||
|
|
||||||
|
PostgreSqlNotificationSchemaActivationIntegrationTest > V1 to V4, disable, re-enable and an interrupted migration all recover FAILED
|
||||||
|
org.opentest4j.AssertionFailedError at PostgreSqlNotificationSchemaActivationIntegrationTest.java:184
|
||||||
|
|
||||||
|
43 tests completed, 1 failed
|
||||||
|
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformMigrationTest FAILED
|
||||||
|
|
||||||
|
FAILURE: Build failed with an exception.
|
||||||
|
|
||||||
|
* What went wrong:
|
||||||
|
Execution failed for task ':adapter:outbound:persistence-jpa:jpaPlatformMigrationTest'.
|
||||||
|
> There were failing tests. See the report at: file:///home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/reports/tests/jpaPlatformMigrationTest/index.html
|
||||||
|
|
||||||
|
* Try:
|
||||||
|
> Run with --scan to generate a Build Scan (Powered by Develocity).
|
||||||
|
|
||||||
|
BUILD FAILED in 4m 58s
|
||||||
|
21 actionable tasks: 3 executed, 18 up-to-date
|
||||||
|
EXIT(16)=1
|
||||||
|
===== PostgreSQL 17 =====
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :shared-contract:compileJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processResources UP-TO-DATE
|
||||||
|
> Task :shared-contract:classes UP-TO-DATE
|
||||||
|
> Task :shared-contract:jar UP-TO-DATE
|
||||||
|
> Task :application-core:compileJava UP-TO-DATE
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes UP-TO-DATE
|
||||||
|
> Task :application-core:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformFailureTest
|
||||||
|
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformMigrationTest
|
||||||
|
|
||||||
|
PostgreSqlNotificationSchemaActivationIntegrationTest > V1 to V4, disable, re-enable and an interrupted migration all recover FAILED
|
||||||
|
org.opentest4j.AssertionFailedError at PostgreSqlNotificationSchemaActivationIntegrationTest.java:184
|
||||||
|
|
||||||
|
43 tests completed, 1 failed
|
||||||
|
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformMigrationTest FAILED
|
||||||
|
|
||||||
|
FAILURE: Build failed with an exception.
|
||||||
|
|
||||||
|
* What went wrong:
|
||||||
|
Execution failed for task ':adapter:outbound:persistence-jpa:jpaPlatformMigrationTest'.
|
||||||
|
> There were failing tests. See the report at: file:///home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/reports/tests/jpaPlatformMigrationTest/index.html
|
||||||
|
|
||||||
|
* Try:
|
||||||
|
> Run with --scan to generate a Build Scan (Powered by Develocity).
|
||||||
|
|
||||||
|
BUILD FAILED in 5m 42s
|
||||||
|
21 actionable tasks: 3 executed, 18 up-to-date
|
||||||
|
EXIT(17)=1
|
||||||
|
===== PostgreSQL 18 =====
|
||||||
|
> Task :build-logic:extractPluginRequests
|
||||||
|
> Task :build-logic:generatePluginAdapters
|
||||||
|
> Task :build-logic:compileJava
|
||||||
|
> Task :build-logic:compileGroovy
|
||||||
|
> Task :build-logic:compileGroovyPlugins
|
||||||
|
> Task :build-logic:pluginDescriptors
|
||||||
|
> Task :build-logic:processResources
|
||||||
|
> Task :build-logic:classes
|
||||||
|
> Task :build-logic:jar
|
||||||
|
> Task :shared-contract:compileJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processResources UP-TO-DATE
|
||||||
|
> Task :shared-contract:classes UP-TO-DATE
|
||||||
|
> Task :shared-contract:jar UP-TO-DATE
|
||||||
|
> Task :application-core:compileJava UP-TO-DATE
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes UP-TO-DATE
|
||||||
|
> Task :application-core:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest FAILED
|
||||||
|
|
||||||
|
FAILURE: Build failed with an exception.
|
||||||
|
|
||||||
|
* What went wrong:
|
||||||
|
Execution failed for task ':adapter:outbound:persistence-jpa:jpaPlatformContractTest'.
|
||||||
|
> Multiple build operations failed.
|
||||||
|
Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.StablePostgreSqlMatrixContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.StablePostgreSqlMatrixContractTest.xml.
|
||||||
|
Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlArrayRangeContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlArrayRangeContractTest.xml.
|
||||||
|
Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlPessimisticLockContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlPessimisticLockContractTest.xml.
|
||||||
|
Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.OptimisticRetryIntegrationTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.OptimisticRetryIntegrationTest.xml.
|
||||||
|
Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlCopyLoaderIntegrationTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlCopyLoaderIntegrationTest.xml.
|
||||||
|
Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.experimental.ReadAfterWriteRoutingContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.experimental.ReadAfterWriteRoutingContractTest.xml.
|
||||||
|
Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlNotificationIdempotencyRaceIntegrationTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlNotificationIdempotencyRaceIntegrationTest.xml.
|
||||||
|
Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.experimental.TenantColumnIsolationTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.experimental.TenantColumnIsolationTest.xml.
|
||||||
|
Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.experimental.TenantPoolCapacityContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.experimental.TenantPoolCapacityContractTest.xml.
|
||||||
|
Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlWorkClaimContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlWorkClaimContractTest.xml.
|
||||||
|
...and 7 more failures.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.StablePostgreSqlMatrixContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.StablePostgreSqlMatrixContractTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlArrayRangeContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlArrayRangeContractTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlPessimisticLockContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlPessimisticLockContractTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.OptimisticRetryIntegrationTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.OptimisticRetryIntegrationTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlCopyLoaderIntegrationTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlCopyLoaderIntegrationTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.experimental.ReadAfterWriteRoutingContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.experimental.ReadAfterWriteRoutingContractTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlNotificationIdempotencyRaceIntegrationTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlNotificationIdempotencyRaceIntegrationTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.experimental.TenantColumnIsolationTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.experimental.TenantColumnIsolationTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.experimental.TenantPoolCapacityContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.experimental.TenantPoolCapacityContractTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlWorkClaimContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlWorkClaimContractTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.JpaLifecycleAssociationContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.JpaLifecycleAssociationContractTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlUpsertContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlUpsertContractTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlJsonbContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlJsonbContractTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.JpaValueMappingContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.JpaValueMappingContractTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.JpaPlatformContractSupportOwnershipTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.JpaPlatformContractSupportOwnershipTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlSqlStateContractTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlSqlStateContractTest.xml.
|
||||||
|
> Could not write XML test results for dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlRecipientLeaseFencingIntegrationTest to file /home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/build/test-results/jpaPlatformContractTest/TEST-dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlRecipientLeaseFencingIntegrationTest.xml.
|
||||||
|
|
||||||
|
* Try:
|
||||||
|
> Run with --stacktrace option to get the stack trace.
|
||||||
|
> Run with --info or --debug option to get more log output.
|
||||||
|
> Run with --scan to generate a Build Scan (Powered by Develocity).
|
||||||
|
> Get more help at https://help.gradle.org.
|
||||||
|
|
||||||
|
BUILD FAILED in 5m 20s
|
||||||
|
19 actionable tasks: 9 executed, 10 up-to-date
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
$ ./gradlew :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate -Pjpa.matrix.versions=16 --console=plain
|
||||||
|
run-at: 2026-08-20T07:38:10Z
|
||||||
|
Mem: 30Gi 15Gi 8.9Gi 770Mi 8.2Gi 15Gi
|
||||||
|
Starting a Gradle Daemon, 5 stopped Daemons could not be reused, use --status for details
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :shared-contract:compileJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processResources UP-TO-DATE
|
||||||
|
> Task :shared-contract:classes UP-TO-DATE
|
||||||
|
> Task :shared-contract:jar UP-TO-DATE
|
||||||
|
> Task :application-core:compileJava UP-TO-DATE
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes UP-TO-DATE
|
||||||
|
> Task :application-core:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformFailureTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformMigrationTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJpaPlatformPerformanceTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processJpaPlatformPerformanceTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformPerformanceTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformPoolContractTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformQueryPlanTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformSecurityTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 8m 14s
|
||||||
|
27 actionable tasks: 6 executed, 21 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
exit=0
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
$ ./gradlew :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate -Pjpa.matrix.versions=17 --console=plain
|
||||||
|
run-at: 2026-08-20T07:46:38Z
|
||||||
|
Mem: 30Gi 19Gi 4.6Gi 782Mi 8.5Gi 11Gi
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :shared-contract:compileJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processResources UP-TO-DATE
|
||||||
|
> Task :shared-contract:classes UP-TO-DATE
|
||||||
|
> Task :shared-contract:jar UP-TO-DATE
|
||||||
|
> Task :application-core:compileJava UP-TO-DATE
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes UP-TO-DATE
|
||||||
|
> Task :application-core:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformFailureTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformMigrationTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJpaPlatformPerformanceTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processJpaPlatformPerformanceTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformPerformanceTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformPoolContractTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformQueryPlanTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformSecurityTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 7m 26s
|
||||||
|
27 actionable tasks: 6 executed, 21 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
exit=0
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
$ ./gradlew :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate -Pjpa.matrix.versions=18 --console=plain
|
||||||
|
run-at: 2026-08-20T07:54:35Z
|
||||||
|
Mem: 30Gi 19Gi 4.3Gi 1.1Gi 8.2Gi 10Gi
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :shared-contract:compileJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processResources UP-TO-DATE
|
||||||
|
> Task :shared-contract:classes UP-TO-DATE
|
||||||
|
> Task :shared-contract:jar UP-TO-DATE
|
||||||
|
> Task :application-core:compileJava UP-TO-DATE
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes UP-TO-DATE
|
||||||
|
> Task :application-core:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformFailureTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformMigrationTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJpaPlatformPerformanceTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processJpaPlatformPerformanceTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformPerformanceTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformPoolContractTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformQueryPlanTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformSecurityTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 5m 45s
|
||||||
|
27 actionable tasks: 6 executed, 21 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
exit=0
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
===== PostgreSQL 16 =====
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :shared-contract:compileJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processResources UP-TO-DATE
|
||||||
|
> Task :shared-contract:classes UP-TO-DATE
|
||||||
|
> Task :shared-contract:jar UP-TO-DATE
|
||||||
|
> Task :application-core:compileJava UP-TO-DATE
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes UP-TO-DATE
|
||||||
|
> Task :application-core:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformFailureTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformMigrationTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJpaPlatformPerformanceTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processJpaPlatformPerformanceTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformPerformanceTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformPoolContractTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformQueryPlanTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformSecurityTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 7m 26s
|
||||||
|
27 actionable tasks: 6 executed, 21 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
EXIT(16)=0
|
||||||
|
===== PostgreSQL 17 =====
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :shared-contract:compileJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processResources UP-TO-DATE
|
||||||
|
> Task :shared-contract:classes UP-TO-DATE
|
||||||
|
> Task :shared-contract:jar UP-TO-DATE
|
||||||
|
> Task :application-core:compileJava UP-TO-DATE
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes UP-TO-DATE
|
||||||
|
> Task :application-core:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformFailureTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformMigrationTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJpaPlatformPerformanceTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processJpaPlatformPerformanceTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformPerformanceTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformPoolContractTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformQueryPlanTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformSecurityTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 5m 55s
|
||||||
|
27 actionable tasks: 6 executed, 21 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
EXIT(17)=0
|
||||||
|
===== PostgreSQL 18 =====
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :shared-contract:compileJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processResources UP-TO-DATE
|
||||||
|
> Task :shared-contract:classes UP-TO-DATE
|
||||||
|
> Task :shared-contract:jar UP-TO-DATE
|
||||||
|
> Task :application-core:compileJava UP-TO-DATE
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes UP-TO-DATE
|
||||||
|
> Task :application-core:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformFailureTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformMigrationTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJpaPlatformPerformanceTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processJpaPlatformPerformanceTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformPerformanceTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformPoolContractTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformQueryPlanTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformSecurityTest
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:test UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 5m 58s
|
||||||
|
27 actionable tasks: 6 executed, 21 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
EXIT(18)=0
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
$ ./gradlew :messaging:messaging-kafka:verifyMessagingCertificationEvidence --console=plain
|
||||||
|
run-at: 2026-08-20T08:00:43Z
|
||||||
|
Mem: 30Gi 16Gi 7.8Gi 1.0Gi 8.3Gi 14Gi
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-runtime:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-core-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-observability:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-policy:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-security:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:processResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-transport-spi:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:messagingCertificationTest
|
||||||
|
> Task :messaging:messaging-kafka:verifyMessagingCertificationEvidence
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 39s
|
||||||
|
31 actionable tasks: 2 executed, 29 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
exit=0
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
$ ./gradlew :messaging:messaging-kafka:cleanTest :messaging:messaging-kafka:test --tests '*IT' :messaging:messaging-rabbit:cleanTest :messaging:messaging-rabbit:test --tests '*IT' --console=plain
|
||||||
|
(cleanTest first: the previous invocation reused 6.5-hour-old XML because the task was up-to-date)
|
||||||
|
run-at: 2026-08-20T08:03:01Z
|
||||||
|
Mem: 30Gi 17Gi 6.5Gi 1.0Gi 8.5Gi 13Gi
|
||||||
|
Starting a Gradle Daemon, 1 busy and 35 stopped Daemons could not be reused, use --status for details
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:cleanTest
|
||||||
|
> Task :messaging:messaging-core-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-runtime:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-core-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-observability:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-policy:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-security:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:processResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-testkit:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-transport-spi:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:test
|
||||||
|
> Task :messaging:messaging-rabbit:cleanTest
|
||||||
|
> Task :messaging:messaging-rabbit:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-rabbit:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:compileTestJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:processTestResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-rabbit:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:test
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 1m 29s
|
||||||
|
35 actionable tasks: 4 executed, 31 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
exit=0
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
$ ./gradlew :adapter:outbound:persistence-mongo:mongoCompatibilityTest --console=plain
|
||||||
|
run-at: 2026-08-20T07:31:58Z
|
||||||
|
Mem: 30Gi 19Gi 632Mi 834Mi 11Gi 11Gi
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoCompatibilityTest
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 18s
|
||||||
|
13 actionable tasks: 1 executed, 12 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
exit=0
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
$ ./gradlew :adapter:outbound:persistence-mongo:mongoFailoverTest --console=plain
|
||||||
|
run-at: 2026-08-20T07:33:19Z
|
||||||
|
Mem: 30Gi 19Gi 1.0Gi 839Mi 11Gi 11Gi
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoFailoverTest
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 1m 17s
|
||||||
|
13 actionable tasks: 1 executed, 12 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
exit=0
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoStableContractTest
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoReplicaSetTest
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoFailoverTest
|
||||||
|
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoMigrationTest
|
||||||
|
|
||||||
|
MongoMigrationLaneTest > aCheckpointSurvivesTheProcessThatWroteIt() FAILED
|
||||||
|
dev.caskeleton.adapter.outbound.mongo.api.error.MongoOperationRejectedException at MongoMigrationLaneTest.java:155
|
||||||
|
|
||||||
|
6 tests completed, 1 failed
|
||||||
|
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoMigrationTest FAILED
|
||||||
|
|
||||||
|
FAILURE: Build failed with an exception.
|
||||||
|
|
||||||
|
* What went wrong:
|
||||||
|
Execution failed for task ':adapter:outbound:persistence-mongo:mongoMigrationTest'.
|
||||||
|
> There were failing tests. See the report at: file:///home/donghyeon/workspace/desktop-server-git/clean-architecture-backend-template/src/adapter/outbound/persistence-mongo/build/reports/tests/mongoMigrationTest/index.html
|
||||||
|
|
||||||
|
* Try:
|
||||||
|
> Run with --scan to generate a Build Scan (Powered by Develocity).
|
||||||
|
|
||||||
|
BUILD FAILED in 2m 11s
|
||||||
|
16 actionable tasks: 4 executed, 12 up-to-date
|
||||||
|
EXIT=1
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoStableContractTest
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoReplicaSetTest
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoFailoverTest
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoMigrationTest
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoCompatibilityTest
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoSecurityIntegrationTest
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileMongoPerformanceTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processMongoPerformanceTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoPerformanceTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoPerformanceTest
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 2m 50s
|
||||||
|
20 actionable tasks: 7 executed, 13 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
EXIT=0
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
$ ./gradlew :adapter:outbound:persistence-mongo:mongoMigrationTest --console=plain
|
||||||
|
run-at: 2026-08-20T07:31:22Z
|
||||||
|
Mem: 30Gi 19Gi 858Mi 817Mi 12Gi 11Gi
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoMigrationTest
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 13s
|
||||||
|
13 actionable tasks: 1 executed, 12 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
exit=0
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
$ ./gradlew :adapter:outbound:persistence-mongo:mongoPerformanceTest --console=plain
|
||||||
|
run-at: 2026-08-20T07:37:00Z
|
||||||
|
Mem: 30Gi 21Gi 664Mi 789Mi 9.5Gi 9.0Gi
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileMongoPerformanceTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processMongoPerformanceTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoPerformanceTestClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoPerformanceTest
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 26s
|
||||||
|
13 actionable tasks: 1 executed, 12 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
exit=0
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
$ ./gradlew :adapter:outbound:persistence-mongo:mongoReplicaSetTest --console=plain
|
||||||
|
run-at: 2026-08-20T07:33:01Z
|
||||||
|
Mem: 30Gi 19Gi 1.0Gi 804Mi 11Gi 11Gi
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoReplicaSetTest
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 9s
|
||||||
|
13 actionable tasks: 1 executed, 12 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
exit=0
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
$ ./gradlew :adapter:outbound:persistence-mongo:mongoSecurityIntegrationTest --console=plain
|
||||||
|
run-at: 2026-08-20T07:34:53Z
|
||||||
|
Mem: 30Gi 19Gi 1.6Gi 795Mi 11Gi 11Gi
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileTestJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processTestResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:testClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:mongoSecurityIntegrationTest
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 1m 54s
|
||||||
|
13 actionable tasks: 1 executed, 12 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
exit=0
|
||||||
+164
@@ -0,0 +1,164 @@
|
|||||||
|
Starting a Gradle Daemon, 1 busy and 61 stopped Daemons could not be reused, use --status for details
|
||||||
|
> Task :build-logic:extractPluginRequests UP-TO-DATE
|
||||||
|
> Task :build-logic:generatePluginAdapters UP-TO-DATE
|
||||||
|
> Task :build-logic:compileJava UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovy UP-TO-DATE
|
||||||
|
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
|
||||||
|
> Task :build-logic:pluginDescriptors UP-TO-DATE
|
||||||
|
> Task :build-logic:processResources UP-TO-DATE
|
||||||
|
> Task :build-logic:classes UP-TO-DATE
|
||||||
|
> Task :build-logic:jar UP-TO-DATE
|
||||||
|
> Task :shared-contract:compileJava UP-TO-DATE
|
||||||
|
> Task :shared-contract:processResources UP-TO-DATE
|
||||||
|
> Task :shared-contract:classes UP-TO-DATE
|
||||||
|
> Task :shared-contract:jar UP-TO-DATE
|
||||||
|
> Task :application-core:compileJava UP-TO-DATE
|
||||||
|
> Task :application-core:processResources NO-SOURCE
|
||||||
|
> Task :application-core:classes UP-TO-DATE
|
||||||
|
> Task :application-core:jar UP-TO-DATE
|
||||||
|
> Task :domain-core:compileJava UP-TO-DATE
|
||||||
|
> Task :domain-core:processResources NO-SOURCE
|
||||||
|
> Task :domain-core:classes UP-TO-DATE
|
||||||
|
> Task :domain-core:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:compileJava UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:graphql:jar UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:processResources NO-SOURCE
|
||||||
|
> Task :adapter:inbound:web:classes UP-TO-DATE
|
||||||
|
> Task :adapter:inbound:web:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:support:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:support:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:cache-redis:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:fileserver:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:fileserver:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:httpclient:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:httpclient:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
|
||||||
|
> Task :adapter:outbound:identifier:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:identifier:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:identifier:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:messaging:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:notification:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:notification:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:processResources UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-mongo:jar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:compileJava UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:processResources UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:classes UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:compileJava UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:processResources UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:classes UP-TO-DATE
|
||||||
|
> Task :sample-portfolio:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:persistence-jpa:testkitJar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:compileTestJava UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:runtimeClasspathManifest UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:processTestResources UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:testClasses UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-admin-runtime:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-admin-runtime:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-claim-check:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-claim-check:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-cloudevents:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-cloudevents:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-core-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-core-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-inbox-jdbc-postgresql:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-kafka:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-kafka:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-observability:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-observability:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-outbox-jdbc-postgresql:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-policy:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-policy:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-rabbit:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-rabbit:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-reliability-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-reliability-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-runtime-core:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-runtime-core:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-api:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-api:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-schema-json:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-schema-json:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-security:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-security:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:processResources UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-spring-boot-starter:jar UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
|
||||||
|
> Task :messaging:messaging-transport-spi:classes UP-TO-DATE
|
||||||
|
> Task :messaging:messaging-transport-spi:jar UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:objectstorage:compileJava UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
|
||||||
|
> Task :adapter:outbound:objectstorage:classes UP-TO-DATE
|
||||||
|
> Task :adapter:outbound:objectstorage:jar UP-TO-DATE
|
||||||
|
> Task :app-bootstrap:test
|
||||||
|
|
||||||
|
BUILD SUCCESSFUL in 1m 6s
|
||||||
|
94 actionable tasks: 1 executed, 93 up-to-date
|
||||||
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
||||||
|
EXIT=0
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Task 3 — the ten-row activation matrix
|
||||||
|
|
||||||
|
Every row below names where its evidence is, and nothing is ticked from reasoning about the code.
|
||||||
|
Rows 1–7 and 10 are runtime lanes from one Compose matrix run, `20260819T025416Z-200172`; rows 8 and 9
|
||||||
|
needed work this wave and are described in full.
|
||||||
|
|
||||||
|
The per-lane `activation.json` is the application's own answer about which switches are on, not the
|
||||||
|
flags the harness passed in — which is the point of reading it rather than the lane definition.
|
||||||
|
|
||||||
|
| # | Matrix | Evidence | Resolved switches (from `activation.json`) |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | five off | `off-local`, `off-dev`, `off-prod` lanes + `FiveAdapterOffInventoryTest` | `local` / `dev` / `prod`, **none on** |
|
||||||
|
| 2 | JPA only | `local-jpa` lane | `local`, `persistence-jpa` |
|
||||||
|
| 3 | Mongo only | `local-mongo` lane | `local`, `persistence-mongo` |
|
||||||
|
| 4 | Messaging only | `local-messaging` lane | `local`, `messaging` |
|
||||||
|
| 5 | Notification + JPA, `INGEST_ONLY` | `local-notification-ingest` + `local-notification-handoff` | `local`, `persistence-jpa`+`notification.platform`, `APP_NOTIFICATION_PLATFORM_MODE: INGEST_ONLY` |
|
||||||
|
| 6 | Notification + JPA, `SERVING` | `local-notification-serving` | same switches, `MODE: SERVING` |
|
||||||
|
| 7 | GraphQL only | `local-graphql` lane | `local`, `graphql` |
|
||||||
|
| 8 | relay on, dependency missing | **this wave** — see below | n/a: the deployment is refused |
|
||||||
|
| 9 | JPA + Mongo | `all-adapters` lane + `PortResolutionContractTest` | `local`, both persistence switches on |
|
||||||
|
| 10 | five on | `all-adapters` lane | `local`, `graphql`,`messaging`,`persistence-jpa`,`persistence-mongo`,`notification.platform` |
|
||||||
|
|
||||||
|
Row 5's exactly-once claim is not inferred from the lane passing; the smoke client says it:
|
||||||
|
|
||||||
|
```
|
||||||
|
notification-smoke: b0e083cb-06c5-4dde-b454-4c4e26f65edc delivered exactly once and stayed that way
|
||||||
|
```
|
||||||
|
|
||||||
|
## Row 8 — a defect, found by running the row
|
||||||
|
|
||||||
|
The plan is explicit that Row 8 must be checked for the *name*, not merely for a failure. Checking it
|
||||||
|
that way found that the name was not what an operator got.
|
||||||
|
|
||||||
|
`ca-skeleton.outbox.enabled=true` with the JPA switch off is a dependency error this repository
|
||||||
|
names, and `CapabilityDependencyValidator` produces the sentence that names it. But the outbox is
|
||||||
|
also a registered relational consumer, so `DataSourceRequirement` reports that a pool is required,
|
||||||
|
`JpaOffAutoConfigurationImportFilter` therefore keeps Boot's relational auto-configurations in the
|
||||||
|
candidate set, and Hibernate and Flyway were instantiated during refresh — ahead of the
|
||||||
|
`InitializingBean` that carried the check. What actually came out was:
|
||||||
|
|
||||||
|
```
|
||||||
|
Unable to obtain connection from database: Connection to localhost:5433 refused.
|
||||||
|
```
|
||||||
|
|
||||||
|
Both components were right on their own. A pool does have consumers besides JPA, and the outbox does
|
||||||
|
need the JPA switch. The disagreement was only ever visible in **which one spoke first**, and no test
|
||||||
|
could see it: the existing `CapabilityDependencyValidatorTest` calls the validator's static method
|
||||||
|
against a `MockEnvironment`, which proves the rule computes the right sentence and nothing about
|
||||||
|
whether anything runs it in time.
|
||||||
|
|
||||||
|
**Fix.** The check moved to the environment stage
|
||||||
|
(`CapabilityDependencyEnvironmentValidator`, an `EnvironmentPostProcessor` at
|
||||||
|
`LOWEST_PRECEDENCE` alongside the master-switch and profile validators), where every property
|
||||||
|
source is resolved and nothing has been instantiated. The `InitializingBean` stays: a context built
|
||||||
|
without `spring.factories` — an `ApplicationContextRunner`, a slice test — never reaches the
|
||||||
|
post-processor, and the rule should not be optional there.
|
||||||
|
|
||||||
|
**Evidence, from the built jar rather than from a test harness** — see
|
||||||
|
[task3-row8-dependency-error.log](task3-row8-dependency-error.log):
|
||||||
|
|
||||||
|
```
|
||||||
|
### outbox on, JPA off exit code: 1
|
||||||
|
This deployment enables capabilities whose dependencies are off:
|
||||||
|
- ca-skeleton.outbox.enabled=true needs relational persistence to store rows;
|
||||||
|
set ca-skeleton.persistence-jpa.enabled=true or turn the outbox off.
|
||||||
|
- ca-skeleton.outbox.enabled=true needs somewhere to publish;
|
||||||
|
set app.messaging.enabled=true or turn the outbox off.
|
||||||
|
|
||||||
|
### relay on, outbox off exit code: 1
|
||||||
|
This deployment enables capabilities whose dependencies are off:
|
||||||
|
- ca-skeleton.outbox.relay-enabled=true only starts the scheduler for a capability that is off;
|
||||||
|
set ca-skeleton.outbox.enabled=true or turn the relay off.
|
||||||
|
```
|
||||||
|
|
||||||
|
`DependencyErrorStartupContractTest` pins this at the composition-root level, including a case that
|
||||||
|
boots all-off successfully — without it, every other assertion in that class would also be satisfied
|
||||||
|
by a validator that refuses everything.
|
||||||
|
|
||||||
|
## Row 9 — the positive half runs; the conflict half is pinned as a rule
|
||||||
|
|
||||||
|
`all-adapters` starts PostgreSQL and MongoDB and the application together, and the application
|
||||||
|
reports both persistence switches on. The two adapters implement disjoint ports, so there is no
|
||||||
|
ambiguity to resolve and no `@Primary` involved.
|
||||||
|
|
||||||
|
That absence is what needs pinning, because manufacturing a conflict would test Spring's
|
||||||
|
`NoUniqueBeanDefinitionException` rather than this repository. What can silently change is the
|
||||||
|
property the row depends on: that no port is resolved by preferring one bean over another. A
|
||||||
|
`@Primary` added later to settle an ambiguity would convert a startup rejection into a silent pick,
|
||||||
|
and every existing test would stay green, because the composition would still boot.
|
||||||
|
|
||||||
|
`PortResolutionContractTest` states the rule directly — a `@Primary` on a port implementation is
|
||||||
|
permitted only when a condition decides which one is active, which makes it a selector rather than a
|
||||||
|
tiebreak. The whole repository has three `@Primary` beans:
|
||||||
|
|
||||||
|
| bean | type | guard |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `inProcessDistributedLock` | `DistributedLockPort` | `multi-instance-enabled` false or absent |
|
||||||
|
| `distributedLockProvider` | `DistributedLockPort` | `multi-instance-enabled` true |
|
||||||
|
| `applicationTaskExecutor` | `TaskExecutor` | none — Boot's own contract wants a primary executor, and it is not a port |
|
||||||
|
|
||||||
|
The two lock beans cannot coexist, so neither is being preferred; one of them simply is not there.
|
||||||
|
|
||||||
|
The rule was falsified before being trusted: removing the `@ConditionalOnProperty` from
|
||||||
|
`distributedLockProvider` fails the test, and restoring it passes.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Task 3 — the activation matrix, and where each row's evidence comes from
|
||||||
|
|
||||||
|
The plan's matrix is ten rows; the Compose contract is fifteen lanes; the in-process activation
|
||||||
|
suite is nineteen classes. They are not three copies of one thing, so this records which artefact
|
||||||
|
answers which row before anything is run. A row whose evidence is a passing unit test is recorded as
|
||||||
|
that, not as a runtime lane.
|
||||||
|
|
||||||
|
| # | Matrix row | Evidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | five off | `off-local`, `off-dev`, `off-prod` lanes + `FiveAdapterOffInventoryTest` (bean/thread/endpoint inventory is in-process; a container cannot count beans) |
|
||||||
|
| 2 | JPA only | `local-jpa` lane (db + app) |
|
||||||
|
| 3 | Mongo only | `local-mongo` lane (mongo + rs-init + app) |
|
||||||
|
| 4 | Messaging only | `local-messaging` lane; `local-messaging-outbox` additionally proves the relay's database dependency |
|
||||||
|
| 5 | Notification + JPA, `INGEST_ONLY` | `local-notification-ingest`, and `local-notification-handoff` for the accept → restart → deliver-once half |
|
||||||
|
| 6 | Notification + JPA, `SERVING` | `local-notification-serving` |
|
||||||
|
| 7 | GraphQL only | `local-graphql` lane (keycloak + auth-smoke + graphql-smoke) |
|
||||||
|
| 8 | relay on, dependency missing | **no lane** — a startup that must be *rejected* is an in-process contract: `DependencyErrorStartupContractTest`, `CapabilityDependencyValidatorTest`. The plan requires the *name* of the missing switch, not merely a failure. |
|
||||||
|
| 9 | JPA + Mongo | `all-adapters` covers the both-on half; the two-implementations-of-one-port rejection is in-process (`CleanArchitectureTest` plus the composition tests), because it is a context-startup outcome |
|
||||||
|
| 10 | five on | `all-adapters` lane |
|
||||||
|
|
||||||
|
Rows 8 and 9 are deliberately not Compose lanes. Both are assertions that a context *refuses* to
|
||||||
|
start for a stated reason, and a Compose lane can only observe that a container exited — which is
|
||||||
|
the same observation for a missing switch, a bad password and a typo in a YAML key.
|
||||||
|
|
||||||
|
Lanes with no matrix row of their own — `shared-infra-local`, `shared-infra-dev`, `prod-smoke` —
|
||||||
|
carry Task 4's environment evidence rather than Task 3's activation evidence.
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# Task 3 Row 8 — a capability on with its dependency off: the real jar, the real exit code
|
||||||
|
|
||||||
|
### outbox on, JPA off
|
||||||
|
```
|
||||||
|
java.lang.IllegalStateException: This deployment enables capabilities whose dependencies are off:
|
||||||
|
- ca-skeleton.outbox.enabled=true needs relational persistence to store rows; set ca-skeleton.persistence-jpa.enabled=true or turn the outbox off.
|
||||||
|
- ca-skeleton.outbox.enabled=true needs somewhere to publish; set app.messaging.enabled=true or turn the outbox off.
|
||||||
|
exit code: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### relay on, outbox off
|
||||||
|
```
|
||||||
|
java.lang.IllegalStateException: This deployment enables capabilities whose dependencies are off:
|
||||||
|
- ca-skeleton.outbox.relay-enabled=true only starts the scheduler for a capability that is off; set ca-skeleton.outbox.enabled=true or turn the relay off.
|
||||||
|
exit code: 1
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# Task 6 — the five reviews' P0 findings, reconciled
|
||||||
|
|
||||||
|
The remediation design this wave executed never referenced a review id (`grep -c` over it: zero).
|
||||||
|
It took one theme from the five reviews — five adapters ship and do not run — and Waves 0–6 executed
|
||||||
|
that theme. So "how much of the reviews is reflected" had never been measured. This is the first
|
||||||
|
measurement, and then the work that followed it.
|
||||||
|
|
||||||
|
Three read-only audits ran in parallel, one per review family, each instructed to treat a passing
|
||||||
|
test as evidence only when it exercises the real composition. Their headline claims were then
|
||||||
|
re-checked by hand before being acted on; one was wrong and is recorded as such below.
|
||||||
|
|
||||||
|
## Where the P0 set stood when measured
|
||||||
|
|
||||||
|
| Review | P0 | CLOSED | PARTIAL | OPEN |
|
||||||
|
| --- | ---: | ---: | ---: | ---: |
|
||||||
|
| jpa | 6 | 5 | 1 | 0 |
|
||||||
|
| messaging | 5 | 2 | 3 | 0 |
|
||||||
|
| graphql | 2 | 1 | 1 | 0 |
|
||||||
|
| notification | 12 | 1 | 11 | 0 |
|
||||||
|
| mongodb | 9 | 0 | 9 | 0 |
|
||||||
|
| **total** | **34** | **9** | **25** | **0** |
|
||||||
|
|
||||||
|
Nothing was OPEN: every P0 had been worked. What the audits found instead, in three independent
|
||||||
|
voices, was the same shape — **the implementation is substantially real and the gate under it is
|
||||||
|
thin**. Six notification adapter classes were referenced by zero tests. Both Mongo executors were
|
||||||
|
referenced by zero tests. Every messaging real-broker test skips silently without Docker. And the
|
||||||
|
Compose matrix, the strongest evidence this repository produces, runs in no workflow.
|
||||||
|
|
||||||
|
## What was fixed, and how each was proven
|
||||||
|
|
||||||
|
### NTF-004 / JPA-004 — the completion write was not fenced
|
||||||
|
|
||||||
|
Two audits reached this independently from different reviews, which is why it was taken first.
|
||||||
|
|
||||||
|
The claim is fenced (a single `FOR UPDATE SKIP LOCKED` CTE that bumps `lease_fence`) and the renew
|
||||||
|
is fenced. The *completion* was `findById → mutate → saveAndFlush` with no owner or fence predicate.
|
||||||
|
A worker whose lease expired during the provider call — the one stretch the platform deliberately
|
||||||
|
spends outside a transaction — came back and wrote its outcome over the row a new holder had already
|
||||||
|
claimed. The `@Version` column does not stop that: it detects a concurrent edit, not a superseded
|
||||||
|
writer, and the late worker's read is recent enough to win.
|
||||||
|
|
||||||
|
Fixed with conditional statements in the same idiom as the renew, `saveHeldBy`/`transitionHeldBy` on
|
||||||
|
the port returning empty when the lease is gone, and all four branches of `applyNextAction` moved
|
||||||
|
onto them. Losing the lease is not an error: the new holder owns the job and will record its own
|
||||||
|
outcome.
|
||||||
|
|
||||||
|
`@Modifying(clearAutomatically = true)` on both, because a native update bypasses the persistence
|
||||||
|
context and the immediate re-read would otherwise be served the values it just replaced — a trap one
|
||||||
|
of the audits had found elsewhere in this same platform.
|
||||||
|
|
||||||
|
**Proof:** two real-PostgreSQL cases in the JPA contract lane. A superseded holder's completion
|
||||||
|
matches zero rows and the live holder's state is untouched; the current holder's completion writes.
|
||||||
|
The second exists because without it the first is satisfied by a statement that matches nothing ever.
|
||||||
|
|
||||||
|
### NTF-012 — the SSRF guard had no callers
|
||||||
|
|
||||||
|
`requireExternallyRoutable` refuses the cloud metadata service, RFC 1918, link-local, IPv6 local,
|
||||||
|
userinfo disguise and multi-answer DNS. It had an eight-case test suite, all green, and **one
|
||||||
|
occurrence in the repository: its own definition.** The two sites it was written for —
|
||||||
|
`WebhookSubscription` and `SesProviderProperties` — still called `requireSecureOrLoopback`, which
|
||||||
|
reads the scheme and nothing else.
|
||||||
|
|
||||||
|
Both now call it. The new test goes through the constructors rather than the helper, because testing
|
||||||
|
the helper again is exactly what failed to catch this.
|
||||||
|
|
||||||
|
What is not closed: `allowLoopback` is true, so a user-supplied target naming `localhost` still
|
||||||
|
passes. Closing it means the allowance becomes a decision the caller states, and the caller does not
|
||||||
|
exist — WEBHOOK has no `ProviderRuntimeAssembler`, so a webhook profile refuses to boot and nothing
|
||||||
|
in production constructs the record. Writing the policy now would mean choosing its default with no
|
||||||
|
caller to check it against. Recorded in the code at the call site.
|
||||||
|
|
||||||
|
### NTF-001 — the documented callback switch broke startup
|
||||||
|
|
||||||
|
`CallbackRequestFactory` is a constructor argument of the MVC controller, the WebFlux handler and the
|
||||||
|
WebFlux configuration, and was produced by no production code — the only instantiation was in a test.
|
||||||
|
So `APP_NOTIFICATION_PLATFORM_CALLBACKS_ENABLED=true`, a key in the env registry and in the
|
||||||
|
configuration reference, did not enable callbacks; it failed the boot on an unsatisfied dependency.
|
||||||
|
|
||||||
|
The beans now exist, conditioned on the same switch. The missing `trusted-proxies` setting came with
|
||||||
|
them, defaulting to empty — with no entry, forwarded headers are not believed, because honouring
|
||||||
|
them unconditionally lets any caller choose the URL its own signature is checked against. Registered
|
||||||
|
in `application.yml`, the env registry, `.env.example` and the configuration reference; both
|
||||||
|
notification gates pass.
|
||||||
|
|
||||||
|
### MNG-007 — a health view Actuator could not read, and a reactive half nothing built
|
||||||
|
|
||||||
|
`MongoPlatformHealthIndicator` computed topology mismatch, secondary availability and a bounded
|
||||||
|
detail map, and implemented neither `HealthIndicator` nor `HealthContributor`. The adapter carries no
|
||||||
|
Actuator dependency and should not: the delivery platform had already established the shape, where
|
||||||
|
the adapter computes the facts and the composition root maps them onto `Health`. Done the same way.
|
||||||
|
|
||||||
|
Separately, every reactive class in the leaf — executor, consistency binder, session factory, cursor
|
||||||
|
guard — was declared by no configuration. They shipped and no configuration could construct them.
|
||||||
|
Now wired in a nested configuration conditioned on a `ReactiveMongoTemplate` *bean*, not just the
|
||||||
|
class: the class is on the compile classpath unconditionally, so a class condition alone would try to
|
||||||
|
build the reactive path in a servlet-only deployment and fail a startup for a capability nobody asked
|
||||||
|
for. Both the positive and the negative case are asserted.
|
||||||
|
|
||||||
|
### MNG-005 / MNG-023 — a deadline that only produced a report
|
||||||
|
|
||||||
|
The blocking executor compared elapsed time to the declared timeout *after* the callback returned,
|
||||||
|
and said so in its own comment: a Java callback cannot be interrupted mid driver call. That is an
|
||||||
|
overrun report, not a deadline.
|
||||||
|
|
||||||
|
The scoped API is the narrowed surface where the number can actually be sent, so every method taking
|
||||||
|
a `Query` or an `Aggregation` now carries it as `maxTimeMS`, which the server enforces. `insert` has
|
||||||
|
no query to attach it to. `Duration.ZERO` is refused, because zero means "no limit" to the server and
|
||||||
|
accepting it would turn a misconfiguration into an unbounded operation.
|
||||||
|
|
||||||
|
The raw escape hatches (`rawOperations()`, `executeInternal(...)`) are genuinely used inside the
|
||||||
|
platform by the geospatial, atomic and bulk operations, and their callers live in sibling packages,
|
||||||
|
so package-private cannot express the rule. It is enforced as a boundary from the composition root —
|
||||||
|
the only place that sees both the platform and everything consuming it. The rule was falsified by
|
||||||
|
widening its scope until it fired, and a second case asserts the platform still uses them, so the
|
||||||
|
rule cannot pass by the hatches having been deleted.
|
||||||
|
|
||||||
|
### MNG-006 — a guard that compared a declaration against nothing
|
||||||
|
|
||||||
|
`LocalDateTimeMappingGuard` was constructed `withoutConverters()` and then asked to validate the
|
||||||
|
manifest. It could only ever reject `LOCAL_DATE_TIME_WITH_REGISTERED_CONVERTER`: a deployment that
|
||||||
|
*had* registered the named converter was rejected exactly as loudly as one that had not, so the check
|
||||||
|
that exists to distinguish those two cases could not tell them apart. It now reads the converters the
|
||||||
|
deployment actually registered, and a test asserts the same manifest passes with the converter and
|
||||||
|
fails without it.
|
||||||
|
|
||||||
|
### MNG-008 — a promotion gate that required five of the six categories it declares
|
||||||
|
|
||||||
|
`MongoAdvancedPromotionEvidence.REQUIRED` listed six; `MongoAdvancedPromotionGate.verify()` required
|
||||||
|
five. `migration` was missing, so a promotion could pass with no migration evidence at all.
|
||||||
|
|
||||||
|
### GQL-002 — the batch policy applied to nothing
|
||||||
|
|
||||||
|
`GraphQlBatchLoaderRegistrar` carried the chunking, the budget and the request scope, was unit
|
||||||
|
tested, and was declared by no configuration — the only file mentioning it was itself. A field
|
||||||
|
resolving through `@BatchMapping` or a `DataLoader` met none of it. The chain
|
||||||
|
(`BatchPolicyRegistry → DataLoaderFactory → BatchLoaderRegistrar`) is now assembled by the platform.
|
||||||
|
The batch ceiling reuses `maximumPageSize` rather than adding a setting: both answer how many rows
|
||||||
|
one downstream call may ask for, and a batch limit above the page limit would let one request fan out
|
||||||
|
past the bound it already accepted.
|
||||||
|
|
||||||
|
## One thing an audit got wrong, and one fix that was reverted
|
||||||
|
|
||||||
|
The mongodb audit reported that the UUID axis is "declaration-only" and mapped onto the driver
|
||||||
|
nowhere. It is mapped — `MongoClientSettingsFactory` calls `.uuidRepresentation(...)`. Verified
|
||||||
|
before acting.
|
||||||
|
|
||||||
|
Acting on the adjacent concern — the manifest and the profile describing the same fact independently
|
||||||
|
— a startup check was written to refuse a disagreement between them. Writing its test showed the
|
||||||
|
disagreement cannot occur: `MongoUuidRepresentation` has two values, only `STANDARD` is writable, and
|
||||||
|
an existing check already refuses the other. **A guard for a state that cannot arise is the same
|
||||||
|
"declaration nothing checks" this wave has been removing**, so it was reverted rather than kept with
|
||||||
|
an unfalsifiable test.
|
||||||
|
|
||||||
|
## What remains, and why
|
||||||
|
|
||||||
|
- **NTF-012 loopback residue** — belongs with the change set that gives WEBHOOK an assembler.
|
||||||
|
- **MSG-015** — the application-owned port and anti-corruption bridge between `application-core` and
|
||||||
|
the messaging platform. The review names it and specifies the target shape; it is a runtime wiring
|
||||||
|
change, and the review itself says the physical work is a separate change set.
|
||||||
|
- **MSG-003/004/005 residue** — on code no deployment can execute: the platform's outbox and inbox
|
||||||
|
migrations are applied only by tests, the production Kafka transport is publish-only, and Rabbit
|
||||||
|
declares no transport bean.
|
||||||
|
- **P1 and P2 — 100 findings, never audited.** They were not in this remediation's scope and their
|
||||||
|
state is unknown. Saying so is the honest position; the P0 audit took three parallel agents and
|
||||||
|
the P1/P2 set is three times larger.
|
||||||
Executable
+75
@@ -0,0 +1,75 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# The GraphQL transport, exercised as a request rather than as a bean inventory.
|
||||||
|
#
|
||||||
|
# auth-smoke proves the realm issues a usable token and that the application answers its public
|
||||||
|
# health path. It never sends an authenticated request, and never touches /graphql at all — so the
|
||||||
|
# JWT decoder, the security filter chain and the GraphQL execution path were each covered by their
|
||||||
|
# own tests and by nothing that put them in one line together.
|
||||||
|
#
|
||||||
|
# Three requests, in this order, because each is only meaningful given the one before:
|
||||||
|
#
|
||||||
|
# 1. an unauthenticated query, which must be refused — otherwise steps 2 and 3 prove nothing about
|
||||||
|
# authentication, they just prove the endpoint answers;
|
||||||
|
# 2. a malformed token, which must be refused without a server error — a 500 here means the
|
||||||
|
# decoder threw where it should have rejected;
|
||||||
|
# 3. the real token, which must return the schema's liveness field — the request path, the policy
|
||||||
|
# instrumentation and the resolver, in one call.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SECRET_FILE="/run/secrets/keycloak-graphql-smoke-client-secret"
|
||||||
|
GRAPHQL_PATH="${GRAPHQL_PATH:-/graphql}"
|
||||||
|
QUERY='{"query":"{ _health }"}'
|
||||||
|
|
||||||
|
fail() { echo "graphql-smoke: $1" >&2; exit 1; }
|
||||||
|
|
||||||
|
post() {
|
||||||
|
# $1 = Authorization header value, or empty for none. Prints the status code; body to /tmp/gql.json.
|
||||||
|
if [ -n "$1" ]; then
|
||||||
|
curl -s -o /tmp/gql.json -w '%{http_code}' -X POST "${APP_BASE_URL}${GRAPHQL_PATH}" \
|
||||||
|
-H "Authorization: $1" -H 'Content-Type: application/json' -d "${QUERY}"
|
||||||
|
else
|
||||||
|
curl -s -o /tmp/gql.json -w '%{http_code}' -X POST "${APP_BASE_URL}${GRAPHQL_PATH}" \
|
||||||
|
-H 'Content-Type: application/json' -d "${QUERY}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. no credential at all
|
||||||
|
ANON_STATUS="$(post '')"
|
||||||
|
case "${ANON_STATUS}" in
|
||||||
|
401|403) : ;;
|
||||||
|
200) fail "an unauthenticated GraphQL query was answered (${ANON_STATUS}); /graphql is not guarded" ;;
|
||||||
|
*) fail "an unauthenticated GraphQL query answered ${ANON_STATUS}: $(cat /tmp/gql.json)" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 2. a credential that is not a token
|
||||||
|
BAD_STATUS="$(post 'Bearer not-a-real-token')"
|
||||||
|
[ "${BAD_STATUS}" != "500" ] \
|
||||||
|
|| fail "a malformed token produced a server error rather than a refusal"
|
||||||
|
case "${BAD_STATUS}" in
|
||||||
|
401|403) : ;;
|
||||||
|
*) fail "a malformed token answered ${BAD_STATUS}, which is neither a refusal nor a server error" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 3. the real thing
|
||||||
|
[ -r "${SECRET_FILE}" ] || fail "the client secret was not mounted"
|
||||||
|
TOKEN_RESPONSE="$(curl -sf -X POST "${KEYCLOAK_ISSUER}/protocol/openid-connect/token" \
|
||||||
|
-d grant_type=client_credentials \
|
||||||
|
-d "client_id=${KEYCLOAK_CLIENT_ID}" \
|
||||||
|
--data-urlencode "client_secret=$(cat "${SECRET_FILE}")")" \
|
||||||
|
|| fail "client-credentials token request failed"
|
||||||
|
ACCESS_TOKEN="$(echo "${TOKEN_RESPONSE}" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')"
|
||||||
|
[ -n "${ACCESS_TOKEN}" ] || fail "the token response carried no access_token"
|
||||||
|
|
||||||
|
OK_STATUS="$(post "Bearer ${ACCESS_TOKEN}")"
|
||||||
|
[ "${OK_STATUS}" = "200" ] \
|
||||||
|
|| fail "an authenticated GraphQL query answered ${OK_STATUS}: $(cat /tmp/gql.json)"
|
||||||
|
|
||||||
|
# The body, not just the status. A 200 carrying a GraphQL `errors` array is how a refused or failed
|
||||||
|
# execution looks over HTTP, so a status-only check would pass on an unresolved field.
|
||||||
|
grep -q '"_health"' /tmp/gql.json \
|
||||||
|
|| fail "the response carried no _health field: $(cat /tmp/gql.json)"
|
||||||
|
if grep -q '"errors"' /tmp/gql.json; then
|
||||||
|
fail "the query returned GraphQL errors: $(cat /tmp/gql.json)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "graphql-smoke: /graphql refused anonymous and malformed credentials and answered the authenticated query"
|
||||||
@@ -331,6 +331,8 @@ APP_NOTIFICATION_PLATFORM_ALLOW_AMBIGUOUS_FALLBACK=false
|
|||||||
APP_NOTIFICATION_PLATFORM_CALLBACKS_ENABLED=false
|
APP_NOTIFICATION_PLATFORM_CALLBACKS_ENABLED=false
|
||||||
APP_NOTIFICATION_PLATFORM_CALLBACK_MAX_BODY_BYTES=65508
|
APP_NOTIFICATION_PLATFORM_CALLBACK_MAX_BODY_BYTES=65508
|
||||||
APP_NOTIFICATION_PLATFORM_CALLBACK_REPLAY_SKEW=5m
|
APP_NOTIFICATION_PLATFORM_CALLBACK_REPLAY_SKEW=5m
|
||||||
|
# Empty: forwarded headers are not believed. List load-balancer peers to honour them.
|
||||||
|
APP_NOTIFICATION_PLATFORM_CALLBACK_TRUSTED_PROXIES=
|
||||||
|
|
||||||
# ---- Secrets — supply out of band; never commit a value here ------------------
|
# ---- Secrets — supply out of band; never commit a value here ------------------
|
||||||
APP_DATASOURCE_PASSWORD=
|
APP_DATASOURCE_PASSWORD=
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ COPY gradlew ./
|
|||||||
COPY gradle/ gradle/
|
COPY gradle/ gradle/
|
||||||
COPY config/ ./config/
|
COPY config/ ./config/
|
||||||
COPY --parents settings.gradle build.gradle **/build.gradle **/gradle.lockfile ./
|
COPY --parents settings.gradle build.gradle **/build.gradle **/gradle.lockfile ./
|
||||||
|
# The convention plugins, whole. The glob above copies files named build.gradle and gradle.lockfile,
|
||||||
|
# which picks up build-logic's own build script and misses the precompiled script plugins beside it —
|
||||||
|
# so settings.gradle's `includeBuild('build-logic')` resolved against a build that declared no
|
||||||
|
# plugins and every leaf failed on an unknown plugin id, in the dependency-resolution stage below.
|
||||||
|
COPY build-logic/ ./build-logic/
|
||||||
|
|
||||||
# Resolve every module configuration in STRICT mode (no --write-locks in a release build). This
|
# Resolve every module configuration in STRICT mode (no --write-locks in a release build). This
|
||||||
# custom task fails on drift; Gradle's diagnostic `dependencies` report can print FAILED entries
|
# custom task fails on drift; Gradle's diagnostic `dependencies` report can print FAILED entries
|
||||||
|
|||||||
+63
-5
@@ -241,6 +241,59 @@ vendor/build나 container base image까지 byte-for-byte 같음을 주장하지
|
|||||||
- **`APP_ASYNC_EXECUTOR_QUEUE_CAPACITY`** — 백로그 큐 용량. **bounded(유한) 필수, unbounded 금지(D7)**.
|
- **`APP_ASYNC_EXECUTOR_QUEUE_CAPACITY`** — 백로그 큐 용량. **bounded(유한) 필수, unbounded 금지(D7)**.
|
||||||
1 이상 정수.
|
1 이상 정수.
|
||||||
|
|
||||||
|
### 다섯 master switch (activation SSOT)
|
||||||
|
|
||||||
|
한 bootJar가 다섯 어댑터를 모두 싣고, 각각은 아래 스위치 하나로만 켜집니다. **전부 기본 `false`** 이고,
|
||||||
|
다섯이 모두 꺼진 배포는 외부 자원 없이 기동합니다. 값의 SSOT는
|
||||||
|
`dev.caskeleton.shared.activation.MasterSwitch`이며 `docs/registries/env-keys.yaml`이 같은 이름을
|
||||||
|
등록합니다.
|
||||||
|
|
||||||
|
| 환경 변수 | Spring property | 기본값 |
|
||||||
|
|---|---|---|
|
||||||
|
| `APP_PERSISTENCE_JPA_ENABLED` | `ca-skeleton.persistence-jpa.enabled` | `false` |
|
||||||
|
| `APP_PERSISTENCE_MONGO_ENABLED` | `ca-skeleton.persistence-mongo.enabled` | `false` |
|
||||||
|
| `APP_MESSAGING_ENABLED` | `app.messaging.enabled` | `false` |
|
||||||
|
| `APP_NOTIFICATION_PLATFORM_ENABLED` | `ca-skeleton.notification.platform.enabled` | `false` |
|
||||||
|
| `APP_GRAPHQL_ENABLED` | `backend.graphql.enabled` | `false` |
|
||||||
|
|
||||||
|
**어댑터가 꺼진 것과 없는 것은 다릅니다.** 다섯 어댑터의 클래스는 언제나 아티팩트 안에 있고, 운영자는
|
||||||
|
재빌드 없이 스위치만으로 켭니다. 클래스가 없으면 애초에 켤 수 없습니다.
|
||||||
|
|
||||||
|
켜진 capability가 의존하는 것이 꺼져 있으면 기동이 거부되고, **거부 메시지는 설정해야 할 정확한
|
||||||
|
property 이름을 말합니다**(`CapabilityDependencyValidator`). 예:
|
||||||
|
|
||||||
|
```
|
||||||
|
This deployment enables capabilities whose dependencies are off:
|
||||||
|
- ca-skeleton.outbox.enabled=true needs relational persistence to store rows;
|
||||||
|
set ca-skeleton.persistence-jpa.enabled=true or turn the outbox off.
|
||||||
|
```
|
||||||
|
|
||||||
|
종속 선택자 둘:
|
||||||
|
|
||||||
|
- `APP_GRAPHQL_DEPLOYMENT_MODE` — GraphQL이 켜지면 **필수**이고 기본값이 없습니다. 예전의 boolean과
|
||||||
|
enum 두 기본값이 서로 다른 말을 했기 때문에, 안전 태세는 배포가 명시적으로 고릅니다. 허용되는 값은
|
||||||
|
런타임 환경(`local`/`dev`/`prod`)마다 다릅니다.
|
||||||
|
- `APP_NOTIFICATION_PLATFORM_MODE` — `SERVING`(기본) 또는 `INGEST_ONLY`. 선택 사항이고, 허용 값은
|
||||||
|
`NotificationModeSsotTest`가 enum에서 파생합니다.
|
||||||
|
|
||||||
|
### Compose와 런타임 스모크
|
||||||
|
|
||||||
|
- **Docker Compose 최소 버전 `2.24.4`.** SSOT는 `src/config/runtime/compose-profile-contracts.json`
|
||||||
|
이고, 정본 스크립트가 검증합니다.
|
||||||
|
- 진입점은 둘뿐이고, 그 둘만이 증거입니다. 워크플로에 명령 일부를 인라인하면 one-shot 없이 도는 레인이
|
||||||
|
초록으로 보고됩니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 정적: profile별 정확한 service set, 병합된 모델 전체, mount target 유일성
|
||||||
|
./scripts/verify-compose-profile-contracts.sh
|
||||||
|
|
||||||
|
# 동적: 15개 blocking 레인을 zero-skip으로. create → up --wait → 필수 one-shot →
|
||||||
|
# sanitized evidence → 고유 project teardown
|
||||||
|
./scripts/run-compose-runtime-smoke.sh --matrix src/config/runtime/compose-profile-contracts.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`--lane <id>`는 실패 재현용이고, 레인 하나가 초록인 것은 matrix가 통과했다는 증거가 아닙니다.
|
||||||
|
|
||||||
### Optional integration adapters
|
### Optional integration adapters
|
||||||
|
|
||||||
선택형 Kafka / Redis / Slack / Google Email 어댑터 템플릿입니다. **기본은 전부 비활성**(비활성 = 선택
|
선택형 Kafka / Redis / Slack / Google Email 어댑터 템플릿입니다. **기본은 전부 비활성**(비활성 = 선택
|
||||||
@@ -252,13 +305,18 @@ fail-fast sentinel 이 포트를 충족합니다(Layer 3).
|
|||||||
`disabled`(기본) | `redis`. `redis`는 canonical Redis CACHE role binding을 함께 요구합니다.
|
`disabled`(기본) | `redis`. `redis`는 canonical Redis CACHE role binding을 함께 요구합니다.
|
||||||
- **`APP_CACHE_REDIS_CLIENT_MODE`** — `managed`는 내장 Lettuce runtime, `external`은 프로젝트가
|
- **`APP_CACHE_REDIS_CLIENT_MODE`** — `managed`는 내장 Lettuce runtime, `external`은 프로젝트가
|
||||||
제공한 `RedisClient` bean을 사용합니다.
|
제공한 `RedisClient` bean을 사용합니다.
|
||||||
- **`APP_MESSAGING_BROKER`** — 활성 메시지 브로커 id(예: `kafka`). 빈 값 = 메시징 비활성(사용 시
|
아래 세 키는 **활성화 스위치가 아니라 선택자**입니다. 어떤 capability가 켜지는지는 위의 master switch가
|
||||||
fail-fast).
|
정하고, 이 값들은 켜진 capability가 *무엇으로* 동작할지만 고릅니다. 예전에는 "빈 값 = 비활성"으로
|
||||||
|
설명돼 있었고, 그 문장이 남아 있는 동안 두 개의 활성화 모델이 공존했습니다.
|
||||||
|
|
||||||
|
- **`APP_MESSAGING_BROKER`** — 활성 메시지 브로커 id(예: `kafka`). `APP_MESSAGING_ENABLED=true`일 때
|
||||||
|
**필수**이고, 빈 값이면 기동이 거부되면서 이 키 이름을 지목합니다. 이 키를 비워도 메시징이 꺼지지는
|
||||||
|
않습니다 — 끄는 것은 master switch입니다.
|
||||||
- **`APP_MESSAGING_KAFKA_BROKERS`** — `host:port` CSV. `APP_MESSAGING_BROKER=kafka` 일 때만 필수,
|
- **`APP_MESSAGING_KAFKA_BROKERS`** — `host:port` CSV. `APP_MESSAGING_BROKER=kafka` 일 때만 필수,
|
||||||
아니면 빈 값.
|
아니면 빈 값.
|
||||||
- **`APP_NOTIFICATION_SLACK_PROVIDER`** — 활성 Slack provider id(예: `webhook`). 빈 값 = Slack 비활성.
|
- **`APP_NOTIFICATION_SLACK_PROVIDER`** — Slack provider id(예: `webhook`). notification 플랫폼이
|
||||||
- **`APP_NOTIFICATION_EMAIL_PROVIDER`** — 활성 email provider id(예: `google-email`). 빈 값 = email
|
켜졌을 때 어떤 provider를 쓸지 고르는 값입니다.
|
||||||
비활성.
|
- **`APP_NOTIFICATION_EMAIL_PROVIDER`** — email provider id(예: `google-email`). 위와 같습니다.
|
||||||
|
|
||||||
### Outbound HTTP client
|
### Outbound HTTP client
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,10 @@ SSOT(`src/config/architecture/modules.json`)까지 밀어올리지 않는다는
|
|||||||
|
|
||||||
## Allowed
|
## Allowed
|
||||||
|
|
||||||
- `:application-core`, `:domain-core`, `:shared-contract`.
|
- `:application-core`, `:domain-core`, `:shared-contract`. `:application-core` 는 선언만이 아니라 실제
|
||||||
|
의존이다 — object 인가의 **답하는 계약**(`dev.caskeleton.application.security.ObjectAccessPolicy`)이
|
||||||
|
거기 살기 때문이다. 이 leaf 가 그 계약을 소유했다면 application 구현체가 인바운드 전송을 컴파일
|
||||||
|
의존해야 했고, 그건 의존 방향이 뒤집힌다.
|
||||||
- `spring-boot-starter-graphql` (Spring Boot BOM 관리 — 버전 명시 없음).
|
- `spring-boot-starter-graphql` (Spring Boot BOM 관리 — 버전 명시 없음).
|
||||||
- test scope 에 한해 `spring-boot-starter-web`(random-port 전송 테스트용),
|
- test scope 에 한해 `spring-boot-starter-web`(random-port 전송 테스트용),
|
||||||
`spring-boot-starter-security`(HTTP 인증/CORS qualification 용),
|
`spring-boot-starter-security`(HTTP 인증/CORS qualification 용),
|
||||||
@@ -140,7 +143,9 @@ health 스키마만 소유한다.
|
|||||||
| preparsed document cache (`execution/`) | `wired` | `GraphQlPreparsedDocumentAdapter` + 같은 테스트의 캐시 hit 케이스 |
|
| preparsed document cache (`execution/`) | `wired` | `GraphQlPreparsedDocumentAdapter` + 같은 테스트의 캐시 hit 케이스 |
|
||||||
| 커스텀 scalar (`scalar/`) | `wired` | 같은 테스트의 scalar coercion 케이스 |
|
| 커스텀 scalar (`scalar/`) | `wired` | 같은 테스트의 scalar coercion 케이스 |
|
||||||
| 요청 크기/Accept 협상 (`http/`) | `wired` | `GraphQlRequestBoundsTest`, `GraphQlAcceptNegotiationTest` |
|
| 요청 크기/Accept 협상 (`http/`) | `wired` | `GraphQlRequestBoundsTest`, `GraphQlAcceptNegotiationTest` |
|
||||||
| DataLoader/batching (`dataloader/`) | `wired` | `runtime/GraphQlBatchLoaderRegistrar` + `dataloader/GraphQlBatchContractTest` |
|
| 관측 tag cardinality (`observation/`) | `wired` | `runtime/GraphQlRequestObservationConventionAdapter` 가 Spring 의 `ExecutionRequestObservationConvention` 을 구현해 Boot 의 `GraphQlObservationAutoConfiguration` 이 이 컨벤션을 가져간다. `autoconfigure/GraphQlObservationWiringTest`(프레임워크가 실제로 해석), `runtime/GraphQlRequestObservationConventionAdapterTest`(실제 `MeterRegistry` 에 임의 이름 10,000개 → series 1개) |
|
||||||
|
| object 인가 (`security/`) | `modelled` | 답하는 계약은 중립 `dev.caskeleton.application.security.ObjectAccessPolicy` 가 소유하고 이 leaf 는 `ApplicationObjectAuthorization` 매핑만 가진다. 실행 경로에 연결하는 configuration 은 없다 |
|
||||||
|
| DataLoader/batching (`dataloader/`) | `wired` | `runtime/GraphQlBatchLoaderRegistrationTest` (실제 graphql-java 실행 + Spring `BatchLoaderRegistry`, 50 parent → 3 downstream 호출), `dataloader/GraphQlBatchContractTest` |
|
||||||
| cursor 서명 (`pagination/`) | `modelled` | `HmacGraphQlCursorCodec`·`GraphQlCursorKeyRing` 단위 테스트만. **auto-configuration 이 둘 중 무엇도 생성하지 않는다** — `autoconfigure/GraphQlPolicyRequestPathTest` 가 그 사실을 고정 |
|
| cursor 서명 (`pagination/`) | `modelled` | `HmacGraphQlCursorCodec`·`GraphQlCursorKeyRing` 단위 테스트만. **auto-configuration 이 둘 중 무엇도 생성하지 않는다** — `autoconfigure/GraphQlPolicyRequestPathTest` 가 그 사실을 고정 |
|
||||||
| mutation 멱등성 (`mutation/`) | `modelled` | `GraphQlMutationIdempotencyInterceptor` 를 참조하는 configuration 이 없다. 같은 테스트가 고정 |
|
| mutation 멱등성 (`mutation/`) | `modelled` | `GraphQlMutationIdempotencyInterceptor` 를 참조하는 configuration 이 없다. 같은 테스트가 고정 |
|
||||||
| persisted operation (`advanced/persisted/`) | `modelled` | 중립 `OperationalRecordStorePort` 기반 레지스트리 + 방향성 테스트. durable 구현체는 미제공 |
|
| persisted operation (`advanced/persisted/`) | `modelled` | 중립 `OperationalRecordStorePort` 기반 레지스트리 + 방향성 테스트. durable 구현체는 미제공 |
|
||||||
@@ -190,7 +195,7 @@ cd src
|
|||||||
`quarantine`·`graphql-performance` 태그를 제외한다:
|
`quarantine`·`graphql-performance` 태그를 제외한다:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew :adapter:inbound:graphql:graphqlStableTest --console=plain # 551 tests
|
./gradlew :adapter:inbound:graphql:graphqlStableTest --console=plain # 605 tests
|
||||||
./gradlew :adapter:inbound:graphql:graphqlContractTest --console=plain # 9 tests
|
./gradlew :adapter:inbound:graphql:graphqlContractTest --console=plain # 9 tests
|
||||||
./gradlew :adapter:inbound:graphql:graphqlAdvancedTest --console=plain # 152 tests
|
./gradlew :adapter:inbound:graphql:graphqlAdvancedTest --console=plain # 152 tests
|
||||||
./gradlew :adapter:inbound:graphql:graphqlPerformanceTest --console=plain # 실부하 인프라 필요
|
./gradlew :adapter:inbound:graphql:graphqlPerformanceTest --console=plain # 실부하 인프라 필요
|
||||||
@@ -199,7 +204,7 @@ cd src
|
|||||||
`graphqlPerformanceTest` 는 `@Tag("graphql-performance")` 가 하나도 없으면 **실패한다** — 이는
|
`graphqlPerformanceTest` 는 `@Tag("graphql-performance")` 가 하나도 없으면 **실패한다** — 이는
|
||||||
버그가 아니라 "성능 증거 없음"을 통과로 위장하지 않기 위한 fail-closed 설계다.
|
버그가 아니라 "성능 증거 없음"을 통과로 위장하지 않기 위한 fail-closed 설계다.
|
||||||
|
|
||||||
위 숫자는 `build/test-results/<lane>/*.xml` 의 실제 실행 결과다(기본 `test` 703, transport
|
위 숫자는 `build/test-results/<lane>/*.xml` 의 실제 실행 결과다(기본 `test` 757, transport
|
||||||
qualification 8). 문서에 옮겨 적은 숫자는 반드시 마지막 green 실행에서 다시 읽어 갱신한다 —
|
qualification 8). 문서에 옮겨 적은 숫자는 반드시 마지막 green 실행에서 다시 읽어 갱신한다 —
|
||||||
컴파일이 깨진 채로 남은 과거 숫자는 통과 증거가 아니라 통과했다는 인상일 뿐이다.
|
컴파일이 깨진 채로 남은 과거 숫자는 통과 증거가 아니라 통과했다는 인상일 뿐이다.
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,17 @@ description = 'Inbound adapter: GraphQL API (Spring for GraphQL, GraphQL executi
|
|||||||
// the second half rather than trusting it.
|
// the second half rather than trusting it.
|
||||||
apply plugin: 'java-test-fixtures'
|
apply plugin: 'java-test-fixtures'
|
||||||
|
|
||||||
apply from: "${rootProject.projectDir}/gradle/strict-qualification-test.gradle"
|
|
||||||
apply from: "${rootProject.projectDir}/gradle/graphql-platform-conventions.gradle"
|
apply from: "${rootProject.projectDir}/gradle/graphql-platform-conventions.gradle"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':shared-contract')
|
implementation project(':shared-contract')
|
||||||
|
|
||||||
|
// The object-access rule this platform consults belongs to the application, and so does its
|
||||||
|
// contract: an inbound adapter that declared it would force every implementation to compile
|
||||||
|
// against this transport. The leaf keeps only the mapping from a GraphQL request context to
|
||||||
|
// the four plain values that contract speaks.
|
||||||
|
implementation project(':application-core')
|
||||||
|
|
||||||
// The fixtures exercise the platform through the same contracts an adopter uses.
|
// The fixtures exercise the platform through the same contracts an adopter uses.
|
||||||
testFixturesImplementation project(':shared-contract')
|
testFixturesImplementation project(':shared-contract')
|
||||||
testFixturesImplementation 'org.springframework.boot:spring-boot-starter-graphql'
|
testFixturesImplementation 'org.springframework.boot:spring-boot-starter-graphql'
|
||||||
@@ -152,95 +157,14 @@ tasks.named('check') {
|
|||||||
// prerequisite for shrinking it: the `api` and `spi` packages are the surface an adopter is meant
|
// prerequisite for shrinking it: the `api` and `spi` packages are the surface an adopter is meant
|
||||||
// to use, and everything else in this file is a candidate for becoming internal when the leaf is
|
// to use, and everything else in this file is a candidate for becoming internal when the leaf is
|
||||||
// split into capability artifacts. Until then the number cannot grow by accident.
|
// split into capability artifacts. Until then the number cannot grow by accident.
|
||||||
def graphQlApiSurfaceFile = rootProject.file('../docs/architecture/graphql-api-surface.txt')
|
apiSurface {
|
||||||
|
label = 'GraphQl'
|
||||||
Closure<String> renderGraphQlApiSurface = {
|
baseline = rootProject.file('../docs/architecture/graphql-api-surface.txt')
|
||||||
def sourceRoot = file('src/main/java')
|
description = 'GraphQL leaf public API surface — every public top-level type in src/main/java.'
|
||||||
def typePattern = ~/(?m)^public\s+(?:final\s+|abstract\s+|sealed\s+|non-sealed\s+)*(class|interface|enum|record|@interface)\s+(\w+)/
|
rationale = [
|
||||||
def packagePattern = ~/(?m)^package\s+([\w.]+)\s*;/
|
'A public type in a single-jar leaf is reachable from every adopter\'s code, so',
|
||||||
List<String> types = []
|
'additions are reviewed rather than discovered. `api` and `spi` are the intended',
|
||||||
sourceRoot.eachFileRecurse { candidate ->
|
'external surface; the rest are candidates to become internal when this leaf is',
|
||||||
if (!candidate.isFile() || !candidate.name.endsWith('.java')) {
|
'split into capability artifacts.',
|
||||||
return
|
]
|
||||||
}
|
|
||||||
String text = candidate.getText('UTF-8')
|
|
||||||
def packageMatcher = packagePattern.matcher(text)
|
|
||||||
if (!packageMatcher.find()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
String packageName = packageMatcher.group(1)
|
|
||||||
def typeMatcher = typePattern.matcher(text)
|
|
||||||
while (typeMatcher.find()) {
|
|
||||||
types << "${packageName}.${typeMatcher.group(2)}".toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
types = types.unique().toSorted()
|
|
||||||
String header =
|
|
||||||
"# GraphQL leaf public API surface — every public top-level type in src/main/java.\n" +
|
|
||||||
"# A public type in a single-jar leaf is reachable from every adopter's code, so\n" +
|
|
||||||
"# additions are reviewed rather than discovered. `api` and `spi` are the intended\n" +
|
|
||||||
"# external surface; the rest are candidates to become internal when this leaf is\n" +
|
|
||||||
"# split into capability artifacts.\n" +
|
|
||||||
"# Update only after review with:\n" +
|
|
||||||
"# ./gradlew :adapter:inbound:graphql:updateGraphQlApiSurface -PapproveGraphQlApiSurfaceChange\n" +
|
|
||||||
"# types: ${types.size()}\n"
|
|
||||||
header + (types.isEmpty() ? '' : types.join('\n') + '\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
// The approval flag is read at configuration time and carried in, not fetched from `project`
|
|
||||||
// inside doLast. Task.project at execution time is deprecated and fails under Gradle 10, and it is
|
|
||||||
// incompatible with the configuration cache — which this build will need before it can adopt one.
|
|
||||||
boolean graphQlApiSurfaceUpdateApproved = project.hasProperty('approveGraphQlApiSurfaceChange')
|
|
||||||
|
|
||||||
tasks.register('verifyGraphQlApiSurface') {
|
|
||||||
group = 'verification'
|
|
||||||
description = 'Fails without mutation when the committed GraphQL public API surface drifts.'
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
if (graphQlApiSurfaceUpdateApproved) {
|
|
||||||
throw new GradleException(
|
|
||||||
'verifyGraphQlApiSurface is read-only; use updateGraphQlApiSurface to record an ' +
|
|
||||||
'approved change.')
|
|
||||||
}
|
|
||||||
String rendered = renderGraphQlApiSurface()
|
|
||||||
if (!graphQlApiSurfaceFile.isFile()) {
|
|
||||||
throw new GradleException(
|
|
||||||
"verifyGraphQlApiSurface: missing committed baseline ${graphQlApiSurfaceFile}")
|
|
||||||
}
|
|
||||||
String committed = graphQlApiSurfaceFile.getText('UTF-8')
|
|
||||||
if (committed != rendered) {
|
|
||||||
List<String> committedTypes = committed.readLines().findAll { !it.startsWith('#') }
|
|
||||||
List<String> renderedTypes = rendered.readLines().findAll { !it.startsWith('#') }
|
|
||||||
List<String> added = (renderedTypes - committedTypes).toSorted()
|
|
||||||
List<String> removed = (committedTypes - renderedTypes).toSorted()
|
|
||||||
throw new GradleException(
|
|
||||||
"verifyGraphQlApiSurface: the public API surface changed.\n" +
|
|
||||||
(added.isEmpty() ? '' : " added:\n " + added.join('\n ') + '\n') +
|
|
||||||
(removed.isEmpty() ? '' : " removed:\n " + removed.join('\n ') + '\n') +
|
|
||||||
"Review the change, then record it with:\n" +
|
|
||||||
" ./gradlew :adapter:inbound:graphql:updateGraphQlApiSurface " +
|
|
||||||
"-PapproveGraphQlApiSurfaceChange")
|
|
||||||
}
|
|
||||||
logger.lifecycle('verifyGraphQlApiSurface: OK — the committed public API surface is unchanged.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('updateGraphQlApiSurface') {
|
|
||||||
group = 'verification'
|
|
||||||
description = 'Rewrites the committed GraphQL public API surface baseline after review.'
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
if (!project.hasProperty('approveGraphQlApiSurfaceChange')) {
|
|
||||||
throw new GradleException(
|
|
||||||
'updateGraphQlApiSurface requires -PapproveGraphQlApiSurfaceChange: growing the ' +
|
|
||||||
'public surface is a review decision, not a build step.')
|
|
||||||
}
|
|
||||||
graphQlApiSurfaceFile.parentFile.mkdirs()
|
|
||||||
graphQlApiSurfaceFile.setText(renderGraphQlApiSurface(), 'UTF-8')
|
|
||||||
logger.lifecycle("updateGraphQlApiSurface: wrote ${graphQlApiSurfaceFile}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named('check') {
|
|
||||||
dependsOn tasks.named('verifyGraphQlApiSurface')
|
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-29
@@ -1,9 +1,6 @@
|
|||||||
package dev.caskeleton.adapter.inbound.graphql.advanced.subscription;
|
package dev.caskeleton.adapter.inbound.graphql.advanced.subscription;
|
||||||
|
|
||||||
import dev.caskeleton.adapter.inbound.graphql.security.GraphQlContextCleanup;
|
import dev.caskeleton.adapter.inbound.graphql.execution.GraphQlCancellation;
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Propagates cancellation from the client to the source.
|
* Propagates cancellation from the client to the source.
|
||||||
@@ -12,47 +9,32 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
* consumer, the polling task and the nested publishers all continue for a subscriber that has gone,
|
* consumer, the polling task and the nested publishers all continue for a subscriber that has gone,
|
||||||
* and nothing in the request path notices.
|
* and nothing in the request path notices.
|
||||||
*
|
*
|
||||||
* <p>Every hook runs exactly once, and one that throws does not stop the rest. The loop used to
|
* <p>The signal itself is {@link GraphQlCancellation}, not a second copy of it. There used to be
|
||||||
* abandon the queue at the first failure, so a broken consumer-close left the polling task and the
|
* two one-way cancellation state machines in this platform with the same queue, the same flag and
|
||||||
* nested publishers running — the leak the second and third hooks existed to prevent, caused by the
|
* the same late-registration rule, which is two places for the run-every-hook guarantee to be
|
||||||
* first one failing.
|
* correct in — and it was correct in one of them. Whatever the request path guarantees, a
|
||||||
|
* subscription now guarantees by construction: every hook runs exactly once, one that throws does
|
||||||
|
* not stop the rest, and the first failure carries the later ones as suppressed.
|
||||||
*/
|
*/
|
||||||
public final class GraphQlSubscriptionCancellation {
|
public final class GraphQlSubscriptionCancellation {
|
||||||
|
|
||||||
private final AtomicBoolean cancelled = new AtomicBoolean();
|
private final GraphQlCancellation cancellation = GraphQlCancellation.create();
|
||||||
private final Queue<Runnable> upstream = new ConcurrentLinkedQueue<>();
|
|
||||||
|
|
||||||
/** Registers upstream work to stop on cancellation. */
|
/** Registers upstream work to stop on cancellation. */
|
||||||
public void onCancel(Runnable stopUpstream) {
|
public void onCancel(Runnable stopUpstream) {
|
||||||
if (stopUpstream == null) {
|
if (stopUpstream == null) {
|
||||||
throw new IllegalArgumentException("upstream cancellation hook is required");
|
throw new IllegalArgumentException("upstream cancellation hook is required");
|
||||||
}
|
}
|
||||||
upstream.add(stopUpstream);
|
cancellation.onCancel(stopUpstream);
|
||||||
if (cancelled.get()) {
|
|
||||||
drain();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cancels the subscription and everything upstream of it. */
|
/** Cancels the subscription and everything upstream of it. */
|
||||||
public void cancel() {
|
public void cancel() {
|
||||||
if (cancelled.compareAndSet(false, true)) {
|
cancellation.cancel();
|
||||||
drain();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether the subscription has been cancelled. */
|
/** Whether the subscription has been cancelled. */
|
||||||
public boolean cancelled() {
|
public boolean cancelled() {
|
||||||
return cancelled.get();
|
return cancellation.cancelled();
|
||||||
}
|
|
||||||
|
|
||||||
private void drain() {
|
|
||||||
GraphQlContextCleanup cleanup = GraphQlContextCleanup.create();
|
|
||||||
Runnable hook = upstream.poll();
|
|
||||||
while (hook != null) {
|
|
||||||
cleanup.register(hook);
|
|
||||||
hook = upstream.poll();
|
|
||||||
}
|
|
||||||
// The same run-all-then-rethrow-with-suppressed semantics the request path already uses.
|
|
||||||
cleanup.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+91
@@ -32,6 +32,7 @@ import dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlPlatformInstrumenta
|
|||||||
import dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlPlatformWebInterceptor;
|
import dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlPlatformWebInterceptor;
|
||||||
import dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlPreparsedDocumentAdapter;
|
import dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlPreparsedDocumentAdapter;
|
||||||
import dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlPrincipalResolver;
|
import dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlPrincipalResolver;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlRequestObservationConventionAdapter;
|
||||||
import dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlWireErrorMapper;
|
import dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlWireErrorMapper;
|
||||||
import dev.caskeleton.adapter.inbound.graphql.runtime.servlet.GraphQlRequestBodyLimitFilter;
|
import dev.caskeleton.adapter.inbound.graphql.runtime.servlet.GraphQlRequestBodyLimitFilter;
|
||||||
import dev.caskeleton.adapter.inbound.graphql.scalar.GraphQlScalarWiringConfigurer;
|
import dev.caskeleton.adapter.inbound.graphql.scalar.GraphQlScalarWiringConfigurer;
|
||||||
@@ -60,6 +61,7 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
|
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
|
||||||
import org.springframework.graphql.execution.GraphQlSource;
|
import org.springframework.graphql.execution.GraphQlSource;
|
||||||
|
import org.springframework.graphql.observation.ExecutionRequestObservationConvention;
|
||||||
import org.springframework.graphql.server.WebGraphQlInterceptor;
|
import org.springframework.graphql.server.WebGraphQlInterceptor;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
@@ -526,6 +528,23 @@ public class GraphQlPlatformAutoConfiguration {
|
|||||||
return new GraphQlRequestObservationConvention(filter, operationNames);
|
return new GraphQlRequestObservationConvention(filter, operationNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hands the bounded request tags to Spring for GraphQL's observation instrumentation.
|
||||||
|
*
|
||||||
|
* <p>The convention above is a value; this is the bean Boot's {@code
|
||||||
|
* GraphQlObservationAutoConfiguration} looks for. Without it the framework falls back to its own
|
||||||
|
* convention and the platform's cardinality policy applies to nothing that is exported — the
|
||||||
|
* shape of defect where a control passes its tests and no request reaches it.
|
||||||
|
*
|
||||||
|
* @param convention the platform's bounded tag policy
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(ExecutionRequestObservationConvention.class)
|
||||||
|
public GraphQlRequestObservationConventionAdapter graphQlExecutionRequestObservationConvention(
|
||||||
|
GraphQlRequestObservationConvention convention) {
|
||||||
|
return new GraphQlRequestObservationConventionAdapter(convention);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Which operation names may become metric labels.
|
* Which operation names may become metric labels.
|
||||||
*
|
*
|
||||||
@@ -601,6 +620,78 @@ public class GraphQlPlatformAutoConfiguration {
|
|||||||
builder.configureGraphQl(graphQl -> graphQl.preparsedDocumentProvider(adapter));
|
builder.configureGraphQl(graphQl -> graphQl.preparsedDocumentProvider(adapter));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The batch policies an adopter registers, empty until one does.
|
||||||
|
*
|
||||||
|
* <p>A bean rather than something each adopter constructs, because the two things downstream of
|
||||||
|
* it — the loader factory and the registrar — were beans nowhere, and a platform whose N+1
|
||||||
|
* protection has to be assembled by hand is a platform whose N+1 protection is not applied.
|
||||||
|
*
|
||||||
|
* @return the registry
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(
|
||||||
|
dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlBatchPolicyRegistry.class)
|
||||||
|
public dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlBatchPolicyRegistry
|
||||||
|
graphQlBatchPolicyRegistry() {
|
||||||
|
return new dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlBatchPolicyRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supplies the per-loader batch policy and executor.
|
||||||
|
*
|
||||||
|
* @param policies the registered policies
|
||||||
|
* @param properties the platform settings, which supply the batch ceiling
|
||||||
|
* @param graphQlPlatformClock the clock deadlines are measured against
|
||||||
|
* @return the factory
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(
|
||||||
|
dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlDataLoaderFactory.class)
|
||||||
|
public dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlDataLoaderFactory
|
||||||
|
graphQlDataLoaderFactory(
|
||||||
|
dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlBatchPolicyRegistry policies,
|
||||||
|
GraphQlPlatformSettings properties,
|
||||||
|
Clock graphQlPlatformClock) {
|
||||||
|
// The page ceiling is the batch ceiling. Both answer the same question — how many rows one
|
||||||
|
// downstream call may ask for — and a batch limit larger than the page limit would let a single
|
||||||
|
// request fan out past the bound the same request already accepted for its page.
|
||||||
|
return new dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlDataLoaderFactory(
|
||||||
|
policies, properties.limits().maximumPageSize(), graphQlPlatformClock);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps an adopter's batch loader in the platform's chunking, budget and request scope.
|
||||||
|
*
|
||||||
|
* <p>This was the missing link. {@code GraphQlBatchLoaderRegistrar} existed, was tested, and was
|
||||||
|
* declared by no configuration — so a field resolving through {@code @BatchMapping} or a {@code
|
||||||
|
* DataLoader} met none of the platform's batch policy. The chunking and the budget were a set of
|
||||||
|
* well-tested objects no request could reach.
|
||||||
|
*
|
||||||
|
* <p>An adopter still supplies the downstream call, because only the adopter has one. What it no
|
||||||
|
* longer supplies is the machinery around it.
|
||||||
|
*
|
||||||
|
* @param factory the loader factory
|
||||||
|
* @param blockingBridge the bounded hand-off, when a runtime provides one
|
||||||
|
* @param properties the platform settings, which declare the runtime the loaders will execute on
|
||||||
|
* @return the registrar
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(
|
||||||
|
dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlBatchLoaderRegistrar.class)
|
||||||
|
public dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlBatchLoaderRegistrar
|
||||||
|
graphQlBatchLoaderRegistrar(
|
||||||
|
dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlDataLoaderFactory factory,
|
||||||
|
ObjectProvider<dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlBlockingBridge>
|
||||||
|
blockingBridge,
|
||||||
|
GraphQlPlatformSettings properties) {
|
||||||
|
// The profile decides where a chunk may run, so the registrar receives it rather than assuming
|
||||||
|
// the servlet answer. A reactive deployment that registers a blocking loader with no bridge is
|
||||||
|
// refused at startup instead of discovering it as event-loop starvation.
|
||||||
|
return new dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlBatchLoaderRegistrar(
|
||||||
|
factory, blockingBridge.getIfAvailable(), properties.executionProfile());
|
||||||
|
}
|
||||||
|
|
||||||
private static String schemaContractHash(ObjectProvider<GraphQlSource> graphQlSource) {
|
private static String schemaContractHash(ObjectProvider<GraphQlSource> graphQlSource) {
|
||||||
GraphQlSource source = graphQlSource.getIfAvailable();
|
GraphQlSource source = graphQlSource.getIfAvailable();
|
||||||
if (source == null) {
|
if (source == null) {
|
||||||
|
|||||||
+10
-1
@@ -20,5 +20,14 @@ import org.springframework.context.annotation.Import;
|
|||||||
@AutoConfiguration
|
@AutoConfiguration
|
||||||
@ConditionalOnProperty(prefix = "backend.graphql", name = "enabled", havingValue = "true")
|
@ConditionalOnProperty(prefix = "backend.graphql", name = "enabled", havingValue = "true")
|
||||||
@EnableConfigurationProperties(GraphQlPlatformSettings.class)
|
@EnableConfigurationProperties(GraphQlPlatformSettings.class)
|
||||||
@Import(GraphQlPlatformAutoConfiguration.class)
|
@Import({
|
||||||
|
GraphQlPlatformAutoConfiguration.class,
|
||||||
|
// The resolver for the schema's only field. It is a @Controller in a package the composition
|
||||||
|
// root's component scan excludes by regex — the exclusion that makes this capability optional —
|
||||||
|
// and no root imported it, so a deployment with GraphQL on served a schema declaring
|
||||||
|
// `_health: String!` with nothing to resolve it. Every query answered
|
||||||
|
// NullValueInNonNullableField. Its own tests passed throughout by registering the class
|
||||||
|
// themselves, which is the shape of the defect rather than a defence against it.
|
||||||
|
dev.caskeleton.adapter.inbound.graphql.HealthGraphqlController.class
|
||||||
|
})
|
||||||
public class GraphQlRootAutoConfiguration {}
|
public class GraphQlRootAutoConfiguration {}
|
||||||
|
|||||||
+175
-40
@@ -2,18 +2,23 @@ package dev.caskeleton.adapter.inbound.graphql.compat;
|
|||||||
|
|
||||||
import graphql.language.AstPrinter;
|
import graphql.language.AstPrinter;
|
||||||
import graphql.language.EnumTypeDefinition;
|
import graphql.language.EnumTypeDefinition;
|
||||||
|
import graphql.language.EnumTypeExtensionDefinition;
|
||||||
import graphql.language.EnumValueDefinition;
|
import graphql.language.EnumValueDefinition;
|
||||||
import graphql.language.FieldDefinition;
|
import graphql.language.FieldDefinition;
|
||||||
import graphql.language.ImplementingTypeDefinition;
|
import graphql.language.ImplementingTypeDefinition;
|
||||||
import graphql.language.InputObjectTypeDefinition;
|
import graphql.language.InputObjectTypeDefinition;
|
||||||
|
import graphql.language.InputObjectTypeExtensionDefinition;
|
||||||
import graphql.language.InputValueDefinition;
|
import graphql.language.InputValueDefinition;
|
||||||
import graphql.language.InterfaceTypeDefinition;
|
import graphql.language.InterfaceTypeDefinition;
|
||||||
|
import graphql.language.InterfaceTypeExtensionDefinition;
|
||||||
import graphql.language.NonNullType;
|
import graphql.language.NonNullType;
|
||||||
import graphql.language.ObjectTypeDefinition;
|
import graphql.language.ObjectTypeDefinition;
|
||||||
|
import graphql.language.ObjectTypeExtensionDefinition;
|
||||||
import graphql.language.ScalarTypeDefinition;
|
import graphql.language.ScalarTypeDefinition;
|
||||||
import graphql.language.Type;
|
import graphql.language.Type;
|
||||||
import graphql.language.TypeDefinition;
|
import graphql.language.TypeDefinition;
|
||||||
import graphql.language.UnionTypeDefinition;
|
import graphql.language.UnionTypeDefinition;
|
||||||
|
import graphql.language.UnionTypeExtensionDefinition;
|
||||||
import graphql.schema.idl.ScalarInfo;
|
import graphql.schema.idl.ScalarInfo;
|
||||||
import graphql.schema.idl.SchemaParser;
|
import graphql.schema.idl.SchemaParser;
|
||||||
import graphql.schema.idl.TypeDefinitionRegistry;
|
import graphql.schema.idl.TypeDefinitionRegistry;
|
||||||
@@ -57,26 +62,150 @@ public final class GraphQlSchemaComparator {
|
|||||||
|
|
||||||
List<GraphQlSchemaChange> changes = new ArrayList<>();
|
List<GraphQlSchemaChange> changes = new ArrayList<>();
|
||||||
|
|
||||||
compareTypePresence(previous, candidate, changes);
|
// Extensions folded in first. A registry keeps `extend type Query { … }` in a separate map from
|
||||||
compareTypeKinds(previous, candidate, changes);
|
// `type Query { … }`, so a comparison that reads only the base definitions cannot see a field
|
||||||
compareOutputTypes(previous, candidate, changes);
|
// an extension contributed — and cannot see it disappear either. Every schema that composes
|
||||||
compareInputTypes(previous, candidate, changes);
|
// from several files is exactly this shape, which made the omission a breaking change the gate
|
||||||
compareEnums(previous, candidate, changes);
|
// reported as no change at all.
|
||||||
compareUnions(previous, candidate, changes);
|
Map<String, TypeDefinition> previousTypes = withExtensions(previous);
|
||||||
|
Map<String, TypeDefinition> candidateTypes = withExtensions(candidate);
|
||||||
|
|
||||||
|
compareTypePresence(previousTypes, candidateTypes, changes);
|
||||||
|
compareTypeKinds(previousTypes, candidateTypes, changes);
|
||||||
|
compareOutputTypes(previousTypes, candidateTypes, changes);
|
||||||
|
compareInputTypes(previousTypes, candidateTypes, changes);
|
||||||
|
compareEnums(previousTypes, candidateTypes, changes);
|
||||||
|
compareUnions(previousTypes, candidateTypes, changes);
|
||||||
compareScalars(previous, candidate, changes);
|
compareScalars(previous, candidate, changes);
|
||||||
compareDirectives(previous, candidate, changes);
|
compareDirectives(previous, candidate, changes);
|
||||||
compareAppliedDirectives(previous, candidate, changes);
|
compareAppliedDirectives(previousTypes, candidateTypes, changes);
|
||||||
|
|
||||||
return new GraphQlCompatibilityReport(changes.stream().sorted(DETERMINISTIC_ORDER).toList());
|
return new GraphQlCompatibilityReport(changes.stream().sorted(DETERMINISTIC_ORDER).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every type definition with the members its extensions contribute already merged in.
|
||||||
|
*
|
||||||
|
* <p>The merged form is what a client sees: the engine builds one type out of the base
|
||||||
|
* declaration and every extension of it, and a field's origin is invisible on the wire.
|
||||||
|
*/
|
||||||
|
private static Map<String, TypeDefinition> withExtensions(TypeDefinitionRegistry registry) {
|
||||||
|
Map<String, TypeDefinition> merged = new LinkedHashMap<>();
|
||||||
|
registry.types().forEach((name, type) -> merged.put(name, mergeExtensions(registry, type)));
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TypeDefinition mergeExtensions(
|
||||||
|
TypeDefinitionRegistry registry, TypeDefinition type) {
|
||||||
|
|
||||||
|
String name = type.getName();
|
||||||
|
if (type instanceof ObjectTypeDefinition object) {
|
||||||
|
List<ObjectTypeExtensionDefinition> extensions =
|
||||||
|
registry.objectTypeExtensions().getOrDefault(name, List.of());
|
||||||
|
if (extensions.isEmpty()) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
List<FieldDefinition> fields = new ArrayList<>(object.getFieldDefinitions());
|
||||||
|
List<Type> interfaces = new ArrayList<>(object.getImplements());
|
||||||
|
List<graphql.language.Directive> directives = new ArrayList<>(object.getDirectives());
|
||||||
|
extensions.forEach(
|
||||||
|
extension -> {
|
||||||
|
fields.addAll(extension.getFieldDefinitions());
|
||||||
|
interfaces.addAll(extension.getImplements());
|
||||||
|
directives.addAll(extension.getDirectives());
|
||||||
|
});
|
||||||
|
return object.transform(
|
||||||
|
builder ->
|
||||||
|
builder.fieldDefinitions(fields).implementz(interfaces).directives(directives));
|
||||||
|
}
|
||||||
|
if (type instanceof InterfaceTypeDefinition definition) {
|
||||||
|
List<InterfaceTypeExtensionDefinition> extensions =
|
||||||
|
registry.interfaceTypeExtensions().getOrDefault(name, List.of());
|
||||||
|
if (extensions.isEmpty()) {
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
List<FieldDefinition> fields = new ArrayList<>(definition.getFieldDefinitions());
|
||||||
|
List<Type> interfaces = new ArrayList<>(definition.getImplements());
|
||||||
|
List<graphql.language.Directive> directives = new ArrayList<>(definition.getDirectives());
|
||||||
|
extensions.forEach(
|
||||||
|
extension -> {
|
||||||
|
fields.addAll(extension.getFieldDefinitions());
|
||||||
|
interfaces.addAll(extension.getImplements());
|
||||||
|
directives.addAll(extension.getDirectives());
|
||||||
|
});
|
||||||
|
return definition.transform(
|
||||||
|
builder -> builder.definitions(fields).implementz(interfaces).directives(directives));
|
||||||
|
}
|
||||||
|
if (type instanceof InputObjectTypeDefinition input) {
|
||||||
|
List<InputObjectTypeExtensionDefinition> extensions =
|
||||||
|
registry.inputObjectTypeExtensions().getOrDefault(name, List.of());
|
||||||
|
if (extensions.isEmpty()) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
List<InputValueDefinition> fields = new ArrayList<>(input.getInputValueDefinitions());
|
||||||
|
List<graphql.language.Directive> directives = new ArrayList<>(input.getDirectives());
|
||||||
|
extensions.forEach(
|
||||||
|
extension -> {
|
||||||
|
fields.addAll(extension.getInputValueDefinitions());
|
||||||
|
directives.addAll(extension.getDirectives());
|
||||||
|
});
|
||||||
|
return input.transform(
|
||||||
|
builder -> builder.inputValueDefinitions(fields).directives(directives));
|
||||||
|
}
|
||||||
|
if (type instanceof EnumTypeDefinition enumeration) {
|
||||||
|
List<EnumTypeExtensionDefinition> extensions =
|
||||||
|
registry.enumTypeExtensions().getOrDefault(name, List.of());
|
||||||
|
if (extensions.isEmpty()) {
|
||||||
|
return enumeration;
|
||||||
|
}
|
||||||
|
List<EnumValueDefinition> values = new ArrayList<>(enumeration.getEnumValueDefinitions());
|
||||||
|
List<graphql.language.Directive> directives = new ArrayList<>(enumeration.getDirectives());
|
||||||
|
extensions.forEach(
|
||||||
|
extension -> {
|
||||||
|
values.addAll(extension.getEnumValueDefinitions());
|
||||||
|
directives.addAll(extension.getDirectives());
|
||||||
|
});
|
||||||
|
return enumeration.transform(
|
||||||
|
builder -> builder.enumValueDefinitions(values).directives(directives));
|
||||||
|
}
|
||||||
|
if (type instanceof UnionTypeDefinition union) {
|
||||||
|
List<UnionTypeExtensionDefinition> extensions =
|
||||||
|
registry.unionTypeExtensions().getOrDefault(name, List.of());
|
||||||
|
if (extensions.isEmpty()) {
|
||||||
|
return union;
|
||||||
|
}
|
||||||
|
List<Type> members = new ArrayList<>(union.getMemberTypes());
|
||||||
|
List<graphql.language.Directive> directives = new ArrayList<>(union.getDirectives());
|
||||||
|
extensions.forEach(
|
||||||
|
extension -> {
|
||||||
|
members.addAll(extension.getMemberTypes());
|
||||||
|
directives.addAll(extension.getDirectives());
|
||||||
|
});
|
||||||
|
return union.transform(builder -> builder.memberTypes(members).directives(directives));
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The merged definitions of one kind. */
|
||||||
|
private static <T extends TypeDefinition> Map<String, T> typesOf(
|
||||||
|
Map<String, TypeDefinition> types, Class<T> kind) {
|
||||||
|
Map<String, T> selected = new LinkedHashMap<>();
|
||||||
|
types.forEach(
|
||||||
|
(name, type) -> {
|
||||||
|
if (kind.isInstance(type)) {
|
||||||
|
selected.put(name, kind.cast(type));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
private static void compareTypePresence(
|
private static void compareTypePresence(
|
||||||
TypeDefinitionRegistry previous,
|
Map<String, TypeDefinition> previous,
|
||||||
TypeDefinitionRegistry candidate,
|
Map<String, TypeDefinition> candidate,
|
||||||
List<GraphQlSchemaChange> changes) {
|
List<GraphQlSchemaChange> changes) {
|
||||||
|
|
||||||
Set<String> previousTypes = new TreeSet<>(previous.types().keySet());
|
Set<String> previousTypes = new TreeSet<>(previous.keySet());
|
||||||
Set<String> candidateTypes = new TreeSet<>(candidate.types().keySet());
|
Set<String> candidateTypes = new TreeSet<>(candidate.keySet());
|
||||||
|
|
||||||
previousTypes.stream()
|
previousTypes.stream()
|
||||||
.filter(name -> !candidateTypes.contains(name))
|
.filter(name -> !candidateTypes.contains(name))
|
||||||
@@ -94,12 +223,10 @@ public final class GraphQlSchemaComparator {
|
|||||||
* to another, or as nothing at all.
|
* to another, or as nothing at all.
|
||||||
*/
|
*/
|
||||||
private static void compareTypeKinds(
|
private static void compareTypeKinds(
|
||||||
TypeDefinitionRegistry previous,
|
Map<String, TypeDefinition> previousTypes,
|
||||||
TypeDefinitionRegistry candidate,
|
Map<String, TypeDefinition> candidateTypes,
|
||||||
List<GraphQlSchemaChange> changes) {
|
List<GraphQlSchemaChange> changes) {
|
||||||
|
|
||||||
Map<String, TypeDefinition> previousTypes = previous.types();
|
|
||||||
Map<String, TypeDefinition> candidateTypes = candidate.types();
|
|
||||||
for (String name : new TreeSet<>(previousTypes.keySet())) {
|
for (String name : new TreeSet<>(previousTypes.keySet())) {
|
||||||
TypeDefinition after = candidateTypes.get(name);
|
TypeDefinition after = candidateTypes.get(name);
|
||||||
if (after == null) {
|
if (after == null) {
|
||||||
@@ -119,12 +246,10 @@ public final class GraphQlSchemaComparator {
|
|||||||
* definition untouched.
|
* definition untouched.
|
||||||
*/
|
*/
|
||||||
private static void compareAppliedDirectives(
|
private static void compareAppliedDirectives(
|
||||||
TypeDefinitionRegistry previous,
|
Map<String, TypeDefinition> previousTypes,
|
||||||
TypeDefinitionRegistry candidate,
|
Map<String, TypeDefinition> candidateTypes,
|
||||||
List<GraphQlSchemaChange> changes) {
|
List<GraphQlSchemaChange> changes) {
|
||||||
|
|
||||||
Map<String, TypeDefinition> previousTypes = previous.types();
|
|
||||||
Map<String, TypeDefinition> candidateTypes = candidate.types();
|
|
||||||
for (String name : new TreeSet<>(previousTypes.keySet())) {
|
for (String name : new TreeSet<>(previousTypes.keySet())) {
|
||||||
TypeDefinition before = previousTypes.get(name);
|
TypeDefinition before = previousTypes.get(name);
|
||||||
TypeDefinition after = candidateTypes.get(name);
|
TypeDefinition after = candidateTypes.get(name);
|
||||||
@@ -202,8 +327,8 @@ public final class GraphQlSchemaComparator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void compareOutputTypes(
|
private static void compareOutputTypes(
|
||||||
TypeDefinitionRegistry previous,
|
Map<String, TypeDefinition> previous,
|
||||||
TypeDefinitionRegistry candidate,
|
Map<String, TypeDefinition> candidate,
|
||||||
List<GraphQlSchemaChange> changes) {
|
List<GraphQlSchemaChange> changes) {
|
||||||
|
|
||||||
Map<String, ImplementingTypeDefinition<?>> previousTypes = implementingTypes(previous);
|
Map<String, ImplementingTypeDefinition<?>> previousTypes = implementingTypes(previous);
|
||||||
@@ -349,14 +474,14 @@ public final class GraphQlSchemaComparator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void compareInputTypes(
|
private static void compareInputTypes(
|
||||||
TypeDefinitionRegistry previous,
|
Map<String, TypeDefinition> previous,
|
||||||
TypeDefinitionRegistry candidate,
|
Map<String, TypeDefinition> candidate,
|
||||||
List<GraphQlSchemaChange> changes) {
|
List<GraphQlSchemaChange> changes) {
|
||||||
|
|
||||||
Map<String, InputObjectTypeDefinition> previousTypes =
|
Map<String, InputObjectTypeDefinition> previousTypes =
|
||||||
previous.getTypesMap(InputObjectTypeDefinition.class);
|
typesOf(previous, InputObjectTypeDefinition.class);
|
||||||
Map<String, InputObjectTypeDefinition> candidateTypes =
|
Map<String, InputObjectTypeDefinition> candidateTypes =
|
||||||
candidate.getTypesMap(InputObjectTypeDefinition.class);
|
typesOf(candidate, InputObjectTypeDefinition.class);
|
||||||
|
|
||||||
for (String typeName : new TreeSet<>(previousTypes.keySet())) {
|
for (String typeName : new TreeSet<>(previousTypes.keySet())) {
|
||||||
InputObjectTypeDefinition after = candidateTypes.get(typeName);
|
InputObjectTypeDefinition after = candidateTypes.get(typeName);
|
||||||
@@ -426,13 +551,12 @@ public final class GraphQlSchemaComparator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void compareEnums(
|
private static void compareEnums(
|
||||||
TypeDefinitionRegistry previous,
|
Map<String, TypeDefinition> previous,
|
||||||
TypeDefinitionRegistry candidate,
|
Map<String, TypeDefinition> candidate,
|
||||||
List<GraphQlSchemaChange> changes) {
|
List<GraphQlSchemaChange> changes) {
|
||||||
|
|
||||||
Map<String, EnumTypeDefinition> previousTypes = previous.getTypesMap(EnumTypeDefinition.class);
|
Map<String, EnumTypeDefinition> previousTypes = typesOf(previous, EnumTypeDefinition.class);
|
||||||
Map<String, EnumTypeDefinition> candidateTypes =
|
Map<String, EnumTypeDefinition> candidateTypes = typesOf(candidate, EnumTypeDefinition.class);
|
||||||
candidate.getTypesMap(EnumTypeDefinition.class);
|
|
||||||
|
|
||||||
for (String typeName : new TreeSet<>(previousTypes.keySet())) {
|
for (String typeName : new TreeSet<>(previousTypes.keySet())) {
|
||||||
EnumTypeDefinition after = candidateTypes.get(typeName);
|
EnumTypeDefinition after = candidateTypes.get(typeName);
|
||||||
@@ -462,14 +586,12 @@ public final class GraphQlSchemaComparator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void compareUnions(
|
private static void compareUnions(
|
||||||
TypeDefinitionRegistry previous,
|
Map<String, TypeDefinition> previous,
|
||||||
TypeDefinitionRegistry candidate,
|
Map<String, TypeDefinition> candidate,
|
||||||
List<GraphQlSchemaChange> changes) {
|
List<GraphQlSchemaChange> changes) {
|
||||||
|
|
||||||
Map<String, UnionTypeDefinition> previousTypes =
|
Map<String, UnionTypeDefinition> previousTypes = typesOf(previous, UnionTypeDefinition.class);
|
||||||
previous.getTypesMap(UnionTypeDefinition.class);
|
Map<String, UnionTypeDefinition> candidateTypes = typesOf(candidate, UnionTypeDefinition.class);
|
||||||
Map<String, UnionTypeDefinition> candidateTypes =
|
|
||||||
candidate.getTypesMap(UnionTypeDefinition.class);
|
|
||||||
|
|
||||||
for (String typeName : new TreeSet<>(previousTypes.keySet())) {
|
for (String typeName : new TreeSet<>(previousTypes.keySet())) {
|
||||||
UnionTypeDefinition after = candidateTypes.get(typeName);
|
UnionTypeDefinition after = candidateTypes.get(typeName);
|
||||||
@@ -512,7 +634,7 @@ public final class GraphQlSchemaComparator {
|
|||||||
changes.add(GraphQlSchemaChange.of("scalar " + name, GraphQlChangeKind.SCALAR_REMOVED));
|
changes.add(GraphQlSchemaChange.of("scalar " + name, GraphQlChangeKind.SCALAR_REMOVED));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!print(previousScalars.get(name)).equals(print(after))) {
|
if (!declaration(previousScalars.get(name)).equals(declaration(after))) {
|
||||||
changes.add(
|
changes.add(
|
||||||
GraphQlSchemaChange.of("scalar " + name, GraphQlChangeKind.SCALAR_DECLARATION_CHANGED));
|
GraphQlSchemaChange.of("scalar " + name, GraphQlChangeKind.SCALAR_DECLARATION_CHANGED));
|
||||||
}
|
}
|
||||||
@@ -555,13 +677,26 @@ public final class GraphQlSchemaComparator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Map<String, ImplementingTypeDefinition<?>> implementingTypes(
|
private static Map<String, ImplementingTypeDefinition<?>> implementingTypes(
|
||||||
TypeDefinitionRegistry registry) {
|
Map<String, TypeDefinition> merged) {
|
||||||
Map<String, ImplementingTypeDefinition<?>> types = new LinkedHashMap<>();
|
Map<String, ImplementingTypeDefinition<?>> types = new LinkedHashMap<>();
|
||||||
registry.getTypesMap(ObjectTypeDefinition.class).forEach(types::put);
|
typesOf(merged, ObjectTypeDefinition.class).forEach(types::put);
|
||||||
registry.getTypesMap(InterfaceTypeDefinition.class).forEach(types::put);
|
typesOf(merged, InterfaceTypeDefinition.class).forEach(types::put);
|
||||||
return types;
|
return types;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A scalar's declaration without its description.
|
||||||
|
*
|
||||||
|
* <p>The declaration is compared because it is the only signal SDL carries about how a scalar
|
||||||
|
* coerces — the {@code Coercing} implementation behind it is Java, and swapping it changes the
|
||||||
|
* wire contract without changing a character of schema. Prose is not that signal: reporting a
|
||||||
|
* reworded sentence as a possible coercion change is how a review that matters gets approved
|
||||||
|
* without being read.
|
||||||
|
*/
|
||||||
|
private static String declaration(ScalarTypeDefinition scalar) {
|
||||||
|
return print(scalar.transform(builder -> builder.description(null)));
|
||||||
|
}
|
||||||
|
|
||||||
private static Map<String, ScalarTypeDefinition> customScalars(TypeDefinitionRegistry registry) {
|
private static Map<String, ScalarTypeDefinition> customScalars(TypeDefinitionRegistry registry) {
|
||||||
return registry.scalars().entrySet().stream()
|
return registry.scalars().entrySet().stream()
|
||||||
.filter(entry -> !ScalarInfo.isGraphqlSpecifiedScalar(entry.getKey()))
|
.filter(entry -> !ScalarInfo.isGraphqlSpecifiedScalar(entry.getKey()))
|
||||||
|
|||||||
+14
-7
@@ -10,9 +10,9 @@ import java.util.HexFormat;
|
|||||||
*
|
*
|
||||||
* <p>The platform needs an actor identity for authorization, idempotency scoping and audit, but the
|
* <p>The platform needs an actor identity for authorization, idempotency scoping and audit, but the
|
||||||
* error contract and the observability contract both forbid a raw user identifier from reaching a
|
* error contract and the observability contract both forbid a raw user identifier from reaching a
|
||||||
* response or a metric label. So the context carries this reference and exposes {@link
|
* response or a metric label. So the context carries this reference and never carries an access
|
||||||
* #fingerprint()} for anything that must be shared outward, and it never carries an access token,
|
* token, cookie or raw provider claim; what may be shared outward is a keyed fingerprint from
|
||||||
* cookie or raw provider claim.
|
* {@link GraphQlIdentityFingerprinter}.
|
||||||
*
|
*
|
||||||
* @param value opaque, stable caller reference supplied by the authentication adapter
|
* @param value opaque, stable caller reference supplied by the authentication adapter
|
||||||
* @param authenticated whether a credential was actually verified
|
* @param authenticated whether a credential was actually verified
|
||||||
@@ -49,12 +49,19 @@ public record ActorRef(String value, boolean authenticated) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A stable, non-reversible fingerprint of this actor.
|
* A stable partition token for this actor, for use inside one running request.
|
||||||
*
|
*
|
||||||
* <p>Safe to use where the raw reference must not appear — idempotency scoping, audit correlation
|
* <p>Deliberately not called a fingerprint. An actor reference is low-entropy — a numeric user
|
||||||
* and subscription principals.
|
* id, a service account name — and a digest of a guessable input is recovered by digesting the
|
||||||
|
* guesses, so this hides the reference from a casual reader and from nobody else. It is a
|
||||||
|
* separator for in-memory structures such as the request-scoped DataLoader cache, where the only
|
||||||
|
* requirement is that two actors never share one bucket.
|
||||||
|
*
|
||||||
|
* <p>Anything durable or outward-facing — an idempotency record, an audit trail, a value handed
|
||||||
|
* to another system — uses {@link GraphQlIdentityFingerprinter} instead, which is keyed and can
|
||||||
|
* rotate.
|
||||||
*/
|
*/
|
||||||
public String fingerprint() {
|
public String cachePartition() {
|
||||||
return sha256Prefix(value);
|
return sha256Prefix(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+150
@@ -0,0 +1,150 @@
|
|||||||
|
package dev.caskeleton.adapter.inbound.graphql.context;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns an actor or tenant identity into a value that may be stored outside this process.
|
||||||
|
*
|
||||||
|
* <p>A plain digest is not that value. An actor reference and a tenant name are low-entropy: they
|
||||||
|
* come from a bounded set a reader can enumerate — {@code tenant-a}, {@code acme}, a numeric user
|
||||||
|
* id, an email address — and a digest of a guessable input is recovered by digesting the guesses.
|
||||||
|
* An idempotency record keyed on such a digest therefore still names the caller to anyone holding
|
||||||
|
* the store, which is the one thing hashing it was meant to prevent. A keyed MAC removes the
|
||||||
|
* dictionary attack, because the attacker cannot compute the candidate values without the key.
|
||||||
|
*
|
||||||
|
* <p>Keys are addressed by identity and the identity travels with the fingerprint, so a deployment
|
||||||
|
* can rotate. Rotation matters here more than for a signature: a fingerprint is durable, it sits in
|
||||||
|
* stored idempotency records for as long as they are retained, and a key that can never change is a
|
||||||
|
* key that is compromised permanently. Records written under a retired key stay readable because
|
||||||
|
* their key identity still resolves; new ones are written under the active key.
|
||||||
|
*
|
||||||
|
* <p>The platform supplies no key and no default. A default key is public knowledge, and a
|
||||||
|
* fingerprint under a public key is a plain digest wearing a MAC's name — so the deployment's
|
||||||
|
* secret source is the only way to build this, and a mutation cannot derive an idempotency scope
|
||||||
|
* without one.
|
||||||
|
*
|
||||||
|
* <p>These values are not metric labels. A fingerprint is per actor and per tenant by construction,
|
||||||
|
* which is exactly the unbounded cardinality the observability contract refuses; {@link
|
||||||
|
* dev.caskeleton.adapter.inbound.graphql.observation.GraphQlSensitiveAttributeFilter} is what
|
||||||
|
* decides what a metric may carry.
|
||||||
|
*/
|
||||||
|
public final class GraphQlIdentityFingerprinter {
|
||||||
|
|
||||||
|
/** Shortest key accepted, matching the block size a truncated key would be padded to anyway. */
|
||||||
|
public static final int MINIMUM_KEY_BYTES = 16;
|
||||||
|
|
||||||
|
private static final String ALGORITHM = "HmacSHA256";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain tags, so the same string used as an actor and as a tenant does not fingerprint the same.
|
||||||
|
*
|
||||||
|
* <p>Without them a service account named {@code acme} and the tenant named {@code acme} share a
|
||||||
|
* fingerprint, and an idempotency scope built from both halves collapses to one repeated value.
|
||||||
|
*/
|
||||||
|
private static final String ACTOR_DOMAIN = "actor";
|
||||||
|
|
||||||
|
private static final String TENANT_DOMAIN = "tenant";
|
||||||
|
|
||||||
|
private final Map<String, byte[]> keys;
|
||||||
|
private final String activeKeyId;
|
||||||
|
|
||||||
|
private GraphQlIdentityFingerprinter(Map<String, byte[]> keys, String activeKeyId) {
|
||||||
|
this.keys = keys;
|
||||||
|
this.activeKeyId = activeKeyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a fingerprinter over a rotating key ring.
|
||||||
|
*
|
||||||
|
* @param keys secrets by key identity, from the deployment's secret source
|
||||||
|
* @param activeKeyId the key new fingerprints are computed under
|
||||||
|
*/
|
||||||
|
public static GraphQlIdentityFingerprinter of(Map<String, byte[]> keys, String activeKeyId) {
|
||||||
|
if (keys == null || keys.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("identity fingerprint key ring cannot be empty");
|
||||||
|
}
|
||||||
|
if (activeKeyId == null || activeKeyId.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("an active identity fingerprint key is required");
|
||||||
|
}
|
||||||
|
if (!keys.containsKey(activeKeyId)) {
|
||||||
|
throw new IllegalArgumentException("active identity fingerprint key is not in the key ring");
|
||||||
|
}
|
||||||
|
Map<String, byte[]> copy = new LinkedHashMap<>();
|
||||||
|
keys.forEach(
|
||||||
|
(keyId, secret) -> {
|
||||||
|
if (keyId == null || keyId.isBlank() || keyId.indexOf(':') >= 0) {
|
||||||
|
// The key identity is the prefix of every fingerprint it produces, so a colon in it
|
||||||
|
// would make the prefix ambiguous and two rings could mint the same fingerprint text.
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"identity fingerprint key id is required and opaque");
|
||||||
|
}
|
||||||
|
if (secret == null || secret.length < MINIMUM_KEY_BYTES) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"identity fingerprint key " + keyId + " is too short");
|
||||||
|
}
|
||||||
|
copy.put(keyId, secret.clone());
|
||||||
|
});
|
||||||
|
return new GraphQlIdentityFingerprinter(copy, activeKeyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A single-key ring, for a deployment that has not rotated yet. */
|
||||||
|
public static GraphQlIdentityFingerprinter single(String keyId, byte[] secret) {
|
||||||
|
return of(Map.of(keyId, secret), keyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The key new fingerprints are computed under. */
|
||||||
|
public String activeKeyId() {
|
||||||
|
return activeKeyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key identities a stored fingerprint may still be attributed to.
|
||||||
|
*
|
||||||
|
* <p>A copy, so handing the set out cannot retire a key by removing it from the live view.
|
||||||
|
*/
|
||||||
|
public Set<String> keyIds() {
|
||||||
|
return Set.copyOf(keys.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The fingerprint of an actor, safe to store alongside an idempotency record. */
|
||||||
|
public String actor(ActorRef actor) {
|
||||||
|
if (actor == null) {
|
||||||
|
throw new IllegalArgumentException("actor is required");
|
||||||
|
}
|
||||||
|
return fingerprint(ACTOR_DOMAIN, actor.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The fingerprint of a tenant, safe to store alongside an idempotency record. */
|
||||||
|
public String tenant(TenantContext tenant) {
|
||||||
|
if (tenant == null) {
|
||||||
|
throw new IllegalArgumentException("tenant is required");
|
||||||
|
}
|
||||||
|
return fingerprint(TENANT_DOMAIN, tenant.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fingerprint(String domain, String value) {
|
||||||
|
try {
|
||||||
|
Mac mac = Mac.getInstance(ALGORITHM);
|
||||||
|
mac.init(new SecretKeySpec(keys.get(activeKeyId), ALGORITHM));
|
||||||
|
// Length-framed, for the reason the canonical mutation input is: concatenating a domain and a
|
||||||
|
// caller-influenced value lets one of them absorb the other's boundary, and two different
|
||||||
|
// identities then produce one fingerprint.
|
||||||
|
byte[] digest =
|
||||||
|
mac.doFinal(
|
||||||
|
(domain.length() + ":" + domain + "|" + value.length() + ":" + value + "|")
|
||||||
|
.getBytes(StandardCharsets.UTF_8));
|
||||||
|
return activeKeyId + ":" + HexFormat.of().formatHex(digest);
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeyException unavailable) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"HMAC-SHA256 is required for identity fingerprints", unavailable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-4
@@ -51,12 +51,14 @@ public record TenantContext(String value, TenantSource source) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A stable, non-reversible fingerprint of this tenant.
|
* A stable partition token for this tenant, for use inside one running request.
|
||||||
*
|
*
|
||||||
* <p>The observability contract forbids a raw tenant identifier as a metric label; this is what
|
* <p>A tenant name comes from a set small enough to enumerate, so a digest of it is recovered by
|
||||||
* goes outward instead.
|
* digesting the candidates. That is acceptable for what this is used for — keeping one tenant's
|
||||||
|
* request-scoped cache entries out of another's — and not acceptable for anything stored or
|
||||||
|
* shared, which uses the keyed {@link GraphQlIdentityFingerprinter} instead.
|
||||||
*/
|
*/
|
||||||
public String fingerprint() {
|
public String cachePartition() {
|
||||||
return ActorRef.sha256Prefix(value);
|
return ActorRef.sha256Prefix(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-1
@@ -20,9 +20,18 @@ import java.util.function.BiFunction;
|
|||||||
* more downstream call and wait for it however long it took, which is the case the budget exists
|
* more downstream call and wait for it however long it took, which is the case the budget exists
|
||||||
* for. Bounding the call itself is the loader's job, and the deadline is handed to it for that;
|
* for. Bounding the call itself is the loader's job, and the deadline is handed to it for that;
|
||||||
* this check is what stops the batch continuing past a budget that has already gone.
|
* this check is what stops the batch continuing past a budget that has already gone.
|
||||||
|
*
|
||||||
|
* <p>Every chunk's answer goes through {@link GraphQlBatchResultMapper} before it is believed. The
|
||||||
|
* mapper is where "the loader answered a question nobody asked" and "this key has no row" are
|
||||||
|
* distinguished, and while the executor handed the loader's map straight back, neither distinction
|
||||||
|
* reached a request: a result under the wrong keys rendered every parent's child as null, and a
|
||||||
|
* loader declared as always resolving reported its absences as legitimate nulls. Both are the shape
|
||||||
|
* that produces plausible data instead of an error.
|
||||||
*/
|
*/
|
||||||
public final class GraphQlBatchExecutor {
|
public final class GraphQlBatchExecutor {
|
||||||
|
|
||||||
|
private static final GraphQlBatchResultMapper MAPPER = new GraphQlBatchResultMapper();
|
||||||
|
|
||||||
private final GraphQlBatchPolicy policy;
|
private final GraphQlBatchPolicy policy;
|
||||||
private final GraphQlBatchChunker chunker;
|
private final GraphQlBatchChunker chunker;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
@@ -61,7 +70,24 @@ public final class GraphQlBatchExecutor {
|
|||||||
|
|
||||||
for (List<K> chunk : chunker.chunk(keys)) {
|
for (List<K> chunk : chunker.chunk(keys)) {
|
||||||
requireBudget(started, budget);
|
requireBudget(started, budget);
|
||||||
loaded.putAll(loadChunk.apply(chunk, context));
|
Map<K, V> answered = loadChunk.apply(chunk, context);
|
||||||
|
if (answered == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"loader " + policy.loaderName().value() + " returned no result for a chunk");
|
||||||
|
}
|
||||||
|
// One outcome per requested key, so a missing row and a wrong-key answer are two different
|
||||||
|
// facts. A key whose policy is NULL_VALUE is simply absent from the returned map, which is
|
||||||
|
// how a mapped DataLoader spells "no value" without claiming one.
|
||||||
|
MAPPER
|
||||||
|
.map(chunk, answered)
|
||||||
|
.values()
|
||||||
|
.forEach(
|
||||||
|
(key, value) -> {
|
||||||
|
V resolved = MAPPER.resolve(value, policy.missingKeyPolicy());
|
||||||
|
if (resolved != null) {
|
||||||
|
loaded.put(key, resolved);
|
||||||
|
}
|
||||||
|
});
|
||||||
// After, too: a chunk that overran the budget must not have its result used and must not be
|
// After, too: a chunk that overran the budget must not have its result used and must not be
|
||||||
// followed by another one.
|
// followed by another one.
|
||||||
requireBudget(started, budget);
|
requireBudget(started, budget);
|
||||||
|
|||||||
+19
@@ -14,6 +14,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
* <p>Cancellation is one-way, and each listener runs exactly once — listeners are drained from a
|
* <p>Cancellation is one-way, and each listener runs exactly once — listeners are drained from a
|
||||||
* queue rather than iterated, so a listener registered concurrently with cancellation is neither
|
* queue rather than iterated, so a listener registered concurrently with cancellation is neither
|
||||||
* dropped nor run twice.
|
* dropped nor run twice.
|
||||||
|
*
|
||||||
|
* <p>A listener that throws does not stop the rest. The drain used to abandon the queue at the
|
||||||
|
* first failure, which meant a downstream client that refused to close left the statement and the
|
||||||
|
* publisher behind it running — the leaks the later listeners existed to prevent, caused by the
|
||||||
|
* first one failing and hidden behind the exception that stopped it. Every listener is now
|
||||||
|
* attempted; the first failure is rethrown once the queue is empty, with the later ones attached to
|
||||||
|
* it as suppressed, so a second broken listener is not invisible until the first is fixed.
|
||||||
*/
|
*/
|
||||||
public final class GraphQlCancellation {
|
public final class GraphQlCancellation {
|
||||||
|
|
||||||
@@ -65,10 +72,22 @@ public final class GraphQlCancellation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void drain() {
|
private void drain() {
|
||||||
|
RuntimeException firstFailure = null;
|
||||||
Runnable listener = listeners.poll();
|
Runnable listener = listeners.poll();
|
||||||
while (listener != null) {
|
while (listener != null) {
|
||||||
|
try {
|
||||||
listener.run();
|
listener.run();
|
||||||
|
} catch (RuntimeException failure) {
|
||||||
|
if (firstFailure == null) {
|
||||||
|
firstFailure = failure;
|
||||||
|
} else {
|
||||||
|
firstFailure.addSuppressed(failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
listener = listeners.poll();
|
listener = listeners.poll();
|
||||||
}
|
}
|
||||||
|
if (firstFailure != null) {
|
||||||
|
throw firstFailure;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -83,6 +83,7 @@ public enum GraphQlAdvancedModule {
|
|||||||
"advanced.subscription",
|
"advanced.subscription",
|
||||||
"advanced.security",
|
"advanced.security",
|
||||||
"api",
|
"api",
|
||||||
|
"execution",
|
||||||
"http",
|
"http",
|
||||||
"security"),
|
"security"),
|
||||||
|
|
||||||
|
|||||||
+1
@@ -118,6 +118,7 @@ public enum GraphQlStableModule {
|
|||||||
"error",
|
"error",
|
||||||
"execution",
|
"execution",
|
||||||
"http",
|
"http",
|
||||||
|
"observation",
|
||||||
"policy",
|
"policy",
|
||||||
"security"),
|
"security"),
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -17,8 +17,8 @@ package dev.caskeleton.adapter.inbound.graphql.mutation;
|
|||||||
* implement replay, record storage or locking: those need transactional guarantees the transport
|
* implement replay, record storage or locking: those need transactional guarantees the transport
|
||||||
* layer cannot give.
|
* layer cannot give.
|
||||||
*
|
*
|
||||||
* @param actorFingerprint non-reversible actor identity
|
* @param actorFingerprint keyed actor fingerprint, from the deployment's identity fingerprinter
|
||||||
* @param tenantFingerprint non-reversible tenant identity
|
* @param tenantFingerprint keyed tenant fingerprint, from the same fingerprinter
|
||||||
* @param coordinate the mutation the key belongs to
|
* @param coordinate the mutation the key belongs to
|
||||||
* @param contractVersion the mutation contract version the key was issued under
|
* @param contractVersion the mutation contract version the key was issued under
|
||||||
* @param key the client-supplied key
|
* @param key the client-supplied key
|
||||||
@@ -74,10 +74,10 @@ public record GraphQlMutationIdempotencyContext(
|
|||||||
/**
|
/**
|
||||||
* The storage scope for the Application's idempotency record.
|
* The storage scope for the Application's idempotency record.
|
||||||
*
|
*
|
||||||
* <p>Uses fingerprints rather than the raw actor and tenant, so the scope can be persisted and
|
* <p>Uses the keyed fingerprints rather than the raw actor and tenant, so the scope can be
|
||||||
* logged. Length-framed for the same reason the canonical input form is: joining five
|
* persisted and logged without naming the caller to whoever holds the store. Length-framed for
|
||||||
* caller-influenced values with a separator lets one of them contain the separator and collide
|
* the same reason the canonical input form is: joining five caller-influenced values with a
|
||||||
* with a different scope.
|
* separator lets one of them contain the separator and collide with a different scope.
|
||||||
*/
|
*/
|
||||||
public String scope() {
|
public String scope() {
|
||||||
StringBuilder scope = new StringBuilder();
|
StringBuilder scope = new StringBuilder();
|
||||||
|
|||||||
+11
-2
@@ -1,5 +1,6 @@
|
|||||||
package dev.caskeleton.adapter.inbound.graphql.mutation;
|
package dev.caskeleton.adapter.inbound.graphql.mutation;
|
||||||
|
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlIdentityFingerprinter;
|
||||||
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlRequestContext;
|
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlRequestContext;
|
||||||
import dev.caskeleton.adapter.inbound.graphql.http.GraphQlExtensionsPolicy;
|
import dev.caskeleton.adapter.inbound.graphql.http.GraphQlExtensionsPolicy;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -23,6 +24,7 @@ public final class GraphQlMutationIdempotencyInterceptor {
|
|||||||
* Derives the idempotency scope for one mutation.
|
* Derives the idempotency scope for one mutation.
|
||||||
*
|
*
|
||||||
* @param context the request context, whose actor scopes the key
|
* @param context the request context, whose actor scopes the key
|
||||||
|
* @param fingerprinter the deployment's keyed identity fingerprinter
|
||||||
* @param coordinate the mutation being executed
|
* @param coordinate the mutation being executed
|
||||||
* @param extensions request extensions, which may carry the key
|
* @param extensions request extensions, which may carry the key
|
||||||
* @param normalizedInput the mutation's normalised business input
|
* @param normalizedInput the mutation's normalised business input
|
||||||
@@ -30,11 +32,18 @@ public final class GraphQlMutationIdempotencyInterceptor {
|
|||||||
*/
|
*/
|
||||||
public static Optional<GraphQlMutationIdempotencyContext> from(
|
public static Optional<GraphQlMutationIdempotencyContext> from(
|
||||||
GraphQlRequestContext context,
|
GraphQlRequestContext context,
|
||||||
|
GraphQlIdentityFingerprinter fingerprinter,
|
||||||
GraphQlMutationCoordinate coordinate,
|
GraphQlMutationCoordinate coordinate,
|
||||||
String contractVersion,
|
String contractVersion,
|
||||||
Map<String, Object> extensions,
|
Map<String, Object> extensions,
|
||||||
Map<String, ?> normalizedInput) {
|
Map<String, ?> normalizedInput) {
|
||||||
|
|
||||||
|
if (fingerprinter == null) {
|
||||||
|
// No key, no scope. Falling back to a plain digest here would put a recoverable actor and
|
||||||
|
// tenant into a record the Application persists, which is the failure the keyed fingerprint
|
||||||
|
// exists to stop — and it would do it silently, on a deployment that never configured a key.
|
||||||
|
throw new IllegalArgumentException("an identity fingerprinter is required");
|
||||||
|
}
|
||||||
Object supplied =
|
Object supplied =
|
||||||
extensions == null ? null : extensions.get(GraphQlExtensionsPolicy.IDEMPOTENCY_KEY);
|
extensions == null ? null : extensions.get(GraphQlExtensionsPolicy.IDEMPOTENCY_KEY);
|
||||||
if (supplied == null) {
|
if (supplied == null) {
|
||||||
@@ -45,8 +54,8 @@ public final class GraphQlMutationIdempotencyInterceptor {
|
|||||||
}
|
}
|
||||||
return Optional.of(
|
return Optional.of(
|
||||||
GraphQlMutationIdempotencyContext.of(
|
GraphQlMutationIdempotencyContext.of(
|
||||||
context.actor().fingerprint(),
|
fingerprinter.actor(context.actor()),
|
||||||
context.tenant().fingerprint(),
|
fingerprinter.tenant(context.tenant()),
|
||||||
coordinate,
|
coordinate,
|
||||||
contractVersion,
|
contractVersion,
|
||||||
new GraphQlIdempotencyKey(key),
|
new GraphQlIdempotencyKey(key),
|
||||||
|
|||||||
+20
@@ -57,6 +57,26 @@ public final class GraphQlOperationNameCardinality {
|
|||||||
return registered.contains(operationName.value()) ? operationName.value() : UNREGISTERED;
|
return registered.contains(operationName.value()) ? operationName.value() : UNREGISTERED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bounded label for one name as it arrived on the wire.
|
||||||
|
*
|
||||||
|
* <p>The wire carries a string, and it is a wider string than {@link GraphQlOperationName}
|
||||||
|
* accepts: graphql-java admits any GraphQL {@code Name}, so {@code ab} and {@code _internal}
|
||||||
|
* reach execution and would make the value type throw. A policy that can only be asked about
|
||||||
|
* names it already considers well formed is not a bound on what a client can send, so the
|
||||||
|
* question is answered against the raw string — and a raw string becomes a label only by being
|
||||||
|
* exactly one the deployment declared.
|
||||||
|
*
|
||||||
|
* @param wireOperationName the client-supplied name, or {@code null} for an anonymous operation
|
||||||
|
* @return the registered name, the anonymous value, or {@link #UNREGISTERED}
|
||||||
|
*/
|
||||||
|
public String labelForWireName(String wireOperationName) {
|
||||||
|
if (wireOperationName == null || wireOperationName.isBlank()) {
|
||||||
|
return GraphQlOperationName.ANONYMOUS_OBSERVATION_VALUE;
|
||||||
|
}
|
||||||
|
return registered.contains(wireOperationName) ? wireOperationName : UNREGISTERED;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of distinct labels this policy can ever produce.
|
* The number of distinct labels this policy can ever produce.
|
||||||
*
|
*
|
||||||
|
|||||||
+60
-1
@@ -89,8 +89,67 @@ public final class GraphQlRequestObservationConvention {
|
|||||||
GraphQlComplexityResult complexity,
|
GraphQlComplexityResult complexity,
|
||||||
int depth) {
|
int depth) {
|
||||||
|
|
||||||
|
return tags(
|
||||||
|
operationNames.labelFor(operationName),
|
||||||
|
operationType,
|
||||||
|
clientProfile,
|
||||||
|
persisted,
|
||||||
|
outcome,
|
||||||
|
errorCategory,
|
||||||
|
complexity,
|
||||||
|
depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the request tags from the operation name as it arrived on the wire.
|
||||||
|
*
|
||||||
|
* <p>The overload exists for the runtime seam, which sees the raw request rather than a validated
|
||||||
|
* name. Routing it through the same cardinality policy is what keeps one bound instead of two: a
|
||||||
|
* second place that decides which names become labels is a second place for the bound to be
|
||||||
|
* missing.
|
||||||
|
*
|
||||||
|
* @param wireOperationName client-supplied operation name, or {@code null} when anonymous
|
||||||
|
* @param operationType root operation type
|
||||||
|
* @param clientProfile bounded client profile
|
||||||
|
* @param persisted whether the operation came from the persisted registry
|
||||||
|
* @param outcome bounded outcome name
|
||||||
|
* @param errorCategory bounded error category, or {@code null}
|
||||||
|
* @param complexity computed complexity, or {@code null} when the request never reached costing
|
||||||
|
* @param depth measured selection depth
|
||||||
|
*/
|
||||||
|
public Map<String, String> tagsForWireName(
|
||||||
|
String wireOperationName,
|
||||||
|
GraphQlOperationType operationType,
|
||||||
|
GraphQlClientProfile clientProfile,
|
||||||
|
boolean persisted,
|
||||||
|
String outcome,
|
||||||
|
String errorCategory,
|
||||||
|
GraphQlComplexityResult complexity,
|
||||||
|
int depth) {
|
||||||
|
|
||||||
|
return tags(
|
||||||
|
operationNames.labelForWireName(wireOperationName),
|
||||||
|
operationType,
|
||||||
|
clientProfile,
|
||||||
|
persisted,
|
||||||
|
outcome,
|
||||||
|
errorCategory,
|
||||||
|
complexity,
|
||||||
|
depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> tags(
|
||||||
|
String operationNameLabel,
|
||||||
|
GraphQlOperationType operationType,
|
||||||
|
GraphQlClientProfile clientProfile,
|
||||||
|
boolean persisted,
|
||||||
|
String outcome,
|
||||||
|
String errorCategory,
|
||||||
|
GraphQlComplexityResult complexity,
|
||||||
|
int depth) {
|
||||||
|
|
||||||
Map<String, String> tags = new LinkedHashMap<>();
|
Map<String, String> tags = new LinkedHashMap<>();
|
||||||
tags.put("graphql.operation.name", operationNames.labelFor(operationName));
|
tags.put("graphql.operation.name", operationNameLabel);
|
||||||
tags.put("graphql.operation.type", operationType.name());
|
tags.put("graphql.operation.type", operationType.name());
|
||||||
tags.put("graphql.client.profile", clientProfile.value());
|
tags.put("graphql.client.profile", clientProfile.value());
|
||||||
tags.put("graphql.persisted", Boolean.toString(persisted));
|
tags.put("graphql.persisted", Boolean.toString(persisted));
|
||||||
|
|||||||
+26
-3
@@ -6,6 +6,8 @@ import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlBatchExecutor;
|
|||||||
import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlBatchTimeoutException;
|
import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlBatchTimeoutException;
|
||||||
import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlDataLoaderFactory;
|
import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlDataLoaderFactory;
|
||||||
import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlDataLoaderName;
|
import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlDataLoaderName;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.execution.GraphQlExecutionProfile;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.execution.GraphQlExecutionProfileException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -24,11 +26,20 @@ import org.springframework.graphql.execution.BatchLoaderRegistry;
|
|||||||
* <p>The chunking, budget and scope arrive as a decorator around the adopter's loader rather than
|
* <p>The chunking, budget and scope arrive as a decorator around the adopter's loader rather than
|
||||||
* as something the adopter has to remember. What the adopter supplies is the downstream call; what
|
* as something the adopter has to remember. What the adopter supplies is the downstream call; what
|
||||||
* this adds is everything that makes it safe to run on a shared request budget.
|
* this adds is everything that makes it safe to run on a shared request budget.
|
||||||
|
*
|
||||||
|
* <p>Where the chunk actually runs is a property of the runtime, so the registrar is told which one
|
||||||
|
* it is. On a servlet stack the answer is "here": Spring already put the request on a thread, and a
|
||||||
|
* second pool would only add a queue and a wait. On a reactive stack the same inline call runs on
|
||||||
|
* the event loop, where one slow downstream stalls every request the loop is serving — so a
|
||||||
|
* reactive deployment either supplies the bounded bridge or does not get to register a blocking
|
||||||
|
* loader at all. Refusing at registration makes that a startup failure rather than a latency
|
||||||
|
* mystery under load.
|
||||||
*/
|
*/
|
||||||
public final class GraphQlBatchLoaderRegistrar {
|
public final class GraphQlBatchLoaderRegistrar {
|
||||||
|
|
||||||
private final GraphQlDataLoaderFactory factory;
|
private final GraphQlDataLoaderFactory factory;
|
||||||
private final GraphQlBlockingBridge blockingBridge;
|
private final GraphQlBlockingBridge blockingBridge;
|
||||||
|
private final GraphQlExecutionProfile profile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a registrar that runs loaders on the calling thread.
|
* Creates a registrar that runs loaders on the calling thread.
|
||||||
@@ -37,9 +48,11 @@ public final class GraphQlBatchLoaderRegistrar {
|
|||||||
* a second pool adds a queue, a wait and a context hop, and buys nothing on a servlet stack.
|
* a second pool adds a queue, a wait and a context hop, and buys nothing on a servlet stack.
|
||||||
*
|
*
|
||||||
* @param factory supplies the per-loader batch policy and executor
|
* @param factory supplies the per-loader batch policy and executor
|
||||||
|
* @param profile the runtime this deployment declared
|
||||||
*/
|
*/
|
||||||
public GraphQlBatchLoaderRegistrar(GraphQlDataLoaderFactory factory) {
|
public GraphQlBatchLoaderRegistrar(
|
||||||
this(factory, null);
|
GraphQlDataLoaderFactory factory, GraphQlExecutionProfile profile) {
|
||||||
|
this(factory, null, profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,11 +60,15 @@ public final class GraphQlBatchLoaderRegistrar {
|
|||||||
*
|
*
|
||||||
* @param factory supplies the per-loader batch policy and executor
|
* @param factory supplies the per-loader batch policy and executor
|
||||||
* @param blockingBridge the bounded hand-off, for a runtime where blocking in place is unsafe
|
* @param blockingBridge the bounded hand-off, for a runtime where blocking in place is unsafe
|
||||||
|
* @param profile the runtime this deployment declared
|
||||||
*/
|
*/
|
||||||
public GraphQlBatchLoaderRegistrar(
|
public GraphQlBatchLoaderRegistrar(
|
||||||
GraphQlDataLoaderFactory factory, GraphQlBlockingBridge blockingBridge) {
|
GraphQlDataLoaderFactory factory,
|
||||||
|
GraphQlBlockingBridge blockingBridge,
|
||||||
|
GraphQlExecutionProfile profile) {
|
||||||
this.factory = Objects.requireNonNull(factory, "data loader factory is required");
|
this.factory = Objects.requireNonNull(factory, "data loader factory is required");
|
||||||
this.blockingBridge = blockingBridge;
|
this.blockingBridge = blockingBridge;
|
||||||
|
this.profile = Objects.requireNonNull(profile, "execution profile is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,6 +89,12 @@ public final class GraphQlBatchLoaderRegistrar {
|
|||||||
GraphQlDataLoaderName loaderName,
|
GraphQlDataLoaderName loaderName,
|
||||||
BiFunction<List<K>, GraphQlBatchContext, Map<K, V>> loadChunk) {
|
BiFunction<List<K>, GraphQlBatchContext, Map<K, V>> loadChunk) {
|
||||||
|
|
||||||
|
if (profile != GraphQlExecutionProfile.BLOCKING_MVC && blockingBridge == null) {
|
||||||
|
throw new GraphQlExecutionProfileException(
|
||||||
|
"loader "
|
||||||
|
+ loaderName.value()
|
||||||
|
+ " would block the event loop; declare a bounded blocking bridge");
|
||||||
|
}
|
||||||
GraphQlBatchExecutor executor = factory.executorFor(loaderName);
|
GraphQlBatchExecutor executor = factory.executorFor(loaderName);
|
||||||
|
|
||||||
registry
|
registry
|
||||||
|
|||||||
+10
@@ -36,6 +36,16 @@ public record GraphQlExecutionContext(
|
|||||||
GraphQlDocumentShape shape,
|
GraphQlDocumentShape shape,
|
||||||
GraphQlComplexityResult complexity) {
|
GraphQlComplexityResult complexity) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key under which the settled pipeline state is published for the rest of the request.
|
||||||
|
*
|
||||||
|
* <p>The measured shape and the scored complexity are computed once, by the cost stage, and were
|
||||||
|
* then discarded when the chain returned. Anything later in the request that wants to describe
|
||||||
|
* how large this request was — an observation convention, a diagnostic — had no way to ask, so it
|
||||||
|
* either re-measured the document or reported a number it had not measured.
|
||||||
|
*/
|
||||||
|
public static final String CONTEXT_KEY = "dev.caskeleton.graphql.executionContext";
|
||||||
|
|
||||||
public GraphQlExecutionContext {
|
public GraphQlExecutionContext {
|
||||||
Objects.requireNonNull(request, "request is required");
|
Objects.requireNonNull(request, "request is required");
|
||||||
Objects.requireNonNull(requestContext, "request context is required");
|
Objects.requireNonNull(requestContext, "request context is required");
|
||||||
|
|||||||
+3
@@ -95,6 +95,9 @@ public final class GraphQlPlatformInstrumentation extends SimplePerformantInstru
|
|||||||
execution
|
execution
|
||||||
.getGraphQLContext()
|
.getGraphQLContext()
|
||||||
.put(GraphQlRequestContext.CONTEXT_KEY, completed.requestContext());
|
.put(GraphQlRequestContext.CONTEXT_KEY, completed.requestContext());
|
||||||
|
// The whole settled state, not only the context: the cost stage has measured this document's
|
||||||
|
// depth and scored its complexity, and those numbers exist nowhere else once the chain returns.
|
||||||
|
execution.getGraphQLContext().put(GraphQlExecutionContext.CONTEXT_KEY, completed);
|
||||||
return SimpleInstrumentationContext.noOp();
|
return SimpleInstrumentationContext.noOp();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
package dev.caskeleton.adapter.inbound.graphql.runtime;
|
||||||
|
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.api.GraphQlClientProfile;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlRequestContext;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.error.GraphQlErrorCategory;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.observation.GraphQlRequestObservationConvention;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.policy.GraphQlOperationType;
|
||||||
|
import graphql.ExecutionResult;
|
||||||
|
import graphql.GraphQLError;
|
||||||
|
import graphql.execution.ExecutionContext;
|
||||||
|
import io.micrometer.common.KeyValue;
|
||||||
|
import io.micrometer.common.KeyValues;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import org.springframework.graphql.observation.ExecutionRequestObservationContext;
|
||||||
|
import org.springframework.graphql.observation.ExecutionRequestObservationConvention;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the platform's bounded request tags the ones a metrics backend actually receives.
|
||||||
|
*
|
||||||
|
* <p>The convention was a well-tested object with no consumer. Spring for GraphQL emits the {@code
|
||||||
|
* graphql.request} observation from its own instrumentation and asks an {@link
|
||||||
|
* ExecutionRequestObservationConvention} bean what to tag it with; the platform's convention did
|
||||||
|
* not implement that interface, so the cardinality bound it computes was never applied to a series
|
||||||
|
* anybody stored. A tag policy that no exporter consults bounds nothing.
|
||||||
|
*
|
||||||
|
* <p>The adapter lives here rather than in the observation package because the bound has to be
|
||||||
|
* framework-free to be testable without a running application, and the seam that applies it has to
|
||||||
|
* speak Spring and graphql-java. So the decision stays a value, and this class translates.
|
||||||
|
*
|
||||||
|
* <p>Everything it reports is bounded by construction. The operation name goes through the
|
||||||
|
* deployment's registry, the type is an enum, the profile is a validated identity, the outcome and
|
||||||
|
* the error category are closed sets, and the size numbers are buckets. The per-request identity
|
||||||
|
* that a debugger needs — the execution id — is reported as a high-cardinality value, which
|
||||||
|
* Micrometer carries on the trace and keeps off the meter.
|
||||||
|
*/
|
||||||
|
public final class GraphQlRequestObservationConventionAdapter
|
||||||
|
implements ExecutionRequestObservationConvention {
|
||||||
|
|
||||||
|
/** Outcome of a request that produced no error. */
|
||||||
|
public static final String OUTCOME_SUCCESS = "SUCCESS";
|
||||||
|
|
||||||
|
/** Outcome of a request the caller could have avoided. */
|
||||||
|
public static final String OUTCOME_REQUEST_ERROR = "REQUEST_ERROR";
|
||||||
|
|
||||||
|
/** Outcome of a request that failed for a reason the caller cannot fix. */
|
||||||
|
public static final String OUTCOME_INTERNAL_ERROR = "INTERNAL_ERROR";
|
||||||
|
|
||||||
|
private static final GraphQlClientProfile ANONYMOUS_PROFILE =
|
||||||
|
new GraphQlClientProfile("anonymous");
|
||||||
|
|
||||||
|
private final GraphQlRequestObservationConvention convention;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the adapter.
|
||||||
|
*
|
||||||
|
* @param convention the platform's bounded tag policy
|
||||||
|
*/
|
||||||
|
public GraphQlRequestObservationConventionAdapter(
|
||||||
|
GraphQlRequestObservationConvention convention) {
|
||||||
|
this.convention = Objects.requireNonNull(convention, "observation convention is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return convention.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContextualName(ExecutionRequestObservationContext context) {
|
||||||
|
// The span name, and therefore as bounded as a tag: the operation type, never the client's
|
||||||
|
// chosen operation name.
|
||||||
|
return "graphql " + operationType(context).name().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyValues getLowCardinalityKeyValues(ExecutionRequestObservationContext context) {
|
||||||
|
GraphQlExecutionContext settled = settled(context);
|
||||||
|
Map<String, String> tags =
|
||||||
|
convention.tagsForWireName(
|
||||||
|
context.getExecutionInput().getOperationName(),
|
||||||
|
operationType(context),
|
||||||
|
clientProfile(context),
|
||||||
|
// False rather than unknown: the platform's persisted-operation stage is not on this
|
||||||
|
// execution path, so no request reaching here came out of the persisted registry.
|
||||||
|
false,
|
||||||
|
outcome(context),
|
||||||
|
errorCategory(context),
|
||||||
|
settled == null ? null : settled.complexity(),
|
||||||
|
depth(settled));
|
||||||
|
|
||||||
|
KeyValues keyValues = KeyValues.empty();
|
||||||
|
for (Map.Entry<String, String> tag : tags.entrySet()) {
|
||||||
|
keyValues = keyValues.and(KeyValue.of(tag.getKey(), tag.getValue()));
|
||||||
|
}
|
||||||
|
return keyValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyValues getHighCardinalityKeyValues(ExecutionRequestObservationContext context) {
|
||||||
|
Object executionId = context.getExecutionInput().getExecutionId();
|
||||||
|
return executionId == null
|
||||||
|
? KeyValues.empty()
|
||||||
|
: KeyValues.of("graphql.execution.id", executionId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GraphQlOperationType operationType(ExecutionRequestObservationContext context) {
|
||||||
|
ExecutionContext execution = context.getExecutionContext();
|
||||||
|
if (execution == null || execution.getOperationDefinition() == null) {
|
||||||
|
// The observation is stopped even when parsing never selected an operation. Reporting the
|
||||||
|
// read type is what keeps the series shape stable across a request that failed early.
|
||||||
|
return GraphQlOperationType.QUERY;
|
||||||
|
}
|
||||||
|
return switch (execution.getOperationDefinition().getOperation()) {
|
||||||
|
case QUERY -> GraphQlOperationType.QUERY;
|
||||||
|
case MUTATION -> GraphQlOperationType.MUTATION;
|
||||||
|
case SUBSCRIPTION -> GraphQlOperationType.SUBSCRIPTION;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GraphQlClientProfile clientProfile(ExecutionRequestObservationContext context) {
|
||||||
|
GraphQlRequestContext requestContext =
|
||||||
|
context.getExecutionInput().getGraphQLContext().get(GraphQlRequestContext.CONTEXT_KEY);
|
||||||
|
// Anonymous is a profile, not a missing value: a request that proved nothing is still one the
|
||||||
|
// operator has to be able to count.
|
||||||
|
return requestContext == null ? ANONYMOUS_PROFILE : requestContext.clientProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GraphQlExecutionContext settled(ExecutionRequestObservationContext context) {
|
||||||
|
return context.getExecutionInput().getGraphQLContext().get(GraphQlExecutionContext.CONTEXT_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int depth(GraphQlExecutionContext settled) {
|
||||||
|
return settled == null || settled.shape() == null ? 0 : settled.shape().depth();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String outcome(ExecutionRequestObservationContext context) {
|
||||||
|
if (context.getError() != null) {
|
||||||
|
return OUTCOME_INTERNAL_ERROR;
|
||||||
|
}
|
||||||
|
ExecutionResult result = context.getExecutionResult();
|
||||||
|
if (result == null) {
|
||||||
|
return OUTCOME_INTERNAL_ERROR;
|
||||||
|
}
|
||||||
|
List<GraphQLError> errors = result.getErrors();
|
||||||
|
if (errors == null || errors.isEmpty()) {
|
||||||
|
return OUTCOME_SUCCESS;
|
||||||
|
}
|
||||||
|
return internal(errors) ? OUTCOME_INTERNAL_ERROR : OUTCOME_REQUEST_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String errorCategory(ExecutionRequestObservationContext context) {
|
||||||
|
if (context.getError() != null) {
|
||||||
|
return GraphQlErrorCategory.INTERNAL.name();
|
||||||
|
}
|
||||||
|
ExecutionResult result = context.getExecutionResult();
|
||||||
|
List<GraphQLError> errors = result == null ? null : result.getErrors();
|
||||||
|
if (errors == null || errors.isEmpty()) {
|
||||||
|
// Absent rather than a "none" label: an outcome tag already says the request succeeded, and a
|
||||||
|
// second tag saying the same thing doubles the series for no extra answer.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return internal(errors)
|
||||||
|
? GraphQlErrorCategory.INTERNAL.name()
|
||||||
|
: GraphQlErrorCategory.REQUEST.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean internal(List<GraphQLError> errors) {
|
||||||
|
// A resolver that threw is the server's problem; a document the schema refused is the caller's.
|
||||||
|
// Splitting them is the difference between an alert and a client-side bug report.
|
||||||
|
return errors.stream()
|
||||||
|
.anyMatch(error -> error.getErrorType() == graphql.ErrorType.DataFetchingException);
|
||||||
|
}
|
||||||
|
}
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
package dev.caskeleton.adapter.inbound.graphql.security;
|
||||||
|
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlCommandAttribution;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlRequestContext;
|
||||||
|
import dev.caskeleton.application.security.ObjectAccessDecision;
|
||||||
|
import dev.caskeleton.application.security.ObjectAccessPolicy;
|
||||||
|
import dev.caskeleton.application.security.ObjectAccessRequest;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asks the application's object-access rule on the platform's behalf.
|
||||||
|
*
|
||||||
|
* <p>The direction is what this class is for. The platform decides <em>when</em> an object needs an
|
||||||
|
* access check; the application decides the answer, because the answer depends on domain state.
|
||||||
|
* Those two facts used to be expressed by a port declared in this leaf whose method took a {@link
|
||||||
|
* GraphQlRequestContext} — a contract the application layer could not implement without depending
|
||||||
|
* on the inbound GraphQL adapter, which the dependency gate forbids and which would have dragged
|
||||||
|
* the transport into every persistence adapter behind the use case.
|
||||||
|
*
|
||||||
|
* <p>So the contract moved to {@code application-core} in terms of plain values, and what stays
|
||||||
|
* here is the mapping: request context in, four strings out, decision back. It is the same shape
|
||||||
|
* the persisted-operation registry uses against a neutral store contract, for the same reason.
|
||||||
|
*/
|
||||||
|
public final class ApplicationObjectAuthorization implements GraphQlObjectAuthorizationPort {
|
||||||
|
|
||||||
|
private final ObjectAccessPolicy policy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the bridge.
|
||||||
|
*
|
||||||
|
* @param policy the application's object-access rule
|
||||||
|
*/
|
||||||
|
public ApplicationObjectAuthorization(ObjectAccessPolicy policy) {
|
||||||
|
this.policy = Objects.requireNonNull(policy, "object access policy is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GraphQlAuthorizationDecision authorize(
|
||||||
|
GraphQlRequestContext context, String objectType, String objectId) {
|
||||||
|
|
||||||
|
GraphQlCommandAttribution attribution = GraphQlCommandAttribution.from(context);
|
||||||
|
return map(
|
||||||
|
policy.decide(
|
||||||
|
new ObjectAccessRequest(
|
||||||
|
attribution.actorId(), attribution.tenantId(), objectType, objectId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, GraphQlAuthorizationDecision> authorizeAll(
|
||||||
|
GraphQlRequestContext context, String objectType, List<String> objectIds) {
|
||||||
|
|
||||||
|
GraphQlCommandAttribution attribution = GraphQlCommandAttribution.from(context);
|
||||||
|
Map<String, ObjectAccessDecision> decided =
|
||||||
|
policy.decideAll(
|
||||||
|
attribution.actorId(), attribution.tenantId(), objectType, List.copyOf(objectIds));
|
||||||
|
|
||||||
|
Map<String, GraphQlAuthorizationDecision> mapped = new LinkedHashMap<>();
|
||||||
|
objectIds.forEach(
|
||||||
|
objectId -> {
|
||||||
|
ObjectAccessDecision decision = decided.get(objectId);
|
||||||
|
if (decision == null) {
|
||||||
|
// A missing answer is a denial, never an omission: a caller that dropped ids from its
|
||||||
|
// response would leave the loader with no decision for those objects, and "no decision"
|
||||||
|
// is the one state that must not read as permission.
|
||||||
|
mapped.put(objectId, GraphQlAuthorizationDecision.deny("OBJECT_NOT_AUTHORIZED"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mapped.put(objectId, map(decision));
|
||||||
|
});
|
||||||
|
return Map.copyOf(mapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GraphQlAuthorizationDecision map(ObjectAccessDecision decision) {
|
||||||
|
if (decision.allowed()) {
|
||||||
|
return GraphQlAuthorizationDecision.allow();
|
||||||
|
}
|
||||||
|
return decision.hideExistence()
|
||||||
|
? GraphQlAuthorizationDecision.denyHidingExistence(decision.code())
|
||||||
|
: GraphQlAuthorizationDecision.deny(decision.code());
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-2
@@ -37,9 +37,12 @@ public record GraphQlBatchContext(ActorRef actor, TenantContext tenant, GraphQlD
|
|||||||
/**
|
/**
|
||||||
* The cache-key prefix that keeps one tenant's loaded values out of another's.
|
* The cache-key prefix that keeps one tenant's loaded values out of another's.
|
||||||
*
|
*
|
||||||
* <p>A fingerprint rather than the raw tenant, so the key is safe if it is ever logged.
|
* <p>Partition tokens rather than the raw identities, so a cache key that reaches a debug log
|
||||||
|
* does not read as a list of who called and for whom. The tokens are not a privacy control — the
|
||||||
|
* identities behind them are guessable — but this key never leaves the request that built it, and
|
||||||
|
* what does leave uses the keyed fingerprinter.
|
||||||
*/
|
*/
|
||||||
public String cacheScope() {
|
public String cacheScope() {
|
||||||
return actor.fingerprint() + ":" + tenant.fingerprint();
|
return actor.cachePartition() + ":" + tenant.cachePartition();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-4
@@ -5,12 +5,18 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Application port that answers "may this caller see this object?".
|
* The platform-side seam for "may this caller see this object?".
|
||||||
*
|
*
|
||||||
* <p>A port rather than a repository call from platform code: whether a caller may see an object
|
* <p>A seam rather than a repository call from platform code: whether a caller may see an object
|
||||||
* depends on domain state — ownership, membership, workflow status — which the transport layer has
|
* depends on domain state — ownership, membership, workflow status — which the transport layer has
|
||||||
* no business querying. The platform decides <em>when</em> to ask; the Application decides the
|
* no business querying. The platform decides <em>when</em> to ask.
|
||||||
* answer.
|
*
|
||||||
|
* <p>It does not decide who answers. The contract that answers is {@code
|
||||||
|
* dev.caskeleton.application.security.ObjectAccessPolicy}, phrased in plain values so the
|
||||||
|
* application layer can own it; {@link ApplicationObjectAuthorization} maps between the two. This
|
||||||
|
* interface used to describe itself as the Application port, which no application code could have
|
||||||
|
* implemented — its method signature named a GraphQL request context, so implementing it required
|
||||||
|
* depending on this transport adapter.
|
||||||
*
|
*
|
||||||
* <p>The batch method exists because object authorization inside a DataLoader would otherwise
|
* <p>The batch method exists because object authorization inside a DataLoader would otherwise
|
||||||
* reintroduce the N+1 the loader was added to remove.
|
* reintroduce the N+1 the loader was added to remove.
|
||||||
|
|||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package dev.caskeleton.adapter.inbound.graphql.autoconfigure;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlBatchPolicyRegistry;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlDataLoaderFactory;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlBatchLoaderRegistrar;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The batch-loader chain is assembled by the platform, not by each adopter.
|
||||||
|
*
|
||||||
|
* <p>{@code GraphQlBatchLoaderRegistrar} carried the chunking, the budget and the request scope,
|
||||||
|
* was unit tested, and was declared by no configuration — the only file in the repository that
|
||||||
|
* mentioned it was itself. A field resolving through {@code @BatchMapping} or a {@code DataLoader}
|
||||||
|
* therefore met none of it: the platform's N+1 protection existed as a set of objects no request
|
||||||
|
* could reach.
|
||||||
|
*
|
||||||
|
* <p>An adopter still supplies the downstream call, because only the adopter has one. What it no
|
||||||
|
* longer supplies is the machinery around it, which is what this asserts.
|
||||||
|
*/
|
||||||
|
class GraphQlBatchLoaderWiringTest {
|
||||||
|
|
||||||
|
private final ApplicationContextRunner runner =
|
||||||
|
new ApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(GraphQlRootAutoConfiguration.class))
|
||||||
|
.withPropertyValues(
|
||||||
|
"backend.graphql.enabled=true", "backend.graphql.deployment-mode=LOCAL");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("the platform supplies the whole batch chain when GraphQL is on")
|
||||||
|
void theChainIsSupplied() {
|
||||||
|
runner.run(
|
||||||
|
context ->
|
||||||
|
assertThat(context)
|
||||||
|
.hasNotFailed()
|
||||||
|
.hasSingleBean(GraphQlBatchPolicyRegistry.class)
|
||||||
|
.hasSingleBean(GraphQlDataLoaderFactory.class)
|
||||||
|
.hasSingleBean(GraphQlBatchLoaderRegistrar.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("an adopter's own registry replaces the platform's, rather than colliding with it")
|
||||||
|
void anAdopterRegistryWins() {
|
||||||
|
runner
|
||||||
|
.withBean(GraphQlBatchPolicyRegistry.class, GraphQlBatchPolicyRegistry::new)
|
||||||
|
.run(
|
||||||
|
context ->
|
||||||
|
assertThat(context).hasNotFailed().hasSingleBean(GraphQlBatchPolicyRegistry.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package dev.caskeleton.adapter.inbound.graphql.autoconfigure;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.runtime.GraphQlRequestObservationConventionAdapter;
|
||||||
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.graphql.autoconfigure.observation.GraphQlObservationAutoConfiguration;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
import org.springframework.graphql.observation.ExecutionRequestObservationConvention;
|
||||||
|
import org.springframework.graphql.observation.GraphQlObservationInstrumentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tag policy has to reach the instrumentation that emits the observation.
|
||||||
|
*
|
||||||
|
* <p>Spring for GraphQL resolves one {@link ExecutionRequestObservationConvention} bean and falls
|
||||||
|
* back to its own when it finds none. The platform's cardinality policy used to be a bean of a type
|
||||||
|
* nothing looked for, so the framework took the fallback and the bounded operation-name label was
|
||||||
|
* computed for nobody — a control with tests, no consumer, and no way to tell from the metrics.
|
||||||
|
*/
|
||||||
|
class GraphQlObservationWiringTest {
|
||||||
|
|
||||||
|
private final ApplicationContextRunner runner =
|
||||||
|
new ApplicationContextRunner()
|
||||||
|
.withConfiguration(
|
||||||
|
AutoConfigurations.of(
|
||||||
|
GraphQlRootAutoConfiguration.class, GraphQlObservationAutoConfiguration.class))
|
||||||
|
.withBean(ObservationRegistry.class, ObservationRegistry::create)
|
||||||
|
.withPropertyValues(
|
||||||
|
"backend.graphql.enabled=true", "backend.graphql.deployment-mode=LOCAL");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void theFrameworkInstrumentationResolvesThePlatformConvention() {
|
||||||
|
runner.run(
|
||||||
|
context -> {
|
||||||
|
assertThat(context).hasNotFailed();
|
||||||
|
assertThat(context).hasSingleBean(GraphQlObservationInstrumentation.class);
|
||||||
|
assertThat(context)
|
||||||
|
.getBean(ExecutionRequestObservationConvention.class)
|
||||||
|
.as("the only convention the framework can resolve must be the bounded one")
|
||||||
|
.isInstanceOf(GraphQlRequestObservationConventionAdapter.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anAdopterConventionKeepsTheFrameworkFromTakingThePlatformOne() {
|
||||||
|
runner
|
||||||
|
.withBean(
|
||||||
|
"adopterConvention",
|
||||||
|
ExecutionRequestObservationConvention.class,
|
||||||
|
org.springframework.graphql.observation.DefaultExecutionRequestObservationConvention
|
||||||
|
::new)
|
||||||
|
.run(
|
||||||
|
context -> {
|
||||||
|
assertThat(context).hasNotFailed();
|
||||||
|
assertThat(context)
|
||||||
|
.as("a platform default that cannot be replaced is not a default")
|
||||||
|
.doesNotHaveBean(GraphQlRequestObservationConventionAdapter.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
@@ -7,6 +7,69 @@ import org.junit.jupiter.api.Test;
|
|||||||
/** Schema compatibility diff and breaking policy (Stable plan Task 10). */
|
/** Schema compatibility diff and breaking policy (Stable plan Task 10). */
|
||||||
class GraphQlSchemaComparatorTest {
|
class GraphQlSchemaComparatorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aFieldRemovedFromATypeExtensionIsBreaking() {
|
||||||
|
GraphQlCompatibilityReport report =
|
||||||
|
GraphQlSchemaComparator.compare(
|
||||||
|
"type Query { a: String } extend type Query { b: String }", "type Query { a: String }");
|
||||||
|
|
||||||
|
assertThat(report.changesOf(GraphQlChangeKind.OUTPUT_FIELD_REMOVED))
|
||||||
|
.as("a field an extension contributed is on the wire like any other")
|
||||||
|
.hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aFieldAddedByATypeExtensionIsReported() {
|
||||||
|
GraphQlCompatibilityReport report =
|
||||||
|
GraphQlSchemaComparator.compare(
|
||||||
|
"type Query { a: String }", "type Query { a: String } extend type Query { b: String }");
|
||||||
|
|
||||||
|
assertThat(report.changesOf(GraphQlChangeKind.OUTPUT_FIELD_ADDED_NULLABLE)).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anExtensionThatStrengthensAnInputIsBreaking() {
|
||||||
|
GraphQlCompatibilityReport report =
|
||||||
|
GraphQlSchemaComparator.compare(
|
||||||
|
"input OrderFilter { status: String } extend input OrderFilter { region: String }",
|
||||||
|
"input OrderFilter { status: String } extend input OrderFilter { region: String! }");
|
||||||
|
|
||||||
|
assertThat(report.changesOf(GraphQlChangeKind.INPUT_FIELD_STRENGTHENED)).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anEnumValueRemovedFromAnExtensionIsReported() {
|
||||||
|
GraphQlCompatibilityReport report =
|
||||||
|
GraphQlSchemaComparator.compare(
|
||||||
|
"enum Status { NEW } extend enum Status { ARCHIVED }", "enum Status { NEW }");
|
||||||
|
|
||||||
|
assertThat(report.changesOf(GraphQlChangeKind.ENUM_VALUE_REMOVED)).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aUnionMemberRemovedFromAnExtensionIsReported() {
|
||||||
|
GraphQlCompatibilityReport report =
|
||||||
|
GraphQlSchemaComparator.compare(
|
||||||
|
"type A { a: String } type B { b: String } union Result = A extend union Result = B",
|
||||||
|
"type A { a: String } type B { b: String } union Result = A");
|
||||||
|
|
||||||
|
assertThat(report.changesOf(GraphQlChangeKind.UNION_MEMBER_REMOVED)).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rewordingAScalarsDescriptionIsNotACoercionChange() {
|
||||||
|
GraphQlCompatibilityReport report =
|
||||||
|
GraphQlSchemaComparator.compare(
|
||||||
|
"\"An ISO-8601 instant.\" scalar Instant type Query { at: Instant }",
|
||||||
|
"\"An instant, in ISO-8601.\" scalar Instant type Query { at: Instant }");
|
||||||
|
|
||||||
|
assertThat(report.changesOf(GraphQlChangeKind.SCALAR_DECLARATION_CHANGED))
|
||||||
|
.as(
|
||||||
|
"prose is not a coercion; a review triggered by an edited sentence trains people to "
|
||||||
|
+ "approve the report without reading it")
|
||||||
|
.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void requiredArgumentAdditionIsBreaking() {
|
void requiredArgumentAdditionIsBreaking() {
|
||||||
GraphQlCompatibilityReport report =
|
GraphQlCompatibilityReport report =
|
||||||
|
|||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
package dev.caskeleton.adapter.inbound.graphql.context;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The fingerprint that leaves this process has to survive someone guessing the identity behind it.
|
||||||
|
*
|
||||||
|
* <p>The interesting case is the low-entropy one, so the tests use the identifiers a real
|
||||||
|
* deployment has: {@code user-42}, {@code tenant-a}. A plain digest of either is recovered in the
|
||||||
|
* time it takes to hash a wordlist, which is what the dictionary test measures directly.
|
||||||
|
*/
|
||||||
|
class GraphQlIdentityFingerprinterTest {
|
||||||
|
|
||||||
|
private static final ActorRef ACTOR = ActorRef.authenticated("user-42");
|
||||||
|
private static final TenantContext TENANT = TenantContext.fromTrustedSession("tenant-a");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aGuessableIdentityIsNotRecoverableFromItsFingerprint() {
|
||||||
|
GraphQlIdentityFingerprinter fingerprinter =
|
||||||
|
GraphQlIdentityFingerprinter.single("k1", randomKey());
|
||||||
|
|
||||||
|
String fingerprint = fingerprinter.actor(ACTOR);
|
||||||
|
Map<String, String> dictionary = new LinkedHashMap<>();
|
||||||
|
for (int candidate = 0; candidate < 100; candidate++) {
|
||||||
|
dictionary.put(sha256Prefix("user-" + candidate), "user-" + candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(dictionary.keySet())
|
||||||
|
.as("a digest of an enumerable identifier names its owner to anyone who enumerates it")
|
||||||
|
.contains(sha256Prefix("user-42"));
|
||||||
|
assertThat(dictionary)
|
||||||
|
.as("the same enumeration must not resolve the keyed fingerprint")
|
||||||
|
.doesNotContainKey(fingerprint.substring(fingerprint.indexOf(':') + 1));
|
||||||
|
assertThat(fingerprint).doesNotContain("user-42");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void twoKeysProduceTwoFingerprintsForOneIdentity() {
|
||||||
|
assertThat(GraphQlIdentityFingerprinter.single("k1", randomKey()).actor(ACTOR))
|
||||||
|
.as("a fingerprint a deployment cannot change is a fingerprint it cannot revoke")
|
||||||
|
.isNotEqualTo(GraphQlIdentityFingerprinter.single("k2", randomKey()).actor(ACTOR));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void theSameKeyProducesTheSameFingerprint() {
|
||||||
|
byte[] secret = randomKey();
|
||||||
|
|
||||||
|
assertThat(GraphQlIdentityFingerprinter.single("k1", secret).actor(ACTOR))
|
||||||
|
.as("a retry has to fingerprint to the stored value or idempotency never matches")
|
||||||
|
.isEqualTo(GraphQlIdentityFingerprinter.single("k1", secret.clone()).actor(ACTOR));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void theFingerprintNamesTheKeyItWasComputedUnder() {
|
||||||
|
byte[] retired = randomKey();
|
||||||
|
byte[] active = randomKey();
|
||||||
|
GraphQlIdentityFingerprinter rotated =
|
||||||
|
GraphQlIdentityFingerprinter.of(Map.of("k1", retired, "k2", active), "k2");
|
||||||
|
|
||||||
|
assertThat(rotated.activeKeyId()).isEqualTo("k2");
|
||||||
|
assertThat(rotated.actor(ACTOR)).startsWith("k2:");
|
||||||
|
assertThat(rotated.keyIds())
|
||||||
|
.as("records written under the retired key stay attributable while they are retained")
|
||||||
|
.containsExactlyInAnyOrder("k1", "k2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anActorAndATenantOfTheSameNameFingerprintDifferently() {
|
||||||
|
GraphQlIdentityFingerprinter fingerprinter =
|
||||||
|
GraphQlIdentityFingerprinter.single("k1", randomKey());
|
||||||
|
|
||||||
|
assertThat(fingerprinter.actor(ActorRef.authenticated("acme")))
|
||||||
|
.isNotEqualTo(fingerprinter.tenant(TenantContext.fromAuthenticatedCredential("acme")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aTenantFingerprintNeverCarriesTheTenantName() {
|
||||||
|
assertThat(GraphQlIdentityFingerprinter.single("k1", randomKey()).tenant(TENANT))
|
||||||
|
.doesNotContain("tenant-a");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aRingWithoutAUsableKeyIsRefused() {
|
||||||
|
byte[] usable = randomKey();
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> GraphQlIdentityFingerprinter.of(Map.of(), "k1"))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class);
|
||||||
|
assertThatThrownBy(() -> GraphQlIdentityFingerprinter.of(Map.of("k1", usable), "k2"))
|
||||||
|
.as("an active key that is not in the ring cannot fingerprint anything")
|
||||||
|
.isInstanceOf(IllegalArgumentException.class);
|
||||||
|
assertThatThrownBy(() -> GraphQlIdentityFingerprinter.single("k1", new byte[8]))
|
||||||
|
.as("a short key is the part of a MAC an attacker attacks first")
|
||||||
|
.isInstanceOf(IllegalArgumentException.class);
|
||||||
|
assertThatThrownBy(() -> GraphQlIdentityFingerprinter.single("k:1", usable))
|
||||||
|
.as("the key id is the fingerprint's prefix, so it cannot contain the separator")
|
||||||
|
.isInstanceOf(IllegalArgumentException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One generator for the whole class.
|
||||||
|
*
|
||||||
|
* <p>Seeding a fresh {@code SecureRandom} per call is the wasteful shape SpotBugs reports as
|
||||||
|
* {@code DMI_RANDOM_USED_ONLY_ONCE}: each instance pays for seeding and then produces a single
|
||||||
|
* value. The keys still never appear in this file, which is the property that matters.
|
||||||
|
*/
|
||||||
|
private static final SecureRandom KEYS = new SecureRandom();
|
||||||
|
|
||||||
|
private static byte[] randomKey() {
|
||||||
|
byte[] secret = new byte[32];
|
||||||
|
KEYS.nextBytes(secret);
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The unkeyed form, reproduced here only so the dictionary attack on it can be demonstrated. */
|
||||||
|
private static String sha256Prefix(String source) {
|
||||||
|
try {
|
||||||
|
byte[] digest =
|
||||||
|
MessageDigest.getInstance("SHA-256").digest(source.getBytes(StandardCharsets.UTF_8));
|
||||||
|
byte[] prefix = new byte[16];
|
||||||
|
System.arraycopy(digest, 0, prefix, 0, prefix.length);
|
||||||
|
return HexFormat.of().formatHex(prefix);
|
||||||
|
} catch (Exception unavailable) {
|
||||||
|
throw new IllegalStateException(unavailable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -48,12 +48,12 @@ class GraphQlRequestContextTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void actorAndTenantExposeFingerprintsInsteadOfRawIdentifiers() {
|
void actorAndTenantExposePartitionTokensInsteadOfRawIdentifiers() {
|
||||||
ActorRef actor = ActorRef.authenticated("user-42");
|
ActorRef actor = ActorRef.authenticated("user-42");
|
||||||
TenantContext tenant = TenantContext.fromTrustedSession("tenant-a");
|
TenantContext tenant = TenantContext.fromTrustedSession("tenant-a");
|
||||||
|
|
||||||
assertThat(actor.fingerprint()).doesNotContain("user-42").hasSize(32);
|
assertThat(actor.cachePartition()).doesNotContain("user-42").hasSize(32);
|
||||||
assertThat(tenant.fingerprint()).doesNotContain("tenant-a").hasSize(32);
|
assertThat(tenant.cachePartition()).doesNotContain("tenant-a").hasSize(32);
|
||||||
assertThat(ActorRef.anonymous().authenticated()).isFalse();
|
assertThat(ActorRef.anonymous().authenticated()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+81
-3
@@ -4,7 +4,10 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.ActorRef;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlIdentityFingerprinter;
|
||||||
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlRequestContext;
|
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlRequestContext;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.TenantContext;
|
||||||
import dev.caskeleton.adapter.inbound.graphql.http.GraphQlExtensionsPolicy;
|
import dev.caskeleton.adapter.inbound.graphql.http.GraphQlExtensionsPolicy;
|
||||||
import dev.caskeleton.adapter.inbound.graphql.testkit.GraphQlRequestContexts;
|
import dev.caskeleton.adapter.inbound.graphql.testkit.GraphQlRequestContexts;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@@ -15,11 +18,33 @@ import org.junit.jupiter.api.Test;
|
|||||||
/** Mutation idempotency scope and fingerprint (Stable plan Task 43). */
|
/** Mutation idempotency scope and fingerprint (Stable plan Task 43). */
|
||||||
class GraphQlMutationIdempotencyContextTest {
|
class GraphQlMutationIdempotencyContextTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One generator for the whole class, declared before the keys that draw from it.
|
||||||
|
*
|
||||||
|
* <p>Static initialisers run in textual order, so a generator declared below the two key
|
||||||
|
* constants would still be null when they are built. Seeding a fresh {@code SecureRandom} per
|
||||||
|
* call is also what SpotBugs reports as {@code DMI_RANDOM_USED_ONLY_ONCE}.
|
||||||
|
*/
|
||||||
|
private static final java.security.SecureRandom KEYS = new java.security.SecureRandom();
|
||||||
|
|
||||||
private static final String TENANT = "tenant-fingerprint";
|
private static final String TENANT = "tenant-fingerprint";
|
||||||
private static final String VERSION = "v1";
|
private static final String VERSION = "v1";
|
||||||
private static final GraphQlMutationCoordinate CREATE =
|
private static final GraphQlMutationCoordinate CREATE =
|
||||||
new GraphQlMutationCoordinate("Mutation.createOrder");
|
new GraphQlMutationCoordinate("Mutation.createOrder");
|
||||||
|
|
||||||
|
// Two rings standing for one deployment before and after a rotation. Both secrets are generated
|
||||||
|
// here rather than written down, so nothing in this file is a key anyone could reuse.
|
||||||
|
private static final GraphQlIdentityFingerprinter FIRST_KEY =
|
||||||
|
GraphQlIdentityFingerprinter.single("k1", randomKey());
|
||||||
|
private static final GraphQlIdentityFingerprinter ROTATED_KEY =
|
||||||
|
GraphQlIdentityFingerprinter.single("k2", randomKey());
|
||||||
|
|
||||||
|
private static byte[] randomKey() {
|
||||||
|
byte[] secret = new byte[32];
|
||||||
|
KEYS.nextBytes(secret);
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void sameKeyWithDifferentFingerprintIsConflict() {
|
void sameKeyWithDifferentFingerprintIsConflict() {
|
||||||
var first = context(CREATE, new GraphQlMutationFingerprint("sha256:a"));
|
var first = context(CREATE, new GraphQlMutationFingerprint("sha256:a"));
|
||||||
@@ -189,6 +214,7 @@ class GraphQlMutationIdempotencyContextTest {
|
|||||||
var derived =
|
var derived =
|
||||||
GraphQlMutationIdempotencyInterceptor.from(
|
GraphQlMutationIdempotencyInterceptor.from(
|
||||||
context,
|
context,
|
||||||
|
FIRST_KEY,
|
||||||
CREATE,
|
CREATE,
|
||||||
VERSION,
|
VERSION,
|
||||||
Map.of(GraphQlExtensionsPolicy.IDEMPOTENCY_KEY, "request-1"),
|
Map.of(GraphQlExtensionsPolicy.IDEMPOTENCY_KEY, "request-1"),
|
||||||
@@ -198,16 +224,68 @@ class GraphQlMutationIdempotencyContextTest {
|
|||||||
.hasValueSatisfying(
|
.hasValueSatisfying(
|
||||||
scope -> {
|
scope -> {
|
||||||
assertThat(scope.key().value()).isEqualTo("request-1");
|
assertThat(scope.key().value()).isEqualTo("request-1");
|
||||||
assertThat(scope.actorFingerprint()).isEqualTo(context.actor().fingerprint());
|
assertThat(scope.actorFingerprint()).isEqualTo(FIRST_KEY.actor(context.actor()));
|
||||||
assertThat(scope.tenantFingerprint()).isEqualTo(context.tenant().fingerprint());
|
assertThat(scope.tenantFingerprint()).isEqualTo(FIRST_KEY.tenant(context.tenant()));
|
||||||
assertThat(scope.contractVersion()).isEqualTo(VERSION);
|
assertThat(scope.contractVersion()).isEqualTo(VERSION);
|
||||||
});
|
});
|
||||||
assertThat(
|
assertThat(
|
||||||
GraphQlMutationIdempotencyInterceptor.from(
|
GraphQlMutationIdempotencyInterceptor.from(
|
||||||
context, CREATE, VERSION, Map.of(), Map.of()))
|
context, FIRST_KEY, CREATE, VERSION, Map.of(), Map.of()))
|
||||||
.isEmpty();
|
.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rotatingTheFingerprintKeyChangesTheStoredScope() {
|
||||||
|
GraphQlRequestContext context = GraphQlRequestContexts.testContext("tenant-a");
|
||||||
|
|
||||||
|
String first = derivedScope(context, FIRST_KEY);
|
||||||
|
String rotated = derivedScope(context, ROTATED_KEY);
|
||||||
|
|
||||||
|
assertThat(first)
|
||||||
|
.as(
|
||||||
|
"an unkeyed digest of a guessable actor is recovered by digesting the guesses, and a "
|
||||||
|
+ "deployment that suspects its stored scopes have been read has no way to change "
|
||||||
|
+ "them unless the fingerprint depends on a key it controls")
|
||||||
|
.isNotEqualTo(rotated);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anActorAndATenantOfTheSameNameDoNotShareAFingerprint() {
|
||||||
|
assertThat(FIRST_KEY.actor(ActorRef.authenticated("acme")))
|
||||||
|
.as("a service account and a tenant that happen to share a name are two identities")
|
||||||
|
.isNotEqualTo(FIRST_KEY.tenant(TenantContext.fromAuthenticatedCredential("acme")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void derivingAScopeWithoutAFingerprintKeyIsRefused() {
|
||||||
|
GraphQlRequestContext context = GraphQlRequestContexts.testContext("tenant-a");
|
||||||
|
|
||||||
|
assertThatThrownBy(
|
||||||
|
() ->
|
||||||
|
GraphQlMutationIdempotencyInterceptor.from(
|
||||||
|
context,
|
||||||
|
null,
|
||||||
|
CREATE,
|
||||||
|
VERSION,
|
||||||
|
Map.of(GraphQlExtensionsPolicy.IDEMPOTENCY_KEY, "request-1"),
|
||||||
|
Map.of("customerId", "c-1")))
|
||||||
|
.as("a deployment with no key must not silently fall back to a recoverable digest")
|
||||||
|
.isInstanceOf(IllegalArgumentException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String derivedScope(
|
||||||
|
GraphQlRequestContext context, GraphQlIdentityFingerprinter fingerprinter) {
|
||||||
|
return GraphQlMutationIdempotencyInterceptor.from(
|
||||||
|
context,
|
||||||
|
fingerprinter,
|
||||||
|
CREATE,
|
||||||
|
VERSION,
|
||||||
|
Map.of(GraphQlExtensionsPolicy.IDEMPOTENCY_KEY, "request-1"),
|
||||||
|
Map.of("customerId", "c-1"))
|
||||||
|
.orElseThrow()
|
||||||
|
.scope();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void fingerprintsAndScopesNeverCarryRawInputOrActor() {
|
void fingerprintsAndScopesNeverCarryRawInputOrActor() {
|
||||||
var fingerprint = GraphQlMutationFingerprint.of(Map.of("card", "4111111111111111"));
|
var fingerprint = GraphQlMutationFingerprint.of(Map.of("card", "4111111111111111"));
|
||||||
|
|||||||
+431
@@ -0,0 +1,431 @@
|
|||||||
|
package dev.caskeleton.adapter.inbound.graphql.runtime;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlDeadline;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlRequestContext;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlBatchContext;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlBatchErrorPolicy;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlBatchPolicy;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlBatchPolicyRegistry;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlDataLoaderFactory;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlDataLoaderName;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.dataloader.GraphQlMissingKeyPolicy;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.execution.GraphQlExecutionProfile;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.execution.GraphQlExecutionProfileException;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.security.GraphQlContextPropagator;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.testkit.GraphQlRequestContexts;
|
||||||
|
import graphql.ExecutionInput;
|
||||||
|
import graphql.ExecutionResult;
|
||||||
|
import graphql.GraphQL;
|
||||||
|
import graphql.schema.DataFetcher;
|
||||||
|
import graphql.schema.GraphQLSchema;
|
||||||
|
import graphql.schema.idl.RuntimeWiring;
|
||||||
|
import graphql.schema.idl.SchemaGenerator;
|
||||||
|
import graphql.schema.idl.SchemaParser;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import org.dataloader.DataLoaderRegistry;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The batch policy has to be reached by an executing query, not only by a unit test.
|
||||||
|
*
|
||||||
|
* <p>Everything the platform says about N+1 lived in objects that a request never met: the registry
|
||||||
|
* held {@code Object} and was connected to neither Spring's {@link
|
||||||
|
* org.springframework.graphql.execution.BatchLoaderRegistry} nor java-dataloader, and the chunk
|
||||||
|
* counts the contract suite asserted were numbers the caller had passed in. A suite that measures
|
||||||
|
* its own argument passes whatever the runtime does.
|
||||||
|
*
|
||||||
|
* <p>So this drives the real path — the registrar registers with Spring's registry, Spring builds
|
||||||
|
* the {@code DataLoaderRegistry} for one execution, graphql-java dispatches through it — and counts
|
||||||
|
* calls on a fake downstream. Fifty parents that produce fifty calls, or a batch that outlives the
|
||||||
|
* request budget, fail here and nowhere else.
|
||||||
|
*/
|
||||||
|
class GraphQlBatchLoaderRegistrationTest {
|
||||||
|
|
||||||
|
private static final GraphQlDataLoaderName LOADER = new GraphQlDataLoaderName("customer-by-id");
|
||||||
|
|
||||||
|
private static final String SCHEMA =
|
||||||
|
"""
|
||||||
|
type Query { orders: [Order!]! }
|
||||||
|
type Order { id: ID!, customer: Customer }
|
||||||
|
type Customer { id: ID!, name: String! }
|
||||||
|
""";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fiftyParentsBecomeABoundedNumberOfDownstreamCalls() {
|
||||||
|
RecordingLoader downstream = new RecordingLoader();
|
||||||
|
Fixture fixture = fixture(chunkSize(20), Clock.systemUTC());
|
||||||
|
|
||||||
|
ExecutionResult result = fixture.execute(orders(50, 50), downstream);
|
||||||
|
|
||||||
|
assertThat(result.getErrors()).isEmpty();
|
||||||
|
assertThat(resolvedCustomers(result)).hasSize(50);
|
||||||
|
assertThat(downstream.calls)
|
||||||
|
.as("fifty parents resolving one child each must not be fifty downstream calls")
|
||||||
|
.hasValue(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void oneRequestLoadsARepeatedKeyOnce() {
|
||||||
|
RecordingLoader downstream = new RecordingLoader();
|
||||||
|
Fixture fixture = fixture(chunkSize(20), Clock.systemUTC());
|
||||||
|
|
||||||
|
ExecutionResult result = fixture.execute(orders(50, 5), downstream);
|
||||||
|
|
||||||
|
assertThat(result.getErrors()).isEmpty();
|
||||||
|
assertThat(downstream.requestedKeys)
|
||||||
|
.as("the loader dedupes within a request; fifty orders share five customers")
|
||||||
|
.hasSize(5);
|
||||||
|
assertThat(downstream.calls).hasValue(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void oneRequestsCacheIsNotHandedToTheNext() {
|
||||||
|
RecordingLoader downstream = new RecordingLoader();
|
||||||
|
Fixture fixture = fixture(chunkSize(20), Clock.systemUTC());
|
||||||
|
|
||||||
|
fixture.execute(orders(5, 5), downstream);
|
||||||
|
fixture.execute(orders(5, 5), downstream);
|
||||||
|
|
||||||
|
assertThat(downstream.calls)
|
||||||
|
.as(
|
||||||
|
"a DataLoader cache that outlived its request would serve one caller's rows to the next")
|
||||||
|
.hasValue(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void everyChunkRunsUnderTheRequestsOwnActorAndTenant() {
|
||||||
|
RecordingLoader downstream = new RecordingLoader();
|
||||||
|
Fixture tenantA = fixture(chunkSize(20), Clock.systemUTC());
|
||||||
|
Fixture tenantB = fixture(chunkSize(20), Clock.systemUTC(), "tenant-b");
|
||||||
|
|
||||||
|
tenantA.execute(orders(50, 50), downstream);
|
||||||
|
Set<String> afterTenantA = Set.copyOf(downstream.observedScopes);
|
||||||
|
tenantB.execute(orders(50, 50), downstream);
|
||||||
|
|
||||||
|
assertThat(afterTenantA)
|
||||||
|
.as("one request's chunks all carry the same scope, or a chunk read for someone else")
|
||||||
|
.hasSize(1);
|
||||||
|
assertThat(downstream.observedScopes)
|
||||||
|
.as("two tenants must not share one batch scope")
|
||||||
|
.hasSize(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aLoaderWithoutThePlatformContextFailsClosedRatherThanLoading() {
|
||||||
|
RecordingLoader downstream = new RecordingLoader();
|
||||||
|
Fixture fixture = fixture(chunkSize(20), Clock.systemUTC());
|
||||||
|
|
||||||
|
ExecutionResult result = fixture.executeWithoutRequestContext(orders(5, 5), downstream);
|
||||||
|
|
||||||
|
assertThat(result.getErrors())
|
||||||
|
.as("a batch with no deadline, tenant or actor has nothing to bound or scope it")
|
||||||
|
.isNotEmpty();
|
||||||
|
assertThat(downstream.calls).hasValue(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aBatchThatOverrunsTheRequestBudgetStopsAtTheChunkThatSpentIt() {
|
||||||
|
MutableClock clock = new MutableClock(Instant.parse("2026-08-14T00:00:00Z"));
|
||||||
|
RecordingLoader downstream = new RecordingLoader();
|
||||||
|
downstream.onCall = () -> clock.advance(Duration.ofMinutes(1));
|
||||||
|
Fixture fixture = fixture(chunkSize(10), clock);
|
||||||
|
|
||||||
|
ExecutionResult result = fixture.execute(orders(50, 50), downstream);
|
||||||
|
|
||||||
|
assertThat(result.getErrors()).isNotEmpty();
|
||||||
|
assertThat(downstream.calls)
|
||||||
|
.as("the chunk after an exhausted budget must never reach the downstream")
|
||||||
|
.hasValue(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aLoaderThatAnswersAKeyNobodyAskedForIsRefused() {
|
||||||
|
RecordingLoader downstream = new RecordingLoader();
|
||||||
|
downstream.extraKey = "c-from-another-question";
|
||||||
|
Fixture fixture = fixture(chunkSize(20), Clock.systemUTC());
|
||||||
|
|
||||||
|
ExecutionResult result = fixture.execute(orders(5, 5), downstream);
|
||||||
|
|
||||||
|
assertThat(result.getErrors())
|
||||||
|
.as("matching cardinality is not matching keys, and the platform declares that a violation")
|
||||||
|
.isNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aRowTheLoaderReportsAsNullRendersAsAMissingChild() {
|
||||||
|
RecordingLoader downstream = new RecordingLoader();
|
||||||
|
downstream.nullValueKey = "c-2";
|
||||||
|
Fixture fixture = fixture(chunkSize(20), Clock.systemUTC());
|
||||||
|
|
||||||
|
ExecutionResult result = fixture.execute(orders(5, 5), downstream);
|
||||||
|
|
||||||
|
assertThat(result.getErrors())
|
||||||
|
.as("a nullable relation with no row is data, not a failure of the whole batch")
|
||||||
|
.isEmpty();
|
||||||
|
assertThat(resolvedCustomers(result)).containsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aMissingRowUnderTheFieldErrorPolicyFailsTheField() {
|
||||||
|
RecordingLoader downstream = new RecordingLoader();
|
||||||
|
downstream.omittedKey = "c-2";
|
||||||
|
Fixture fixture =
|
||||||
|
fixture(
|
||||||
|
new GraphQlBatchPolicy(
|
||||||
|
LOADER,
|
||||||
|
20,
|
||||||
|
Duration.ofSeconds(5),
|
||||||
|
true,
|
||||||
|
GraphQlMissingKeyPolicy.FIELD_ERROR,
|
||||||
|
GraphQlBatchErrorPolicy.PER_KEY),
|
||||||
|
Clock.systemUTC());
|
||||||
|
|
||||||
|
ExecutionResult result = fixture.execute(orders(5, 5), downstream);
|
||||||
|
|
||||||
|
assertThat(result.getErrors())
|
||||||
|
.as("a loader declared as always resolving must not render its absence as legitimate null")
|
||||||
|
.isNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aReactiveRuntimeWithoutABridgeRefusesTheLoader() {
|
||||||
|
GraphQlBatchLoaderRegistrar registrar =
|
||||||
|
new GraphQlBatchLoaderRegistrar(
|
||||||
|
factory(chunkSize(20), Clock.systemUTC()), GraphQlExecutionProfile.REACTIVE_WEBFLUX);
|
||||||
|
|
||||||
|
assertThatThrownBy(
|
||||||
|
() ->
|
||||||
|
registrar.<String, Map<String, Object>>register(
|
||||||
|
new DefaultBatchLoaderRegistry(), LOADER, (keys, batchContext) -> Map.of()))
|
||||||
|
.as("an inline blocking chunk on an event loop stalls every request that loop is serving")
|
||||||
|
.isInstanceOf(GraphQlExecutionProfileException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aBridgedLoaderRunsOffTheCallingThreadAndStillSeesTheRequest() {
|
||||||
|
RecordingLoader downstream = new RecordingLoader();
|
||||||
|
try (GraphQlBlockingBridge bridge = GraphQlBlockingBridge.bounded(1, 4)) {
|
||||||
|
Fixture fixture =
|
||||||
|
new Fixture(
|
||||||
|
new GraphQlBatchLoaderRegistrar(
|
||||||
|
factory(chunkSize(20), Clock.systemUTC()),
|
||||||
|
bridge,
|
||||||
|
GraphQlExecutionProfile.REACTIVE_WEBFLUX),
|
||||||
|
"tenant-a",
|
||||||
|
Clock.systemUTC());
|
||||||
|
|
||||||
|
ExecutionResult result = fixture.execute(orders(5, 5), downstream);
|
||||||
|
|
||||||
|
assertThat(result.getErrors()).isEmpty();
|
||||||
|
assertThat(downstream.observedThreads)
|
||||||
|
.as("the bridge exists to move the blocking call off the thread that subscribed")
|
||||||
|
.doesNotContain(Thread.currentThread().getName());
|
||||||
|
assertThat(downstream.observedBoundContexts)
|
||||||
|
.as("a batch that arrives on the bridge thread with no context has no tenant")
|
||||||
|
.containsExactly(Boolean.TRUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aServletRuntimeLoadsOnTheThreadItWasGiven() {
|
||||||
|
RecordingLoader downstream = new RecordingLoader();
|
||||||
|
|
||||||
|
fixture(chunkSize(20), Clock.systemUTC()).execute(orders(5, 5), downstream);
|
||||||
|
|
||||||
|
assertThat(downstream.observedThreads)
|
||||||
|
.as("a second pool on a servlet stack is a queue and a wait that buys nothing")
|
||||||
|
.containsExactly(Thread.currentThread().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GraphQlBatchPolicy chunkSize(int size) {
|
||||||
|
return new GraphQlBatchPolicy(
|
||||||
|
LOADER,
|
||||||
|
size,
|
||||||
|
Duration.ofSeconds(5),
|
||||||
|
true,
|
||||||
|
GraphQlMissingKeyPolicy.NULL_VALUE,
|
||||||
|
GraphQlBatchErrorPolicy.PER_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Fixture fixture(GraphQlBatchPolicy policy, Clock clock) {
|
||||||
|
return fixture(policy, clock, "tenant-a");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Fixture fixture(GraphQlBatchPolicy policy, Clock clock, String tenant) {
|
||||||
|
return new Fixture(
|
||||||
|
new GraphQlBatchLoaderRegistrar(
|
||||||
|
factory(policy, clock), GraphQlExecutionProfile.BLOCKING_MVC),
|
||||||
|
tenant,
|
||||||
|
clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GraphQlDataLoaderFactory factory(GraphQlBatchPolicy policy, Clock clock) {
|
||||||
|
return new GraphQlDataLoaderFactory(
|
||||||
|
new GraphQlBatchPolicyRegistry().register(policy), 100, clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@code count} orders spread over {@code customers} distinct customer ids. */
|
||||||
|
private static List<Order> orders(int count, int customers) {
|
||||||
|
List<Order> orders = new ArrayList<>();
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
orders.add(new Order("o-" + index, "c-" + (index % customers)));
|
||||||
|
}
|
||||||
|
return orders;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static List<Map<String, Object>> resolvedCustomers(ExecutionResult result) {
|
||||||
|
Map<String, Object> data = result.getData();
|
||||||
|
return ((List<Map<String, Object>>) data.get("orders"))
|
||||||
|
.stream().map(order -> (Map<String, Object>) order.get("customer")).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Order(String id, String customerId) {}
|
||||||
|
|
||||||
|
/** The adopter's downstream call, counting what the platform actually asked it for. */
|
||||||
|
private static final class RecordingLoader {
|
||||||
|
|
||||||
|
private final AtomicInteger calls = new AtomicInteger();
|
||||||
|
private final Set<String> requestedKeys = new LinkedHashSet<>();
|
||||||
|
private final Set<String> observedScopes = new LinkedHashSet<>();
|
||||||
|
private Runnable onCall = () -> {};
|
||||||
|
private final Set<String> observedThreads = new LinkedHashSet<>();
|
||||||
|
private final Set<Boolean> observedBoundContexts = new LinkedHashSet<>();
|
||||||
|
private String extraKey;
|
||||||
|
private String nullValueKey;
|
||||||
|
private String omittedKey;
|
||||||
|
|
||||||
|
Map<String, Map<String, Object>> load(List<String> keys, GraphQlBatchContext context) {
|
||||||
|
calls.incrementAndGet();
|
||||||
|
requestedKeys.addAll(keys);
|
||||||
|
observedScopes.add(context.cacheScope());
|
||||||
|
observedThreads.add(Thread.currentThread().getName());
|
||||||
|
observedBoundContexts.add(GraphQlContextPropagator.current().isPresent());
|
||||||
|
onCall.run();
|
||||||
|
Map<String, Map<String, Object>> rows = new LinkedHashMap<>();
|
||||||
|
for (String key : keys) {
|
||||||
|
if (key.equals(omittedKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rows.put(
|
||||||
|
key, key.equals(nullValueKey) ? null : Map.of("id", key, "name", "customer " + key));
|
||||||
|
}
|
||||||
|
if (extraKey != null) {
|
||||||
|
rows.put(extraKey, Map.of("id", extraKey, "name", "customer " + extraKey));
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Registrar, schema and execution wired the way Spring wires them at runtime. */
|
||||||
|
private static final class Fixture {
|
||||||
|
|
||||||
|
private final GraphQlBatchLoaderRegistrar registrar;
|
||||||
|
private final String tenant;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
|
Fixture(GraphQlBatchLoaderRegistrar registrar, String tenant, Clock clock) {
|
||||||
|
this.registrar = registrar;
|
||||||
|
this.tenant = tenant;
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExecutionResult execute(List<Order> orders, RecordingLoader downstream) {
|
||||||
|
GraphQlRequestContext context =
|
||||||
|
GraphQlRequestContexts.testContext(tenant)
|
||||||
|
.withDeadline(GraphQlDeadline.after(Duration.ofSeconds(5), clock));
|
||||||
|
return execute(orders, downstream, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExecutionResult executeWithoutRequestContext(List<Order> orders, RecordingLoader downstream) {
|
||||||
|
return execute(orders, downstream, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExecutionResult execute(
|
||||||
|
List<Order> orders, RecordingLoader downstream, GraphQlRequestContext context) {
|
||||||
|
|
||||||
|
DefaultBatchLoaderRegistry batchLoaders = new DefaultBatchLoaderRegistry();
|
||||||
|
registrar.<String, Map<String, Object>>register(
|
||||||
|
batchLoaders, LOADER, (keys, batchContext) -> downstream.load(keys, batchContext));
|
||||||
|
|
||||||
|
ExecutionInput input =
|
||||||
|
ExecutionInput.newExecutionInput("{ orders { id customer { id name } } }")
|
||||||
|
.graphQLContext(
|
||||||
|
builder -> {
|
||||||
|
if (context != null) {
|
||||||
|
builder.of(GraphQlRequestContext.CONTEXT_KEY, context);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
// A registry per execution, exactly as Spring builds one per request: this is what makes the
|
||||||
|
// loader's cache request-scoped rather than a process-wide store of other people's rows.
|
||||||
|
DataLoaderRegistry dataLoaders = new DataLoaderRegistry();
|
||||||
|
batchLoaders.registerDataLoaders(dataLoaders, input.getGraphQLContext());
|
||||||
|
|
||||||
|
return GraphQL.newGraphQL(schema(orders))
|
||||||
|
.build()
|
||||||
|
.execute(input.transform(builder -> builder.dataLoaderRegistry(dataLoaders)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GraphQLSchema schema(List<Order> orders) {
|
||||||
|
DataFetcher<?> customer =
|
||||||
|
environment -> {
|
||||||
|
Order order = environment.getSource();
|
||||||
|
return environment
|
||||||
|
.<String, Map<String, Object>>getDataLoader(LOADER.value())
|
||||||
|
.load(order.customerId());
|
||||||
|
};
|
||||||
|
return new SchemaGenerator()
|
||||||
|
.makeExecutableSchema(
|
||||||
|
new SchemaParser().parse(SCHEMA),
|
||||||
|
RuntimeWiring.newRuntimeWiring()
|
||||||
|
.type("Query", builder -> builder.dataFetcher("orders", environment -> orders))
|
||||||
|
.type("Order", builder -> builder.dataFetcher("customer", customer))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A clock the test advances itself, so elapsed time is caused by the work under test. */
|
||||||
|
private static final class MutableClock extends Clock {
|
||||||
|
|
||||||
|
private Instant current;
|
||||||
|
|
||||||
|
MutableClock(Instant start) {
|
||||||
|
this.current = start;
|
||||||
|
}
|
||||||
|
|
||||||
|
void advance(Duration step) {
|
||||||
|
current = current.plus(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ZoneId getZone() {
|
||||||
|
return ZoneOffset.UTC;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Clock withZone(ZoneId zone) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Instant instant() {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
package dev.caskeleton.adapter.inbound.graphql.runtime;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.api.GraphQlClientProfile;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.api.GraphQlOperationId;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.ActorRef;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlDeadline;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlRequestContext;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.TenantContext;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.observation.GraphQlObservationNames;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.observation.GraphQlOperationNameCardinality;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.observation.GraphQlRequestObservationConvention;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.observation.GraphQlSensitiveAttributeFilter;
|
||||||
|
import graphql.ExecutionInput;
|
||||||
|
import graphql.ExecutionResult;
|
||||||
|
import io.micrometer.core.instrument.Meter;
|
||||||
|
import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
|
||||||
|
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||||
|
import io.micrometer.observation.Observation;
|
||||||
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.graphql.observation.ExecutionRequestObservationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bound, measured where a metrics backend would see it.
|
||||||
|
*
|
||||||
|
* <p>The platform's tag policy was a value object with no consumer: Spring for GraphQL emits the
|
||||||
|
* {@code graphql.request} observation and asks its own convention what to tag it with, so the
|
||||||
|
* cardinality policy applied to nothing that was ever exported. These cases drive the observation
|
||||||
|
* through a real {@code ObservationRegistry} and a registry that actually stores series, and count
|
||||||
|
* the meters afterwards.
|
||||||
|
*/
|
||||||
|
class GraphQlRequestObservationConventionAdapterTest {
|
||||||
|
|
||||||
|
private static final int ARBITRARY_NAMES = 10_000;
|
||||||
|
|
||||||
|
private final SimpleMeterRegistry meters = new SimpleMeterRegistry();
|
||||||
|
private final ObservationRegistry observations = ObservationRegistry.create();
|
||||||
|
|
||||||
|
GraphQlRequestObservationConventionAdapterTest() {
|
||||||
|
observations.observationConfig().observationHandler(new DefaultMeterObservationHandler(meters));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tenThousandArbitraryNamesProduceOneExportedSeries() {
|
||||||
|
var adapter = new GraphQlRequestObservationConventionAdapter(collapsingConvention());
|
||||||
|
|
||||||
|
for (int index = 0; index < ARBITRARY_NAMES; index++) {
|
||||||
|
observe(adapter, "Query" + index, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(requestSeries())
|
||||||
|
.as("a client that renames its operation per request must not rename the time series")
|
||||||
|
.hasSize(1);
|
||||||
|
assertThat(operationNameLabels()).containsExactly(GraphQlOperationNameCardinality.UNREGISTERED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aNameTheValueTypeWouldRejectIsCollapsedRatherThanThrown() {
|
||||||
|
var adapter = new GraphQlRequestObservationConventionAdapter(collapsingConvention());
|
||||||
|
|
||||||
|
// Valid GraphQL names, invalid platform operation names: too short, and leading underscore.
|
||||||
|
observe(adapter, "ab", null);
|
||||||
|
observe(adapter, "_internal", null);
|
||||||
|
|
||||||
|
assertThat(requestSeries())
|
||||||
|
.as("an observation convention that throws takes the request down with it")
|
||||||
|
.hasSize(1);
|
||||||
|
assertThat(operationNameLabels()).containsExactly(GraphQlOperationNameCardinality.UNREGISTERED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aRegisteredOperationKeepsItsOwnSeriesAndTheRestCollapse() {
|
||||||
|
var convention =
|
||||||
|
new GraphQlRequestObservationConvention(
|
||||||
|
GraphQlSensitiveAttributeFilter.standard(),
|
||||||
|
new GraphQlOperationNameCardinality(Set.of("OrderById")));
|
||||||
|
var adapter = new GraphQlRequestObservationConventionAdapter(convention);
|
||||||
|
|
||||||
|
observe(adapter, "OrderById", null);
|
||||||
|
for (int index = 0; index < 100; index++) {
|
||||||
|
observe(adapter, "Query" + index, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(operationNameLabels())
|
||||||
|
.containsExactlyInAnyOrder("OrderById", GraphQlOperationNameCardinality.UNREGISTERED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void theExportedTagsCarryTheRequestProfileAndOutcome() {
|
||||||
|
var adapter = new GraphQlRequestObservationConventionAdapter(collapsingConvention());
|
||||||
|
|
||||||
|
observe(adapter, "Anything", requestContext("partner"));
|
||||||
|
|
||||||
|
Meter.Id id = requestSeries().get(0);
|
||||||
|
assertThat(id.getTag("graphql.client.profile")).isEqualTo("partner");
|
||||||
|
assertThat(id.getTag("graphql.outcome"))
|
||||||
|
.isEqualTo(GraphQlRequestObservationConventionAdapter.OUTCOME_SUCCESS);
|
||||||
|
assertThat(id.getTag("graphql.operation.type")).isEqualTo("QUERY");
|
||||||
|
assertThat(id.getTag("graphql.document"))
|
||||||
|
.as("the document is the unbounded value the allowlist exists to drop")
|
||||||
|
.isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anUnauthenticatedRequestIsCountedUnderTheAnonymousProfile() {
|
||||||
|
var adapter = new GraphQlRequestObservationConventionAdapter(collapsingConvention());
|
||||||
|
|
||||||
|
observe(adapter, "Anything", null);
|
||||||
|
|
||||||
|
assertThat(requestSeries().get(0).getTag("graphql.client.profile")).isEqualTo("anonymous");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void theObservationNameMatchesTheOneSpringAlreadyEmits() {
|
||||||
|
var adapter = new GraphQlRequestObservationConventionAdapter(collapsingConvention());
|
||||||
|
|
||||||
|
assertThat(adapter.getName()).isEqualTo(GraphQlObservationNames.REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GraphQlRequestObservationConvention collapsingConvention() {
|
||||||
|
return GraphQlRequestObservationConvention.standard();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void observe(
|
||||||
|
GraphQlRequestObservationConventionAdapter adapter,
|
||||||
|
String operationName,
|
||||||
|
GraphQlRequestContext requestContext) {
|
||||||
|
|
||||||
|
ExecutionInput input =
|
||||||
|
ExecutionInput.newExecutionInput("{ __typename }").operationName(operationName).build();
|
||||||
|
if (requestContext != null) {
|
||||||
|
input.getGraphQLContext().put(GraphQlRequestContext.CONTEXT_KEY, requestContext);
|
||||||
|
}
|
||||||
|
var context = new ExecutionRequestObservationContext(input);
|
||||||
|
context.setExecutionResult(ExecutionResult.newExecutionResult().data(Map.of()).build());
|
||||||
|
Observation.createNotStarted(adapter, () -> context, observations).observe(() -> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GraphQlRequestContext requestContext(String profile) {
|
||||||
|
return new GraphQlRequestContext(
|
||||||
|
ActorRef.anonymous(),
|
||||||
|
TenantContext.system("public"),
|
||||||
|
new GraphQlClientProfile(profile),
|
||||||
|
Locale.ENGLISH,
|
||||||
|
new GraphQlOperationId("order.by-id"),
|
||||||
|
"trace-1",
|
||||||
|
new GraphQlDeadline(Instant.now().plus(Duration.ofSeconds(5))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Meter.Id> requestSeries() {
|
||||||
|
return meters.getMeters().stream()
|
||||||
|
.map(Meter::getId)
|
||||||
|
.filter(id -> GraphQlObservationNames.REQUEST.equals(id.getName()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> operationNameLabels() {
|
||||||
|
return requestSeries().stream()
|
||||||
|
.map(id -> id.getTag("graphql.operation.name"))
|
||||||
|
.distinct()
|
||||||
|
.sorted()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
+156
@@ -0,0 +1,156 @@
|
|||||||
|
package dev.caskeleton.adapter.inbound.graphql.security;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.api.GraphQlClientProfile;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.api.GraphQlOperationId;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.ActorRef;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlDeadline;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlRequestContext;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.context.TenantContext;
|
||||||
|
import dev.caskeleton.application.security.ObjectAccessDecision;
|
||||||
|
import dev.caskeleton.application.security.ObjectAccessPolicy;
|
||||||
|
import dev.caskeleton.application.security.ObjectAccessRequest;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GraphQL side of object access is a mapping, and nothing more.
|
||||||
|
*
|
||||||
|
* <p>What the application receives has to be four plain values, because a contract that names this
|
||||||
|
* leaf's request context cannot be implemented from application-core at all — the dependency gate
|
||||||
|
* refuses the edge, and the transport would arrive in every persistence adapter behind the use
|
||||||
|
* case. These cases pin the mapping: the actor and tenant come from the request context, the
|
||||||
|
* decision comes back unaltered in meaning, and an answer the application failed to give is a
|
||||||
|
* denial rather than an omission.
|
||||||
|
*/
|
||||||
|
class ApplicationObjectAuthorizationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void theApplicationSeesTheActorAndTenantFromTheRequestContext() {
|
||||||
|
List<ObjectAccessRequest> asked = new ArrayList<>();
|
||||||
|
var bridge =
|
||||||
|
new ApplicationObjectAuthorization(
|
||||||
|
request -> {
|
||||||
|
asked.add(request);
|
||||||
|
return ObjectAccessDecision.allow();
|
||||||
|
});
|
||||||
|
|
||||||
|
bridge.authorize(context(ActorRef.authenticated("actor-1"), "tenant-a"), "Order", "order-1");
|
||||||
|
|
||||||
|
assertThat(asked).hasSize(1);
|
||||||
|
assertThat(asked.get(0).actor()).contains("actor-1");
|
||||||
|
assertThat(asked.get(0).tenantId()).isEqualTo("tenant-a");
|
||||||
|
assertThat(asked.get(0).objectType()).isEqualTo("Order");
|
||||||
|
assertThat(asked.get(0).objectId()).isEqualTo("order-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anUnauthenticatedCallerReachesTheApplicationWithNoActor() {
|
||||||
|
List<ObjectAccessRequest> asked = new ArrayList<>();
|
||||||
|
var bridge =
|
||||||
|
new ApplicationObjectAuthorization(
|
||||||
|
request -> {
|
||||||
|
asked.add(request);
|
||||||
|
return ObjectAccessDecision.allow();
|
||||||
|
});
|
||||||
|
|
||||||
|
bridge.authorize(context(ActorRef.anonymous(), "public"), "Order", "order-1");
|
||||||
|
|
||||||
|
assertThat(asked.get(0).actor())
|
||||||
|
.as("an anonymous reference is not an identity the rule may match on")
|
||||||
|
.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aHiddenDenialStaysHiddenAcrossTheBoundary() {
|
||||||
|
var bridge =
|
||||||
|
new ApplicationObjectAuthorization(
|
||||||
|
request -> ObjectAccessDecision.denyHidingExistence("OBJECT_NOT_FOUND"));
|
||||||
|
|
||||||
|
GraphQlAuthorizationDecision decision =
|
||||||
|
bridge.authorize(context(ActorRef.authenticated("actor-1"), "tenant-a"), "Order", "o-1");
|
||||||
|
|
||||||
|
assertThat(decision.allowed()).isFalse();
|
||||||
|
assertThat(decision.code()).isEqualTo("OBJECT_NOT_FOUND");
|
||||||
|
assertThat(decision.hideExistence())
|
||||||
|
.as("dropping this flag turns a not-found into a forbidden, which discloses the object")
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void theBatchFormAsksOnceAndAnswersEveryId() {
|
||||||
|
List<String> batches = new ArrayList<>();
|
||||||
|
ObjectAccessPolicy policy =
|
||||||
|
new ObjectAccessPolicy() {
|
||||||
|
@Override
|
||||||
|
public ObjectAccessDecision decide(ObjectAccessRequest request) {
|
||||||
|
throw new AssertionError("the batch form must not fall back to one call per object");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, ObjectAccessDecision> decideAll(
|
||||||
|
String actorId, String tenantId, String objectType, List<String> objectIds) {
|
||||||
|
batches.add(String.join(",", objectIds));
|
||||||
|
return Map.of(
|
||||||
|
"order-1", ObjectAccessDecision.allow(),
|
||||||
|
"order-2", ObjectAccessDecision.deny("OBJECT_NOT_AUTHORIZED"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Map<String, GraphQlAuthorizationDecision> decisions =
|
||||||
|
new ApplicationObjectAuthorization(policy)
|
||||||
|
.authorizeAll(
|
||||||
|
context(ActorRef.authenticated("actor-1"), "tenant-a"),
|
||||||
|
"Order",
|
||||||
|
List.of("order-1", "order-2"));
|
||||||
|
|
||||||
|
assertThat(batches).containsExactly("order-1,order-2");
|
||||||
|
assertThat(decisions.get("order-1").allowed()).isTrue();
|
||||||
|
assertThat(decisions.get("order-2").allowed()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anObjectTheApplicationDidNotAnswerForIsDenied() {
|
||||||
|
ObjectAccessPolicy policy =
|
||||||
|
new ObjectAccessPolicy() {
|
||||||
|
@Override
|
||||||
|
public ObjectAccessDecision decide(ObjectAccessRequest request) {
|
||||||
|
return ObjectAccessDecision.allow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, ObjectAccessDecision> decideAll(
|
||||||
|
String actorId, String tenantId, String objectType, List<String> objectIds) {
|
||||||
|
return Map.of("order-1", ObjectAccessDecision.allow());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Map<String, GraphQlAuthorizationDecision> decisions =
|
||||||
|
new ApplicationObjectAuthorization(policy)
|
||||||
|
.authorizeAll(
|
||||||
|
context(ActorRef.authenticated("actor-1"), "tenant-a"),
|
||||||
|
"Order",
|
||||||
|
List.of("order-1", "order-2"));
|
||||||
|
|
||||||
|
assertThat(decisions.get("order-2").allowed())
|
||||||
|
.as("no decision is the one state that must never read as permission")
|
||||||
|
.isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GraphQlRequestContext context(ActorRef actor, String tenant) {
|
||||||
|
return new GraphQlRequestContext(
|
||||||
|
actor,
|
||||||
|
TenantContext.system(tenant),
|
||||||
|
new GraphQlClientProfile("first-party"),
|
||||||
|
Locale.ENGLISH,
|
||||||
|
new GraphQlOperationId("order.by-id"),
|
||||||
|
"trace-1",
|
||||||
|
new GraphQlDeadline(Instant.now().plus(Duration.ofSeconds(5))));
|
||||||
|
}
|
||||||
|
}
|
||||||
+56
@@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
import dev.caskeleton.adapter.inbound.graphql.advanced.subscription.GraphQlSubscriptionCancellation;
|
import dev.caskeleton.adapter.inbound.graphql.advanced.subscription.GraphQlSubscriptionCancellation;
|
||||||
|
import dev.caskeleton.adapter.inbound.graphql.execution.GraphQlCancellation;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -63,6 +64,61 @@ class GraphQlCancellationAggregationTest {
|
|||||||
assertThat(suppressed).isInstanceOf(IllegalStateException.class)));
|
assertThat(suppressed).isInstanceOf(IllegalStateException.class)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aFailingRequestListenerNeverStopsTheOtherWorkFromStopping() {
|
||||||
|
List<String> stopped = new ArrayList<>();
|
||||||
|
var cancellation = GraphQlCancellation.create();
|
||||||
|
cancellation.onCancel(() -> stopped.add("statement"));
|
||||||
|
cancellation.onCancel(
|
||||||
|
() -> {
|
||||||
|
stopped.add("downstream-call");
|
||||||
|
throw new IllegalStateException("the downstream client refused to close");
|
||||||
|
});
|
||||||
|
cancellation.onCancel(() -> stopped.add("publisher"));
|
||||||
|
|
||||||
|
assertThatThrownBy(cancellation::cancel).isInstanceOf(IllegalStateException.class);
|
||||||
|
|
||||||
|
assertThat(stopped)
|
||||||
|
.as("a deadline that reaches only the first listener is not a cancellation")
|
||||||
|
.containsExactlyInAnyOrder("statement", "downstream-call", "publisher");
|
||||||
|
assertThat(cancellation.cancelled()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aRequestListenerFailingLateIsAttachedToTheFirstFailure() {
|
||||||
|
var cancellation = GraphQlCancellation.create();
|
||||||
|
cancellation.onCancel(
|
||||||
|
() -> {
|
||||||
|
throw new IllegalStateException("registered-first");
|
||||||
|
});
|
||||||
|
cancellation.onCancel(
|
||||||
|
() -> {
|
||||||
|
throw new IllegalArgumentException("registered-second");
|
||||||
|
});
|
||||||
|
|
||||||
|
assertThatThrownBy(cancellation::cancel)
|
||||||
|
.isInstanceOf(IllegalStateException.class)
|
||||||
|
.satisfies(
|
||||||
|
thrown ->
|
||||||
|
assertThat(thrown.getSuppressed())
|
||||||
|
.hasSize(1)
|
||||||
|
.allSatisfy(
|
||||||
|
suppressed ->
|
||||||
|
assertThat(suppressed).isInstanceOf(IllegalArgumentException.class)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aRequestListenerRegisteredAfterCancellationStillRunsExactlyOnce() {
|
||||||
|
List<String> stopped = new ArrayList<>();
|
||||||
|
var cancellation = GraphQlCancellation.create();
|
||||||
|
cancellation.cancel();
|
||||||
|
|
||||||
|
cancellation.onCancel(() -> stopped.add("late"));
|
||||||
|
cancellation.cancel();
|
||||||
|
|
||||||
|
assertThat(stopped).containsExactly("late");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void aFailingUpstreamHookNeverStopsTheOthersFromStopping() {
|
void aFailingUpstreamHookNeverStopsTheOthersFromStopping() {
|
||||||
List<String> stopped = new ArrayList<>();
|
List<String> stopped = new ArrayList<>();
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
// catalog, so the grpc-bom + protobuf-bom platforms are imported HERE (module scope) using the root
|
// catalog, so the grpc-bom + protobuf-bom platforms are imported HERE (module scope) using the root
|
||||||
// `ext.grpcVersion` / `ext.protobufVersion` SSOT — this keeps the strict-locking blast radius to
|
// `ext.grpcVersion` / `ext.protobufVersion` SSOT — this keeps the strict-locking blast radius to
|
||||||
// this module (the shared root dependencyManagement block stays io.grpc-free).
|
// this module (the shared root dependencyManagement block stays io.grpc-free).
|
||||||
apply from: "${rootProject.projectDir}/gradle/strict-qualification-test.gradle"
|
|
||||||
|
|
||||||
dependencyManagement {
|
dependencyManagement {
|
||||||
imports {
|
imports {
|
||||||
|
|||||||
@@ -27,46 +27,37 @@ dependencies {
|
|||||||
testImplementation 'io.projectreactor:reactor-test'
|
testImplementation 'io.projectreactor:reactor-test'
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('jpaPersistenceRedactionContractTest', Test) {
|
|
||||||
group = 'verification'
|
|
||||||
description = 'Runs the exact persistence error log/trace redaction contract used by JPA evidence.'
|
|
||||||
testClassesDirs = sourceSets.test.output.classesDirs
|
|
||||||
classpath = sourceSets.test.runtimeClasspath
|
|
||||||
useJUnitPlatform()
|
|
||||||
filter {
|
|
||||||
includeTestsMatching(
|
|
||||||
'dev.caskeleton.adapter.inbound.web.error.GlobalExceptionHandlerTest.persistenceFailureObservabilityDoesNotCarryRawDatabaseDetails')
|
|
||||||
includeTestsMatching(
|
|
||||||
'dev.caskeleton.adapter.inbound.web.error.SpanErrorRecorderHookTest.persistenceFailureHandlerRecordsSanitizedExceptionWithClassifiedCode')
|
|
||||||
}
|
|
||||||
failOnNoDiscoveredTests = true
|
|
||||||
outputs.upToDateWhen { false }
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named('test') {
|
tasks.named('test') {
|
||||||
useJUnitPlatform {
|
useJUnitPlatform {
|
||||||
excludeTags 'security-boundary'
|
excludeTags 'security-boundary'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('webSecurityBoundaryTest', Test) {
|
strictTestLanes {
|
||||||
group = 'verification'
|
lane('jpaPersistenceRedactionContractTest') {
|
||||||
description = 'Runs hermetic JWT/JWKS and CORS filter-boundary contracts with no skips.'
|
description = 'Runs the exact persistence error log/trace redaction contract used by JPA evidence.'
|
||||||
testClassesDirs = sourceSets.test.output.classesDirs
|
// Named rather than tagged. This lane is JPA evidence's proof that a database failure never
|
||||||
classpath = sourceSets.test.runtimeClasspath
|
// reaches a log line or a span, and it must stay exactly these two contracts — a tag would
|
||||||
useJUnitPlatform {
|
// let a later test opt itself in and change what the evidence covers.
|
||||||
includeTags 'security-boundary'
|
requires(
|
||||||
|
'dev.caskeleton.adapter.inbound.web.error.GlobalExceptionHandlerTest.persistenceFailureObservabilityDoesNotCarryRawDatabaseDetails',
|
||||||
|
'dev.caskeleton.adapter.inbound.web.error.SpanErrorRecorderHookTest.persistenceFailureHandlerRecordsSanitizedExceptionWithClassifiedCode')
|
||||||
}
|
}
|
||||||
failOnNoDiscoveredTests = true
|
|
||||||
outputs.upToDateWhen { false }
|
lane('webSecurityBoundaryTest') {
|
||||||
shouldRunAfter tasks.named('test')
|
tag = 'security-boundary'
|
||||||
jvmArgs '-Duser.timezone=UTC'
|
description = 'Runs hermetic JWT/JWKS and CORS filter-boundary contracts with no skips.'
|
||||||
afterSuite { descriptor, result ->
|
customize = { test ->
|
||||||
|
test.shouldRunAfter test.project.tasks.named('test')
|
||||||
|
test.jvmArgs '-Duser.timezone=UTC'
|
||||||
|
test.afterSuite { descriptor, result ->
|
||||||
if (descriptor.parent == null && result.skippedTestCount > 0) {
|
if (descriptor.parent == null && result.skippedTestCount > 0) {
|
||||||
throw new GradleException(
|
throw new GradleException(
|
||||||
"webSecurityBoundaryTest forbids skipped tests: ${result.skippedTestCount}")
|
"webSecurityBoundaryTest forbids skipped tests: ${result.skippedTestCount}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('check') {
|
tasks.named('check') {
|
||||||
|
|||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
package dev.caskeleton.adapter.inbound.web.notification.platform.callback;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The two beans the callback endpoints need, and neither of which existed.
|
||||||
|
*
|
||||||
|
* <p>{@code NotificationCallbackMvcController}, {@code NotificationCallbackWebFluxHandler} and
|
||||||
|
* {@code CallbackWebFluxConfiguration} all take {@code CallbackRequestFactory} as a constructor
|
||||||
|
* argument, and it was produced nowhere in production code — the only instantiation in the
|
||||||
|
* repository was inside a test. So the documented, env-registered switch {@code
|
||||||
|
* APP_NOTIFICATION_PLATFORM_CALLBACKS_ENABLED=true} did not enable callbacks; it made the
|
||||||
|
* application fail to start on an unsatisfied dependency. An operator following the configuration
|
||||||
|
* reference got a deployment that would not boot.
|
||||||
|
*
|
||||||
|
* <p>Conditioned on the same switch as the controller, so a deployment that leaves callbacks off
|
||||||
|
* carries no URL resolver and no clock binding for them.
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
prefix = "ca-skeleton.notification.platform.callbacks",
|
||||||
|
name = "enabled",
|
||||||
|
havingValue = "true")
|
||||||
|
public class CallbackRequestConfiguration {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the URL a provider actually called.
|
||||||
|
*
|
||||||
|
* <p>The trusted-proxy set is empty by default, and that default is the safe one rather than the
|
||||||
|
* convenient one: with no entry, forwarded headers are never honoured and the resolver uses what
|
||||||
|
* the container observed. Honouring them unconditionally would let any caller choose the URL that
|
||||||
|
* gets signature-verified, which defeats the signature. A deployment behind a load balancer names
|
||||||
|
* its proxies explicitly.
|
||||||
|
*
|
||||||
|
* @param trustedProxies peers whose forwarded headers may be believed
|
||||||
|
* @return the resolver
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(ExternalRequestUrlResolver.class)
|
||||||
|
public ExternalRequestUrlResolver externalRequestUrlResolver(
|
||||||
|
@Value("${ca-skeleton.notification.platform.callbacks.trusted-proxies:}")
|
||||||
|
Set<String> trustedProxies) {
|
||||||
|
return new ExternalRequestUrlResolver(trustedProxies);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the canonical callback request both transports share.
|
||||||
|
*
|
||||||
|
* @param urlResolver the external URL resolver
|
||||||
|
* @param clock the clock timestamps are read from
|
||||||
|
* @return the factory
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(CallbackRequestFactory.class)
|
||||||
|
public CallbackRequestFactory callbackRequestFactory(
|
||||||
|
ExternalRequestUrlResolver urlResolver, Clock clock) {
|
||||||
|
return new CallbackRequestFactory(urlResolver, clock);
|
||||||
|
}
|
||||||
|
}
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
package dev.caskeleton.adapter.inbound.web.notification.platform.callback;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turning callbacks on produces the beans the endpoints need.
|
||||||
|
*
|
||||||
|
* <p>The endpoints already existed and were already conditioned on this switch; what did not exist
|
||||||
|
* was any producer of {@code CallbackRequestFactory}. Every reference to it in production code was
|
||||||
|
* a constructor parameter, so the switch documented in the configuration reference did not enable a
|
||||||
|
* feature — it failed the startup. Nothing caught that because the controller's own test builds the
|
||||||
|
* factory by hand, which is precisely the dependency a running application has to supply.
|
||||||
|
*/
|
||||||
|
class CallbackRequestConfigurationTest {
|
||||||
|
|
||||||
|
private final ApplicationContextRunner runner =
|
||||||
|
new ApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(CallbackRequestConfiguration.class))
|
||||||
|
.withUserConfiguration(ClockConfiguration.class);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("callbacks on supplies both beans the endpoints take")
|
||||||
|
void callbacksOnSuppliesTheBeans() {
|
||||||
|
runner
|
||||||
|
.withPropertyValues("ca-skeleton.notification.platform.callbacks.enabled=true")
|
||||||
|
.run(
|
||||||
|
context ->
|
||||||
|
assertThat(context)
|
||||||
|
.hasNotFailed()
|
||||||
|
.hasSingleBean(ExternalRequestUrlResolver.class)
|
||||||
|
.hasSingleBean(CallbackRequestFactory.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("callbacks off supplies neither, so an off deployment carries nothing")
|
||||||
|
void callbacksOffSuppliesNothing() {
|
||||||
|
runner.run(
|
||||||
|
context ->
|
||||||
|
assertThat(context)
|
||||||
|
.hasNotFailed()
|
||||||
|
.doesNotHaveBean(ExternalRequestUrlResolver.class)
|
||||||
|
.doesNotHaveBean(CallbackRequestFactory.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("no trusted proxy is configured by default")
|
||||||
|
void noTrustedProxyByDefault() {
|
||||||
|
// The safe default rather than the convenient one: with no entry, a forwarded header cannot
|
||||||
|
// choose the URL that gets signature-verified.
|
||||||
|
runner
|
||||||
|
.withPropertyValues("ca-skeleton.notification.platform.callbacks.enabled=true")
|
||||||
|
.run(context -> assertThat(context).hasSingleBean(ExternalRequestUrlResolver.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class ClockConfiguration {
|
||||||
|
@Bean
|
||||||
|
Clock clock() {
|
||||||
|
return Clock.systemUTC();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -278,6 +278,16 @@ class NotificationCallbackMvcControllerTest {
|
|||||||
byProviderRequestId(ProviderProfileId profileId, String providerRequestIdHash) {
|
byProviderRequestId(ProviderProfileId profileId, String providerRequestIdHash) {
|
||||||
return java.util.Optional.empty();
|
return java.util.Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.Optional<
|
||||||
|
dev.caskeleton.application.notification.platform.callback.DeliveryAttemptSnapshot>
|
||||||
|
byProviderRequestIdHash(
|
||||||
|
ProviderProfileId profileId,
|
||||||
|
dev.caskeleton.application.notification.platform.callback.ProviderRequestIdHash
|
||||||
|
providerRequestIdHash) {
|
||||||
|
return java.util.Optional.empty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Never reached: projection runs only after a signature has been accepted. */
|
/** Never reached: projection runs only after a signature has been accepted. */
|
||||||
@@ -397,5 +407,10 @@ class NotificationCallbackMvcControllerTest {
|
|||||||
dev.caskeleton.application.notification.platform.api.DeliveryAttemptId attemptId) {
|
dev.caskeleton.application.notification.platform.api.DeliveryAttemptId attemptId) {
|
||||||
return java.util.List.of();
|
return java.util.List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean bindAttempt(ProviderEventRecordId eventId, DeliveryAttemptId attemptId) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
// io.grpc coordinates the BOM does not manage).
|
// io.grpc coordinates the BOM does not manage).
|
||||||
description = 'Inbound adapter: WebSocket (STOMP over SockJS, skeleton machinery)'
|
description = 'Inbound adapter: WebSocket (STOMP over SockJS, skeleton machinery)'
|
||||||
|
|
||||||
apply from: "${rootProject.projectDir}/gradle/strict-qualification-test.gradle"
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':domain-core')
|
implementation project(':domain-core')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Redis SDK leaf — see docs/superpowers/specs/2026-08-07-redis-wrapper-typed-api-design.md.
|
// Redis SDK leaf — see docs/superpowers/specs/2026-08-07-redis-wrapper-typed-api-design.md.
|
||||||
//
|
//
|
||||||
// The design models the SDK as separate Gradle modules. This repository's fail-closed 19-leaf
|
// The design models the SDK as separate Gradle modules. This repository's fail-closed module
|
||||||
// registry outranks that layout, so the module boundaries are packages under
|
// registry outranks that layout, so the module boundaries are packages under
|
||||||
// dev.caskeleton.adapter.outbound.cache.redis.sdk and RedisSdkModuleBoundaryTest enforces them.
|
// dev.caskeleton.adapter.outbound.cache.redis.sdk and RedisSdkModuleBoundaryTest enforces them.
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// docs/httpclient/repository-adaptation.md (how the design's 19 library modules map here).
|
// docs/httpclient/repository-adaptation.md (how the design's 19 library modules map here).
|
||||||
//
|
//
|
||||||
// The design models the platform as 19 separate Gradle modules. This repository's fail-closed
|
// The design models the platform as 19 separate Gradle modules. This repository's fail-closed
|
||||||
// 19-leaf registry (src/config/architecture/modules.json) outranks that layout, so the module
|
// module registry (src/config/architecture/modules.json) outranks that layout, so the module
|
||||||
// boundaries are packages under dev.caskeleton.adapter.outbound.httpclient and
|
// boundaries are packages under dev.caskeleton.adapter.outbound.httpclient and
|
||||||
// HttpClientModuleBoundaryTest enforces the design's module dependency table.
|
// HttpClientModuleBoundaryTest enforces the design's module dependency table.
|
||||||
description = 'Outbound adapter: HTTP client platform (typed clients, profiles, evidence-based retry)'
|
description = 'Outbound adapter: HTTP client platform (typed clients, profiles, evidence-based retry)'
|
||||||
@@ -73,33 +73,12 @@ dependencies {
|
|||||||
// Performance certification and JMH benchmarks are separate source sets for their own reason: they
|
// Performance certification and JMH benchmarks are separate source sets for their own reason: they
|
||||||
// are slow, they assert on resource bounds rather than behaviour, and they must never be part of
|
// are slow, they assert on resource bounds rather than behaviour, and they must never be part of
|
||||||
// the default unit lane.
|
// the default unit lane.
|
||||||
sourceSets {
|
strictTestLanes {
|
||||||
testkit {
|
// The testkit compiles against exactly what a test does: `implementation` inheritance runs
|
||||||
java.srcDir 'src/testkit/java'
|
// through testImplementation, so this is the module's own dependencies plus the test libraries.
|
||||||
compileClasspath += sourceSets.main.output
|
sourceSet('testkit') { compilesAgainst 'main' }
|
||||||
runtimeClasspath += output + compileClasspath
|
sourceSet('httpClientPerformanceTest') { compilesAgainst 'main', 'testkit' }
|
||||||
}
|
sourceSet('jmh') { compilesAgainst 'main', 'testkit' }
|
||||||
httpClientPerformanceTest {
|
|
||||||
java.srcDir 'src/httpClientPerformanceTest/java'
|
|
||||||
compileClasspath += sourceSets.main.output + sourceSets.testkit.output
|
|
||||||
runtimeClasspath += output + compileClasspath
|
|
||||||
}
|
|
||||||
jmh {
|
|
||||||
java.srcDir 'src/jmh/java'
|
|
||||||
compileClasspath += sourceSets.main.output + sourceSets.testkit.output
|
|
||||||
runtimeClasspath += output + compileClasspath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations {
|
|
||||||
// The testkit compiles against exactly what a test does: testImplementation already extends
|
|
||||||
// implementation, so this is the module's own dependencies plus the test libraries.
|
|
||||||
testkitImplementation.extendsFrom testImplementation
|
|
||||||
testkitRuntimeOnly.extendsFrom testRuntimeOnly
|
|
||||||
httpClientPerformanceTestImplementation.extendsFrom testImplementation
|
|
||||||
httpClientPerformanceTestRuntimeOnly.extendsFrom testRuntimeOnly
|
|
||||||
jmhImplementation.extendsFrom testImplementation
|
|
||||||
jmhRuntimeOnly.extendsFrom testRuntimeOnly
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every test lane compiles and runs against the testkit.
|
// Every test lane compiles and runs against the testkit.
|
||||||
@@ -166,70 +145,59 @@ tasks.named('test', Test) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('httpClientBlockHoundTest', Test) {
|
// Four tag-selected lanes, declared rather than assembled.
|
||||||
group = 'verification'
|
//
|
||||||
|
// Two of them — the stable contract suite and the security suite — did not carry
|
||||||
|
// failOnNoDiscoveredTests at all. Five lanes were written by copying the block above, and the
|
||||||
|
// property that makes a lane mean anything was lost on two of the copies, so the cross-transport
|
||||||
|
// contract suite and the SSRF/credential-leak suite would each have reported success on discovering
|
||||||
|
// nothing. Declaring the lanes removes the opportunity: the convention has no opt-out.
|
||||||
|
strictTestLanes {
|
||||||
|
lane('httpClientBlockHoundTest') {
|
||||||
|
tag = 'httpclient-blockhound'
|
||||||
description = 'Proves no platform code blocks a Reactor event loop (design §18.2, §28.6).'
|
description = 'Proves no platform code blocks a Reactor event loop (design §18.2, §28.6).'
|
||||||
testClassesDirs = sourceSets.test.output.classesDirs
|
customize = { test ->
|
||||||
classpath = sourceSets.test.runtimeClasspath
|
applyContractSelection(test)
|
||||||
useJUnitPlatform { includeTags 'httpclient-blockhound' }
|
|
||||||
applyContractSelection(it)
|
|
||||||
// BlockHound instruments already-loaded JDK classes; Java 13+ needs this to redefine them.
|
// BlockHound instruments already-loaded JDK classes; Java 13+ needs this to redefine them.
|
||||||
jvmArgs '-XX:+AllowRedefinitionToAddDeleteMethods'
|
test.jvmArgs '-XX:+AllowRedefinitionToAddDeleteMethods'
|
||||||
// The lane exists to run BlockHound. Discovering nothing means it did not, which is a failure.
|
}
|
||||||
failOnNoDiscoveredTests = true
|
}
|
||||||
outputs.upToDateWhen { false }
|
lane('httpClientStableContractTest') {
|
||||||
}
|
tag = 'httpclient-contract'
|
||||||
|
|
||||||
tasks.register('httpClientStableContractTest', Test) {
|
|
||||||
group = 'verification'
|
|
||||||
description = 'Runs the cross-transport stable contract suite (design §28.2, §33).'
|
description = 'Runs the cross-transport stable contract suite (design §28.2, §33).'
|
||||||
testClassesDirs = sourceSets.test.output.classesDirs
|
customize = { test -> applyContractSelection(test) }
|
||||||
classpath = sourceSets.test.runtimeClasspath
|
}
|
||||||
useJUnitPlatform { includeTags 'httpclient-contract' }
|
lane('httpClientSecurityTest') {
|
||||||
applyContractSelection(it)
|
tag = 'httpclient-security'
|
||||||
outputs.upToDateWhen { false }
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('httpClientSecurityTest', Test) {
|
|
||||||
group = 'verification'
|
|
||||||
description = 'Runs the SSRF, credential-leak, and cardinality suite (design §28.5, §28.7).'
|
description = 'Runs the SSRF, credential-leak, and cardinality suite (design §28.5, §28.7).'
|
||||||
testClassesDirs = sourceSets.test.output.classesDirs
|
customize = { test -> applyContractSelection(test) }
|
||||||
classpath = sourceSets.test.runtimeClasspath
|
}
|
||||||
useJUnitPlatform { includeTags 'httpclient-security' }
|
// Its own source set rather than a tag, so the source set is the selection.
|
||||||
applyContractSelection(it)
|
lane('httpClientPerformanceTest') {
|
||||||
outputs.upToDateWhen { false }
|
sourceSet = 'httpClientPerformanceTest'
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('httpClientFailureInjectionTest', Test) {
|
|
||||||
group = 'verification'
|
|
||||||
description = 'Runs the Toxiproxy fault-injection suite; fails closed without Docker (design §28.3).'
|
|
||||||
testClassesDirs = sourceSets.test.output.classesDirs
|
|
||||||
classpath = sourceSets.test.runtimeClasspath
|
|
||||||
useJUnitPlatform { includeTags 'httpclient-fault' }
|
|
||||||
applyContractSelection(it)
|
|
||||||
// The upstream image is mutable by default. Passing a digest here is what makes a red fault
|
|
||||||
// run attributable to this repository rather than to someone else's image push.
|
|
||||||
systemProperty 'httpclient.fault.httpbin.image',
|
|
||||||
(project.findProperty('httpclient.fault.httpbin.image') ?: 'kennethreitz/httpbin:latest').toString()
|
|
||||||
// A fault suite that never injected a fault must not report success, so a selected lane with no
|
|
||||||
// discovered test is an error rather than an empty pass.
|
|
||||||
failOnNoDiscoveredTests = true
|
|
||||||
outputs.upToDateWhen { false }
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('httpClientPerformanceTest', Test) {
|
|
||||||
group = 'verification'
|
|
||||||
description = 'Certifies pool, streaming, retry, and rotation resource bounds (design §28.8).'
|
description = 'Certifies pool, streaming, retry, and rotation resource bounds (design §28.8).'
|
||||||
testClassesDirs = sourceSets.httpClientPerformanceTest.output.classesDirs
|
customize = { test ->
|
||||||
classpath = sourceSets.httpClientPerformanceTest.runtimeClasspath
|
applyContractSelection(test)
|
||||||
useJUnitPlatform()
|
test.systemProperty 'performance.assertions.enabled',
|
||||||
applyContractSelection(it)
|
|
||||||
systemProperty 'performance.assertions.enabled',
|
|
||||||
(project.findProperty('performance.assertions.enabled') ?: 'false').toString()
|
(project.findProperty('performance.assertions.enabled') ?: 'false').toString()
|
||||||
failOnNoDiscoveredTests = true
|
}
|
||||||
outputs.upToDateWhen { false }
|
}
|
||||||
|
lane('httpClientFailureInjectionTest') {
|
||||||
|
tag = 'httpclient-fault'
|
||||||
|
description = 'Runs the Toxiproxy fault-injection suite; fails closed without Docker ' +
|
||||||
|
'(design §28.3).'
|
||||||
|
customize = { test ->
|
||||||
|
applyContractSelection(test)
|
||||||
|
// The upstream image is mutable by default. Passing a digest here is what makes a red
|
||||||
|
// fault run attributable to this repository rather than to someone else's image push.
|
||||||
|
test.systemProperty 'httpclient.fault.httpbin.image',
|
||||||
|
(project.findProperty('httpclient.fault.httpbin.image')
|
||||||
|
?: 'kennethreitz/httpbin:latest').toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
tasks.register('jmh', JavaExec) {
|
tasks.register('jmh', JavaExec) {
|
||||||
group = 'verification'
|
group = 'verification'
|
||||||
description = 'Runs the JMH benchmarks for the blocking and reactive clients (design §28.8).'
|
description = 'Runs the JMH benchmarks for the blocking and reactive clients (design §28.8).'
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user