1// SPDX-License-Identifier: MIT
2
3package workflow
4
5import (
6 "strings"
7
8 "bnema.dev/ermite-actions/expr"
9 "bnema.dev/ermite-actions/template"
10)
11
12// convertConcurrency validates a concurrency token. Expressions are skipped.
13func (c *converter) convertConcurrency(t template.Token) {
14 if template.IsExpression(t) {
15 return
16 }
17 if _, ok := t.(*template.String); ok {
18 return
19 }
20 m := asMap(t, "concurrency group")
21 for _, p := range m.Pairs {
22 name := asString(p.Key, "concurrency group key")
23 if template.IsExpression(p.Key) || template.IsExpression(p.Value) {
24 continue
25 }
26 switch name.Value {
27 case "group":
28 asString(p.Value, "concurrency group")
29 case "cancel-in-progress":
30 asBool(p.Value, "cancel-in-progress")
31 case "queue":
32 asString(p.Value, "queue")
33 default:
34 c.ctx.Errorf(name, "Invalid property name: %s", name.Value)
35 }
36 }
37}
38
39// convertEnvironment validates a job environment token.
40func (c *converter) convertEnvironment(t template.Token) {
41 if template.IsExpression(t) || template.IsScalar(t) {
42 return
43 }
44 m := asMap(t, "job environment")
45 for _, p := range m.Pairs {
46 name := asString(p.Key, "job environment key")
47 if template.IsExpression(p.Key) || template.IsExpression(p.Value) {
48 continue
49 }
50 switch name.Value {
51 case "name":
52 asScalar(p.Value, "job environment name key")
53 case "deployment":
54 asBool(p.Value, "job environment deployment")
55 }
56 }
57}
58
59// convertRunsOn validates a runs-on token.
60func (c *converter) convertRunsOn(t template.Token) {
61 labels := runsOnLabels(t)
62 _ = labels
63 m, ok := t.(*template.Mapping)
64 if !ok {
65 return
66 }
67 for _, p := range m.Pairs {
68 key := asString(p.Key, "job runs-on property name")
69 switch key.Value {
70 case "group":
71 if template.IsExpression(p.Value) {
72 continue
73 }
74 group := asString(p.Value, "job runs-on group name").Value
75 if msg := checkGroup(group); msg != "" {
76 c.ctx.ErrorAt(p.Value, msg)
77 }
78 case "labels":
79 runsOnLabels(p.Value)
80 }
81 }
82}
83
84// runsOnLabels collects the literal labels of a runs-on token, asserting that
85// the items are strings.
86func runsOnLabels(t template.Token) []string {
87 if template.IsExpression(t) {
88 return nil
89 }
90 switch t := t.(type) {
91 case *template.String:
92 return []string{t.Value}
93 case *template.Sequence:
94 var out []string
95 for _, it := range t.Items {
96 if template.IsExpression(it) {
97 continue
98 }
99 out = append(out, asString(it, "job runs-on label sequence item").Value)
100 }
101 return out
102 }
103 return nil
104}
105
106// statusFunctions are the functions that make a condition a status check.
107var statusFunctions = map[string]bool{"success": true, "failure": true, "cancelled": true, "always": true}
108
109func hasStatusFunction(n expr.Node) bool {
110 switch n := n.(type) {
111 case *expr.FunctionCall:
112 if statusFunctions[strings.ToLower(n.Name.Lexeme)] {
113 return true
114 }
115 for _, a := range n.Args {
116 if hasStatusFunction(a) {
117 return true
118 }
119 }
120 case *expr.Binary:
121 return hasStatusFunction(n.Left) || hasStatusFunction(n.Right)
122 case *expr.Unary:
123 return hasStatusFunction(n.Expr)
124 case *expr.Logical:
125 for _, a := range n.Args {
126 if hasStatusFunction(a) {
127 return true
128 }
129 }
130 case *expr.Grouping:
131 return hasStatusFunction(n.Group)
132 case *expr.IndexAccess:
133 return hasStatusFunction(n.Expr) || hasStatusFunction(n.Index)
134 }
135 return false
136}
137
138// convertIf converts an if condition: the result always contains a status
139// function, wrapping the condition as "success() && (cond)" when it has none.
140// A condition that does not parse is recorded as an error and returns nil.
141func (c *converter) convertIf(t template.Token, defName string) *template.BasicExpression {
142 scalar := asScalar(t, "if condition")
143 def := t.Definition()
144 if def == nil {
145 def = c.schema.Info(defName)
146 }
147 var condition string
148 switch s := scalar.(type) {
149 case *template.BasicExpression:
150 condition = s.Expression
151 default:
152 condition = strings.TrimSpace(asString(scalar, "if condition").Value)
153 }
154 final := "success()"
155 if condition != "" {
156 final = condition
157 if node, err := expr.Parse(condition, def.NamedContexts(), def.Functions()); err == nil && !hasStatusFunction(node) {
158 final = "success() && (" + condition + ")"
159 }
160 }
161 if _, err := expr.Parse(final, def.NamedContexts(), def.Functions()); err != nil {
162 c.ctx.ErrorAt(t, err.Error())
163 return nil
164 }
165 return template.NewExpression(origin(t), final, def)
166}
167
168// defaultIf is the condition of a job or step that has none.
169func (c *converter) defaultIf(defName string) *template.BasicExpression {
170 return template.NewExpression(template.Origin{}, "success()", c.schema.Info(defName))
171}