You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part of #1029 (linked deployment changes epic — read it first for the big picture). Needs #1031.
Goal
Make the links mean something. The fail-closed gate additionally requires every linked query to be APPROVED or EXECUTED; confirm-execution gains an optional execute_linked_queries=true that
runs the still-APPROVED links in sequence_order through the normal query execution path before
the deployment becomes EXECUTED; the first failed link marks the deployment FAILED and leaves
the rest untouched.
Today DefaultDeploymentGateService.releasable(status, frozen, scheduledFor, now) (:162-168)
is the one pure function, evaluate (:172-192) wraps the freeze lookup fail-closed, and confirmExecution (:101-131) is @Transactional, moves APPROVED → EXECUTED through DeploymentRequestStateService.apply and audits DEPLOYMENT_EXECUTED with trigger=pipeline. DefaultDeploymentSimulationService replays the same function (docs/18:502).
Design constraints agreed on the epic:
deploygov must not import workflow — workflow/internal/DeploymentBreakGlassReviewListener.java:3
already imports deploygov.events, so the reverse arrow is a cycle. Execution is reached
through a new core.api interface that workflow implements (precedent: core.api.SessionRevocationService ← security/internal/DefaultSessionRevocationService.java).
Not a copy of requestgroups/internal/GroupExecutionService.runQuery (:148-179): that
calls proxy.api.QueryExecutor.execute(new QueryExecutionRequest(...)) (:168) on a group item and audits REQUEST_GROUP_EXECUTED (:204-210). A linked item is a query_requests
row, which must move to EXECUTED with QUERY_EXECUTED, results and QueryExecutedEvent —
only DefaultQueryLifecycleService.doExecute (:273-275) does that.
docs/04-api-spec.md — document first (:7775): linked_queries_ready and linked_queries on the gate response, the optional confirm-execution body { "execute_linked_queries": true }, and the new 409 reason.
core/api/LinkedQueryExecutionService.java (new) — QueryStatus executeLinked(UUID queryRequestId, String trigger): runs an APPROVED query as its submitter, returns the
resulting EXECUTED / FAILED; returns the current status untouched when the row is already EXECUTED (idempotent) and throws core.api.QueryRequestNotFoundException when gone. JDK +
project types only.
workflow/internal/DefaultLinkedQueryExecutionService.java (new) — delegates to DefaultQueryLifecycleService.doExecute(snapshot, snapshot.submittedByUserId(), trigger, false, AuditAction.QUERY_EXECUTED), exactly as executeScheduled (:166-175) does with "scheduled"; doExecute is private today — widen it to package-private or expose a small
package-private entry on the lifecycle service rather than duplicating the body.
Gate — releasable(QueryStatus, boolean frozen, Instant scheduledFor, Instant now, boolean linkedQueriesReady); evaluate computes linkedQueriesReady inside its existing try (every
link APPROVED or EXECUTED via QueryRequestLookupService.findById; a missing row counts as
not ready), so any lookup error still answers not-releasable. GateEvaluation and deploygov/api/DeploymentGateView gain linkedQueriesReady and List<DeploymentLinkedQueryView> linkedQueries; DeploymentGateResponse renders them. Update the call in DefaultDeploymentSimulationService and add a LINKED_QUERIESDeploymentDecisionStepKind
step to the trace so the simulator explains a held gate.
confirm-execution — new optional body record ConfirmDeploymentExecutionRequest(Boolean executeLinkedQueries) in internal/web/ (@RequestBody(required = false); boxed, since CI
payloads omit it). DeploymentGateService.confirmExecution gains the flag. Restructure so the
linked run happens outside the transaction: a non-transactional orchestration method
performs require + requireActor + the releasable check, runs the links, then calls the
existing transactional APPROVED → EXECUTED write.
Serialize concurrent confirmations per request with scheduling.api.DistributedLockService
(deploygov may import scheduling.api; ai already does) so two CI retries cannot run the
same migration twice; a second caller that finds the row EXECUTED returns it (idempotent,
as today at :107-110).
Without the flag, an APPROVED link is not executed and the gate contract is unchanged: the
pipeline is expected to have run it, or to have chosen not to.
With the flag, iterate links in sequence_order: skip EXECUTED; call executeLinked(id, "linked_query"); on FAILED or any RuntimeException, stateService.apply(request, FAILED) (allowed, :59), audit DEPLOYMENT_EXECUTED-sibling action DEPLOYMENT_LINKED_QUERY_FAILED (new AuditAction value; audit_log.action is VARCHAR(100), no migration) with null actor and Map.of("trigger", "linked_query", "query_request_id", …, "sequence_order", …), stop, and throw DeploymentLinkedQueryFailedException → 409 DEPLOYMENT_LINKED_QUERY_FAILED. Remaining
links stay as they are.
On success, the existing DEPLOYMENT_EXECUTED row gains linked_queries_executed (count).
i18n — the new exception detail in all six messages*.properties.
Acceptance criteria
DefaultDeploymentGateServiceTest: releasable truth table with the new parameter; a PENDING_REVIEW link holds the gate; a lookup exception answers not-releasable; confirm without
the flag ignores APPROVED links; confirm with the flag executes in order, skips EXECUTED,
stops at the first FAILED, marks the deployment FAILED, writes the system audit row, and
leaves later links APPROVED; a redelivered confirm after success is idempotent.
DefaultLinkedQueryExecutionServiceTest per .claude/patterns/backend-test-parity.md, plus DefaultQueryLifecycleServiceTest proving trigger=linked_query lands in the audit metadata.
DeploymentGateFlowIntegrationTest extended end to end: trigger with two approved DDL links →
gate held while one is PENDING_REVIEW → approve it → releasable → confirm with the flag →
both queries EXECUTED, deployment EXECUTED; and the failing variant.
DefaultDeploymentSimulationServiceTest covers the new trace step.
Part of #1029 (linked deployment changes epic — read it first for the big picture). Needs #1031.
Goal
Make the links mean something. The fail-closed gate additionally requires every linked query to be
APPROVEDorEXECUTED;confirm-executiongains an optionalexecute_linked_queries=truethatruns the still-
APPROVEDlinks insequence_orderthrough the normal query execution path beforethe deployment becomes
EXECUTED; the first failed link marks the deploymentFAILEDand leavesthe rest untouched.
Today
DefaultDeploymentGateService.releasable(status, frozen, scheduledFor, now)(:162-168)is the one pure function,
evaluate(:172-192) wraps the freeze lookup fail-closed, andconfirmExecution(:101-131) is@Transactional, movesAPPROVED → EXECUTEDthroughDeploymentRequestStateService.applyand auditsDEPLOYMENT_EXECUTEDwithtrigger=pipeline.DefaultDeploymentSimulationServicereplays the same function (docs/18:502).Design constraints agreed on the epic:
deploygovmust not importworkflow—workflow/internal/DeploymentBreakGlassReviewListener.java:3already imports
deploygov.events, so the reverse arrow is a cycle. Execution is reachedthrough a new
core.apiinterface thatworkflowimplements (precedent:core.api.SessionRevocationService←security/internal/DefaultSessionRevocationService.java).requestgroups/internal/GroupExecutionService.runQuery(:148-179): thatcalls
proxy.api.QueryExecutor.execute(new QueryExecutionRequest(...))(:168) on a groupitem and audits
REQUEST_GROUP_EXECUTED(:204-210). A linked item is aquery_requestsrow, which must move to
EXECUTEDwithQUERY_EXECUTED, results andQueryExecutedEvent—only
DefaultQueryLifecycleService.doExecute(:273-275) does that.trigger=linked_query),the deploygov: deployment governance — notifications & audit fan-out #695 convention at
DeploymentRequestStateService.java:119-122. deploygov: DeploymentVerificationJob — window, sampling and breach → FAILED #1025 addstrigger=verificationthe same way.releasable(...)for change tickets — sequence the PRs; both are additive booleans.Steps
docs/04-api-spec.md— document first (:7775):linked_queries_readyandlinked_querieson the gate response, the optionalconfirm-executionbody{ "execute_linked_queries": true }, and the new409reason.core/api/LinkedQueryExecutionService.java(new) —QueryStatus executeLinked(UUID queryRequestId, String trigger): runs anAPPROVEDquery as its submitter, returns theresulting
EXECUTED/FAILED; returns the current status untouched when the row is alreadyEXECUTED(idempotent) and throwscore.api.QueryRequestNotFoundExceptionwhen gone. JDK +project types only.
workflow/internal/DefaultLinkedQueryExecutionService.java(new) — delegates toDefaultQueryLifecycleService.doExecute(snapshot, snapshot.submittedByUserId(), trigger, false, AuditAction.QUERY_EXECUTED), exactly asexecuteScheduled(:166-175) does with"scheduled";doExecuteis private today — widen it to package-private or expose a smallpackage-private entry on the lifecycle service rather than duplicating the body.
releasable(QueryStatus, boolean frozen, Instant scheduledFor, Instant now, boolean linkedQueriesReady);evaluatecomputeslinkedQueriesReadyinside its existingtry(everylink
APPROVEDorEXECUTEDviaQueryRequestLookupService.findById; a missing row counts asnot ready), so any lookup error still answers not-releasable.
GateEvaluationanddeploygov/api/DeploymentGateViewgainlinkedQueriesReadyandList<DeploymentLinkedQueryView> linkedQueries;DeploymentGateResponserenders them. Update the call inDefaultDeploymentSimulationServiceand add aLINKED_QUERIESDeploymentDecisionStepKindstep to the trace so the simulator explains a held gate.
confirm-execution— new optional body recordConfirmDeploymentExecutionRequest(Boolean executeLinkedQueries)ininternal/web/(@RequestBody(required = false); boxed, since CIpayloads omit it).
DeploymentGateService.confirmExecutiongains the flag. Restructure so thelinked run happens outside the transaction: a non-transactional orchestration method
performs
require+requireActor+ the releasable check, runs the links, then calls theexisting transactional
APPROVED → EXECUTEDwrite.scheduling.api.DistributedLockService(
deploygovmay importscheduling.api;aialready does) so two CI retries cannot run thesame migration twice; a second caller that finds the row
EXECUTEDreturns it (idempotent,as today at
:107-110).APPROVEDlink is not executed and the gate contract is unchanged: thepipeline is expected to have run it, or to have chosen not to.
sequence_order: skipEXECUTED; callexecuteLinked(id, "linked_query"); onFAILEDor anyRuntimeException,stateService.apply(request, FAILED)(allowed,:59), auditDEPLOYMENT_EXECUTED-sibling actionDEPLOYMENT_LINKED_QUERY_FAILED(newAuditActionvalue;audit_log.actionisVARCHAR(100), no migration) with null actor andMap.of("trigger", "linked_query", "query_request_id", …, "sequence_order", …), stop, and throwDeploymentLinkedQueryFailedException→409 DEPLOYMENT_LINKED_QUERY_FAILED. Remaininglinks stay as they are.
DEPLOYMENT_EXECUTEDrow gainslinked_queries_executed(count).messages*.properties.Acceptance criteria
DefaultDeploymentGateServiceTest:releasabletruth table with the new parameter; aPENDING_REVIEWlink holds the gate; a lookup exception answers not-releasable; confirm withoutthe flag ignores
APPROVEDlinks; confirm with the flag executes in order, skipsEXECUTED,stops at the first
FAILED, marks the deploymentFAILED, writes the system audit row, andleaves later links
APPROVED; a redelivered confirm after success is idempotent.DefaultLinkedQueryExecutionServiceTestper.claude/patterns/backend-test-parity.md, plusDefaultQueryLifecycleServiceTestprovingtrigger=linked_querylands in the audit metadata.DeploymentGateFlowIntegrationTestextended end to end: trigger with two approved DDL links →gate held while one is
PENDING_REVIEW→ approve it → releasable → confirm with the flag →both queries
EXECUTED, deploymentEXECUTED; and the failing variant.DefaultDeploymentSimulationServiceTestcovers the new trace step.ApplicationModulesTestproves nodeploygov → workflowedge;ApiPackageDependencyTest,MessagesParityTestpass; coverage ≥ 90 / 80.