refactor: 빌드 로직 개선, gradle 파일 경량화
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
# JPA Evidence Gradle Model Decoupling Design
|
||||
|
||||
## Context
|
||||
|
||||
`GenerateJpaEvidenceManifestsTask` currently performs evidence generation after its producer tasks run. Its semantic contract is useful, but the task action reaches back into the live Gradle model through `getProject()`, resolves configurations, locates `Task` instances, reads `Test` report locations, inspects `TaskState`, and reads root extra properties.
|
||||
|
||||
Gradle 9 deprecates `Task.project` access at execution time and Gradle 10 will reject it. More importantly, the current task mixes two responsibilities:
|
||||
|
||||
1. Gradle configuration/model discovery.
|
||||
2. Pure evidence assembly from producer results.
|
||||
|
||||
The refactor must separate those concerns without weakening evidence claims.
|
||||
|
||||
## Goals
|
||||
|
||||
- Preserve the current readiness-card and evidence-manifest semantics.
|
||||
- Remove execution-time `Project`, `Task`, and `TaskState` access from `GenerateJpaEvidenceManifestsTask`.
|
||||
- Preserve JUnit XML as the source of truth for test execution evidence.
|
||||
- Preserve successful non-Test task execution as the source of truth for `task-claims` such as architecture/configuration claims.
|
||||
- Represent generator inputs with typed Gradle properties rather than hidden project lookups.
|
||||
- Keep producer task names and readiness-card schema unchanged.
|
||||
- Remain compatible with `--warning-mode=fail` on Gradle 9 and prepare the evidence lane for Gradle 10.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Do not redesign the readiness-card schema.
|
||||
- Do not change evidence grades, prerequisite semantics, content hashing, R1/R2 rules, or output layout.
|
||||
- Do not introduce marker files into every producer task.
|
||||
- Do not move release orchestration into the persistence-JPA leaf.
|
||||
- Do not add new runtime dependencies to application modules.
|
||||
|
||||
## Architecture
|
||||
|
||||
### 1. Build service owns task completion outcomes
|
||||
|
||||
Introduce `JpaEvidenceExecutionService`, a Gradle shared build service implementing `OperationCompletionListener`.
|
||||
|
||||
The plugin registers it through `BuildEventsListenerRegistry.onTaskCompletion(...)` so the service receives `TaskFinishEvent` events without the generator querying `TaskState`.
|
||||
|
||||
The service stores a thread-safe typed outcome for each task path:
|
||||
|
||||
```text
|
||||
Task path
|
||||
-> SUCCESS
|
||||
-> FAILED
|
||||
-> SKIPPED
|
||||
```
|
||||
|
||||
Only `SUCCESS` satisfies an evidence `task-claim`. Failed or skipped producers do not cover the claim.
|
||||
|
||||
The service is build-scoped and contains no `Project` reference.
|
||||
|
||||
### 2. Test evidence remains file-based
|
||||
|
||||
JUnit evidence already has a durable output: Gradle's JUnit XML result directory. The plugin resolves every readiness/support `Test` task during configuration and supplies a typed mapping:
|
||||
|
||||
```text
|
||||
absolute task path -> JUnit XML result directory
|
||||
```
|
||||
|
||||
The generator reads those directories directly with `JUnitEvidenceReader`; it never locates a `Test` object.
|
||||
|
||||
Non-Test support tasks continue to participate in the task graph but do not produce JUnit evidence.
|
||||
|
||||
### 3. Configuration-derived values become task inputs
|
||||
|
||||
The plugin supplies these inputs before execution:
|
||||
|
||||
- evidence profile
|
||||
- CI job
|
||||
- artifact location
|
||||
- topology
|
||||
- PostgreSQL image
|
||||
- source revision
|
||||
- traceable version
|
||||
- resolved PostgreSQL JDBC version
|
||||
- resolved Hibernate ORM version
|
||||
- resolved Flyway version
|
||||
- repository-relative evidence output location used by the candidate default
|
||||
- JUnit result-directory mapping
|
||||
|
||||
The generator reads only its properties/files plus the execution service.
|
||||
|
||||
`releaseProvenance` is the preferred source for revision/version. The existing extra-property compatibility bridge is no longer read by the generator.
|
||||
|
||||
### 4. Dependency-version discovery stays in plugin configuration
|
||||
|
||||
The JPA evidence plugin owns the Gradle `Configuration` object. It derives the three relevant resolved module versions and writes them into typed task properties before the generator executes.
|
||||
|
||||
This keeps dependency-graph access out of the task action. The existing coordinates remain unchanged:
|
||||
|
||||
- `org.postgresql:postgresql`
|
||||
- `org.hibernate.orm:hibernate-core`
|
||||
- `org.flywaydb:flyway-core`
|
||||
|
||||
### 5. Generator becomes an evidence assembler
|
||||
|
||||
The generator task action may use:
|
||||
|
||||
- its declared Gradle properties/files
|
||||
- `ExecOperations` for git/docker commands already owned by the task
|
||||
- `FileSystemOperations`
|
||||
- `JpaEvidenceExecutionService`
|
||||
- pure parser/verifier/helper classes
|
||||
|
||||
It must not call:
|
||||
|
||||
```java
|
||||
getProject()
|
||||
Project.findProject(...)
|
||||
Task.getState()
|
||||
TaskContainer.findByName(...)
|
||||
ConfigurationContainer.getByName(...)
|
||||
ExtraPropertiesExtension.get(...)
|
||||
```
|
||||
|
||||
### 6. Evidence semantics
|
||||
|
||||
For a readiness card:
|
||||
|
||||
- `evidence.scenarios` are covered only by selectors found in JUnit XML.
|
||||
- `evidence.task-claims` are covered only when the build service reports the named task completed successfully in the current build.
|
||||
- `no-skip` remains based on JUnit result counts.
|
||||
- prerequisite manifest ordering and hashing remain unchanged.
|
||||
- candidate/R2 blockers remain unchanged.
|
||||
|
||||
The primary foundation card still obtains architecture/configuration coverage from successful execution of its declared producer tasks; the mechanism changes from `TaskState` lookup to task-finish events, not the meaning.
|
||||
|
||||
## Error handling
|
||||
|
||||
- A readiness task expected to produce JUnit evidence but missing from the configured result mapping is a hard failure.
|
||||
- A configured JUnit result directory that contains no usable result remains subject to the existing JUnit evidence validation.
|
||||
- A task claim with no successful completion event is simply uncovered and therefore becomes missing required evidence when that claim is required.
|
||||
- Unsupported evidence profile remains a hard failure.
|
||||
- Missing immutable image digest/dependency versions retain the existing blocker behavior.
|
||||
|
||||
## Testing
|
||||
|
||||
1. Unit-test task-event classification in `JpaEvidenceExecutionService`.
|
||||
2. Unit-test pure JUnit result lookup from configured task-path/directory inputs.
|
||||
3. TestKit: apply `ca.jpa-evidence` in a fixture and verify the generator task exposes typed inputs without execution-time project lookup.
|
||||
4. Existing JPA evidence verifier tests must remain green.
|
||||
5. Run `build-tools:check --warning-mode=fail`.
|
||||
6. Run `verifyJpaReadinessRegistry verifyJpaReleaseGateTasks --warning-mode=fail`.
|
||||
7. Run the affected JPA leaf `check`.
|
||||
8. Run a candidate evidence lane far enough to confirm no `Task.project` deprecation is emitted; environment-dependent Docker/Testcontainers failure may be reported separately from Gradle-model warnings.
|
||||
|
||||
## Migration boundary
|
||||
|
||||
This change only decouples evidence generation from the live Gradle model. It does not alter the readiness registry, producer tasks, JUnit test suites, manifest schema, release workflow, or evidence verification policy.
|
||||
@@ -0,0 +1,94 @@
|
||||
# Messaging Platform Bridge Design
|
||||
|
||||
## Goal
|
||||
|
||||
Replace the application-specific broker seam with one canonical anti-corruption bridge:
|
||||
|
||||
```
|
||||
application-core IntegrationEventPublishPort
|
||||
-> adapter/outbound/messaging/platformbridge
|
||||
-> messaging-schema-api EncodedMessagePublisher
|
||||
-> messaging-runtime-core DefaultMessagePublisher
|
||||
-> messaging transport/runtime
|
||||
```
|
||||
|
||||
The bridge must preserve canonical event identity and exact encoded bytes while reusing the platform's destination resolution, authorization, admission, runtime leasing, transport normalization, and observation pipeline.
|
||||
|
||||
## Scope
|
||||
|
||||
This phase introduces and verifies the canonical bridge. It does **not** migrate the legacy outbox storage/relay rows, because `OutboxEvent` does not retain the schema/order/tenant metadata required to reconstruct `ValidatedIntegrationEvent` without invention.
|
||||
|
||||
## Application boundary
|
||||
|
||||
Create `IntegrationEventPublishPort` in `application-core`.
|
||||
|
||||
Signature:
|
||||
|
||||
```java
|
||||
CompletionStage<OutboxPublishOutcome> publish(ValidatedIntegrationEvent event);
|
||||
```
|
||||
|
||||
The application package depends only on its own canonical event model and application outcome vocabulary.
|
||||
|
||||
## Adapter bridge
|
||||
|
||||
`PlatformIntegrationEventPublishAdapter` lives under:
|
||||
|
||||
```
|
||||
adapter/outbound/messaging/platformbridge
|
||||
```
|
||||
|
||||
It depends on `EncodedMessagePublisher`, never on a concrete broker client, runtime-core implementation, or transport SPI.
|
||||
|
||||
Mapping rules:
|
||||
|
||||
- `logicalDestinationId` -> platform `DestinationName`.
|
||||
- `contractId` -> platform `MessageType`.
|
||||
- `payloadVersion` -> `SchemaVersion`.
|
||||
- event and causation identities must parse as UUIDv7; values are preserved exactly. Incompatible identities fail closed before the platform publisher is called.
|
||||
- `occurredAt` is used for both `producedAt` and `occurredAt` until the application canonical model carries a separate production timestamp. The bridge never invents a new timestamp.
|
||||
- producer is an explicit constructor/configuration value.
|
||||
- correlation, partition key, tenant, aggregate order and exact envelope bytes are preserved.
|
||||
- trace context is explicitly absent (`TraceContext.none()`) until the application model owns canonical trace context.
|
||||
- exact `envelopeBytes` become `EncodedMessage` bytes; no re-encoding occurs.
|
||||
- schema/catalog/binding/envelope evidence that has no first-class platform field is preserved as bounded `x-ca-*` headers.
|
||||
- the schema reference subject is the canonical contract id and version is the canonical payload version.
|
||||
|
||||
## Outcome mapping
|
||||
|
||||
Mapping is based on completion **and transmission evidence**, not enum name similarity:
|
||||
|
||||
- CONFIRMED -> `OutboxPublishOutcome.CONFIRMED`.
|
||||
- AMBIGUOUS -> `OutboxPublishOutcome.AMBIGUOUS`.
|
||||
- REJECTED + NOT_TRANSMITTED -> `REJECTED_BEFORE_SEND`.
|
||||
- REJECTED + any evidence that bytes may have left the process -> `REJECTED_AFTER_BROKER`.
|
||||
|
||||
Bridge preparation failures are definite pre-send rejection.
|
||||
|
||||
## Platform boundary
|
||||
|
||||
`EncodedMessagePublisher` is owned by `messaging-schema-api`, because `EncodedMessage` is owned there and the dependency direction remains acyclic.
|
||||
|
||||
`DefaultMessagePublisher` implements both `MessagePublisher` and `EncodedMessagePublisher`. The encoded path skips only codec lookup/encoding; destination resolution, access policy, admission, runtime lease, transport send, deadline handling, result normalization and observation are shared with the normal publish path.
|
||||
|
||||
The starter exposes one `DefaultMessagePublisher` singleton, which therefore satisfies both public interfaces.
|
||||
|
||||
## Spring ownership
|
||||
|
||||
`MessagingBridgeRootAutoConfiguration` owns the bridge bean when an `EncodedMessagePublisher` is present **and** `app.messaging.producer-id` is explicitly configured. Producer identity is never inferred from `spring.application.name` or invented. Application bootstrap must not construct Kafka producer clients or implement broker-specific send behavior.
|
||||
|
||||
The existing `KafkaSender` / `KafkaMessageBroker` path remains temporarily for the legacy `OutboxEvent` and realtime publishers, which do not yet carry enough canonical metadata to enter the new bridge without invention. It is explicitly transitional and is removed only with the legacy outbox/realtime cutover. The new canonical bridge never calls it.
|
||||
|
||||
## Verification
|
||||
|
||||
Required checks:
|
||||
|
||||
1. `DefaultMessagePublisherTest`: pre-encoded publish preserves bytes and skips codec while still exercising central pipeline.
|
||||
2. `PlatformIntegrationEventPublishAdapterTest`: golden mapping, outcome mapping, fail-closed identity behavior.
|
||||
3. outbound messaging module tests/check.
|
||||
4. messaging runtime/starter tests.
|
||||
5. app-bootstrap system test and architecture test after adding the canonical bridge while retaining the documented legacy seam.
|
||||
6. search proving app-bootstrap has no direct native Kafka sender configuration.
|
||||
7. dependency/build lock refresh only where dependency ownership changed.
|
||||
8. `git diff --check`.
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
# Outbox Transport-Only Cutover Design
|
||||
|
||||
## Status
|
||||
|
||||
Approved implementation slice for MSG-015 transport-only cutover.
|
||||
|
||||
This design deliberately does **not** activate `POLLING_V2` and does not migrate the publication authority to the v2 delivery tables. The existing `outbox_event` writer/store/claim/status authority remains the only active authority. The change makes that legacy authority capable of carrying a canonical integration event without losing the exact platform envelope.
|
||||
|
||||
## Goal
|
||||
|
||||
Support both row generations under one legacy relay authority:
|
||||
|
||||
```text
|
||||
business transaction
|
||||
-> legacy NewOutboxEvent -> legacy row
|
||||
-> canonical ValidatedIntegrationEvent -> canonical-compatible row
|
||||
|
||||
one OutboxStorePort claim authority
|
||||
-> legacy claimed row -> MessageBroker compatibility path
|
||||
-> canonical claimed row -> IntegrationEventPublishPort -> messaging platform
|
||||
```
|
||||
|
||||
A row is published through exactly one branch. There is no dual write and no second relay scheduler.
|
||||
|
||||
## Application boundaries
|
||||
|
||||
### Canonical append
|
||||
|
||||
`OutboxAppendPort` becomes the canonical durable append boundary:
|
||||
|
||||
```java
|
||||
void append(ValidatedIntegrationEvent event);
|
||||
```
|
||||
|
||||
### Legacy append
|
||||
|
||||
Raw R0 payload append moves to an explicitly named compatibility port:
|
||||
|
||||
```java
|
||||
LegacyOutboxAppendPort
|
||||
void append(NewOutboxEvent event);
|
||||
```
|
||||
|
||||
Existing sample/durable-operation code that still emits raw `NewOutboxEvent` uses only the legacy port. New canonical code must not call the legacy port.
|
||||
|
||||
### Claimed row model
|
||||
|
||||
The relay-facing row is a sealed application model:
|
||||
|
||||
```text
|
||||
ClaimedOutboxEvent
|
||||
|- OutboxEvent // legacy R0 claim model retained for compatibility
|
||||
`- CanonicalClaimedOutboxEvent // reconstructs one ValidatedIntegrationEvent
|
||||
```
|
||||
|
||||
`OutboxStorePort.claimBatch` returns `List<ClaimedOutboxEvent>`.
|
||||
|
||||
Common relay state is exposed by the sealed interface: event id, event type, aggregate id, occurred-at, status and attempt count. The canonical subtype also exposes the exact `ValidatedIntegrationEvent`.
|
||||
|
||||
A persisted row with a **partial** canonical metadata set is corrupt and fails closed during mapping. It is never downgraded to the legacy path.
|
||||
|
||||
## Storage compatibility projection
|
||||
|
||||
The existing PostgreSQL `outbox_event` remains authoritative. Add a forward migration after current legacy V12 that:
|
||||
|
||||
- widens `event_id` to `varchar(96)`;
|
||||
- widens `correlation_id` to `varchar(128)`;
|
||||
- adds nullable canonical columns to preserve existing rows;
|
||||
- adds an all-or-none canonical-shape check;
|
||||
- stores exact canonical envelope bytes in `bytea`;
|
||||
- keeps the legacy required columns for the rollback window.
|
||||
|
||||
Canonical required columns:
|
||||
|
||||
```text
|
||||
contract_id
|
||||
envelope_version
|
||||
payload_version
|
||||
logical_destination
|
||||
tenant_scope
|
||||
aggregate_type
|
||||
aggregate_sequence
|
||||
event_index
|
||||
partition_key
|
||||
envelope_bytes
|
||||
content_type
|
||||
schema_set_hash
|
||||
envelope_sha256
|
||||
envelope_schema_hash
|
||||
payload_schema_hash
|
||||
contract_catalog_revision
|
||||
destination_binding_revision
|
||||
```
|
||||
|
||||
`causation_id` is optional by the application contract.
|
||||
|
||||
Existing legacy columns remain populated for canonical rows with this compatibility projection:
|
||||
|
||||
```text
|
||||
event_id = canonical event id
|
||||
aggregate_id = canonical aggregate id
|
||||
event_type = contract id
|
||||
payload = exact envelope bytes decoded as strict UTF-8
|
||||
occurred_at = canonical occurred-at
|
||||
status = PENDING
|
||||
attempt_count = 0
|
||||
next_attempt_at = occurred-at
|
||||
correlation_id = canonical correlation id
|
||||
idempotency_key = event id
|
||||
```
|
||||
|
||||
The canonical encoder currently emits a UTF-8 JSON envelope. The append adapter verifies strict UTF-8 round-trip before storing the compatibility text. Invalid UTF-8 fails the business transaction; replacement characters are forbidden.
|
||||
|
||||
`partitionKeyBytes` is not stored separately because the canonical model already requires it to be exactly the US-ASCII bytes of `partitionKeyText`. The claimed model reconstructs those bytes from the stored canonical text.
|
||||
|
||||
## Persistence adapters
|
||||
|
||||
`OutboxStoreAdapter` remains the legacy claim/status store and implements `LegacyOutboxAppendPort`, not `OutboxAppendPort`.
|
||||
|
||||
A separate `CanonicalOutboxAppendAdapter` implements `OutboxAppendPort`. It participates in the caller's existing write transaction exactly like the legacy adapter and never opens a local transaction.
|
||||
|
||||
Both write the same `outbox_event` table; they are alternative semantic inputs, not dual writers for one business fact.
|
||||
|
||||
## Activation
|
||||
|
||||
Introduce:
|
||||
|
||||
```text
|
||||
ca-skeleton.outbox.canonical-transport-enabled=false
|
||||
```
|
||||
|
||||
Default remains false.
|
||||
|
||||
When false:
|
||||
- existing legacy append/relay behavior is unchanged;
|
||||
- canonical append bean is not exposed;
|
||||
- canonical relay routing is not considered an active deployment capability.
|
||||
|
||||
When true:
|
||||
- canonical append bean is exposed;
|
||||
- startup requires an `IntegrationEventPublishPort`;
|
||||
- the relay publisher can route canonical claimed rows to that port;
|
||||
- legacy rows continue through `MessageBroker`;
|
||||
- while mixed legacy rows may still exist, a relay-enabled deployment still requires the legacy broker. Canonical transport is an additional route, not permission to strand legacy backlog.
|
||||
|
||||
The gate is a compatibility/cutover gate only. It does not change DB publication authority and does not activate `POLLING_V2`.
|
||||
|
||||
## Publish routing
|
||||
|
||||
`OutboxMessagePublishPort` remains the one relay publish port and accepts `ClaimedOutboxEvent`.
|
||||
|
||||
Implementation behavior:
|
||||
- `OutboxEvent` -> existing `OutboxEnvelopeJson` + `MessageBroker`.
|
||||
- `CanonicalClaimedOutboxEvent` -> exact stored `ValidatedIntegrationEvent` -> `IntegrationEventPublishPort`.
|
||||
|
||||
The canonical branch blocks on the returned `CompletionStage` only at this legacy compatibility boundary, because the current legacy relay port is synchronous. The platform result is mapped unchanged into `OutboxPublishOutcome`.
|
||||
|
||||
The bridge does not re-encode canonical bytes.
|
||||
|
||||
If canonical transport is disabled or the canonical publisher is absent, canonical publication fails closed before broker/platform transmission. Startup validation prevents the normal configured case from reaching that state.
|
||||
|
||||
## Outcome policy
|
||||
|
||||
The existing legacy relay state machine remains authoritative in this slice:
|
||||
- CONFIRMED -> mark PUBLISHED.
|
||||
- AMBIGUOUS -> retryable legacy FAILED flow.
|
||||
- REJECTED_BEFORE_SEND / REJECTED_AFTER_BROKER -> existing definite-refusal DEAD behavior.
|
||||
|
||||
This is intentionally the existing compatibility semantics. The richer v2 per-attempt state machine is a later storage-authority cutover.
|
||||
|
||||
## Non-goals
|
||||
|
||||
This slice does not:
|
||||
- switch `OutboxPublicationAuthority` to `POLLING_V2`;
|
||||
- mutate/reconcile `outbox_event_log_v2` or `outbox_delivery_v2`;
|
||||
- implement CDC;
|
||||
- remove `MessageBroker`, `KafkaSender`, `NewOutboxEvent`, `OutboxEvent`, or the legacy scheduler;
|
||||
- migrate old rows into canonical rows;
|
||||
- invent tenant, trace, schema or routing metadata for old rows.
|
||||
|
||||
## Verification
|
||||
|
||||
Required:
|
||||
1. application port split compiles and old raw producers use `LegacyOutboxAppendPort`;
|
||||
2. migration integration proves additive columns, exact BYTEA, constraints and legacy compatibility;
|
||||
3. canonical append adapter round-trips every canonical field and exact bytes;
|
||||
4. partial canonical row mapping fails closed;
|
||||
5. legacy row mapping remains unchanged;
|
||||
6. relay unit test proves canonical row invokes only `IntegrationEventPublishPort`;
|
||||
7. legacy row invokes only `MessageBroker`;
|
||||
8. canonical bytes reaching `PlatformIntegrationEventPublishAdapter` are byte-identical;
|
||||
9. startup rejects canonical transport enabled without `IntegrationEventPublishPort`;
|
||||
10. default-off composition preserves current behavior;
|
||||
11. architecture/dependency checks and `git diff --check` pass.
|
||||
Reference in New Issue
Block a user