github.com/nektos/act@v0.2.83/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 ConcurrentJobs int // Number of max concurrent jobs 65 } 66 67 func (config *Config) GetConcurrentJobs() int { 68 if config.ConcurrentJobs >= 1 { 69 return config.ConcurrentJobs 70 } 71 72 ncpu := runtime.NumCPU() 73 log.Debugf("Detected CPUs: %d", ncpu) 74 if ncpu > 1 { 75 return ncpu 76 } 77 return 1 78 } 79 80 type caller struct { 81 runContext *RunContext 82 } 83 84 type runnerImpl struct { 85 config *Config 86 eventJSON string 87 caller *caller // the job calling this runner (caller of a reusable workflow) 88 } 89 90 // New Creates a new Runner 91 func New(runnerConfig *Config) (Runner, error) { 92 runner := &runnerImpl{ 93 config: runnerConfig, 94 } 95 96 return runner.configure() 97 } 98 99 func (runner *runnerImpl) configure() (Runner, error) { 100 runner.eventJSON = "{}" 101 if runner.config.EventPath != "" { 102 log.Debugf("Reading event.json from %s", runner.config.EventPath) 103 eventJSONBytes, err := os.ReadFile(runner.config.EventPath) 104 if err != nil { 105 return nil, err 106 } 107 runner.eventJSON = string(eventJSONBytes) 108 } else if len(runner.config.Inputs) != 0 { 109 eventMap := map[string]map[string]string{ 110 "inputs": runner.config.Inputs, 111 } 112 eventJSON, err := json.Marshal(eventMap) 113 if err != nil { 114 return nil, err 115 } 116 runner.eventJSON = string(eventJSON) 117 } 118 return runner, nil 119 } 120 121 // NewPlanExecutor ... 122 func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor { 123 maxJobNameLen := 0 124 125 stagePipeline := make([]common.Executor, 0) 126 log.Debugf("Plan Stages: %v", plan.Stages) 127 128 for i := range plan.Stages { 129 stage := plan.Stages[i] 130 stagePipeline = append(stagePipeline, func(ctx context.Context) error { 131 pipeline := make([]common.Executor, 0) 132 for _, run := range stage.Runs { 133 log.Debugf("Stages Runs: %v", stage.Runs) 134 stageExecutor := make([]common.Executor, 0) 135 job := run.Job() 136 log.Debugf("Job.Name: %v", job.Name) 137 log.Debugf("Job.RawNeeds: %v", job.RawNeeds) 138 log.Debugf("Job.RawRunsOn: %v", job.RawRunsOn) 139 log.Debugf("Job.Env: %v", job.Env) 140 log.Debugf("Job.If: %v", job.If) 141 for step := range job.Steps { 142 if nil != job.Steps[step] { 143 log.Debugf("Job.Steps: %v", job.Steps[step].String()) 144 } 145 } 146 log.Debugf("Job.TimeoutMinutes: %v", job.TimeoutMinutes) 147 log.Debugf("Job.Services: %v", job.Services) 148 log.Debugf("Job.Strategy: %v", job.Strategy) 149 log.Debugf("Job.RawContainer: %v", job.RawContainer) 150 log.Debugf("Job.Defaults.Run.Shell: %v", job.Defaults.Run.Shell) 151 log.Debugf("Job.Defaults.Run.WorkingDirectory: %v", job.Defaults.Run.WorkingDirectory) 152 log.Debugf("Job.Outputs: %v", job.Outputs) 153 log.Debugf("Job.Uses: %v", job.Uses) 154 log.Debugf("Job.With: %v", job.With) 155 // log.Debugf("Job.RawSecrets: %v", job.RawSecrets) 156 log.Debugf("Job.Result: %v", job.Result) 157 158 if job.Strategy != nil { 159 log.Debugf("Job.Strategy.FailFast: %v", job.Strategy.FailFast) 160 log.Debugf("Job.Strategy.MaxParallel: %v", job.Strategy.MaxParallel) 161 log.Debugf("Job.Strategy.FailFastString: %v", job.Strategy.FailFastString) 162 log.Debugf("Job.Strategy.MaxParallelString: %v", job.Strategy.MaxParallelString) 163 log.Debugf("Job.Strategy.RawMatrix: %v", job.Strategy.RawMatrix) 164 165 strategyRc := runner.newRunContext(ctx, run, nil) 166 if err := strategyRc.NewExpressionEvaluator(ctx).EvaluateYamlNode(ctx, &job.Strategy.RawMatrix); err != nil { 167 log.Errorf("Error while evaluating matrix: %v", err) 168 } 169 } 170 171 var matrixes []map[string]interface{} 172 if m, err := job.GetMatrixes(); err != nil { 173 log.Errorf("Error while get job's matrix: %v", err) 174 } else { 175 log.Debugf("Job Matrices: %v", m) 176 log.Debugf("Runner Matrices: %v", runner.config.Matrix) 177 matrixes = selectMatrixes(m, runner.config.Matrix) 178 } 179 log.Debugf("Final matrix after applying user inclusions '%v'", matrixes) 180 181 maxParallel := 4 182 if job.Strategy != nil { 183 maxParallel = job.Strategy.MaxParallel 184 } 185 186 if len(matrixes) < maxParallel { 187 maxParallel = len(matrixes) 188 } 189 190 for i, matrix := range matrixes { 191 rc := runner.newRunContext(ctx, run, matrix) 192 rc.JobName = rc.Name 193 if len(matrixes) > 1 { 194 rc.Name = fmt.Sprintf("%s-%d", rc.Name, i+1) 195 } 196 if len(rc.String()) > maxJobNameLen { 197 maxJobNameLen = len(rc.String()) 198 } 199 stageExecutor = append(stageExecutor, func(ctx context.Context) error { 200 jobName := fmt.Sprintf("%-*s", maxJobNameLen, rc.String()) 201 executor, err := rc.Executor() 202 203 if err != nil { 204 return err 205 } 206 207 return executor(common.WithJobErrorContainer(WithJobLogger(ctx, rc.Run.JobID, jobName, rc.Config, &rc.Masks, matrix))) 208 }) 209 } 210 pipeline = append(pipeline, common.NewParallelExecutor(maxParallel, stageExecutor...)) 211 } 212 213 log.Debugf("PlanExecutor concurrency: %d", runner.config.GetConcurrentJobs()) 214 return common.NewParallelExecutor(runner.config.GetConcurrentJobs(), pipeline...)(ctx) 215 }) 216 } 217 218 return common.NewPipelineExecutor(stagePipeline...).Then(handleFailure(plan)) 219 } 220 221 func handleFailure(plan *model.Plan) common.Executor { 222 return func(_ context.Context) error { 223 for _, stage := range plan.Stages { 224 for _, run := range stage.Runs { 225 if run.Job().Result == "failure" { 226 return fmt.Errorf("Job '%s' failed", run.String()) 227 } 228 } 229 } 230 return nil 231 } 232 } 233 234 func selectMatrixes(originalMatrixes []map[string]interface{}, targetMatrixValues map[string]map[string]bool) []map[string]interface{} { 235 matrixes := make([]map[string]interface{}, 0) 236 for _, original := range originalMatrixes { 237 flag := true 238 for key, val := range original { 239 if allowedVals, ok := targetMatrixValues[key]; ok { 240 valToString := fmt.Sprintf("%v", val) 241 if _, ok := allowedVals[valToString]; !ok { 242 flag = false 243 } 244 } 245 } 246 if flag { 247 matrixes = append(matrixes, original) 248 } 249 } 250 return matrixes 251 } 252 253 func (runner *runnerImpl) newRunContext(ctx context.Context, run *model.Run, matrix map[string]interface{}) *RunContext { 254 rc := &RunContext{ 255 Config: runner.config, 256 Run: run, 257 EventJSON: runner.eventJSON, 258 StepResults: make(map[string]*model.StepResult), 259 Matrix: matrix, 260 caller: runner.caller, 261 } 262 rc.ExprEval = rc.NewExpressionEvaluator(ctx) 263 rc.Name = rc.ExprEval.Interpolate(ctx, run.String()) 264 265 return rc 266 }