// SPDX-License-Identifier: MIT package executor import ( "context" "net/http" "strconv" "strings" "sync/atomic" "time" "bnema.dev/ermite-actions/action" "bnema.dev/ermite-actions/workflow" ) // ResolvedAction is where to download one action (ermite.ResolvedAction, see // the host). The Token is a secret: the executor masks it. type ResolvedAction struct { Ref, NameWithOwner, Path, ResolvedSHA, TarballURL, Token, Error string // AuthOrigin is the only origin (scheme://host[:port]) the Token is sent to; // the host sets it. Without it the token is not sent. AuthOrigin string } // Resolver resolves "owner/repo[/path]@ref" values of uses with the server. It // is the host's call (the job token stays out of the sandbox); the result has // one entry per value, in order. type Resolver func(ctx context.Context, uses []string) ([]ResolvedAction, error) // ActionsConfig is what the executor needs to run actions. type ActionsConfig struct { // Resolver is required to run actions from repositories; without it a job // that uses one fails "Set up job". Resolver Resolver // HTTPClient downloads the archives; nil means a client with the default // transport. It must not follow a redirect to another host with the // credentials (net/http does not). HTTPClient *http.Client // ExternalsDir is where Node.js lives (/node20/bin/node); empty means // /home/runner/externals. ExternalsDir string // DockerPath is the docker CLI; empty means the one found in PATH. DockerPath string // Node24ByDefault is the server flag actions.runner.usenode24bydefault // (HandlerFactory.cs, NodeUtil.DetermineActionsNodeVersion): ermite has no // feature flags, so the default is pinned here. Node24ByDefault bool // RequireNode24 is the server flag actions.runner.requirenode24 (phase 3 // of the migration): every node20 action runs on node24 whatever // FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 and ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION // say, without the "being deprecated" notice or the conflict warning. // github.com does so today (recordings act-node-*). The zero value is the // runner's phase 1; the guest sets both flags (see DefaultActionsConfig). RequireNode24 bool // Containers are the time limits of the docker commands of job containers // and services; the zero value means the defaults. Containers ContainerTimeouts // Backoff, when set, replaces the random 10 to 30 s wait between download // attempts (tests). Backoff func() time.Duration } // DefaultActionsConfig is the configuration of a job in the sandbox: GitHub's // current node migration phase, in which node12, node16 and node20 actions run // on node24 and the job ends with the "forced to run on Node.js 24" warning. func DefaultActionsConfig(r Resolver) ActionsConfig { return ActionsConfig{Resolver: r, Node24ByDefault: true, RequireNode24: true} } const defaultExternalsDir = "/home/runner/externals" // stage is ActionRunStage. type stage int const ( stageMain stage = iota stagePre stagePost ) // compositeMaxDepth is Constants.CompositeActionsMaxDepth // (Runner.Common/Constants.cs:46). const compositeMaxDepth = 9 // kind is what an action runs. type kind int const ( kindScript kind = iota kindNode kindDocker kindComposite ) // actionDef is a loaded action (ActionManager.LoadAction's Definition). type actionDef struct { kind kind // dir is the directory of the action ("" for docker://). dir string act *action.Action // nil for docker:// and for a Dockerfile without manifest // image is the "runs.image" of a container action, or the image of a // docker:// step; "" for Dockerfile-only actions (see dockerfile). image string // dockerfile is the path of the Dockerfile of a Dockerfile-only action. dockerfile string } func (d *actionDef) runs() *action.Runs { if d.act == nil { return nil } return d.act.Runs } // preIf and postIf are InitCondition and CleanupCondition: always() unless the // manifest says otherwise (ActionExecutionData, ActionManager.cs:1884-1900). func (d *actionDef) preIf() string { return condOrAlways(d.runs(), true) } func (d *actionDef) postIf() string { return condOrAlways(d.runs(), false) } func condOrAlways(r *action.Runs, pre bool) string { if r == nil || r.Kind == action.RunsComposite { return "always()" } c := r.PostIf if pre { c = r.PreIf } if strings.TrimSpace(c) == "" { return "always()" } return c } // node is one step of the job or of a composite action, with what the runner // keeps per action step: its intra-action state, and for a composite its // children and its pre and post steps (CompositeActionExecutionData). type node struct { st *workflow.Step uses action.Uses parent *node // scope is the name of the steps context of the step's children: the // fully qualified context name (ExecutionContext.GetFullyQualifiedContextName). scope string // stateKey names the intra-action state of the action (job.saveState): the // pre, main and post stages of an action, and its inner steps, share it // (the runner keys it by the id of the step, FileCommandManager.cs:305-318). stateKey string // children are the nodes of a composite action's steps. children []*node // preSteps and postSteps are the embedded steps with a pre or a post, in // declaration order (the runner's post steps are a stack: they run last // to first). preSteps, postSteps []*node // image is the image the setup steps pulled or built for the step. image string def *actionDef // postRegistered is Root.StepsWithPostRegistered / EmbeddedStepsWithPostRegistered. postRegistered bool postCond string // runtimePrepared marks the steps of a local composite action, which are // prepared when the action runs (see registrar). runtimePrepared bool } // nodeSeq numbers the nodes, for their state keys. var nodeSeq atomic.Int64 // newNodes makes the nodes of steps. The workflow reader has checked every // uses value. func newNodes(steps []*workflow.Step, parent *node, scope string) ([]*node, error) { out := make([]*node, 0, len(steps)) for _, st := range steps { n := &node{st: st, parent: parent, stateKey: "node-" + strconv.FormatInt(nodeSeq.Add(1), 10)} n.scope = st.ID if scope != "" { n.scope = scope + "." + st.ID } if st.Uses != nil { u, err := action.ParseUses(st.Uses.Value) if err != nil { return nil, err } n.uses = u } out = append(out, n) } return out, nil }