main 98594d0

convert_jobs.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close