detached 98594d0

steps.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package action
4
5import (
6	"strings"
7
8	"bnema.dev/ermite-actions/expr"
9	"bnema.dev/ermite-actions/template"
10	"bnema.dev/ermite-actions/workflow"
11)
12
13// convertSteps is the runner's PipelineTemplateConverter.ConvertToSteps
14// (Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs).
15// It is not the conversion of package workflow, which reads the steps of a
16// workflow file: this one throws at the first token of the wrong type, keeps
17// a step whose "uses" has the wrong form (and then fails on it later if it has
18// no id), accepts ids that start with "__", and names a "$/" action
19// "selfRepository". See the runner for each of those.
20func (l *loader) convertSteps(items *template.Sequence) []*workflow.Step {
21	names := newRefNames()
22	var result []*workflow.Step
23	var refs []*stepRef
24	for _, it := range items.Items {
25		s, ref := l.convertStep(it, names)
26		if s != nil {
27			result = append(result, s)
28			refs = append(refs, ref)
29		}
30	}
31	for i, s := range result {
32		if s.ID != "" {
33			continue
34		}
35		ref := refs[i]
36		if ref == nil {
37			// The runner dereferences the Reference of a step whose "uses" had
38			// the wrong form.
39			throwf(exNullRef, "Object reference not set to an instance of an object.")
40		}
41		name := ref.name
42		if name == "" {
43			name = "run"
44		}
45		names.appendSegment("__" + name)
46		s.ID = names.build()
47	}
48	return result
49}
50
51// stepRef is what the runner makes of a step's action reference that decides
52// the generated id: the image of a docker:// action, "owner/repo", "self" for
53// a local action, "selfRepository" for a $/ action, "" for a run step.
54type stepRef struct{ name string }
55
56func (l *loader) convertStep(t template.Token, names *refNames) (*workflow.Step, *stepRef) {
57	m := assertMapping(t, "steps item")
58	step := &workflow.Step{Pos: t.Pos()}
59	var run template.Token
60	var ifToken template.Token
61	var uses *template.String
62	for _, p := range m.Pairs {
63		key := assertString(p.Key, "steps item key")
64		v := p.Value
65		switch key.Value {
66		case "continue-on-error":
67			if !template.IsExpression(v) {
68				assertBoolean(v, "step continue-on-error")
69			}
70			step.ContinueOnError = assertScalar(v, "steps continue-on-error")
71		case "env":
72			if !template.IsExpression(v) {
73				for _, kv := range assertMapping(v, "environment").Pairs {
74					if template.IsExpression(kv.Key) {
75						continue
76					}
77					assertString(kv.Key, "environment key")
78					if template.IsExpression(kv.Value) {
79						continue
80					}
81					assertString(kv.Value, "environment value")
82				}
83			}
84			if em, ok := v.(*template.Mapping); ok {
85				step.Env = em
86			} else {
87				step.EnvExpression = v
88			}
89		case "id":
90			id := assertString(v, "steps item id")
91			if id.Value != "" {
92				if msg := names.tryAddKnownName(id.Value); msg != "" {
93					l.ctx.ErrorAt(id, msg)
94				}
95			}
96			step.ID, step.Explicit = id.Value, id.Value != ""
97		case "if":
98			ifToken = assertScalar(v, "steps item if")
99		case "name":
100			step.Name = assertScalar(v, "steps item name")
101		case "run":
102			run = assertScalar(v, "steps item run")
103		case "shell":
104			step.Shell = assertScalar(v, "steps item shell")
105		case "timeout-minutes":
106			if !template.IsExpression(v) {
107				if _, ok := v.(*template.Number); !ok {
108					assertFail(v, "step timeout-minutes", "NumberToken")
109				}
110			}
111			step.TimeoutMinutes = assertScalar(v, "steps item timeout-minutes")
112		case "uses":
113			uses = assertString(v, "steps item uses")
114		case "with":
115			if !template.IsExpression(v) {
116				for _, kv := range assertMapping(v, "inputs").Pairs {
117					if template.IsExpression(kv.Key) {
118						continue
119					}
120					assertString(kv.Key, "inputs key")
121					if template.IsExpression(kv.Value) {
122						continue
123					}
124					assertString(kv.Value, "inputs value")
125				}
126			}
127			if wm, ok := v.(*template.Mapping); ok {
128				step.With = wm
129			} else {
130				step.WithExpression = v
131			}
132		case "working-directory":
133			step.WorkingDirectory = assertScalar(v, "steps item working-directory")
134		case "clean", "fetch-depth", "lfs", "path", "submodules":
135			assertScalar(v, "steps item "+key.Value)
136		default:
137			throwf(exArgument, "Error while reading 'steps item key'. Unexpected value '%s'", key.Value)
138		}
139	}
140	step.If = l.convertIf(ifToken)
141
142	if run != nil {
143		step.Run = run
144		return step, &stepRef{}
145	}
146	if uses == nil {
147		assertFail(nil, "steps item uses", "StringToken")
148	}
149	step.Uses = uses
150	u, err := ParseUses(uses.Value)
151	if err != nil {
152		l.ctx.ErrorAt(uses, err.Error())
153		if strings.HasPrefix(uses.Value, "$/") {
154			// A $/ value with a ref drops the step.
155			return nil, nil
156		}
157		return step, nil
158	}
159	ref := &stepRef{name: "self"}
160	switch u.Kind {
161	case UsesDocker:
162		ref.name = u.Image
163	case UsesRepository:
164		ref.name = u.Name
165	case UsesSelfRepository:
166		ref.name = "selfRepository"
167	}
168	return step, ref
169}
170
171// stepIfContexts are the contexts the runner's converter grants the "if" of a
172// step on top of the ones the schema's step-if gives (which lacks vars and
173// needs): s_stepNamedValues.
174var stepIfContexts = []string{"vars", "needs"}
175
176// convertIf is ConvertToIfCondition for a step: a condition without a status
177// function is wrapped as "success() && (cond)", an empty one is "success()".
178func (l *loader) convertIf(tok template.Token) *template.BasicExpression {
179	def := l.schema.Info("step-if").WithContexts(stepIfContexts...)
180	var condition string
181	switch s := tok.(type) {
182	case nil:
183	case *template.BasicExpression:
184		condition = s.Expression
185	default:
186		condition = assertString(tok, "step if").Value
187	}
188	final := "success()"
189	if !isNullOrWhiteSpace(condition) {
190		node, err := expr.Parse(condition, def.NamedContexts(), def.Functions())
191		if err != nil {
192			l.ctx.ErrorAt(tok, err.Error())
193			return nil
194		}
195		final = condition
196		if node != nil && !workflow.HasStatusFunction(node) {
197			final = "success() && (" + condition + ")"
198		}
199	}
200	var o template.Origin
201	if tok != nil {
202		o = template.Origin{File: tok.File(), Pos: tok.Pos()}
203	}
204	return template.NewExpression(o, final, def)
205}

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