v0.2.0 43088fd

actions_composite.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6	"context"
7
8	"bnema.dev/ermite-actions/expr"
9)
10
11// compositeGitHubKeys are the keys of the github context that a composite
12// action's steps change: the runner gives them a copy of the context
13// (CompositeActionHandler.cs:152-161), the executor restores these afterwards.
14func compositeGitHubKeys() []string {
15	keys := []string{"action_path", "action_status", "action_repository", "action_ref", "event_path"}
16	for _, c := range fileCommands {
17		keys = append(keys, c.ContextName)
18	}
19	return keys
20}
21
22// maxLocalDepth is the nesting of composite actions the executor allows. It is
23// ermite's guard against a local composite action that uses itself, not a limit
24// of GitHub's, whose runner has none for local actions.
25const maxLocalDepth = 100
26
27// prepareLocalComposite is the first part of ActionRunner.RunAsync for a local
28// composite action (ActionRunner.cs:77-96): its steps are prepared when the
29// step runs, in the log of the step, and the pulls and builds they need run
30// first as embedded steps.
31func (j *job) prepareLocalComposite(s *stepRun, def *actionDef, cancel <-chan struct{}) error {
32	n := s.n
33	if n.children != nil {
34		return nil // pre and post stages reuse the nodes of the main stage
35	}
36	// Local composite actions have no depth limit on GitHub: the recording of
37	// ermite-conformance corpus/act-composite-depth has composites of 9, 10 and 12
38	// levels, all of which succeed. The runner prepares the steps of a local
39	// composite when its step runs, with the depth restarting at 1
40	// (ActionRunner.cs:77-96), which is why. This port does the same, and the
41	// resolutions it needs are cached and budgeted by the host (host/resolve.go).
42	//
43	// The one guard that is ermite's own, not GitHub's: a local composite that
44	// uses itself (directly or in a cycle) would recurse until the executor
45	// crashes, so the nesting of composite actions is capped at maxLocalDepth.
46	if d := s.depth(); d >= maxLocalDepth {
47		return ghErrorf("Composite action depth exceeded max depth %d", maxLocalDepth)
48	}
49	kids, err := newNodes(def.act.Runs.Steps, n, n.scope)
50	if err != nil {
51		return err
52	}
53	for _, k := range kids {
54		k.runtimePrepared = true
55	}
56	n.children = kids
57	n.preSteps, n.postSteps = nil, nil
58	j.out(s.num, "Prepare all required actions")
59	if j.actions == nil {
60		j.actions = newActionState()
61	}
62	saved := *j.actions
63	j.actions.pullOrder, j.actions.buildOrder = nil, nil
64	j.actions.pullUsers, j.actions.buildUsers = map[string][]*node{}, map[string][]*node{}
65	// The preparation stops with the step, and only with it: its context ends when
66	// the cancel channel of the step closes (job cancellation for a step that does
67	// not run on always(), timeout-minutes). A step that keeps running after the
68	// job was cancelled keeps the means to prepare its composite.
69	ctx, stop := context.WithCancel(context.Background())
70	defer stop()
71	if cancel != nil {
72		go func() {
73			select {
74			case <-cancel:
75				stop()
76			case <-ctx.Done():
77			}
78		}()
79	}
80	if err := j.prepare(ctx, s.num, kids, 1, n); err != nil {
81		return err
82	}
83	setups := j.setupSteps()
84	j.actions.pullOrder, j.actions.buildOrder = saved.pullOrder, saved.buildOrder
85	j.actions.pullUsers, j.actions.buildUsers = saved.pullUsers, saved.buildUsers
86	for _, q := range setups {
87		if r := q.setup(s, cancel); r != resSucceeded {
88			if r == resCanceled {
89				return errSetupCancelled
90			}
91			return errSetupFailed
92		}
93	}
94	return nil
95}
96
97// errSetupFailed ends a composite whose image setup failed; the cause is in the
98// log already.
99// errSetupCancelled: the step was cancelled while its images were prepared; the
100// cancellation is logged by the docker command already.
101var errSetupCancelled = ghErrorf("The operation was canceled.")
102
103var errSetupFailed = ghErrorf("A container setup step of the composite action failed.")
104
105// runComposite is CompositeActionHandler.RunAsync (CompositeActionHandler.cs:
106// 30-207) and RunStepsAsync: the steps of the stage run in their own steps
107// context with the inputs of the action, and the outputs are mapped from them.
108func (j *job) runComposite(s *stepRun, def *actionDef, in *inputSet, env *ordered) stepResult {
109	n := s.n
110	var kids []*node
111	switch s.stage {
112	case stagePre:
113		kids = n.preSteps
114	case stagePost:
115		// Only the steps that registered a post, last registered first.
116		for i := len(n.postSteps) - 1; i >= 0; i-- {
117			if c := n.postSteps[i]; c.postRegistered {
118				kids = append(kids, c)
119			}
120		}
121	default:
122		kids = n.children
123	}
124	inputs := expr.NewDictionary()
125	for _, k := range in.keys {
126		v, _ := in.Get(k)
127		inputs.Add(k, expr.String(v))
128	}
129	// The pre stage has no context name in the runner, so its steps context is a
130	// random one; post shares the scope of main (siblingScopeName).
131	scope := n.scope
132	if s.stage == stagePre {
133		scope = "__pre_" + n.scope
134	}
135	j.mu.Lock()
136	restoreGH := j.state.GitHubKeys(compositeGitHubKeys()...)
137	restoreScope := j.state.PushScope(scope, inputs)
138	j.state.SetGitHub("action_path", def.dir)
139	j.mu.Unlock()
140	restored := false
141	restore := func() {
142		if !restored {
143			restored = true
144			restoreScope()
145			restoreGH()
146		}
147	}
148	defer func() {
149		j.mu.Lock()
150		restore()
151		j.mu.Unlock()
152	}()
153	ok := resSucceeded
154	s.result = &ok
155	for _, c := range kids {
156		j.runEmbedded(s, c, scope)
157	}
158	// Outputs are evaluated in the steps context of the action and belong to the
159	// step that uses it (ExecutionContext.SetOutput of the composite step).
160	var outs []Pair
161	if s.stage == stageMain {
162		outs = j.compositeOutputs(s, def)
163	}
164	j.mu.Lock()
165	restore()
166	for _, o := range outs {
167		j.state.SetOutput(s.st.ID, o.Key, o.Value)
168	}
169	j.mu.Unlock()
170	return *s.result
171}
172
173// compositeOutputs is ProcessOutputs (CompositeActionHandler.cs:192-228): the
174// "value" of each output of the action, evaluated with the steps and inputs
175// of the action.
176func (j *job) compositeOutputs(s *stepRun, def *actionDef) []Pair {
177	var out []Pair
178	j.mu.Lock()
179	defer j.mu.Unlock()
180	for _, o := range def.act.Outputs {
181		if o.Value == nil {
182			continue
183		}
184		v, err := j.evalString(o.Value, s.env)
185		if err != nil {
186			j.mu.Unlock()
187			j.errorf(s.num, "%s", templateNotValid(err, true))
188			j.mu.Lock()
189			m := resFailed
190			s.result = &m
191			return nil
192		}
193		out = append(out, Pair{o.Name, v})
194	}
195	return out
196}
197
198// runEmbedded is one iteration of CompositeActionHandler.RunStepsAsync: the
199// step of the composite action runs inside the step that uses it, with its own
200// environment, condition, cancellation watch and result. Its log lines belong
201// to the outer step.
202//
203// scope is the steps context of the action; with the id of the step it is the
204// id of the step's markers (the fully qualified context name).
205func (j *job) runEmbedded(parent *stepRun, c *node, scope string) {
206	s := &stepRun{j: j, num: parent.num, st: c.st, n: c, stage: parent.stage, parent: parent, fc: j.fc.Fork()}
207	// The issues, summaries and state of an inner step belong to the step that
208	// uses the action, whose collector counts them (10 per level, the outer
209	// step's own first).
210	s.scope = j.embeddedScope(parent, c.stateKey)
211	defer s.fc.Cleanup()
212	s.cmds = newCommandManager(s, j.cfg.Debug)
213	j.mu.Lock()
214	// github.action_status: the result of the composite action so far.
215	j.state.SetGitHub("action_status", parent.actionStatus())
216	j.mu.Unlock()
217	env, envErr := j.stepEnvironment(s)
218	s.env = env
219	s.name = j.displayNameOf(c.st, env, true)
220	// The display name is masked before it is cut and escaped. The runner masks
221	// the finished line (ExecutionContext.Write), after SanitizeDisplayName and
222	// EscapeValue, so a secret holding ";", "]" or "%" (escaped by then) or cut by
223	// the 1000-character limit would escape its masker. Masking first is stricter;
224	// the line is masked again when it is written, as every line is.
225	id := scope + "." + c.st.ID
226	if envErr != nil {
227		// The error of the environment is logged and the step marked failed
228		// (CompositeActionHandler.cs:296-302, SetStepConclusion :529-533), then the
229		// runner does not stop: it prints the start marker (:308-324), evaluates
230		// the condition (:372-390; success() of an embedded main step reads
231		// github.action_status, set from the composite's result before the env was
232		// evaluated, :246-248, SuccessFunction.cs:28, so the default condition
233		// still holds) and runs the step (RunStepAsync :481-527) without the env
234		// that failed. Its result stays Failed (nothing resets it), and the end
235		// marker (:431-440) carries it with the elapsed time.
236		j.errorf(s.num, "%s", templateNotValid(envErr, true))
237		s.envFailed = true
238		// SetStepConclusion publishes Failed to the steps context at once
239		// (:529-533, UpdateGlobalStepsContext), so the condition of the step, and
240		// of the steps after it, see steps.<id>.outcome == 'failure'. Nothing is
241		// merged into the composite's result yet: that is done when the step ends
242		// (:505-512, finishStep), which also replaces this entry with the final
243		// outcome and conclusion.
244		j.mu.Lock()
245		j.state.SetStepResult(c.st.ID, string(resFailed.conclusion()), string(resFailed.conclusion()))
246		j.mu.Unlock()
247	}
248	q := qstep{n: c, stage: parent.stage}
249	switch parent.stage {
250	case stagePre:
251		if c.def != nil {
252			q.cond = c.def.preIf()
253		}
254	case stagePost:
255		q.cond = c.postCond
256	}
257	j.out(parent.num, startMarker(j.mask(s.name), id))
258	start := j.now()
259	r := j.runGuarded(s, q, true)
260	// Outcome is the result before continue-on-error, conclusion the final one;
261	// a skipped step takes no time (CompositeActionHandler.cs:415, :448).
262	outcome := r
263	if s.outcome != nil {
264		outcome = *s.outcome
265	}
266	ms := j.now().Sub(start).Milliseconds()
267	if r == resSkipped {
268		ms = 0
269	}
270	j.out(parent.num, endMarker(id, markerResult(outcome), markerResult(r), ms))
271}

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 4

Tags 11

↑↓ moveenter openesc close