ISSUES CLOSED: #176
4.9 KiB
Validation Pipeline — Apply Gating
Overview
The validation apply gate enforces that attached validations pass before the apply phase proceeds. Validations are attached to resources and run automatically during the Execute-to-Apply transition. Required validation failures block apply; informational failures are recorded but do not block.
Attachment Model
ValidationAttachment
Represents a validation bound to a resource with scope and mode.
| Field | Type | Description |
|---|---|---|
attachment_id |
str |
Unique ULID for this attachment |
validation_name |
str |
Name of the validation tool |
resource_id |
str |
Target resource ID |
resource_name |
str |
Human-readable resource name |
mode |
ValidationMode |
required or informational |
scope |
AttachmentScope |
direct, project, or plan |
scope_target |
str |
Project name or plan ID for scoping |
arguments |
dict[str, Any] |
Arguments passed to the validation |
AttachmentScope
| Value | Description |
|---|---|
direct |
Always active for the resource |
project |
Active when resource is accessed via a project |
plan |
Active only for a specific plan |
When multiple scopes apply, the union of all applicable validations is collected and run.
Validation Modes
| Mode | Behavior on Failure |
|---|---|
required |
Blocks apply, plan stays in Execute phase |
informational |
Recorded in summary but does not block apply |
Result Models
ApplyValidationResult
Per-validation execution result.
| Field | Type | Description |
|---|---|---|
attachment_id |
str |
Which attachment was evaluated |
validation_name |
str |
Validation tool name |
resource_id |
str |
Target resource |
mode |
ValidationMode |
Required or informational |
passed |
bool |
Whether the validation passed |
message |
str |
Result message |
data |
dict |
Additional structured data |
duration_ms |
int |
Execution time in milliseconds |
error |
`str | None` |
timed_out |
bool |
Whether execution timed out |
ApplyValidationSummary
Aggregated results with gating decision.
Properties: total, required_passed, required_failed,
informational_passed, informational_failed, all_required_passed,
is_empty
Methods:
to_plan_metadata()— Dict for planvalidation_summaryfieldformat_cli_output()— Human-readable summary for CLI display
Runner Interface
ValidationRunner (ABC)
Abstract interface for executing a single validation:
def run_validation(
self,
attachment: ValidationAttachment,
context: dict[str, Any],
) -> ApplyValidationResult: ...
DefaultValidationRunner
Default stub using text matching. Checks if the validation name or argument values appear in the context. Production implementations should invoke the actual validation tool via the tool execution pipeline.
Apply Validation Gate
ApplyValidationGate
Orchestrates the full validation-before-apply flow:
gate = ApplyValidationGate(runner=DefaultValidationRunner())
summary = gate.run(plan_id, attachments, context)
if gate.should_block_apply(summary):
reasons = gate.get_failure_reasons(summary)
# Block apply, report reasons
else:
# Proceed to apply
Example Output
When apply is blocked:
Validation Gate: BLOCKED
Required: 1 passed, 1 failed
Informational: 0 passed, 0 failed
Failures:
- lint-check on res-001: 3 lint errors found
When apply is allowed:
Validation Gate: PASSED
Required: 2 passed, 0 failed
Informational: 1 passed, 0 failed
Integration with Plan Metadata
The validation summary is stored in the plan's validation_summary field
via ApplyValidationSummary.to_plan_metadata():
{
"total": 3,
"required_passed": 2,
"required_failed": 1,
"informational_passed": 0,
"informational_failed": 0,
"all_required_passed": false,
"evaluated_at": "2026-02-22T10:30:00+00:00",
"results": [...]
}