From 3cd345c24307427991ba131b60f538699ca51cfa Mon Sep 17 00:00:00 2001 From: Vishal Kumar Date: Wed, 18 Feb 2026 19:54:57 +0530 Subject: [PATCH] Fix: defkit CUE generation for task health, nested array schemas, and patch traits (#7041) * Fix: Update JobHealth logic to reflect correct health status based on parallelism Signed-off-by: Reetika Malhotra Signed-off-by: vishal210893 * Fix: Correct formatting in status output for better readability Signed-off-by: Reetika Malhotra Signed-off-by: vishal210893 * Fix: Enhance CUE generation for nested array structs and lift shared conditions to parent nodes Signed-off-by: Reetika Malhotra Signed-off-by: vishal210893 * Fix: Improve handling of array values in CUE generation for patchKey operations Signed-off-by: Reetika Malhotra Signed-off-by: vishal210893 * Feat: Defkit Refactor and Clean-Up (#7042) * feat: Enhance status and health policy CUE generation with field grouping, column alignment, `_isHealth` pattern, and annotation-based health disable. Signed-off-by: Ayush Kumar * feat: introduce defkit package for a structured Go API to define KubeVela component and trait templates with outputs, patches, and helpers. Signed-off-by: Ayush Kumar * refactor: Consolidate two `append` calls into one for health expression parts. Signed-off-by: Ayush Kumar --------- Signed-off-by: Ayush Kumar update status.go Signed-off-by: vishal210893 * Fix: Correct formatting in status output for better readability Signed-off-by: Reetika Malhotra # Conflicts: # pkg/definition/defkit/status.go Signed-off-by: vishal210893 * removed unitended commited file Signed-off-by: Reetika Malhotra Signed-off-by: vishal210893 * Fix: Preserve explicit action variable names in BuiltinAction and update CUE generation Signed-off-by: Reetika Malhotra Signed-off-by: vishal210893 * Fix: Update default WebhookConfig settings for improved configuration Signed-off-by: Reetika Malhotra Signed-off-by: vishal210893 * Fix: Enhance CUE generation with ForEachMapOp support and add alias handling in workflow steps Signed-off-by: Vaibhav Agrawal Signed-off-by: vishal210893 --------- Signed-off-by: Reetika Malhotra Signed-off-by: vishal210893 Signed-off-by: Vaibhav Agrawal Co-authored-by: Ayush Kumar <65535504+roguepikachu@users.noreply.github.com> --- pkg/definition/defkit/cuegen.go | 155 +++++++++++++++++++- pkg/definition/defkit/cuegen_test.go | 19 +++ pkg/definition/defkit/param.go | 9 +- pkg/definition/defkit/status.go | 10 +- pkg/definition/defkit/status_test.go | 4 +- pkg/definition/defkit/trait.go | 63 +++++--- pkg/definition/defkit/workflow_step.go | 97 +++++++++--- pkg/definition/defkit/workflow_step_test.go | 24 +++ 8 files changed, 322 insertions(+), 59 deletions(-) diff --git a/pkg/definition/defkit/cuegen.go b/pkg/definition/defkit/cuegen.go index e6cc8264d..039a3ebcb 100644 --- a/pkg/definition/defkit/cuegen.go +++ b/pkg/definition/defkit/cuegen.go @@ -461,11 +461,19 @@ func (g *CUEGenerator) writeStructFieldForHelper(sb *strings.Builder, f *StructF // Check for nested struct if nested := f.GetNested(); nested != nil { - sb.WriteString(fmt.Sprintf("%s%s%s: {\n", indent, name, optional)) - for _, nestedField := range nested.GetFields() { - g.writeStructFieldForHelper(sb, nestedField, depth+1) + if f.FieldType() == ParamTypeArray { + sb.WriteString(fmt.Sprintf("%s%s%s: [...{\n", indent, name, optional)) + for _, nestedField := range nested.GetFields() { + g.writeStructFieldForHelper(sb, nestedField, depth+1) + } + sb.WriteString(fmt.Sprintf("%s}]\n", indent)) + } else { + sb.WriteString(fmt.Sprintf("%s%s%s: {\n", indent, name, optional)) + for _, nestedField := range nested.GetFields() { + g.writeStructFieldForHelper(sb, nestedField, depth+1) + } + sb.WriteString(fmt.Sprintf("%s}\n", indent)) } - sb.WriteString(fmt.Sprintf("%s}\n", indent)) return } @@ -1155,6 +1163,11 @@ func (g *CUEGenerator) insertPatchKeyIntoTree(root *fieldNode, op *PatchKeyOp, c func (g *CUEGenerator) writeFieldTree(sb *strings.Builder, node *fieldNode, depth int) { indent := strings.Repeat(g.indent, depth) + // Lift child conditions to the parent field when all children share the same + // condition and the parent has no own value/spread/foreach/patchKey. This avoids + // emitting empty parent structs like `foo: { if cond { bar: ... } }`. + g.liftChildConditions(node) + // Write spread entries FIRST (user labels spread before OAM labels) // This matches the KubeVela convention of spreading user-provided values // before adding fixed OAM labels, so user values can be overridden if needed. @@ -1200,6 +1213,57 @@ func (g *CUEGenerator) writeFieldTree(sb *strings.Builder, node *fieldNode, dept } } +// liftChildConditions promotes a shared child condition to the parent node. +// It recursively processes the tree so inner nodes are normalized before parent rendering. +func (g *CUEGenerator) liftChildConditions(node *fieldNode) { + for _, name := range node.childOrder { + child := node.children[name] + if child == nil { + continue + } + // Recurse first so deeper nodes are normalized + g.liftChildConditions(child) + + if child.cond != nil { + continue + } + if child.value != nil || child.isArray || len(child.spreads) > 0 || child.forEach != nil || child.patchKey != nil { + continue + } + if len(child.children) == 0 { + continue + } + + var sharedCond Condition + var sharedCondStr string + canLift := true + for _, grandName := range child.childOrder { + grand := child.children[grandName] + if grand == nil || grand.cond == nil { + canLift = false + break + } + condStr := g.conditionToCUE(grand.cond) + if sharedCondStr == "" { + sharedCondStr = condStr + sharedCond = grand.cond + } else if sharedCondStr != condStr { + canLift = false + break + } + } + if canLift && sharedCond != nil { + child.cond = sharedCond + for _, grandName := range child.childOrder { + grand := child.children[grandName] + if grand != nil { + grand.cond = nil + } + } + } + } +} + // writeFieldNode writes a single field node as CUE. func (g *CUEGenerator) writeFieldNode(sb *strings.Builder, name string, node *fieldNode, depth int) { indent := strings.Repeat(g.indent, depth) @@ -1230,6 +1294,45 @@ func (g *CUEGenerator) writeFieldNode(sb *strings.Builder, name string, node *fi return } + // If all children share the same condition and there are no unconditional parts, + // lift the condition to avoid emitting empty parent structs. + if node.value == nil && len(node.children) > 0 && len(node.spreads) == 0 && node.forEach == nil && node.patchKey == nil { + condStr := "" + canLift := true + for _, childName := range node.childOrder { + child := node.children[childName] + if child.cond == nil { + canLift = false + break + } + childCondStr := g.conditionToCUE(child.cond) + if condStr == "" { + condStr = childCondStr + } else if condStr != childCondStr { + canLift = false + break + } + } + if canLift && condStr != "" { + // Clone node with cleared child conditions for rendering. + clone := &fieldNode{ + children: make(map[string]*fieldNode, len(node.children)), + childOrder: append([]string(nil), node.childOrder...), + } + for childName, child := range node.children { + childCopy := *child + childCopy.cond = nil + clone.children[childName] = &childCopy + } + sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr)) + sb.WriteString(fmt.Sprintf("%s\t%s: {\n", indent, name)) + g.writeFieldTree(sb, clone, depth+2) + sb.WriteString(fmt.Sprintf("%s\t}\n", indent)) + sb.WriteString(fmt.Sprintf("%s}\n", indent)) + return + } + } + // Regular field if node.value != nil && len(node.children) == 0 { // Leaf node with value @@ -1320,6 +1423,8 @@ func (g *CUEGenerator) valueToCUE(v Value) string { return fmt.Sprintf("%s.%s", val.VarName(), val.FieldName()) case *IterLetRef: return val.RefName() + case *ForEachMapOp: + return g.forEachMapOpToCUE(val) default: // Try to get name from Param interface if p, ok := v.(Param); ok { @@ -1329,6 +1434,32 @@ func (g *CUEGenerator) valueToCUE(v Value) string { } } +// forEachMapOpToCUE converts a ForEachMapOp to CUE map comprehension syntax. +// Generates: {for k, v in source { (keyExpr): valExpr }}. +func (g *CUEGenerator) forEachMapOpToCUE(op *ForEachMapOp) string { + keyVar := op.KeyVar() + if keyVar == "" { + keyVar = "k" + } + + valVar := op.ValVar() + if valVar == "" { + valVar = "v" + } + + keyExpr := op.KeyExpr() + if keyExpr == "" { + keyExpr = keyVar + } + + valExpr := op.ValExpr() + if valExpr == "" { + valExpr = valVar + } + + return fmt.Sprintf("{for %s, %s in %s { (%s): %s }}", keyVar, valVar, op.Source(), keyExpr, valExpr) +} + // cueFuncToCUE converts a CUE function call to CUE syntax. func (g *CUEGenerator) cueFuncToCUE(fn *CUEFunc) string { args := make([]string, len(fn.Args())) @@ -2343,11 +2474,19 @@ func (g *CUEGenerator) writeStructField(sb *strings.Builder, f *StructField, dep nested := f.GetNested() switch { case nested != nil: - sb.WriteString(fmt.Sprintf("%s%s%s: {\n", indent, name, optional)) - for _, nestedField := range nested.GetFields() { - g.writeStructField(sb, nestedField, depth+1) + if f.FieldType() == ParamTypeArray { + sb.WriteString(fmt.Sprintf("%s%s%s: [...{\n", indent, name, optional)) + for _, nestedField := range nested.GetFields() { + g.writeStructField(sb, nestedField, depth+1) + } + sb.WriteString(fmt.Sprintf("%s}]\n", indent)) + } else { + sb.WriteString(fmt.Sprintf("%s%s%s: {\n", indent, name, optional)) + for _, nestedField := range nested.GetFields() { + g.writeStructField(sb, nestedField, depth+1) + } + sb.WriteString(fmt.Sprintf("%s}\n", indent)) } - sb.WriteString(fmt.Sprintf("%s}\n", indent)) case f.HasDefault(): enumValues := f.GetEnumValues() switch { diff --git a/pkg/definition/defkit/cuegen_test.go b/pkg/definition/defkit/cuegen_test.go index f23243ceb..b444c08cb 100644 --- a/pkg/definition/defkit/cuegen_test.go +++ b/pkg/definition/defkit/cuegen_test.go @@ -182,6 +182,25 @@ var _ = Describe("CUEGenerator", func() { Expect(cue).To(ContainSubstring("name?:")) Expect(cue).To(ContainSubstring("protocol:")) }) + + It("should generate CUE for nested array struct fields", func() { + comp := defkit.NewComponent("test"). + Params( + defkit.Struct("selector").Fields( + defkit.Field("matchExpressions", defkit.ParamTypeArray). + Nested(defkit.Struct("matchExpression").Fields( + defkit.Field("key", defkit.ParamTypeString).Required(), + defkit.Field("operator", defkit.ParamTypeString), + )), + ), + ) + + cue := gen.GenerateParameterSchema(comp) + + Expect(cue).To(ContainSubstring("matchExpressions?: [...{")) + Expect(cue).To(ContainSubstring("key: string")) + Expect(cue).To(ContainSubstring("operator?: string")) + }) }) Describe("GenerateFullDefinition", func() { diff --git a/pkg/definition/defkit/param.go b/pkg/definition/defkit/param.go index 0244d79da..fdf10fda7 100644 --- a/pkg/definition/defkit/param.go +++ b/pkg/definition/defkit/param.go @@ -839,7 +839,14 @@ func (f *StructField) Description(desc string) *StructField { // Nested sets a nested struct definition for this field. func (f *StructField) Nested(s *StructParam) *StructField { f.nested = s - f.fieldType = ParamTypeStruct + if f.fieldType == ParamTypeArray { + // Preserve array type; this indicates an array of nested structs. + if f.elementType == "" { + f.elementType = ParamTypeStruct + } + } else { + f.fieldType = ParamTypeStruct + } return f } diff --git a/pkg/definition/defkit/status.go b/pkg/definition/defkit/status.go index d7acd9327..6ae7796bf 100644 --- a/pkg/definition/defkit/status.go +++ b/pkg/definition/defkit/status.go @@ -134,10 +134,9 @@ func (s *StatusBuilder) buildField(f *StatusField) string { } // Simple field - return fmt.Sprintf(`%s: %s & { - if context.output.%s != _|_ { - %s: context.output.%s - } + return fmt.Sprintf(`%s: %s +if context.output.%s != _|_ { + %s: context.output.%s }`, f.name, defExpr, f.sourcePath, f.name, f.sourcePath) } @@ -439,8 +438,7 @@ func StatefulSetHealth() *HealthBuilder { func JobHealth() *HealthBuilder { return Health(). IntField("succeeded", "status.succeeded", 0). - IntField("failed", "status.failed", 0). - HealthyWhen("succeeded >= 1 || failed >= 1") + HealthyWhen("succeeded == context.output.spec.parallelism") } // CronJobHealth returns a pre-configured health builder for CronJob. diff --git a/pkg/definition/defkit/status_test.go b/pkg/definition/defkit/status_test.go index 32a4bfb69..2dcfa147f 100644 --- a/pkg/definition/defkit/status_test.go +++ b/pkg/definition/defkit/status_test.go @@ -218,10 +218,8 @@ var _ = Describe("Status", func() { h := defkit.JobHealth() cue := h.Build() Expect(cue).To(ContainSubstring("succeeded:")) - Expect(cue).To(ContainSubstring("failed:")) Expect(cue).To(ContainSubstring("status.succeeded")) - Expect(cue).To(ContainSubstring("status.failed")) - Expect(cue).To(ContainSubstring("isHealth: succeeded >= 1 || failed >= 1")) + Expect(cue).To(ContainSubstring("isHealth: succeeded == context.output.spec.parallelism")) }) It("should create CronJobHealth builder", func() { diff --git a/pkg/definition/defkit/trait.go b/pkg/definition/defkit/trait.go index 2e61bd707..0d1f11e89 100644 --- a/pkg/definition/defkit/trait.go +++ b/pkg/definition/defkit/trait.go @@ -642,6 +642,8 @@ func (g *TraitCUEGenerator) writePatchResourceOps(sb *strings.Builder, gen *CUEG // Build a tree structure from operations (similar to component resources) tree := gen.buildFieldTree(ops) + // Normalize conditional nodes to avoid empty parent structs in patches + gen.liftChildConditions(tree) g.writePatchFieldTree(sb, gen, tree, depth) } @@ -787,44 +789,59 @@ func (g *TraitCUEGenerator) writeForEachOp(sb *strings.Builder, gen *CUEGenerato // If cond is provided, wraps in: if cond { ... } func (g *TraitCUEGenerator) writePatchKeyOp(sb *strings.Builder, gen *CUEGenerator, key string, op *PatchKeyOp, cond Condition, depth int) { indent := strings.Repeat(g.indent, depth) + elements := op.Elements() + useArrayValue := len(elements) == 1 + if useArrayValue { + _, useArrayValue = elements[0].(*ArrayParam) + } // Wrap in condition if present if cond != nil { condStr := gen.conditionToCUE(cond) sb.WriteString(fmt.Sprintf("if %s {\n", condStr)) sb.WriteString(fmt.Sprintf("%s\t// +patchKey=%s\n", indent, op.Key())) - sb.WriteString(fmt.Sprintf("%s\t%s: [", indent, key)) - for i, elem := range op.Elements() { - if i > 0 { - sb.WriteString(", ") - } - // Use depth-aware formatting for ArrayElement - if arrElem, ok := elem.(*ArrayElement); ok { - sb.WriteString(gen.arrayElementToCUEWithDepth(arrElem, depth+1)) - } else { - sb.WriteString(gen.valueToCUE(elem)) + if useArrayValue { + valStr := gen.valueToCUEAtDepth(elements[0], depth+1) + sb.WriteString(fmt.Sprintf("%s\t%s: %s\n", indent, key, valStr)) + } else { + sb.WriteString(fmt.Sprintf("%s\t%s: [", indent, key)) + for i, elem := range elements { + if i > 0 { + sb.WriteString(", ") + } + // Use depth-aware formatting for ArrayElement + if arrElem, ok := elem.(*ArrayElement); ok { + sb.WriteString(gen.arrayElementToCUEWithDepth(arrElem, depth+1)) + } else { + sb.WriteString(gen.valueToCUE(elem)) + } } + sb.WriteString("]\n") } - sb.WriteString("]\n") sb.WriteString(fmt.Sprintf("%s}", indent)) } else { // Write the patchKey annotation comment sb.WriteString(fmt.Sprintf("// +patchKey=%s\n", op.Key())) - sb.WriteString(fmt.Sprintf("%s%s: [", indent, key)) + if useArrayValue { + valStr := gen.valueToCUEAtDepth(elements[0], depth) + sb.WriteString(fmt.Sprintf("%s%s: %s", indent, key, valStr)) + } else { + sb.WriteString(fmt.Sprintf("%s%s: [", indent, key)) - // Write elements - for i, elem := range op.Elements() { - if i > 0 { - sb.WriteString(", ") - } - // Use depth-aware formatting for ArrayElement - if arrElem, ok := elem.(*ArrayElement); ok { - sb.WriteString(gen.arrayElementToCUEWithDepth(arrElem, depth)) - } else { - sb.WriteString(gen.valueToCUE(elem)) + // Write elements + for i, elem := range elements { + if i > 0 { + sb.WriteString(", ") + } + // Use depth-aware formatting for ArrayElement + if arrElem, ok := elem.(*ArrayElement); ok { + sb.WriteString(gen.arrayElementToCUEWithDepth(arrElem, depth)) + } else { + sb.WriteString(gen.valueToCUE(elem)) + } } + sb.WriteString("]") } - sb.WriteString("]") } } diff --git a/pkg/definition/defkit/workflow_step.go b/pkg/definition/defkit/workflow_step.go index f6b682abc..9226125bb 100644 --- a/pkg/definition/defkit/workflow_step.go +++ b/pkg/definition/defkit/workflow_step.go @@ -32,6 +32,8 @@ type WorkflowStepDefinition struct { baseDefinition // embedded common fields and methods category string // e.g., "Application Delivery", "Notification" scope string // e.g., "Application", "Workflow" + alias string // optional alias for definition metadata annotation + hasAlias bool // tracks whether alias was explicitly set (including empty string) stepTemplate func(tpl *WorkflowStepTemplate) // template function for step logic (type-specific) } @@ -49,12 +51,21 @@ type WorkflowAction interface { // BuiltinAction represents a call to a vela builtin. type BuiltinAction struct { - name string // e.g., "multicluster.#Deploy", "builtin.#Suspend" - params map[string]Value // parameters to pass + varName string // explicit action variable name in template (e.g., "deploy", "wait") + name string // e.g., "multicluster.#Deploy", "builtin.#Suspend" + params map[string]Value // parameters to pass } func (b *BuiltinAction) isWorkflowAction() {} +// ValueAction represents assigning a value to a template field. +type ValueAction struct { + name string + value Value +} + +func (v *ValueAction) isWorkflowAction() {} + // ConditionalAction represents a conditional workflow action. type ConditionalAction struct { cond Condition @@ -93,6 +104,14 @@ func (w *WorkflowStepDefinition) Scope(scope string) *WorkflowStepDefinition { return w } +// Alias sets an optional alias for the workflow step definition. +// This maps to metadata annotation `definition.oam.dev/alias` in generated YAML. +func (w *WorkflowStepDefinition) Alias(alias string) *WorkflowStepDefinition { + w.alias = alias + w.hasAlias = true + return w +} + // Params adds multiple parameter definitions to the workflow step. func (w *WorkflowStepDefinition) Params(params ...Param) *WorkflowStepDefinition { w.addParams(params...) @@ -201,6 +220,12 @@ func (w *WorkflowStepDefinition) GetCategory() string { return w.category } // GetScope returns the workflow step scope. func (w *WorkflowStepDefinition) GetScope() string { return w.scope } +// GetAlias returns the workflow step alias. +func (w *WorkflowStepDefinition) GetAlias() string { return w.alias } + +// HasAlias returns true if alias was explicitly set. +func (w *WorkflowStepDefinition) HasAlias() bool { return w.hasAlias } + // ToCue generates the complete CUE definition string for this workflow step. func (w *WorkflowStepDefinition) ToCue() string { // If raw CUE is set, use it with the name from NewWorkflowStep() taking precedence @@ -254,8 +279,9 @@ func NewWorkflowStepTemplate() *WorkflowStepTemplate { // Example: tpl.Builtin("deploy", "multicluster.#Deploy").WithParams(...) func (wt *WorkflowStepTemplate) Builtin(name, builtinRef string) *BuiltinActionBuilder { action := &BuiltinAction{ - name: builtinRef, - params: make(map[string]Value), + varName: name, + name: builtinRef, + params: make(map[string]Value), } return &BuiltinActionBuilder{ template: wt, @@ -264,6 +290,23 @@ func (wt *WorkflowStepTemplate) Builtin(name, builtinRef string) *BuiltinActionB } } +// Set assigns a value to a top-level field in the workflow template. +// Example: tpl.Set("object", someValue) +func (wt *WorkflowStepTemplate) Set(name string, value Value) *WorkflowStepTemplate { + wt.actions = append(wt.actions, &ValueAction{name: name, value: value}) + return wt +} + +// SetIf conditionally assigns a value to a top-level field in the workflow template. +// Example: tpl.SetIf(param.IsSet(), "object", someValue) +func (wt *WorkflowStepTemplate) SetIf(cond Condition, name string, value Value) *WorkflowStepTemplate { + wt.actions = append(wt.actions, &ConditionalAction{ + cond: cond, + action: &ValueAction{name: name, value: value}, + }) + return wt +} + // Suspend adds a suspend action. // Example: tpl.Suspend("Waiting for approval") func (wt *WorkflowStepTemplate) Suspend(message string) *WorkflowStepTemplate { @@ -297,7 +340,6 @@ type BuiltinActionBuilder struct { template *WorkflowStepTemplate action *BuiltinAction varName string - cond Condition // optional condition set by If() } // WithParams sets parameters for the builtin. @@ -310,21 +352,18 @@ func (b *BuiltinActionBuilder) WithParams(params map[string]Value) *BuiltinActio // Build finalizes the action and adds it to the template. func (b *BuiltinActionBuilder) Build() *WorkflowStepTemplate { - if b.cond != nil { - b.template.actions = append(b.template.actions, &ConditionalAction{ - cond: b.cond, - action: b.action, - }) - } else { - b.template.actions = append(b.template.actions, b.action) - } + b.template.actions = append(b.template.actions, b.action) return b.template } // If makes this action conditional. -// The condition is applied when Build() is called. func (b *BuiltinActionBuilder) If(cond Condition) *BuiltinActionBuilder { - b.cond = cond + // Replace action with conditional version + condAction := &ConditionalAction{ + cond: cond, + action: b.action, + } + b.template.actions = append(b.template.actions, condAction) return b } @@ -385,6 +424,11 @@ func (g *WorkflowStepCUEGenerator) GenerateFullDefinition(w *WorkflowStepDefinit } sb.WriteString(fmt.Sprintf("%s}\n", g.indent)) + // Write alias when explicitly set (including empty string). + if w.HasAlias() { + sb.WriteString(fmt.Sprintf("%salias: %q\n", g.indent, w.GetAlias())) + } + sb.WriteString(fmt.Sprintf("%sdescription: %q\n", g.indent, w.GetDescription())) sb.WriteString("}\n") @@ -431,12 +475,17 @@ func (g *WorkflowStepCUEGenerator) writeActions(sb *strings.Builder, wt *Workflo switch a := action.(type) { case *BuiltinAction: g.writeBuiltinAction(sb, a, "", indent, gen) + case *ValueAction: + g.writeValueAction(sb, a, "", indent, gen) case *ConditionalAction: condStr := gen.conditionToCUE(a.cond) sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr)) if builtin, ok := a.action.(*BuiltinAction); ok { g.writeBuiltinAction(sb, builtin, "\t", indent, gen) } + if value, ok := a.action.(*ValueAction); ok { + g.writeValueAction(sb, value, "\t", indent, gen) + } sb.WriteString(fmt.Sprintf("%s}\n", indent)) } } @@ -444,9 +493,12 @@ func (g *WorkflowStepCUEGenerator) writeActions(sb *strings.Builder, wt *Workflo // writeBuiltinAction writes a builtin action. func (g *WorkflowStepCUEGenerator) writeBuiltinAction(sb *strings.Builder, a *BuiltinAction, extraIndent, indent string, gen *CUEGenerator) { - // Extract the action name from the builtin reference - // e.g., "multicluster.#Deploy" -> "deploy" - actionName := extractActionName(a.name) + actionName := a.varName + if actionName == "" { + // Backward-compatible fallback when no explicit name is set. + // e.g., "multicluster.#Deploy" -> "deploy" + actionName = extractActionName(a.name) + } sb.WriteString(fmt.Sprintf("%s%s%s: %s & {\n", indent, extraIndent, actionName, a.name)) if len(a.params) > 0 { @@ -459,6 +511,15 @@ func (g *WorkflowStepCUEGenerator) writeBuiltinAction(sb *strings.Builder, a *Bu sb.WriteString(fmt.Sprintf("%s%s}\n", indent, extraIndent)) } +// writeValueAction writes a value assignment action. +func (g *WorkflowStepCUEGenerator) writeValueAction(sb *strings.Builder, a *ValueAction, extraIndent, indent string, gen *CUEGenerator) { + name := a.name + if strings.ContainsAny(name, "-./") { + name = fmt.Sprintf("%q", name) + } + sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, extraIndent, name, gen.valueToCUE(a.value))) +} + // extractActionName extracts a simple action name from a builtin reference. func extractActionName(builtinRef string) string { // "multicluster.#Deploy" -> "deploy" diff --git a/pkg/definition/defkit/workflow_step_test.go b/pkg/definition/defkit/workflow_step_test.go index 02715f2a5..432c2b897 100644 --- a/pkg/definition/defkit/workflow_step_test.go +++ b/pkg/definition/defkit/workflow_step_test.go @@ -234,6 +234,30 @@ template: { cue := step.ToCue() Expect(cue).To(ContainSubstring(`template:`)) }) + + It("should preserve explicit builtin action names", func() { + step := defkit.NewWorkflowStep("apply-deployment"). + Description("Apply deployment with specified image and cmd."). + WithImports("vela/kube", "vela/builtin"). + Template(func(tpl *defkit.WorkflowStepTemplate) { + tpl.Builtin("output", "kube.#Apply"). + WithParams(map[string]defkit.Value{ + "value": defkit.Reference("parameter.value"), + }). + Build() + tpl.Builtin("wait", "builtin.#ConditionalWait"). + WithParams(map[string]defkit.Value{ + "continue": defkit.Reference("output.$returns.value.status.readyReplicas == parameter.replicas"), + }). + Build() + }) + + cue := step.ToCue() + Expect(cue).To(ContainSubstring(`output: kube.#Apply & {`)) + Expect(cue).To(ContainSubstring(`wait: builtin.#ConditionalWait & {`)) + Expect(cue).NotTo(ContainSubstring(`apply: kube.#Apply & {`)) + Expect(cue).NotTo(ContainSubstring(`conditionalwait: builtin.#ConditionalWait & {`)) + }) }) Context("Registry", func() {