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 <malhotra.reetika25@gmail.com>
Signed-off-by: vishal210893 <vishal210893@gmail.com>

* Fix: Correct formatting in status output for better readability

Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com>
Signed-off-by: vishal210893 <vishal210893@gmail.com>

* Fix: Enhance CUE generation for nested array structs and lift shared conditions to parent nodes

Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com>
Signed-off-by: vishal210893 <vishal210893@gmail.com>

* Fix: Improve handling of array values in CUE generation for patchKey operations

Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com>
Signed-off-by: vishal210893 <vishal210893@gmail.com>

* 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 <ayushshyamkumar888@gmail.com>

* 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 <ayushshyamkumar888@gmail.com>

* refactor: Consolidate two `append` calls into one for health expression parts.

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

---------

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

update status.go

Signed-off-by: vishal210893 <vishal210893@gmail.com>

* Fix: Correct formatting in status output for better readability

Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com>

# Conflicts:
#	pkg/definition/defkit/status.go
Signed-off-by: vishal210893 <vishal210893@gmail.com>

* removed unitended commited file

Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com>
Signed-off-by: vishal210893 <vishal210893@gmail.com>

* Fix: Preserve explicit action variable names in BuiltinAction and update CUE generation

Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com>
Signed-off-by: vishal210893 <vishal210893@gmail.com>

* Fix: Update default WebhookConfig settings for improved configuration

Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com>
Signed-off-by: vishal210893 <vishal210893@gmail.com>

* Fix: Enhance CUE generation with ForEachMapOp support and add alias handling in workflow steps

Signed-off-by: Vaibhav Agrawal <vaibhav.agrawal0096@gmail.com>
Signed-off-by: vishal210893 <vishal210893@gmail.com>

---------

Signed-off-by: Reetika Malhotra <malhotra.reetika25@gmail.com>
Signed-off-by: vishal210893 <vishal210893@gmail.com>
Signed-off-by: Vaibhav Agrawal <vaibhav.agrawal0096@gmail.com>
Co-authored-by: Ayush Kumar <65535504+roguepikachu@users.noreply.github.com>
This commit is contained in:
Vishal Kumar
2026-02-18 19:54:57 +05:30
committed by GitHub
parent 413e881e04
commit 3cd345c243
8 changed files with 322 additions and 59 deletions

View File

@@ -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 {

View File

@@ -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() {

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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() {

View File

@@ -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("]")
}
}

View File

@@ -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"

View File

@@ -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() {