Skip to content

Commit 83cbc22

Browse files
Copilotintel352
andauthored
Engine runtime validation: reuse wfctl cross-reference checks at startup (#377)
* Initial plan * Add validation package and engine startup template ref validation Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> Agent-Logs-Url: https://github.com/GoCodeAlone/workflow/sessions/793db56c-189a-4e30-b1c0-47f9fcc38e6a * Add engine validation config documentation to DOCUMENTATION.md Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> Agent-Logs-Url: https://github.com/GoCodeAlone/workflow/sessions/793db56c-189a-4e30-b1c0-47f9fcc38e6a * Address PR review comments: validate templateRefs value, fix doc structure, accept registry param, fix hyphen spurious warnings Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> Agent-Logs-Url: https://github.com/GoCodeAlone/workflow/sessions/b13a8553-fb55-4892-a6e4-21e323b5cb20 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
1 parent 0380fba commit 83cbc22

10 files changed

Lines changed: 1221 additions & 464 deletions

DOCUMENTATION.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,9 +1760,29 @@ triggers:
17601760

17611761
Both approaches work with `wfctl template validate --config` for validation.
17621762

1763-
## Visual Workflow Builder (UI)
1763+
## Engine Validation Config
1764+
1765+
Control the engine's startup validation behaviour via the `engine.validation` block:
1766+
1767+
```yaml
1768+
engine:
1769+
validation:
1770+
templateRefs: warn # "off" | "warn" | "error" (default: "warn")
1771+
```
1772+
1773+
| Value | Behaviour |
1774+
|-------|-----------|
1775+
| `warn` | (default) Log warnings for suspicious pipeline template references at startup. Engine starts normally. |
1776+
| `error` | Refuse to start if any pipeline template reference issues are detected (e.g. dangling step refs, unknown output fields). |
1777+
| `off` | Skip template reference validation entirely. Preserves the previous engine behavior. |
17641778

1765-
A React-based visual editor for composing workflow configurations (`ui/` directory).
1779+
The validation checks performed at startup match those run by `wfctl template validate`, including:
1780+
- Step name references (`{{ .steps.NAME.field }}`) against declared steps in the same pipeline
1781+
- Forward references (referencing a step that appears later in the pipeline)
1782+
- Output field validation against each step type's declared output schema
1783+
- SQL column validation for `step.db_query` steps with a static `query`
1784+
1785+
## Visual Workflow Builder (UI)
17661786

17671787
**Technology stack:** React, ReactFlow, Zustand, TypeScript, Vite
17681788

cmd/wfctl/api_extract.go

Lines changed: 2 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/GoCodeAlone/workflow/config"
1414
"github.com/GoCodeAlone/workflow/module"
15+
"github.com/GoCodeAlone/workflow/validation"
1516
"gopkg.in/yaml.v3"
1617
)
1718

@@ -502,7 +503,7 @@ func inferBodyFromSchema(bodyFrom string, steps []map[string]any) *module.OpenAP
502503
if query == "" {
503504
break
504505
}
505-
columns := extractSQLColumns(query)
506+
columns := validation.ExtractSQLColumns(query)
506507
if len(columns) == 0 {
507508
break
508509
}
@@ -530,80 +531,6 @@ func inferBodyFromSchema(bodyFrom string, steps []map[string]any) *module.OpenAP
530531
return nil
531532
}
532533

533-
// extractSQLColumns parses a SQL SELECT statement and returns the column names
534-
// (or aliases) from the SELECT clause.
535-
func extractSQLColumns(query string) []string {
536-
// Normalize whitespace
537-
query = strings.Join(strings.Fields(query), " ")
538-
539-
// Find SELECT ... FROM
540-
upper := strings.ToUpper(query)
541-
selectIdx := strings.Index(upper, "SELECT ")
542-
fromIdx := strings.Index(upper, " FROM ")
543-
if selectIdx < 0 || fromIdx < 0 || fromIdx <= selectIdx {
544-
return nil
545-
}
546-
547-
selectClause := query[selectIdx+7 : fromIdx]
548-
549-
// Handle DISTINCT
550-
if strings.HasPrefix(strings.ToUpper(strings.TrimSpace(selectClause)), "DISTINCT ") {
551-
selectClause = strings.TrimSpace(selectClause)[9:]
552-
}
553-
554-
// Split by comma, handling parenthesized subexpressions
555-
var columns []string
556-
depth := 0
557-
current := ""
558-
for _, ch := range selectClause {
559-
switch ch {
560-
case '(':
561-
depth++
562-
current += string(ch)
563-
case ')':
564-
depth--
565-
current += string(ch)
566-
case ',':
567-
if depth == 0 {
568-
if col := extractColumnName(strings.TrimSpace(current)); col != "" {
569-
columns = append(columns, col)
570-
}
571-
current = ""
572-
} else {
573-
current += string(ch)
574-
}
575-
default:
576-
current += string(ch)
577-
}
578-
}
579-
if col := extractColumnName(strings.TrimSpace(current)); col != "" {
580-
columns = append(columns, col)
581-
}
582-
return columns
583-
}
584-
585-
// extractColumnName extracts the effective column name from a SELECT expression.
586-
// Handles: "col", "table.col", "expr AS alias", "COALESCE(...) AS alias".
587-
func extractColumnName(expr string) string {
588-
if expr == "" || expr == "*" {
589-
return ""
590-
}
591-
// Check for AS alias (case-insensitive)
592-
upper := strings.ToUpper(expr)
593-
if asIdx := strings.LastIndex(upper, " AS "); asIdx >= 0 {
594-
alias := strings.TrimSpace(expr[asIdx+4:])
595-
// Remove quotes if present
596-
alias = strings.Trim(alias, "\"'`")
597-
return alias
598-
}
599-
// Check for table.column
600-
if dotIdx := strings.LastIndex(expr, "."); dotIdx >= 0 {
601-
return strings.TrimSpace(expr[dotIdx+1:])
602-
}
603-
// Simple column name
604-
return strings.TrimSpace(expr)
605-
}
606-
607534
// userCredentialsSchema returns a schema for email+password request bodies.
608535
func userCredentialsSchema() *module.OpenAPISchema {
609536
return &module.OpenAPISchema{

0 commit comments

Comments
 (0)