1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6 "fmt"
7 "path/filepath"
8 "strings"
9
10 "bnema.dev/ermite-actions/action"
11)
12
13// stageName is for messages.
14func (s stage) String() string {
15 switch s {
16 case stagePre:
17 return "Pre"
18 case stagePost:
19 return "Post"
20 }
21 return "Main"
22}
23
24// definition is ActionManager.LoadAction for the step: the manifest of a local
25// action is read when the step runs; the other actions were loaded by the
26// pre-job phase.
27func (j *job) definition(s *stepRun) (*actionDef, error) {
28 n := s.n
29 switch n.uses.Kind {
30 case action.UsesDocker:
31 return &actionDef{kind: kindDocker, image: n.uses.Image}, nil
32 case action.UsesLocal:
33 dir := j.actionDir(n)
34 def, err := j.loadDef(s.num, dir)
35 if err == errNoAction {
36 return nil, ghErrorf("Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '%s'. Did you forget to run actions/checkout before running your local action?", filepath.Clean(dir))
37 }
38 if err != nil {
39 return nil, err
40 }
41 n.def = def
42 return def, nil
43 }
44 if n.def == nil {
45 return nil, ghErrorf("The action '%s' was not prepared", refString(n.uses))
46 }
47 return n.def, nil
48}
49
50// setActionRepository sets github.action_repository and action_ref
51// (ActionRunner.cs:149-165): the repository and ref of a repository action, empty
52// for anything else.
53func (j *job) setActionRepository(n *node) {
54 repo, ref := "", ""
55 if n != nil && n.uses.Kind == action.UsesRepository {
56 repo, ref = n.uses.Name, n.uses.Ref
57 }
58 j.mu.Lock()
59 j.state.SetGitHub("action_repository", repo)
60 j.state.SetGitHub("action_ref", ref)
61 j.mu.Unlock()
62}
63
64// registerPost registers the post stage of the action (ActionRunner.cs:99-126,
65// ExecutionContext.RegisterPostJobStep): a step "Post <name>" on the stack the
66// job runs after its steps, last registered first; inside a composite action
67// only the fact is recorded, the composite's own post runs the step.
68func (j *job) registerPost(s *stepRun, def *actionDef) {
69 n := s.n
70 name := s.name
71 if s.stage == stagePre && strings.HasPrefix(strings.ToLower(name), "pre ") {
72 name = name[len("Pre "):]
73 }
74 j.debug(s.num, "Register post job cleanup for action: "+registeredRef(n.uses))
75 if s.parent != nil {
76 n.postRegistered, n.postCond = true, def.postIf()
77 return
78 }
79 if n.postRegistered {
80 return // StepsWithPostRegistered
81 }
82 n.postRegistered = true
83 j.mu.Lock()
84 j.postStack = append(j.postStack, qstep{n: n, stage: stagePost, name: "Post " + name, cond: def.postIf()})
85 j.mu.Unlock()
86}
87
88// registeredRef is the text of the debug line of the registration.
89func registeredRef(u action.Uses) string {
90 path := ""
91 if u.Path != "" {
92 path = "/" + u.Path
93 }
94 if u.Ref == "" {
95 return u.Name + path
96 }
97 return u.Name + path + "@" + u.Ref
98}
99
100// handlerGroup is Handler.PrintActionDetails's group name.
101func handlerGroup(u action.Uses) string {
102 switch u.Kind {
103 case action.UsesDocker:
104 return "Run docker://" + u.Image
105 case action.UsesLocal:
106 return "Run " + u.Path
107 }
108 if u.Path == "" {
109 return fmt.Sprintf("Run %s@%s", u.Name, u.Ref)
110 }
111 return fmt.Sprintf("Run %s/%s@%s", u.Name, u.Path, u.Ref)
112}
113
114// printDetails is Handler.PrintActionDetails (Handler.cs:102-165): the group
115// of the step with its inputs that are not empty and its environment. The post
116// stage only prints "Post job cleanup.".
117func (j *job) printDetails(s *stepRun, in *inputSet, env *ordered) {
118 num := s.num
119 if s.stage == stagePost {
120 j.out(num, "Post job cleanup.")
121 return
122 }
123 j.out(num, "##[group]"+handlerGroup(s.n.uses))
124 if in.Len() > 0 {
125 j.out(num, "with:")
126 for _, k := range in.keys {
127 if v, _ := in.Get(k); v != "" {
128 j.out(num, fmt.Sprintf(" %s: %s", k, v))
129 }
130 }
131 }
132 if env.Len() > 0 {
133 j.out(num, "env:")
134 for _, k := range env.Keys() {
135 v, _ := env.Get(k)
136 j.out(num, fmt.Sprintf(" %s: %s", k, v))
137 }
138 }
139 j.out(num, "##[endgroup]")
140}
141
142// runAction is ActionRunner.RunAsync (ActionRunner.cs:62-247) for a uses step
143// in any stage: load the action, evaluate the inputs and the environment, print
144// the details and run the handler of the action.
145func (j *job) runAction(s *stepRun, cancel <-chan struct{}) stepResult {
146 num, n := s.num, s.n
147 fail := func(err error) stepResult { j.errorf(num, "%s", err.Error()); return resFailed }
148 def, err := j.definition(s)
149 if err != nil {
150 return fail(err)
151 }
152 s.def = def
153 local := n.uses.Kind == action.UsesLocal
154 // A local composite action is expanded when it runs (ActionRunner.cs:77-96).
155 if def.kind == kindComposite && s.stage == stageMain && local {
156 if err := j.prepareLocalComposite(s, def, cancel); err != nil {
157 if err == errSetupCancelled {
158 return resCanceled // logged by the command that was stopped
159 }
160 // What the cancellation or the timeout of the step broke (a resolution,
161 // a download, an extraction) is not a failure of the action: the step is
162 // cancelled, and StepsRunner turns it into the timeout message when its
163 // own timer fired. A genuine error keeps failing the step.
164 if done(cancel) {
165 s.logCanceled()
166 return resCanceled
167 }
168 return fail(err)
169 }
170 }
171 if local && n.hasPre() {
172 j.tagged(num, "##[warning]", fmt.Sprintf("`pre` execution is not supported for local action from '%s'", n.uses.Path))
173 }
174 if n.hasPostOf(def) && s.stage != stagePost {
175 j.registerPost(s, def)
176 }
177 j.setActionRepository(n)
178 // ActionRunner.cs:157-164: with a job container, the container must exist.
179 if err := j.containerReady(); err != nil {
180 return fail(err)
181 }
182 if err := j.prepareFileCommands(s); err != nil {
183 return fail(err)
184 }
185 defer s.fc.Process(s)
186
187 j.debug(num, "Loading inputs")
188 in, err := j.evalInputs(s.st, s.env)
189 if err != nil {
190 j.errorf(num, "%s", templateNotValid(err, true))
191 return resFailed
192 }
193 if err := j.mergeInputs(s, def, in); err != nil {
194 j.errorf(num, "%s", templateNotValid(err, true))
195 return resFailed
196 }
197 j.debug(num, "Loading env")
198 env := s.env.Clone()
199 // The intra-action state, applied last (ActionRunner.cs:255-258): names as
200 // first spelled, in the order they were saved.
201 for _, p := range j.intraActionState(n.stateKey) {
202 env.Set("STATE_"+p.Key, p.Value)
203 }
204 s.actEnv = env
205 if err := j.nodeVersionFor(s, def); err != nil {
206 return fail(err)
207 }
208 j.printDetails(s, in, env)
209 switch def.kind {
210 case kindNode:
211 return j.runNode(s, def, in, env, cancel)
212 case kindDocker:
213 return j.runDocker(s, def, in, env, cancel)
214 case kindComposite:
215 return j.runComposite(s, def, in, env)
216 }
217 return fail(ghErrorf("Unsupported action type: %v", def.kind))
218}
219
220// hasPostOf is HasPost of the loaded definition of the node.
221func (n *node) hasPostOf(def *actionDef) bool {
222 switch def.kind {
223 case kindComposite:
224 return len(n.postSteps) > 0
225 case kindScript:
226 return false
227 }
228 if r := def.runs(); r != nil {
229 return r.HasPost()
230 }
231 return false
232}