fix/exec-umask 2cd66dc

actions_prepare.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6	"context"
7	"strings"
8
9	"bnema.dev/ermite-actions/action"
10)
11
12// actionState is what the pre-job phase leaves for the run.
13type actionState struct {
14	nodes []*node
15	// pulled and built map an image to what the setup steps produced.
16	pulled map[string]string // image -> image
17	built  map[string]string // action repository -> tag
18	// setup are the steps that pull and build images, in the order of
19	// ActionManager.PrepareActionsAsync (pulls, then builds).
20	pullOrder, buildOrder []string
21	buildInfo             map[string]buildInfo
22	pullUsers             map[string][]*node
23	buildUsers            map[string][]*node
24	// pre are the pre steps of the job's steps with a pre stage, by node.
25	pre map[*node]bool
26	// resolved caches the download info by "name@ref" (ActionManager's legacy
27	// path asks again at every level; ermite's server caches per run).
28	downloaded map[string]bool
29	// infos are the resolutions of the job by "Name@Ref": an action that a local
30	// composite action brings again is not asked for again.
31	infos map[string]ResolvedAction
32}
33
34type buildInfo struct{ repository, dockerfile, workDir string }
35
36func newActionState() *actionState {
37	return &actionState{
38		pulled: map[string]string{}, built: map[string]string{}, buildInfo: map[string]buildInfo{},
39		pullUsers: map[string][]*node{}, buildUsers: map[string][]*node{}, pre: map[*node]bool{},
40		downloaded: map[string]bool{}, infos: map[string]ResolvedAction{},
41	}
42}
43
44// lookupKey is ActionManager.GetDownloadInfoLookupKey: "Name@Ref" for a
45// repository action, "" for a local one.
46func lookupKey(n *node) (string, error) {
47	switch n.uses.Kind {
48	case action.UsesRepository:
49		return n.uses.Name + "@" + n.uses.Ref, nil
50	case action.UsesSelfRepository:
51		return "", ghErrorf("Unable to resolve self-reference '$/'. This can occur when the server does not support this syntax, the feature flag is disabled, or the workflow context (repository/SHA) is unavailable.")
52	}
53	return "", nil
54}
55
56// prepare is ActionManager.PrepareActionsRecursiveLegacyAsync
57// (ActionManager.cs:439-654): resolve and download the repository actions of
58// nodes, read their manifests, recurse into composite actions, and note the
59// images to pull or build and the pre and post stages. depth is 0 for the
60// steps of the job and 1 for the ones of a local composite action prepared when
61// it runs.
62func (j *job) prepare(ctx context.Context, num int, nodes []*node, depth int, parent *node) error {
63	if depth > compositeMaxDepth {
64		return ghErrorf("Composite action depth exceeded max depth %d", compositeMaxDepth)
65	}
66	st := j.actions
67	var repoNodes []*node
68	for _, n := range nodes {
69		switch n.uses.Kind {
70		case action.UsesDocker:
71			if n.uses.Image == "" {
72				return ghErrorf("Value cannot be null or empty. (Parameter 'Image')")
73			}
74			if _, ok := st.pullUsers[n.uses.Image]; !ok {
75				st.pullOrder = append(st.pullOrder, n.uses.Image)
76			}
77			st.pullUsers[n.uses.Image] = append(st.pullUsers[n.uses.Image], n)
78		case action.UsesRepository, action.UsesLocal, action.UsesSelfRepository:
79			repoNodes = append(repoNodes, n)
80		}
81	}
82	if len(repoNodes) == 0 {
83		return nil
84	}
85	infos, err := j.downloadInfo(ctx, num, repoNodes)
86	if err != nil {
87		return err
88	}
89	for _, n := range repoNodes {
90		key, err := lookupKey(n)
91		if err != nil {
92			return err
93		}
94		if key == "" {
95			continue
96		}
97		ra, ok := infos[key]
98		if !ok {
99			return ghErrorf("Missing download info for %s", key)
100		}
101		dir := j.actionDirOfRepo(n)
102		// The directory of the archive is the one of the repository itself: the
103		// action's path is below it.
104		root := j.state.Layout().Actions + "/" + n.uses.Name + "/" + n.uses.Ref
105		_ = dir
106		if err := j.downloadAction(ctx, num, ra, ra.NameWithOwner, n.uses.Ref, root); err != nil {
107			return err
108		}
109	}
110	for _, n := range repoNodes {
111		if err := j.prepareRepositoryAction(ctx, num, n, depth); err != nil {
112			return err
113		}
114	}
115	return nil
116}
117
118func (j *job) actionDirOfRepo(n *node) string { return j.actionDir(n) }
119
120// downloadInfo is ActionManager.GetDownloadInfoAsync (ActionManager.cs:1000-1135):
121// it asks the server, through the host, about the distinct repository actions
122// of nodes. The server's answer for one action may be an error in GitHub's
123// words, which is the message of the failure.
124func (j *job) downloadInfo(ctx context.Context, num int, nodes []*node) (map[string]ResolvedAction, error) {
125	j.out(num, "Getting action download info")
126	var uses, keys []string
127	seen := map[string]bool{}
128	for _, n := range nodes {
129		key, err := lookupKey(n)
130		if err != nil {
131			return nil, err
132		}
133		if key == "" || seen[key] {
134			continue
135		}
136		seen[key] = true
137		keys = append(keys, key)
138		uses = append(uses, refString(n.uses))
139	}
140	out := map[string]ResolvedAction{}
141	if len(uses) == 0 {
142		return out, nil
143	}
144	// What the job resolved already is not asked again: the host would not call
145	// the server for it either (host/resolve.go), but the round trip is saved.
146	var askUses, askKeys []string
147	for i, k := range keys {
148		if ra, ok := j.actions.infos[k]; ok {
149			out[k] = ra
150			continue
151		}
152		askUses, askKeys = append(askUses, uses[i]), append(askKeys, k)
153	}
154	if len(askUses) == 0 {
155		return out, nil
156	}
157	uses, keys = askUses, askKeys
158	if j.cfg.Actions.Resolver == nil {
159		return nil, ghErrorf("Failed to resolve action download info.")
160	}
161	res, err := j.cfg.Actions.Resolver(ctx, uses)
162	if err != nil {
163		return nil, ghErrorf("%s", err.Error())
164	}
165	if len(res) != len(uses) {
166		return nil, ghErrorf("Failed to resolve action download info.")
167	}
168	for i, ra := range res {
169		if ra.Error != "" {
170			return nil, ghErrorf("%s", ra.Error)
171		}
172		// The token is a secret from here on (ActionManager.cs:1130).
173		j.addTokenMask(ra.Token)
174		if ra.NameWithOwner == "" {
175			ra.NameWithOwner = strings.SplitN(keys[i], "@", 2)[0]
176		}
177		out[keys[i]] = ra
178		j.actions.infos[keys[i]] = ra
179	}
180	return out, nil
181}
182
183// prepareRepositoryAction is ActionManager.PrepareRepositoryActionAsync
184// (:1411-1550) and the code after it in the loop of the caller: read the
185// manifest, note an image to pull or build, recurse into a composite, and
186// register pre and post stages.
187func (j *job) prepareRepositoryAction(ctx context.Context, num int, n *node, depth int) error {
188	local := n.uses.Kind == action.UsesLocal
189	if local {
190		if depth > 0 {
191			// "if we're in a composite action and haven't loaded the local action
192			// yet we assume it has a post step" (ActionManager.cs:643-654).
193			n.parentPost()
194		}
195		return nil
196	}
197	dir := j.actionDir(n)
198	def, err := j.loadDef(num, dir)
199	if err == errNoAction {
200		return ghErrorf("Can't find 'action.yml', 'action.yaml' or 'Dockerfile' for action '%s'.", refString(n.uses))
201	}
202	if err != nil {
203		return err
204	}
205	n.def = def
206	repo := n.uses.Name + "@" + n.uses.Ref
207	if n.uses.Path != "" {
208		repo = n.uses.Name + "/" + n.uses.Path + "@" + n.uses.Ref
209	}
210	root := j.state.Layout().Actions + "/" + n.uses.Name + "/" + n.uses.Ref
211	st := j.actions
212	switch def.kind {
213	case kindDocker:
214		img, build, err := j.dockerImageOf(def)
215		if err != nil {
216			return err
217		}
218		if build != "" {
219			if _, ok := st.buildUsers[repo]; !ok {
220				st.buildOrder = append(st.buildOrder, repo)
221			}
222			st.buildUsers[repo] = append(st.buildUsers[repo], n)
223			st.buildInfo[repo] = buildInfo{repository: repo, dockerfile: build, workDir: root}
224		} else {
225			if _, ok := st.pullUsers[img]; !ok {
226				st.pullOrder = append(st.pullOrder, img)
227			}
228			st.pullUsers[img] = append(st.pullUsers[img], n)
229		}
230	case kindComposite:
231		if err := j.prepareComposite(ctx, num, n, depth); err != nil {
232			return err
233		}
234	}
235	j.registerStages(n, depth)
236	return nil
237}
238
239// dockerImageOf says what a container action needs: an image to pull, or a
240// Dockerfile to build (absolute path).
241func (j *job) dockerImageOf(def *actionDef) (pull, build string, err error) {
242	if def.act == nil {
243		return "", def.dockerfile, nil
244	}
245	im, err := action.ParseImage(def.image)
246	if err != nil {
247		return "", "", err
248	}
249	if im.Kind == action.ImageDockerfile {
250		return "", pathCombine(def.dir, im.Path), nil
251	}
252	return im.Ref, "", nil
253}
254
255// prepareComposite creates the nodes of a composite action's steps and prepares
256// them one level down.
257func (j *job) prepareComposite(ctx context.Context, num int, n *node, depth int) error {
258	steps := n.def.act.Runs.Steps
259	kids, err := newNodes(steps, n, n.scope)
260	if err != nil {
261		return err
262	}
263	n.children = kids
264	return j.prepare(ctx, num, kids, depth+1, n)
265}
266
267// registerStages notes the pre and post stages of a prepared action that is not
268// local (ActionManager.cs:600-640): for the steps of the job a pre step, for
269// the steps of a composite action a pre and post step of the composite.
270func (j *job) registerStages(n *node, depth int) {
271	pre, post := n.hasPre(), n.hasPost()
272	if depth < 1 {
273		if pre {
274			j.actions.pre[n] = true
275		}
276		return
277	}
278	if p := n.registrar(); p != nil {
279		if pre {
280			p.preSteps = append(p.preSteps, n)
281		}
282		if post {
283			p.postSteps = append(p.postSteps, n)
284		}
285	}
286}
287
288// registrar is the node whose pre and post steps the node is registered with:
289// its parent, except for the steps of a local composite action prepared when
290// the action runs. The runner keys what it registers there by the id of the
291// execution context (ActionManager.cs:92-113 and :424-430, rootStepId =
292// ExecutionContext.Id), and an inner step's context shares the record, hence
293// the id, of the step of the job that contains it: everything lands on that
294// step, and the inner composite actions themselves never get a post of their
295// own (recorded: one "Post job cleanup." for act-composite-depth, not one per
296// level).
297func (n *node) registrar() *node {
298	if n.runtimePrepared {
299		for p := n.parent; p != nil; p = p.parent {
300			if p.parent == nil {
301				return p
302			}
303		}
304	}
305	return n.parent
306}
307
308// parentPost is the assumption for a local action inside a composite action.
309func (n *node) parentPost() {
310	if p := n.registrar(); p != nil {
311		p.postSteps = append(p.postSteps, n)
312	}
313}
314
315// hasPre and hasPost are ActionExecutionData.HasPre and HasPost.
316func (n *node) hasPre() bool {
317	d := n.def
318	if d == nil {
319		return false
320	}
321	if d.kind == kindComposite {
322		return len(n.preSteps) > 0
323	}
324	if r := d.runs(); r != nil {
325		return r.HasPre()
326	}
327	return false
328}
329
330func (n *node) hasPost() bool {
331	d := n.def
332	if d == nil {
333		return false
334	}
335	if d.kind == kindComposite {
336		return len(n.postSteps) > 0
337	}
338	if r := d.runs(); r != nil {
339		return r.HasPost()
340	}
341	return false
342}

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