1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6 "context"
7 "net/http"
8 "strconv"
9 "strings"
10 "sync/atomic"
11 "time"
12
13 "bnema.dev/ermite-actions/action"
14 "bnema.dev/ermite-actions/workflow"
15)
16
17// ResolvedAction is where to download one action (ermite.ResolvedAction, see
18// the host). The Token is a secret: the executor masks it.
19type ResolvedAction struct {
20 Ref, NameWithOwner, Path, ResolvedSHA, TarballURL, Token, Error string
21 // AuthOrigin is the only origin (scheme://host[:port]) the Token is sent to;
22 // the host sets it. Without it the token is not sent.
23 AuthOrigin string
24}
25
26// Resolver resolves "owner/repo[/path]@ref" values of uses with the server. It
27// is the host's call (the job token stays out of the sandbox); the result has
28// one entry per value, in order.
29type Resolver func(ctx context.Context, uses []string) ([]ResolvedAction, error)
30
31// ActionsConfig is what the executor needs to run actions.
32type ActionsConfig struct {
33 // Resolver is required to run actions from repositories; without it a job
34 // that uses one fails "Set up job".
35 Resolver Resolver
36 // HTTPClient downloads the archives; nil means a client with the default
37 // transport. It must not follow a redirect to another host with the
38 // credentials (net/http does not).
39 HTTPClient *http.Client
40 // ExternalsDir is where Node.js lives (<dir>/node20/bin/node); empty means
41 // /home/runner/externals.
42 ExternalsDir string
43 // DockerPath is the docker CLI; empty means the one found in PATH.
44 DockerPath string
45 // Node24ByDefault is the server flag actions.runner.usenode24bydefault
46 // (HandlerFactory.cs, NodeUtil.DetermineActionsNodeVersion): ermite has no
47 // feature flags, so the default is pinned here.
48 Node24ByDefault bool
49 // RequireNode24 is the server flag actions.runner.requirenode24 (phase 3
50 // of the migration): every node20 action runs on node24 whatever
51 // FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 and ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION
52 // say, without the "being deprecated" notice or the conflict warning.
53 // github.com does so today (recordings act-node-*). The zero value is the
54 // runner's phase 1; the guest sets both flags (see DefaultActionsConfig).
55 RequireNode24 bool
56 // Containers are the time limits of the docker commands of job containers
57 // and services; the zero value means the defaults.
58 Containers ContainerTimeouts
59 // Backoff, when set, replaces the random 10 to 30 s wait between download
60 // attempts (tests).
61 Backoff func() time.Duration
62}
63
64// DefaultActionsConfig is the configuration of a job in the sandbox: GitHub's
65// current node migration phase, in which node12, node16 and node20 actions run
66// on node24 and the job ends with the "forced to run on Node.js 24" warning.
67func DefaultActionsConfig(r Resolver) ActionsConfig {
68 return ActionsConfig{Resolver: r, Node24ByDefault: true, RequireNode24: true}
69}
70
71const defaultExternalsDir = "/home/runner/externals"
72
73// stage is ActionRunStage.
74type stage int
75
76const (
77 stageMain stage = iota
78 stagePre
79 stagePost
80)
81
82// compositeMaxDepth is Constants.CompositeActionsMaxDepth
83// (Runner.Common/Constants.cs:46).
84const compositeMaxDepth = 9
85
86// kind is what an action runs.
87type kind int
88
89const (
90 kindScript kind = iota
91 kindNode
92 kindDocker
93 kindComposite
94)
95
96// actionDef is a loaded action (ActionManager.LoadAction's Definition).
97type actionDef struct {
98 kind kind
99 // dir is the directory of the action ("" for docker://).
100 dir string
101 act *action.Action // nil for docker:// and for a Dockerfile without manifest
102 // image is the "runs.image" of a container action, or the image of a
103 // docker:// step; "" for Dockerfile-only actions (see dockerfile).
104 image string
105 // dockerfile is the path of the Dockerfile of a Dockerfile-only action.
106 dockerfile string
107}
108
109func (d *actionDef) runs() *action.Runs {
110 if d.act == nil {
111 return nil
112 }
113 return d.act.Runs
114}
115
116// preIf and postIf are InitCondition and CleanupCondition: always() unless the
117// manifest says otherwise (ActionExecutionData, ActionManager.cs:1884-1900).
118func (d *actionDef) preIf() string { return condOrAlways(d.runs(), true) }
119func (d *actionDef) postIf() string { return condOrAlways(d.runs(), false) }
120
121func condOrAlways(r *action.Runs, pre bool) string {
122 if r == nil || r.Kind == action.RunsComposite {
123 return "always()"
124 }
125 c := r.PostIf
126 if pre {
127 c = r.PreIf
128 }
129 if strings.TrimSpace(c) == "" {
130 return "always()"
131 }
132 return c
133}
134
135// node is one step of the job or of a composite action, with what the runner
136// keeps per action step: its intra-action state, and for a composite its
137// children and its pre and post steps (CompositeActionExecutionData).
138type node struct {
139 st *workflow.Step
140 uses action.Uses
141 parent *node
142 // scope is the name of the steps context of the step's children: the
143 // fully qualified context name (ExecutionContext.GetFullyQualifiedContextName).
144 scope string
145 // stateKey names the intra-action state of the action (job.saveState): the
146 // pre, main and post stages of an action, and its inner steps, share it
147 // (the runner keys it by the id of the step, FileCommandManager.cs:305-318).
148 stateKey string
149 // children are the nodes of a composite action's steps.
150 children []*node
151 // preSteps and postSteps are the embedded steps with a pre or a post, in
152 // declaration order (the runner's post steps are a stack: they run last
153 // to first).
154 preSteps, postSteps []*node
155 // image is the image the setup steps pulled or built for the step.
156 image string
157 def *actionDef
158 // postRegistered is Root.StepsWithPostRegistered / EmbeddedStepsWithPostRegistered.
159 postRegistered bool
160 postCond string
161 // runtimePrepared marks the steps of a local composite action, which are
162 // prepared when the action runs (see registrar).
163 runtimePrepared bool
164}
165
166// nodeSeq numbers the nodes, for their state keys.
167var nodeSeq atomic.Int64
168
169// newNodes makes the nodes of steps. The workflow reader has checked every
170// uses value.
171func newNodes(steps []*workflow.Step, parent *node, scope string) ([]*node, error) {
172 out := make([]*node, 0, len(steps))
173 for _, st := range steps {
174 n := &node{st: st, parent: parent, stateKey: "node-" + strconv.FormatInt(nodeSeq.Add(1), 10)}
175 n.scope = st.ID
176 if scope != "" {
177 n.scope = scope + "." + st.ID
178 }
179 if st.Uses != nil {
180 u, err := action.ParseUses(st.Uses.Value)
181 if err != nil {
182 return nil, err
183 }
184 n.uses = u
185 }
186 out = append(out, n)
187 }
188 return out, nil
189}