merge: integrate JPA production capability
# Conflicts: # .github/ci-gate-matrix.yml # .github/scripts/verify-gate-matrix.sh # .github/workflows/ci-quality-gates.yml # src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
This commit is contained in:
@@ -648,6 +648,615 @@ tasks.register('verifyCleanArchitectureDependencies') {
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> expectedJpaReadinessCardIds = [
|
||||
'jpa-observability-lifecycle',
|
||||
'jpa-security-baseline',
|
||||
'jpa-flyway-migration',
|
||||
'jpa-transaction-runtime',
|
||||
'jpa-aggregate-store',
|
||||
'jpa-query-model',
|
||||
'jpa-primary-foundation',
|
||||
'jpa-idempotency-owner-safe-v2',
|
||||
'jpa-outbox-storage-v2',
|
||||
'jpa-outbox-polling-delivery-v2',
|
||||
'jpa-outbox-cdc-retention-v1',
|
||||
'jpa-inbox-same-store-v1',
|
||||
'jpa-primary-replica',
|
||||
'jpa-tenant-discriminator-rls',
|
||||
'jpa-jdbc-efficiency-coordination'
|
||||
] as Set
|
||||
|
||||
Set<String> expectedJpaOwnedMigrationCardIds = [
|
||||
'jpa-flyway-migration',
|
||||
'jpa-idempotency-owner-safe-v2',
|
||||
'jpa-outbox-storage-v2',
|
||||
'jpa-outbox-polling-delivery-v2',
|
||||
'jpa-inbox-same-store-v1',
|
||||
'jpa-tenant-discriminator-rls',
|
||||
'jpa-jdbc-efficiency-coordination'
|
||||
] as Set
|
||||
|
||||
Closure<List<String>> validateJpaReadinessRegistry = {
|
||||
Map<String, Object> registry,
|
||||
String rawRegistry,
|
||||
Closure<Boolean> taskExists ->
|
||||
List<String> violations = []
|
||||
Set<String> rootKeys = registry.keySet().collect { it as String }.toSet()
|
||||
Set<String> expectedRootKeys = ['schema-version', 'legacy-adoption', 'cards'] as Set
|
||||
if (rootKeys != expectedRootKeys) {
|
||||
violations << "root keys must be exactly ${expectedRootKeys}; got ${rootKeys}"
|
||||
}
|
||||
if (registry['schema-version'] != 1) {
|
||||
violations << "schema-version must be integer 1; got ${registry['schema-version']}"
|
||||
}
|
||||
|
||||
Map<String, Object> legacy = registry['legacy-adoption'] instanceof Map
|
||||
? registry['legacy-adoption'] as Map<String, Object>
|
||||
: [:]
|
||||
Set<String> expectedLegacyKeys = [
|
||||
'state',
|
||||
'location',
|
||||
'history-table',
|
||||
'immutable-applied-versions',
|
||||
'allowed-origin'
|
||||
] as Set
|
||||
if (legacy.keySet().collect { it as String }.toSet() != expectedLegacyKeys) {
|
||||
violations << "legacy-adoption keys must be exactly ${expectedLegacyKeys}"
|
||||
}
|
||||
if (legacy.state != 'transition-only') {
|
||||
violations << "legacy-adoption.state must be transition-only"
|
||||
}
|
||||
if (legacy.location != 'db/migration/postgresql') {
|
||||
violations << "legacy-adoption.location must be db/migration/postgresql"
|
||||
}
|
||||
if (legacy['history-table'] != 'flyway_schema_history') {
|
||||
violations << "legacy-adoption.history-table must be flyway_schema_history"
|
||||
}
|
||||
if (legacy['immutable-applied-versions'] != [1, 3, 4, 5]) {
|
||||
violations << "legacy-adoption immutable versions must be exactly [1, 3, 4, 5]"
|
||||
}
|
||||
if (legacy['allowed-origin'] != 'LEGACY_ADOPTED') {
|
||||
violations << "legacy-adoption.allowed-origin must be LEGACY_ADOPTED"
|
||||
}
|
||||
|
||||
Map<String, Object> cards = registry.cards instanceof Map
|
||||
? registry.cards as Map<String, Object>
|
||||
: [:]
|
||||
Set<String> actualCardIds = cards.keySet().collect { it as String }.toSet()
|
||||
Set<String> missingCards = expectedJpaReadinessCardIds - actualCardIds
|
||||
Set<String> unknownCards = actualCardIds - expectedJpaReadinessCardIds
|
||||
if (!missingCards.isEmpty()) {
|
||||
violations << "missing card ids ${missingCards.toSorted()}"
|
||||
}
|
||||
if (!unknownCards.isEmpty()) {
|
||||
violations << "unknown card ids ${unknownCards.toSorted()}"
|
||||
}
|
||||
|
||||
List<String> rawCardKeys = []
|
||||
def rawCardKeyMatcher = rawRegistry =~ /"(?<card>jpa-[a-z0-9.-]+)"\s*:/
|
||||
while (rawCardKeyMatcher.find()) {
|
||||
rawCardKeys << rawCardKeyMatcher.group('card')
|
||||
}
|
||||
Set<String> duplicateRawCardKeys = rawCardKeys.countBy { it }.findAll {
|
||||
String ignored, Integer count -> count > 1
|
||||
}.keySet()
|
||||
if (!duplicateRawCardKeys.isEmpty()) {
|
||||
violations << "duplicate raw card keys ${duplicateRawCardKeys.toSorted()}"
|
||||
}
|
||||
|
||||
Set<String> allowedCardKeys = [
|
||||
'state',
|
||||
'schema-stream',
|
||||
'prerequisites',
|
||||
'external-prerequisites',
|
||||
'readiness-task',
|
||||
'support-tasks',
|
||||
'required-evidence',
|
||||
'evidence',
|
||||
'dispatch-modes',
|
||||
'migration'
|
||||
] as Set
|
||||
Set<String> allowedStates = ['selected', 'implemented-candidate', 'not-implemented'] as Set
|
||||
Set<String> allowedSchemaStreams = ['none', 'owned', 'contributes-to-core'] as Set
|
||||
Map<String, String> taskOwners = [:]
|
||||
Map<String, String> migrationLocationOwners = [:]
|
||||
Map<String, String> migrationHistoryOwners = [:]
|
||||
Map<String, String> evidenceSelectorOwners = [:]
|
||||
Set<String> actualOwnedMigrationCards = []
|
||||
|
||||
cards.each { String cardId, Object rawCard ->
|
||||
if (!(rawCard instanceof Map)) {
|
||||
violations << "${cardId}: card value must be an object"
|
||||
return
|
||||
}
|
||||
Map<String, Object> card = rawCard as Map<String, Object>
|
||||
Set<String> unknownKeys = card.keySet().collect { it as String }.toSet() - allowedCardKeys
|
||||
if (!unknownKeys.isEmpty()) {
|
||||
violations << "${cardId}: unknown keys ${unknownKeys.toSorted()}"
|
||||
}
|
||||
|
||||
String state = card.state as String
|
||||
String schemaStream = card['schema-stream'] as String
|
||||
if (!allowedStates.contains(state)) {
|
||||
violations << "${cardId}: invalid state '${state}'"
|
||||
}
|
||||
if (!allowedSchemaStreams.contains(schemaStream)) {
|
||||
violations << "${cardId}: invalid schema-stream '${schemaStream}'"
|
||||
}
|
||||
|
||||
if (!(card.prerequisites instanceof List)) {
|
||||
violations << "${cardId}: prerequisites must be a list"
|
||||
}
|
||||
List<String> prerequisites = card.prerequisites instanceof List
|
||||
? (card.prerequisites as List).collect { it as String }
|
||||
: []
|
||||
if (prerequisites.toSet().size() != prerequisites.size()) {
|
||||
violations << "${cardId}: duplicate prerequisites ${prerequisites}"
|
||||
}
|
||||
prerequisites.each { String prerequisite ->
|
||||
if (!cards.containsKey(prerequisite)) {
|
||||
violations << "${cardId}: unknown prerequisite '${prerequisite}'"
|
||||
} else if (state == 'selected' &&
|
||||
((cards[prerequisite] as Map).state as String) != 'selected') {
|
||||
violations << "${cardId}: selected card requires non-selected '${prerequisite}'"
|
||||
}
|
||||
}
|
||||
|
||||
String readinessTask = card['readiness-task'] as String
|
||||
if (readinessTask == null || !readinessTask.startsWith(':')) {
|
||||
violations << "${cardId}: readiness-task must be an absolute Gradle task path"
|
||||
}
|
||||
List<String> supportTasks = card['support-tasks'] instanceof List
|
||||
? (card['support-tasks'] as List).collect { it as String }
|
||||
: []
|
||||
if (supportTasks.toSet().size() != supportTasks.size()) {
|
||||
violations << "${cardId}: duplicate support-tasks ${supportTasks}"
|
||||
}
|
||||
([readinessTask] + supportTasks).findAll { it != null }.each { String taskPath ->
|
||||
if (!taskPath.startsWith(':')) {
|
||||
violations << "${cardId}: task '${taskPath}' must be an absolute Gradle task path"
|
||||
return
|
||||
}
|
||||
String previousOwner = taskOwners.putIfAbsent(taskPath, cardId)
|
||||
if (previousOwner != null) {
|
||||
violations << "duplicate task '${taskPath}' owned by ${previousOwner} and ${cardId}"
|
||||
}
|
||||
if (state == 'selected' && !taskExists(taskPath)) {
|
||||
violations << "${cardId}: selected task does not exist '${taskPath}'"
|
||||
}
|
||||
}
|
||||
|
||||
List<String> requiredEvidence = card['required-evidence'] instanceof List
|
||||
? (card['required-evidence'] as List).collect { it as String }
|
||||
: []
|
||||
if (requiredEvidence.isEmpty()) {
|
||||
violations << "${cardId}: required-evidence must be a non-empty list"
|
||||
} else {
|
||||
if (requiredEvidence.toSet().size() != requiredEvidence.size()) {
|
||||
violations << "${cardId}: duplicate required-evidence ${requiredEvidence}"
|
||||
}
|
||||
if (!requiredEvidence.contains('no-skip')) {
|
||||
violations << "${cardId}: required-evidence must include no-skip"
|
||||
}
|
||||
}
|
||||
|
||||
Object migrationNode = card.migration
|
||||
Set<String> allowedEvidenceClaims = requiredEvidence
|
||||
.findAll { String requirement -> requirement != 'no-skip' }
|
||||
.toSet()
|
||||
Map<String, Object> migrationForEvidence = migrationNode instanceof Map
|
||||
? migrationNode as Map<String, Object>
|
||||
: [:]
|
||||
Object lifecycleEvidenceNode = migrationForEvidence['lifecycle-evidence']
|
||||
if (lifecycleEvidenceNode instanceof List) {
|
||||
(lifecycleEvidenceNode as List).each {
|
||||
Object lifecycle ->
|
||||
allowedEvidenceClaims <<
|
||||
"migration-lifecycle:${lifecycle as String}".toString()
|
||||
}
|
||||
}
|
||||
|
||||
Object evidenceNode = card.evidence
|
||||
if (state == 'not-implemented') {
|
||||
if (evidenceNode != null) {
|
||||
violations << "${cardId}: not-implemented card forbids evidence"
|
||||
}
|
||||
} else if (!(evidenceNode instanceof Map)) {
|
||||
violations << "${cardId}: active card requires evidence"
|
||||
} else {
|
||||
Map<String, Object> evidence = evidenceNode as Map<String, Object>
|
||||
Set<String> evidenceKeys = evidence.keySet().collect { it as String }.toSet()
|
||||
Set<String> expectedEvidenceKeys = ['scenarios', 'task-claims'] as Set
|
||||
if (evidenceKeys != expectedEvidenceKeys) {
|
||||
violations << "${cardId}: evidence keys must be exactly ${expectedEvidenceKeys}"
|
||||
}
|
||||
|
||||
List<Object> scenarios = evidence.scenarios instanceof List
|
||||
? evidence.scenarios as List<Object>
|
||||
: []
|
||||
if (!(evidence.scenarios instanceof List)) {
|
||||
violations << "${cardId}: evidence scenarios must be a list"
|
||||
}
|
||||
List<Object> taskClaims = evidence['task-claims'] instanceof List
|
||||
? evidence['task-claims'] as List<Object>
|
||||
: []
|
||||
if (!(evidence['task-claims'] instanceof List)) {
|
||||
violations << "${cardId}: evidence task-claims must be a list"
|
||||
}
|
||||
if (scenarios.isEmpty() && taskClaims.isEmpty()) {
|
||||
violations << "${cardId}: evidence must contain a scenario or task claim"
|
||||
}
|
||||
|
||||
scenarios.eachWithIndex { Object rawScenario, int index ->
|
||||
if (!(rawScenario instanceof Map)) {
|
||||
violations << "${cardId}: evidence scenario ${index} must be an object"
|
||||
return
|
||||
}
|
||||
Map<String, Object> scenario = rawScenario as Map<String, Object>
|
||||
Set<String> scenarioKeys =
|
||||
scenario.keySet().collect { it as String }.toSet()
|
||||
if (scenarioKeys != ['selector', 'covers'] as Set) {
|
||||
violations << "${cardId}: evidence scenario ${index} has invalid keys ${scenarioKeys}"
|
||||
}
|
||||
String selector = scenario.selector as String
|
||||
if (selector == null ||
|
||||
!(selector ==~ /dev\.caskeleton\.[A-Za-z0-9_.]+\#[A-Za-z][A-Za-z0-9_]*/)) {
|
||||
violations << "${cardId}: invalid evidence selector '${selector}'"
|
||||
} else {
|
||||
String previousOwner = evidenceSelectorOwners.putIfAbsent(selector, cardId)
|
||||
if (previousOwner != null) {
|
||||
violations << "duplicate evidence selector '${selector}' owned by " +
|
||||
"${previousOwner} and ${cardId}"
|
||||
}
|
||||
}
|
||||
List<String> covers = scenario.covers instanceof List
|
||||
? (scenario.covers as List).collect { it as String }
|
||||
: []
|
||||
if (covers.isEmpty()) {
|
||||
violations << "${cardId}: evidence scenario ${index} covers must be non-empty"
|
||||
}
|
||||
if (covers.toSet().size() != covers.size()) {
|
||||
violations << "${cardId}: evidence scenario ${index} has duplicate covers ${covers}"
|
||||
}
|
||||
covers.each { String claim ->
|
||||
if (!allowedEvidenceClaims.contains(claim)) {
|
||||
violations << "${cardId}: evidence covers unknown requirement '${claim}'"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> ownedTasks = ([readinessTask] + supportTasks)
|
||||
.findAll { it != null }
|
||||
.toSet()
|
||||
taskClaims.eachWithIndex { Object rawClaim, int index ->
|
||||
if (!(rawClaim instanceof Map)) {
|
||||
violations << "${cardId}: evidence task claim ${index} must be an object"
|
||||
return
|
||||
}
|
||||
Map<String, Object> claim = rawClaim as Map<String, Object>
|
||||
Set<String> claimKeys = claim.keySet().collect { it as String }.toSet()
|
||||
if (claimKeys != ['task', 'covers'] as Set) {
|
||||
violations << "${cardId}: evidence task claim ${index} has invalid keys ${claimKeys}"
|
||||
}
|
||||
String taskPath = claim.task as String
|
||||
if (!ownedTasks.contains(taskPath)) {
|
||||
violations << "${cardId}: evidence task claim is not owned by card '${taskPath}'"
|
||||
}
|
||||
List<String> covers = claim.covers instanceof List
|
||||
? (claim.covers as List).collect { it as String }
|
||||
: []
|
||||
if (covers.isEmpty()) {
|
||||
violations << "${cardId}: evidence task claim ${index} covers must be non-empty"
|
||||
}
|
||||
if (covers.toSet().size() != covers.size()) {
|
||||
violations << "${cardId}: evidence task claim ${index} has duplicate covers ${covers}"
|
||||
}
|
||||
covers.each { String evidenceClaim ->
|
||||
if (!allowedEvidenceClaims.contains(evidenceClaim)) {
|
||||
violations << "${cardId}: evidence covers unknown requirement '${evidenceClaim}'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (schemaStream == 'owned') {
|
||||
actualOwnedMigrationCards << cardId
|
||||
if (!(migrationNode instanceof Map)) {
|
||||
violations << "${cardId}: owned schema-stream requires migration"
|
||||
}
|
||||
} else if (migrationNode != null) {
|
||||
violations << "${cardId}: schema-stream ${schemaStream} forbids migration"
|
||||
}
|
||||
|
||||
if (migrationNode instanceof Map) {
|
||||
Map<String, Object> migration = migrationNode as Map<String, Object>
|
||||
Set<String> expectedMigrationKeys = [
|
||||
'location',
|
||||
'history-table',
|
||||
'required-core-epoch',
|
||||
'feature-revision',
|
||||
'lifecycle-evidence'
|
||||
] as Set
|
||||
Set<String> migrationKeys = migration.keySet().collect { it as String }.toSet()
|
||||
if (migrationKeys != expectedMigrationKeys) {
|
||||
violations << "${cardId}: migration keys must be exactly ${expectedMigrationKeys}"
|
||||
}
|
||||
|
||||
String location = migration.location as String
|
||||
String historyTable = migration['history-table'] as String
|
||||
if (location == null || !(location ==~ /db\/migration\/jpa\/[a-z0-9-]+/)) {
|
||||
violations << "${cardId}: invalid migration location '${location}'"
|
||||
} else {
|
||||
String previousOwner = migrationLocationOwners.putIfAbsent(location, cardId)
|
||||
if (previousOwner != null) {
|
||||
violations << "duplicate migration location '${location}' for ${previousOwner} and ${cardId}"
|
||||
}
|
||||
}
|
||||
if (historyTable == null || !(historyTable ==~ /flyway_jpa_[a-z0-9_]+_history/)) {
|
||||
violations << "${cardId}: invalid migration history-table '${historyTable}'"
|
||||
} else {
|
||||
String previousOwner = migrationHistoryOwners.putIfAbsent(historyTable, cardId)
|
||||
if (previousOwner != null) {
|
||||
violations << "duplicate migration history-table '${historyTable}' for ${previousOwner} and ${cardId}"
|
||||
}
|
||||
}
|
||||
|
||||
Object coreEpoch = migration['required-core-epoch']
|
||||
Object featureRevision = migration['feature-revision']
|
||||
if (!(coreEpoch instanceof Integer) || (coreEpoch as Integer) < 0) {
|
||||
violations << "${cardId}: required-core-epoch must be a non-negative integer"
|
||||
}
|
||||
if (!(featureRevision instanceof Integer) || (featureRevision as Integer) <= 0) {
|
||||
violations << "${cardId}: feature-revision must be a positive integer"
|
||||
}
|
||||
List<String> lifecycleEvidence = migration['lifecycle-evidence'] instanceof List
|
||||
? (migration['lifecycle-evidence'] as List).collect { it as String }
|
||||
: []
|
||||
if (lifecycleEvidence.isEmpty()) {
|
||||
violations << "${cardId}: lifecycle-evidence must be a non-empty list"
|
||||
} else if (lifecycleEvidence.toSet().size() != lifecycleEvidence.size()) {
|
||||
violations << "${cardId}: duplicate lifecycle-evidence ${lifecycleEvidence}"
|
||||
}
|
||||
}
|
||||
|
||||
if (card['external-prerequisites'] != null) {
|
||||
if (!(card['external-prerequisites'] instanceof List)) {
|
||||
violations << "${cardId}: external-prerequisites must be a list"
|
||||
} else {
|
||||
(card['external-prerequisites'] as List).eachWithIndex {
|
||||
Object rawExternal, int index ->
|
||||
if (!(rawExternal instanceof Map)) {
|
||||
violations << "${cardId}: external prerequisite ${index} must be an object"
|
||||
return
|
||||
}
|
||||
Map<String, Object> external = rawExternal as Map<String, Object>
|
||||
Set<String> externalKeys = external.keySet()
|
||||
.collect { it as String }
|
||||
.toSet()
|
||||
if (externalKeys != ['registry', 'card-id', 'minimum-readiness'] as Set) {
|
||||
violations << "${cardId}: external prerequisite ${index} has invalid keys ${externalKeys}"
|
||||
}
|
||||
if (!((external.registry as String)?.startsWith('src/config/'))) {
|
||||
violations << "${cardId}: external prerequisite ${index} has invalid registry"
|
||||
}
|
||||
if (!((external['card-id'] as String) ==~ /[a-z0-9.-]+/)) {
|
||||
violations << "${cardId}: external prerequisite ${index} has invalid card-id"
|
||||
}
|
||||
if (!((external['minimum-readiness'] as String) ==~ /R[0-3]/)) {
|
||||
violations << "${cardId}: external prerequisite ${index} has invalid minimum-readiness"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (actualOwnedMigrationCards != expectedJpaOwnedMigrationCardIds) {
|
||||
violations << "owned migration cards must be exactly ${expectedJpaOwnedMigrationCardIds}; " +
|
||||
"got ${actualOwnedMigrationCards}"
|
||||
}
|
||||
|
||||
Map<String, Integer> visitState = [:].withDefault { 0 }
|
||||
Closure<Void> visitCard
|
||||
visitCard = { String cardId ->
|
||||
if (visitState[cardId] == 1) {
|
||||
violations << "readiness prerequisite cycle includes '${cardId}'"
|
||||
return
|
||||
}
|
||||
if (visitState[cardId] == 2 || !cards.containsKey(cardId)) {
|
||||
return
|
||||
}
|
||||
visitState[cardId] = 1
|
||||
Map<String, Object> card = cards[cardId] as Map<String, Object>
|
||||
if (card.prerequisites instanceof List) {
|
||||
(card.prerequisites as List).each { Object prerequisite ->
|
||||
visitCard(prerequisite as String)
|
||||
}
|
||||
}
|
||||
visitState[cardId] = 2
|
||||
}
|
||||
cards.keySet().each { Object cardId -> visitCard(cardId as String) }
|
||||
|
||||
boolean pollingSelected =
|
||||
((cards['jpa-outbox-polling-delivery-v2'] as Map)?.state as String) == 'selected'
|
||||
boolean cdcSelected =
|
||||
((cards['jpa-outbox-cdc-retention-v1'] as Map)?.state as String) == 'selected'
|
||||
if (pollingSelected && cdcSelected) {
|
||||
violations << 'polling and CDC outbox delivery cards cannot both be selected'
|
||||
}
|
||||
|
||||
violations
|
||||
}
|
||||
|
||||
Closure<Boolean> jpaTaskExists = { String absoluteTaskPath ->
|
||||
int separator = absoluteTaskPath.lastIndexOf(':')
|
||||
if (separator < 0 || separator == absoluteTaskPath.length() - 1) {
|
||||
return false
|
||||
}
|
||||
String projectPath = separator == 0 ? ':' : absoluteTaskPath.substring(0, separator)
|
||||
String taskName = absoluteTaskPath.substring(separator + 1)
|
||||
Project targetProject = rootProject.findProject(projectPath)
|
||||
targetProject != null && targetProject.tasks.findByName(taskName) != null
|
||||
}
|
||||
|
||||
def verifyJpaReadinessRegistryContract = tasks.register('verifyJpaReadinessRegistryContract') {
|
||||
group = 'verification'
|
||||
description = 'Mutation-tests the fail-closed JPA readiness registry validator.'
|
||||
|
||||
File registryFile = file("${rootProject.projectDir}/config/jpa/readiness-cards.yaml")
|
||||
inputs.file(registryFile)
|
||||
|
||||
doLast {
|
||||
String raw = registryFile.getText('UTF-8')
|
||||
Map<String, Object> baseline = new JsonSlurper().parseText(raw) as Map<String, Object>
|
||||
|
||||
Closure<Map<String, Object>> copyRegistry = {
|
||||
new JsonSlurper().parseText(JsonOutput.toJson(baseline)) as Map<String, Object>
|
||||
}
|
||||
Closure<Void> expectViolation = {
|
||||
String scenario,
|
||||
String expectedText,
|
||||
Closure<Void> mutation,
|
||||
Closure<Boolean> taskExists = { String ignored -> true } ->
|
||||
Map<String, Object> candidate = copyRegistry()
|
||||
mutation(candidate)
|
||||
List<String> candidateViolations = validateJpaReadinessRegistry(
|
||||
candidate,
|
||||
JsonOutput.toJson(candidate),
|
||||
taskExists)
|
||||
if (!candidateViolations.any { String violation ->
|
||||
violation.contains(expectedText)
|
||||
}) {
|
||||
throw new GradleException(
|
||||
"verifyJpaReadinessRegistryContract: scenario '${scenario}' did not " +
|
||||
"produce '${expectedText}'; got ${candidateViolations}")
|
||||
}
|
||||
}
|
||||
|
||||
expectViolation('unknown-card', 'unknown card ids', { Map<String, Object> candidate ->
|
||||
(candidate.cards as Map)['jpa-primary-foundation-alias'] =
|
||||
(candidate.cards as Map)['jpa-primary-foundation']
|
||||
})
|
||||
expectViolation('duplicate-task', 'duplicate task', { Map<String, Object> candidate ->
|
||||
((candidate.cards as Map)['jpa-security-baseline'] as Map)['readiness-task'] =
|
||||
((candidate.cards as Map)['jpa-observability-lifecycle'] as Map)['readiness-task']
|
||||
})
|
||||
expectViolation('missing-prerequisite', 'unknown prerequisite', {
|
||||
Map<String, Object> candidate ->
|
||||
((candidate.cards as Map)['jpa-security-baseline'] as Map).prerequisites =
|
||||
['jpa-does-not-exist']
|
||||
})
|
||||
expectViolation('cycle', 'prerequisite cycle', { Map<String, Object> candidate ->
|
||||
((candidate.cards as Map)['jpa-observability-lifecycle'] as Map).prerequisites =
|
||||
['jpa-security-baseline']
|
||||
})
|
||||
expectViolation('duplicate-location', 'duplicate migration location', {
|
||||
Map<String, Object> candidate ->
|
||||
(((candidate.cards as Map)['jpa-idempotency-owner-safe-v2'] as Map).migration
|
||||
as Map).location = 'db/migration/jpa/core'
|
||||
})
|
||||
expectViolation(
|
||||
'missing-selected-task',
|
||||
'selected task does not exist',
|
||||
{ Map<String, Object> ignored -> },
|
||||
{ String taskPath ->
|
||||
taskPath !=
|
||||
':adapter:outbound:persistence-jpa:postgresqlLifecycleIntegrationTest'
|
||||
})
|
||||
expectViolation('missing-active-evidence', 'active card requires evidence', {
|
||||
Map<String, Object> candidate ->
|
||||
((candidate.cards as Map)['jpa-observability-lifecycle'] as Map)
|
||||
.remove('evidence')
|
||||
})
|
||||
expectViolation('unknown-evidence-requirement', 'evidence covers unknown requirement', {
|
||||
Map<String, Object> candidate ->
|
||||
((candidate.cards as Map)['jpa-observability-lifecycle'] as Map).evidence = [
|
||||
scenarios: [[
|
||||
selector: 'dev.caskeleton.ReadinessTest#startsPostgreSql',
|
||||
covers: ['not-a-card-requirement']
|
||||
]],
|
||||
'task-claims': []
|
||||
]
|
||||
})
|
||||
expectViolation('duplicate-evidence-selector', 'duplicate evidence selector', {
|
||||
Map<String, Object> candidate ->
|
||||
Map<String, Object> card =
|
||||
(candidate.cards as Map)['jpa-observability-lifecycle'] as Map<String, Object>
|
||||
card.evidence = [
|
||||
scenarios: [
|
||||
[
|
||||
selector: 'dev.caskeleton.ReadinessTest#startsPostgreSql',
|
||||
covers: ['real-postgresql']
|
||||
],
|
||||
[
|
||||
selector: 'dev.caskeleton.ReadinessTest#startsPostgreSql',
|
||||
covers: ['lifecycle']
|
||||
]
|
||||
],
|
||||
'task-claims': []
|
||||
]
|
||||
})
|
||||
expectViolation('unknown-evidence-task', 'evidence task claim is not owned by card', {
|
||||
Map<String, Object> candidate ->
|
||||
((candidate.cards as Map)['jpa-primary-foundation'] as Map).evidence = [
|
||||
scenarios: [],
|
||||
'task-claims': [[
|
||||
task: ':test',
|
||||
covers: ['architecture']
|
||||
]]
|
||||
]
|
||||
})
|
||||
|
||||
logger.lifecycle(
|
||||
'verifyJpaReadinessRegistryContract: OK — unknown card, duplicate task, ' +
|
||||
'missing prerequisite, cycle, duplicate migration ownership, missing ' +
|
||||
'selected task, and malformed evidence ownership all fail closed.')
|
||||
}
|
||||
}
|
||||
|
||||
def verifyJpaReadinessRegistry = tasks.register('verifyJpaReadinessRegistry') {
|
||||
group = 'verification'
|
||||
description = 'Validates the JPA readiness card, prerequisite, task, and migration registry.'
|
||||
dependsOn verifyJpaReadinessRegistryContract
|
||||
|
||||
File registryFile = file("${rootProject.projectDir}/config/jpa/readiness-cards.yaml")
|
||||
inputs.file(registryFile)
|
||||
|
||||
doLast {
|
||||
if (!registryFile.isFile()) {
|
||||
throw new GradleException(
|
||||
"verifyJpaReadinessRegistry: missing registry ${registryFile}")
|
||||
}
|
||||
String raw = registryFile.getText('UTF-8')
|
||||
Map<String, Object> registry
|
||||
try {
|
||||
registry = new JsonSlurper().parseText(raw) as Map<String, Object>
|
||||
} catch (RuntimeException ex) {
|
||||
throw new GradleException(
|
||||
"verifyJpaReadinessRegistry: registry is not valid JSON-compatible YAML",
|
||||
ex)
|
||||
}
|
||||
|
||||
List<String> violations =
|
||||
validateJpaReadinessRegistry(registry, raw, jpaTaskExists)
|
||||
if (!violations.isEmpty()) {
|
||||
throw new GradleException(
|
||||
"verifyJpaReadinessRegistry: ${violations.size()} violation(s):\n " +
|
||||
violations.toSorted().join('\n '))
|
||||
}
|
||||
logger.lifecycle(
|
||||
"verifyJpaReadinessRegistry: OK — ${expectedJpaReadinessCardIds.size()} exact " +
|
||||
"cards, ${expectedJpaOwnedMigrationCardIds.size()} owned migration " +
|
||||
'streams, acyclic prerequisites, unique tasks/locations/history tables, ' +
|
||||
'and selected task existence verified.')
|
||||
}
|
||||
}
|
||||
|
||||
configure(subprojects.findAll { it.childProjects.isEmpty() }) {
|
||||
tasks.named('check') {
|
||||
dependsOn verifyJpaReadinessRegistry
|
||||
}
|
||||
}
|
||||
|
||||
def verifyApplicationCoreDependencyPurity = tasks.register('verifyApplicationCoreDependencyPurity') {
|
||||
group = 'verification'
|
||||
description = 'Verifies application-core has only project production dependencies and no diagnostic frameworks on application classpaths.'
|
||||
|
||||
Reference in New Issue
Block a user