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
109// HasStatusFunction reports whether an expression calls success, failure,
110// cancelled or always anywhere in its syntax tree, which decides whether an
111// "if" condition is wrapped as "success() && (cond)".
112func HasStatusFunction(n expr.Node) bool { return hasStatusFunction(n) }
113
114func hasStatusFunction(n expr.Node) bool {
115 switch n := n.(type) {
116 case *expr.FunctionCall:
117 if statusFunctions[strings.ToLower(n.Name.Lexeme)] {
118 return true
119 }
120 for _, a := range n.Args {
121 if hasStatusFunction(a) {
122 return true
123 }
124 }
125 case *expr.Binary:
126 return hasStatusFunction(n.Left) || hasStatusFunction(n.Right)
127 case *expr.Unary:
128 return hasStatusFunction(n.Expr)
129 case *expr.Logical:
130 for _, a := range n.Args {
131 if hasStatusFunction(a) {
132 return true
133 }
134 }
135 case *expr.Grouping:
136 return hasStatusFunction(n.Group)
137 case *expr.IndexAccess:
138 return hasStatusFunction(n.Expr) || hasStatusFunction(n.Index)
139 }
140 return false
141}
142
143// convertIf converts an if condition: the result always contains a status
144// function, wrapping the condition as "success() && (cond)" when it has none.
145// A condition that does not parse is recorded as an error and returns nil.
146func (c *converter) convertIf(t template.Token, defName string) *template.BasicExpression {
147 scalar := asScalar(t, "if condition")
148 def := t.Definition()
149 if def == nil {
150 def = c.schema.Info(defName)
151 }
152 var condition string
153 switch s := scalar.(type) {
154 case *template.BasicExpression:
155 condition = s.Expression
156 default:
157 condition = strings.TrimSpace(asString(scalar, "if condition").Value)
158 }
159 final := "success()"
160 if condition != "" {
161 final = condition
162 if node, err := expr.Parse(condition, def.NamedContexts(), def.Functions()); err == nil && !hasStatusFunction(node) {
163 final = "success() && (" + condition + ")"
164 }
165 }
166 if _, err := expr.Parse(final, def.NamedContexts(), def.Functions()); err != nil {
167 c.ctx.ErrorAt(t, err.Error())
168 return nil
169 }
170 return template.NewExpression(origin(t), final, def)
171}
172
173// defaultIf is the condition of a job or step that has none.
174func (c *converter) defaultIf(defName string) *template.BasicExpression {
175 return template.NewExpression(template.Origin{}, "success()", c.schema.Info(defName))
176}