github.com/nektos/act@v0.2.63/pkg/runner/runner.go (about) 1 package runner 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "os" 8 "runtime" 9 10 docker_container "github.com/docker/docker/api/types/container" 11 "github.com/nektos/act/pkg/common" 12 "github.com/nektos/act/pkg/model" 13 log "github.com/sirupsen/logrus" 14 ) 15 16 // Runner provides capabilities to run GitHub actions 17 type Runner interface { 18 NewPlanExecutor(plan *model.Plan) common.Executor 19 } 20 21 // Config contains the config for a new runner 22 type Config struct { 23 Actor string // the user that triggered the event 24 Workdir string // path to working directory 25 ActionCacheDir string // path used for caching action contents 26 ActionOfflineMode bool // when offline, use caching action contents 27 BindWorkdir bool // bind the workdir to the job container 28 EventName string // name of event to run 29 EventPath string // path to JSON file to use for event.json in containers 30 DefaultBranch string // name of the main branch for this repository 31 ReuseContainers bool // reuse containers to maintain state 32 ForcePull bool // force pulling of the image, even if already present 33 ForceRebuild bool // force rebuilding local docker image action 34 LogOutput bool // log the output from docker run 35 JSONLogger bool // use json or text logger 36 LogPrefixJobID bool // switches from the full job name to the job id 37 Env map[string]string // env for containers 38 Inputs map[string]string // manually passed action inputs 39 Secrets map[string]string // list of secrets 40 Vars map[string]string // list of vars 41 Token string // GitHub token 42 InsecureSecrets bool // switch hiding output when printing to terminal 43 Platforms map[string]string // list of platforms 44 Privileged bool // use privileged mode 45 UsernsMode string // user namespace to use 46 ContainerArchitecture string // Desired OS/architecture platform for running containers 47 ContainerDaemonSocket string // Path to Docker daemon socket 48 ContainerOptions string // Options for the job container 49 UseGitIgnore bool // controls if paths in .gitignore should not be copied into container, default true 50 GitHubInstance string // GitHub instance to use, default "github.com" 51 ContainerCapAdd []string // list of kernel capabilities to add to the containers 52 ContainerCapDrop []string // list of kernel capabilities to remove from the containers 53 AutoRemove bool // controls if the container is automatically removed upon workflow completion 54 ArtifactServerPath string // the path where the artifact server stores uploads 55 ArtifactServerAddr string // the address the artifact server binds to 56 ArtifactServerPort string // the port the artifact server binds to 57 NoSkipCheckout bool // do not skip actions/checkout 58 RemoteName string // remote name in local git repo config 59 ReplaceGheActionWithGithubCom []string // Use actions from GitHub Enterprise instance to GitHub 60 ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub. 61 Matrix map[string]map[string]bool // Matrix config to run 62 ContainerNetworkMode docker_container.NetworkMode // the network mode of job containers (the value of --network) 63 ActionCache ActionCache // Use a custom ActionCache Implementation 64 } 65 66 type caller struct { 67 runContext *RunContext 68 } 69 70 type runnerImpl struct { 71 config *Config 72 eventJSON string 73 caller *caller // the job calling this runner (caller of a reusable workflow) 74 } 75 76 // New Creates a new Runner 77 func New(runnerConfig *Config) (Runner, error) { 78 runner := &runnerImpl{ 79 config: runnerConfig, 80 } 81 82 return runner.configure() 83 } 84 85 func (runner *runnerImpl) configure() (Runner, error) { 86 runner.eventJSON = "{}" 87 if runner.config.EventPath != "" { 88 log.Debugf("Reading event.json from %s", runner.config.EventPath) 89 eventJSONBytes, err := os.ReadFile(runner.config.EventPath) 90 if err != nil { 91 return nil, err 92 } 93 runner.eventJSON = string(eventJSONBytes) 94 } else if len(runner.config.Inputs) != 0 { 95 eventMap := map[string]map[string]string{ 96 "inputs": runner.config.Inputs, 97 } 98 eventJSON, err := json.Marshal(eventMap) 99 if err != nil { 100 return nil, err 101 } 102 runner.eventJSON = string(eventJSON) 103 } 104 return runner, nil 105 } 106 107 // NewPlanExecutor ... 108 func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor { 109 maxJobNameLen := 0 110 111 stagePipeline := make([]common.Executor, 0) 112 log.Debugf("Plan Stages: %v", plan.Stages) 113 114 for i := range plan.Stages { 115 stage := plan.Stages[i] 116 stagePipeline = append(stagePipeline, func(ctx context.Context) error { 117 pipeline := make([]common.Executor, 0) 118 for _, run := range stage.Runs { 119 log.Debugf("Stages Runs: %v", stage.Runs) 120 stageExecutor := make([]common.Executor, 0) 121 job := run.Job() 122 log.Debugf("Job.Name: %v", job.Name) 123 log.Debugf("Job.RawNeeds: %v", job.RawNeeds) 124 log.Debugf("Job.RawRunsOn: %v", job.RawRunsOn) 125 log.Debugf("Job.Env: %v", job.Env) 126 log.Debugf("Job.If: %v", job.If) 127 for step := range job.Steps { 128 if nil != job.Steps[step] { 129 log.Debugf("Job.Steps: %v", job.Steps[step].String()) 130 } 131 } 132 log.Debugf("Job.TimeoutMinutes: %v", job.TimeoutMinutes) 133 log.Debugf("Job.Services: %v", job.Services) 134 log.Debugf("Job.Strategy: %v", job.Strategy) 135 log.Debugf("Job.RawContainer: %v", job.RawContainer) 136 log.Debugf("Job.Defaults.Run.Shell: %v", job.Defaults.Run.Shell) 137 log.Debugf("Job.Defaults.Run.WorkingDirectory: %v", job.Defaults.Run.WorkingDirectory) 138 log.Debugf("Job.Outputs: %v", job.Outputs) 139 log.Debugf("Job.Uses: %v", job.Uses) 140 log.Debugf("Job.With: %v", job.With) 141 // log.Debugf("Job.RawSecrets: %v", job.RawSecrets) 142 log.Debugf("Job.Result: %v", job.Result) 143 144 if job.Strategy != nil { 145 log.Debugf("Job.Strategy.FailFast: %v", job.Strategy.FailFast) 146 log.Debugf("Job.Strategy.MaxParallel: %v", job.Strategy.MaxParallel) 147 log.Debugf("Job.Strategy.FailFastString: %v", job.Strategy.FailFastString) 148 log.Debugf("Job.Strategy.MaxParallelString: %v", job.Strategy.MaxParallelString) 149 log.Debugf("Job.Strategy.RawMatrix: %v", job.Strategy.RawMatrix) 150 151 strategyRc := runner.newRunContext(ctx, run, nil) 152 if err := strategyRc.NewExpressionEvaluator(ctx).EvaluateYamlNode(ctx, &job.Strategy.RawMatrix); err != nil { 153 log.Errorf("Error while evaluating matrix: %v", err) 154 } 155 } 156 157 var matrixes []map[string]interface{} 158 if m, err := job.GetMatrixes(); err != nil { 159 log.Errorf("Error while get job's matrix: %v", err) 160 } else { 161 log.Debugf("Job Matrices: %v", m) 162 log.Debugf("Runner Matrices: %v", runner.config.Matrix) 163 matrixes = selectMatrixes(m, runner.config.Matrix) 164 } 165 log.Debugf("Final matrix after applying user inclusions '%v'", matrixes) 166 167 maxParallel := 4 168 if job.Strategy != nil { 169 maxParallel = job.Strategy.MaxParallel 170 } 171 172 if len(matrixes) < maxParallel { 173 maxParallel = len(matrixes) 174 } 175 176 for i, matrix := range matrixes { 177 matrix := matrix 178 rc := runner.newRunContext(ctx, run, matrix) 179 rc.JobName = rc.Name 180 if len(matrixes) > 1 { 181 rc.Name = fmt.Sprintf("%s-%d", rc.Name, i+1) 182 } 183 if len(rc.String()) > maxJobNameLen { 184 maxJobNameLen = len(rc.String()) 185 } 186 stageExecutor = append(stageExecutor, func(ctx context.Context) error { 187 jobName := fmt.Sprintf("%-*s", maxJobNameLen, rc.String()) 188 executor, err := rc.Executor() 189 190 if err != nil { 191 return err 192 } 193 194 return executor(common.WithJobErrorContainer(WithJobLogger(ctx, rc.Run.JobID, jobName, rc.Config, &rc.Masks, matrix))) 195 }) 196 } 197 pipeline = append(pipeline, common.NewParallelExecutor(maxParallel, stageExecutor...)) 198 } 199 ncpu := runtime.NumCPU() 200 if 1 > ncpu { 201 ncpu = 1 202 } 203 log.Debugf("Detected CPUs: %d", ncpu) 204 return common.NewParallelExecutor(ncpu, pipeline...)(ctx) 205 }) 206 } 207 208 return common.NewPipelineExecutor(stagePipeline...).Then(handleFailure(plan)) 209 } 210 211 func handleFailure(plan *model.Plan) common.Executor { 212 return func(ctx context.Context) error { 213 for _, stage := range plan.Stages { 214 for _, run := range stage.Runs { 215 if run.Job().Result == "failure" { 216 return fmt.Errorf("Job '%s' failed", run.String()) 217 } 218 } 219 } 220 return nil 221 } 222 } 223 224 func selectMatrixes(originalMatrixes []map[string]interface{}, targetMatrixValues map[string]map[string]bool) []map[string]interface{} { 225 matrixes := make([]map[string]interface{}, 0) 226 for _, original := range originalMatrixes { 227 flag := true 228 for key, val := range original { 229 if allowedVals, ok := targetMatrixValues[key]; ok { 230 valToString := fmt.Sprintf("%v", val) 231 if _, ok := allowedVals[valToString]; !ok { 232 flag = false 233 } 234 } 235 } 236 if flag { 237 matrixes = append(matrixes, original) 238 } 239 } 240 return matrixes 241 } 242 243 func (runner *runnerImpl) newRunContext(ctx context.Context, run *model.Run, matrix map[string]interface{}) *RunContext { 244 rc := &RunContext{ 245 Config: runner.config, 246 Run: run, 247 EventJSON: runner.eventJSON, 248 StepResults: make(map[string]*model.StepResult), 249 Matrix: matrix, 250 caller: runner.caller, 251 } 252 rc.ExprEval = rc.NewExpressionEvaluator(ctx) 253 rc.Name = rc.ExprEval.Interpolate(ctx, run.String()) 254 255 return rc 256 }