From 7342c46d99a6b5993456f050254330a195112bb0 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Tue, 21 Jul 2026 10:20:12 +0100 Subject: [PATCH] feat(server): auto-derive outputs on save + resolve inline steps --- server/internal/services/resolve_test.go | 37 ++++++++++++++++++++ server/internal/services/workflow_runner.go | 33 ++++++++++++++++++ server/internal/services/workflows.go | 38 +++++++++++++++++++-- 3 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 server/internal/services/resolve_test.go diff --git a/server/internal/services/resolve_test.go b/server/internal/services/resolve_test.go new file mode 100644 index 0000000..3131bfc --- /dev/null +++ b/server/internal/services/resolve_test.go @@ -0,0 +1,37 @@ +package services + +import ( + "testing" + + "github.com/mrhid6/vantage/server/internal/models" +) + +func TestResolveInlineStep(t *testing.T) { + ref := models.WorkflowStepRef{ + Order: 2, + OnFailure: "", + Inline: &models.WorkflowStep{ + Name: "adhoc", + Interpreter: "bash", + Script: "echo hi", + SecretRefs: []string{"TOKEN"}, + DeclaredInputs: []models.InputParam{ + {Name: "REGION", Default: "eu"}, + }, + }, + Inputs: map[string]string{"REGION": "us"}, + } + rs := resolveInlineStep(ref) + if rs.Name != "adhoc" || rs.Script != "echo hi" || rs.Order != 2 { + t.Fatalf("bad resolve: %+v", rs) + } + if rs.OnFailure != "stop" { + t.Fatalf("want default on_failure=stop, got %q", rs.OnFailure) + } + if rs.Inputs["REGION"] != "us" { + t.Fatalf("want input override us, got %q", rs.Inputs["REGION"]) + } + if len(rs.SecretRefs) != 1 || rs.SecretRefs[0] != "TOKEN" { + t.Fatalf("bad secret refs: %v", rs.SecretRefs) + } +} diff --git a/server/internal/services/workflow_runner.go b/server/internal/services/workflow_runner.go index 1a62160..72d5d73 100644 --- a/server/internal/services/workflow_runner.go +++ b/server/internal/services/workflow_runner.go @@ -83,6 +83,10 @@ func resolveSteps(wf *models.Workflow) ([]models.ResolvedStep, error) { defer cancel() out := make([]models.ResolvedStep, 0, len(wf.Steps)) for _, ref := range wf.Steps { + if ref.Inline != nil { + out = append(out, resolveInlineStep(ref)) + continue + } lib, err := getStep(ctx, ref.StepID) if err != nil { return nil, err @@ -123,6 +127,35 @@ func resolveSteps(wf *models.Workflow) ([]models.ResolvedStep, error) { return out, nil } +// resolveInlineStep freezes an ad-hoc (inline) step ref into a ResolvedStep. +func resolveInlineStep(ref models.WorkflowStepRef) models.ResolvedStep { + in := ref.Inline + inputs := map[string]string{} + for _, p := range in.DeclaredInputs { + if ref.Inputs != nil { + if v, ok := ref.Inputs[p.Name]; ok { + inputs[p.Name] = v + continue + } + } + inputs[p.Name] = p.Default + } + onFailure := ref.OnFailure + if onFailure == "" { + onFailure = "stop" + } + return models.ResolvedStep{ + Order: ref.Order, + Name: in.Name, + Interpreter: in.Interpreter, + Script: in.Script, + SecretRefs: in.SecretRefs, + OnFailure: onFailure, + MaxRetries: ref.MaxRetries, + Inputs: inputs, + } +} + // executeRun fans out one goroutine per server run and waits for all to finish. func executeRun(runID string) { run, err := GetRun(runID) diff --git a/server/internal/services/workflows.go b/server/internal/services/workflows.go index 36ea584..eb0d519 100644 --- a/server/internal/services/workflows.go +++ b/server/internal/services/workflows.go @@ -60,8 +60,9 @@ func CreateStep(s models.WorkflowStep) (*models.WorkflowStep, error) { s.StepID = uuid.New().String() s.CreatedAt = time.Now() s.UpdatedAt = s.CreatedAt - if s.DeclaredOutputs == nil { - s.DeclaredOutputs = []string{} + s.DeclaredOutputs = DeriveOutputs(s.Script) + if s.Source == "" { + s.Source = "user" } if s.SecretRefs == nil { s.SecretRefs = []string{} @@ -83,7 +84,7 @@ func UpdateStep(stepID string, s models.WorkflowStep) error { "description": s.Description, "interpreter": s.Interpreter, "script": s.Script, - "declared_outputs": s.DeclaredOutputs, + "declared_outputs": DeriveOutputs(s.Script), "declared_inputs": s.DeclaredInputs, "secret_refs": s.SecretRefs, "updated_at": time.Now(), @@ -178,6 +179,10 @@ func CreateWorkflow(w models.Workflow) (*models.Workflow, error) { if w.Steps == nil { w.Steps = []models.WorkflowStepRef{} } + if err := ValidateWorkflow(w); err != nil { + return nil, err + } + normalizeInlineSteps(&w) if _, err := db.Col("workflows").InsertOne(ctx, w); err != nil { return nil, err } @@ -187,6 +192,10 @@ func CreateWorkflow(w models.Workflow) (*models.Workflow, error) { func UpdateWorkflow(id string, w models.Workflow) error { ctx, cancel := wfCtx() defer cancel() + if err := ValidateWorkflow(w); err != nil { + return err + } + normalizeInlineSteps(&w) _, err := db.Col("workflows").UpdateOne(ctx, bson.M{"workflow_id": id}, bson.M{"$set": bson.M{ "name": w.Name, "target_server_ids": w.TargetServerIDs, @@ -196,6 +205,29 @@ func UpdateWorkflow(id string, w models.Workflow) error { return err } +// normalizeInlineSteps derives outputs for inline steps and strips fields that +// only belong to library steps. +func normalizeInlineSteps(w *models.Workflow) { + for i := range w.Steps { + in := w.Steps[i].Inline + if in == nil { + continue + } + in.DeclaredOutputs = DeriveOutputs(in.Script) + in.StepID = "" + in.Slug = "" + in.Source = "" + in.CreatedAt = time.Time{} + in.UpdatedAt = time.Time{} + if in.SecretRefs == nil { + in.SecretRefs = []string{} + } + if in.DeclaredInputs == nil { + in.DeclaredInputs = []models.InputParam{} + } + } +} + func DeleteWorkflow(id string) error { ctx, cancel := wfCtx() defer cancel()