1// SPDX-License-Identifier: MIT
2
3package workflow
4
5import (
6 "bnema.dev/ermite-actions/template"
7)
8
9type needsNode struct {
10 name string
11 needs []*template.String
12}
13
14// convertJobs is a port of convertJobs: every job is converted on its own, a
15// job that fails is dropped after its error is recorded, then the needs graph
16// is checked.
17func (c *converter) convertJobs(t template.Token) []*Job {
18 m, ok := t.(*template.Mapping)
19 if !ok {
20 c.ctx.ErrorAt(t, "Invalid format for jobs")
21 return nil
22 }
23 var result []*Job
24 var satisfied, unsatisfied []*needsNode
25 for _, p := range m.Pairs {
26 key := asString(p.Key, "job name")
27 var job *Job
28 c.handle(m, func() {
29 job = c.convertJob(key, asMap(p.Value, "job "+key.Value))
30 })
31 if job == nil {
32 continue
33 }
34 result = append(result, job)
35 n := &needsNode{name: job.ID, needs: append([]*template.String(nil), job.NeedTokens...)}
36 if len(n.needs) > 0 {
37 unsatisfied = append(unsatisfied, n)
38 } else {
39 satisfied = append(satisfied, n)
40 }
41 }
42 c.validateNeeds(m, result, satisfied, unsatisfied)
43 return result
44}
45
46// validateNeeds reports jobs that can never start: it removes satisfied
47// dependencies in the order the reference does and complains about what is
48// left.
49func (c *converter) validateNeeds(t template.Token, jobs []*Job, satisfied, unsatisfied []*needsNode) {
50 if len(satisfied) == 0 {
51 c.ctx.ErrorAt(t, "The workflow must contain at least one job with no dependencies.")
52 return
53 }
54 for len(satisfied) > 0 {
55 current := satisfied[0]
56 satisfied = satisfied[1:]
57 for i := len(unsatisfied) - 1; i >= 0; i-- {
58 u := unsatisfied[i]
59 for j := len(u.needs) - 1; j >= 0; j-- {
60 if u.needs[j].Value != current.name {
61 continue
62 }
63 u.needs = append(u.needs[:j], u.needs[j+1:]...)
64 if len(u.needs) == 0 {
65 satisfied = append(satisfied, u)
66 unsatisfied = append(unsatisfied[:i], unsatisfied[i+1:]...)
67 }
68 }
69 }
70 }
71 if len(unsatisfied) == 0 {
72 return
73 }
74 known := map[string]bool{}
75 for _, j := range jobs {
76 known[j.ID] = true
77 }
78 for _, u := range unsatisfied {
79 for _, need := range u.needs {
80 if known[need.Value] {
81 c.ctx.Errorf(need, "Job '%s' depends on job '%s' which creates a cycle in the dependency graph.", u.name, need.Value)
82 } else {
83 c.ctx.Errorf(need, "Job '%s' depends on unknown job '%s'.", u.name, need.Value)
84 }
85 }
86 }
87}
88
89// convertJob is a port of convertJob.
90func (c *converter) convertJob(key *template.String, m *template.Mapping) *Job {
91 if msg := c.jobIDs.tryAddKnownID(key.Value); msg != "" {
92 c.ctx.ErrorAt(key, msg)
93 }
94 j := &Job{ID: key.Value, IDToken: key}
95 var name template.Token
96 for _, p := range m.Pairs {
97 prop := asString(p.Key, "job property name")
98 v := p.Value
99 switch prop.Value {
100 case "concurrency":
101 c.handle(v, func() { c.convertConcurrency(v) })
102 j.Concurrency = v
103 case "container":
104 c.convertContainer(v, false)
105 j.Container = v
106 case "env":
107 c.handle(v, func() { j.Env = asMap(v, "job env") })
108 case "environment":
109 c.handle(v, func() { c.convertEnvironment(v) })
110 j.Environment = v
111 case "if":
112 j.If = c.convertIf(v, "job-if")
113 case "name":
114 name = asScalar(v, "job name")
115 case "needs":
116 j.NeedTokens = nil
117 if s, ok := v.(*template.String); ok {
118 j.NeedTokens = append(j.NeedTokens, asString(s, "job needs id"))
119 }
120 if seq, ok := v.(*template.Sequence); ok {
121 for _, it := range seq.Items {
122 j.NeedTokens = append(j.NeedTokens, asString(it, "job needs id"))
123 }
124 }
125 case "outputs":
126 c.handle(v, func() { j.Outputs = asMap(v, "job outputs") })
127 case "runs-on":
128 c.handle(v, func() { c.convertRunsOn(v) })
129 j.RunsOn = v
130 case "services":
131 c.convertServices(v)
132 j.Services = v
133 case "snapshot":
134 j.Snapshot = v
135 case "steps":
136 j.Steps = c.convertSteps(v)
137 case "strategy":
138 j.Strategy = v
139 case "uses":
140 j.Uses = asString(v, "job uses value")
141 case "with":
142 c.handle(v, func() { j.With = asMap(v, "uses-with value") })
143 case "secrets":
144 if s, ok := v.(*template.String); ok && s.Value == "inherit" {
145 j.InheritSecrets = true
146 } else {
147 c.handle(v, func() { j.Secrets = asMap(v, "uses-secrets value") })
148 }
149 case "timeout-minutes":
150 j.TimeoutMinutes = v
151 case "cancel-timeout-minutes":
152 j.CancelTimeoutMinutes = v
153 case "continue-on-error":
154 j.ContinueOnError = v
155 case "defaults":
156 j.Defaults = convertDefaults(v)
157 case "permissions":
158 j.Permissions = convertPermissions(v)
159 }
160 }
161 for _, n := range j.NeedTokens {
162 j.Needs = append(j.Needs, n.Value)
163 }
164 if j.If == nil {
165 j.If = c.defaultIf("job-if")
166 }
167 switch {
168 case name == nil:
169 j.Name = key
170 case isEmptyString(name):
171 j.Name = key
172 default:
173 j.Name, j.HasName = name, true
174 }
175 return j
176}
177
178func isEmptyString(t template.Token) bool {
179 s, ok := t.(*template.String)
180 return ok && s.Value == ""
181}