github.com/nektos/act@v0.2.83/pkg/runner/run_context.go (about) 1 package runner 2 3 import ( 4 "archive/tar" 5 "bufio" 6 "bytes" 7 "context" 8 "crypto/rand" 9 "crypto/sha256" 10 "encoding/hex" 11 "encoding/json" 12 "errors" 13 "fmt" 14 "io" 15 "os" 16 "path/filepath" 17 "regexp" 18 "runtime" 19 "strconv" 20 "strings" 21 "time" 22 23 "github.com/docker/go-connections/nat" 24 "github.com/nektos/act/pkg/common" 25 "github.com/nektos/act/pkg/container" 26 "github.com/nektos/act/pkg/exprparser" 27 "github.com/nektos/act/pkg/model" 28 "github.com/opencontainers/selinux/go-selinux" 29 ) 30 31 // RunContext contains info about current job 32 type RunContext struct { 33 Name string 34 Config *Config 35 Matrix map[string]interface{} 36 Run *model.Run 37 EventJSON string 38 Env map[string]string 39 GlobalEnv map[string]string // to pass env changes of GITHUB_ENV and set-env correctly, due to dirty Env field 40 ExtraPath []string 41 CurrentStep string 42 StepResults map[string]*model.StepResult 43 IntraActionState map[string]map[string]string 44 ExprEval ExpressionEvaluator 45 JobContainer container.ExecutionsEnvironment 46 ServiceContainers []container.ExecutionsEnvironment 47 OutputMappings map[MappableOutput]MappableOutput 48 JobName string 49 ActionPath string 50 Parent *RunContext 51 Masks []string 52 cleanUpJobContainer common.Executor 53 caller *caller // job calling this RunContext (reusable workflows) 54 Cancelled bool 55 nodeToolFullPath string 56 } 57 58 func (rc *RunContext) AddMask(mask string) { 59 rc.Masks = append(rc.Masks, mask) 60 } 61 62 type MappableOutput struct { 63 StepID string 64 OutputName string 65 } 66 67 func (rc *RunContext) String() string { 68 name := fmt.Sprintf("%s/%s", rc.Run.Workflow.Name, rc.Name) 69 if rc.caller != nil { 70 // prefix the reusable workflow with the caller job 71 // this is required to create unique container names 72 name = fmt.Sprintf("%s/%s", rc.caller.runContext.Name, name) 73 } 74 return name 75 } 76 77 // GetEnv returns the env for the context 78 func (rc *RunContext) GetEnv() map[string]string { 79 if rc.Env == nil { 80 rc.Env = map[string]string{} 81 if rc.Run != nil && rc.Run.Workflow != nil && rc.Config != nil { 82 job := rc.Run.Job() 83 if job != nil { 84 rc.Env = mergeMaps(rc.Run.Workflow.Env, job.Environment(), rc.Config.Env) 85 } 86 } 87 } 88 rc.Env["ACT"] = "true" 89 return rc.Env 90 } 91 92 func (rc *RunContext) jobContainerName() string { 93 return createContainerName("act", rc.String()) 94 } 95 96 // networkName return the name of the network which will be created by `act` automatically for job, 97 // only create network if using a service container 98 func (rc *RunContext) networkName() (string, bool) { 99 if len(rc.Run.Job().Services) > 0 { 100 return fmt.Sprintf("%s-%s-network", rc.jobContainerName(), rc.Run.JobID), true 101 } 102 if rc.Config.ContainerNetworkMode == "" { 103 return "host", false 104 } 105 return string(rc.Config.ContainerNetworkMode), false 106 } 107 108 func getDockerDaemonSocketMountPath(daemonPath string) string { 109 if protoIndex := strings.Index(daemonPath, "://"); protoIndex != -1 { 110 scheme := daemonPath[:protoIndex] 111 if strings.EqualFold(scheme, "npipe") { 112 // linux container mount on windows, use the default socket path of the VM / wsl2 113 return "/var/run/docker.sock" 114 } else if strings.EqualFold(scheme, "unix") { 115 return daemonPath[protoIndex+3:] 116 } else if strings.IndexFunc(scheme, func(r rune) bool { 117 return (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') 118 }) == -1 { 119 // unknown protocol use default 120 return "/var/run/docker.sock" 121 } 122 } 123 return daemonPath 124 } 125 126 // Returns the binds and mounts for the container, resolving paths as appropriate 127 func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) { 128 name := rc.jobContainerName() 129 130 if rc.Config.ContainerDaemonSocket == "" { 131 rc.Config.ContainerDaemonSocket = "/var/run/docker.sock" 132 } 133 134 binds := []string{} 135 if rc.Config.ContainerDaemonSocket != "-" { 136 daemonPath := getDockerDaemonSocketMountPath(rc.Config.ContainerDaemonSocket) 137 binds = append(binds, fmt.Sprintf("%s:%s", daemonPath, "/var/run/docker.sock")) 138 } 139 140 ext := container.LinuxContainerEnvironmentExtensions{} 141 142 if hostEnv, ok := rc.JobContainer.(*container.HostEnvironment); ok { 143 mounts := map[string]string{} 144 // Permission issues? 145 // binds = append(binds, hostEnv.ToolCache+":/opt/hostedtoolcache") 146 binds = append(binds, hostEnv.GetActPath()+":"+ext.GetActPath()) 147 binds = append(binds, hostEnv.ToContainerPath(rc.Config.Workdir)+":"+ext.ToContainerPath(rc.Config.Workdir)) 148 return binds, mounts 149 } 150 mounts := map[string]string{ 151 "act-toolcache": "/opt/hostedtoolcache", 152 name + "-env": ext.GetActPath(), 153 } 154 155 if job := rc.Run.Job(); job != nil { 156 if container := job.Container(); container != nil { 157 for _, v := range container.Volumes { 158 if !strings.Contains(v, ":") || filepath.IsAbs(v) { 159 // Bind anonymous volume or host file. 160 binds = append(binds, v) 161 } else { 162 // Mount existing volume. 163 paths := strings.SplitN(v, ":", 2) 164 mounts[paths[0]] = paths[1] 165 } 166 } 167 } 168 } 169 170 if rc.Config.BindWorkdir { 171 bindModifiers := "" 172 if runtime.GOOS == "darwin" { 173 bindModifiers = ":delegated" 174 } 175 if selinux.GetEnabled() { 176 bindModifiers = ":z" 177 } 178 binds = append(binds, fmt.Sprintf("%s:%s%s", rc.Config.Workdir, ext.ToContainerPath(rc.Config.Workdir), bindModifiers)) 179 } else { 180 mounts[name] = ext.ToContainerPath(rc.Config.Workdir) 181 } 182 183 return binds, mounts 184 } 185 186 func (rc *RunContext) startHostEnvironment() common.Executor { 187 return func(ctx context.Context) error { 188 logger := common.Logger(ctx) 189 rawLogger := logger.WithField("raw_output", true) 190 logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool { 191 if rc.Config.LogOutput { 192 rawLogger.Infof("%s", s) 193 } else { 194 rawLogger.Debugf("%s", s) 195 } 196 return true 197 }) 198 cacheDir := rc.ActionCacheDir() 199 randBytes := make([]byte, 8) 200 _, _ = rand.Read(randBytes) 201 miscpath := filepath.Join(cacheDir, hex.EncodeToString(randBytes)) 202 actPath := filepath.Join(miscpath, "act") 203 if err := os.MkdirAll(actPath, 0o777); err != nil { 204 return err 205 } 206 path := filepath.Join(miscpath, "hostexecutor") 207 if err := os.MkdirAll(path, 0o777); err != nil { 208 return err 209 } 210 runnerTmp := filepath.Join(miscpath, "tmp") 211 if err := os.MkdirAll(runnerTmp, 0o777); err != nil { 212 return err 213 } 214 toolCache := filepath.Join(cacheDir, "tool_cache") 215 rc.JobContainer = &container.HostEnvironment{ 216 Path: path, 217 TmpDir: runnerTmp, 218 ToolCache: toolCache, 219 Workdir: rc.Config.Workdir, 220 ActPath: actPath, 221 CleanUp: func() { 222 os.RemoveAll(miscpath) 223 }, 224 StdOut: logWriter, 225 } 226 rc.cleanUpJobContainer = rc.JobContainer.Remove() 227 for k, v := range rc.JobContainer.GetRunnerContext(ctx) { 228 if v, ok := v.(string); ok { 229 rc.Env[fmt.Sprintf("RUNNER_%s", strings.ToUpper(k))] = v 230 } 231 } 232 for _, env := range os.Environ() { 233 if k, v, ok := strings.Cut(env, "="); ok { 234 // don't override 235 if _, ok := rc.Env[k]; !ok { 236 rc.Env[k] = v 237 } 238 } 239 } 240 241 return common.NewPipelineExecutor( 242 rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{ 243 Name: "workflow/event.json", 244 Mode: 0o644, 245 Body: rc.EventJSON, 246 }, &container.FileEntry{ 247 Name: "workflow/envs.txt", 248 Mode: 0o666, 249 Body: "", 250 }), 251 )(ctx) 252 } 253 } 254 255 func (rc *RunContext) startJobContainer() common.Executor { 256 return func(ctx context.Context) error { 257 logger := common.Logger(ctx) 258 image := rc.platformImage(ctx) 259 rawLogger := logger.WithField("raw_output", true) 260 logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool { 261 if rc.Config.LogOutput { 262 rawLogger.Infof("%s", s) 263 } else { 264 rawLogger.Debugf("%s", s) 265 } 266 return true 267 }) 268 269 username, password, err := rc.handleCredentials(ctx) 270 if err != nil { 271 return fmt.Errorf("failed to handle credentials: %s", err) 272 } 273 274 logger.Infof("\U0001f680 Start image=%s", image) 275 name := rc.jobContainerName() 276 277 envList := make([]string, 0) 278 279 envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TOOL_CACHE", "/opt/hostedtoolcache")) 280 envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_OS", "Linux")) 281 envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_ARCH", container.RunnerArch(ctx))) 282 envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp")) 283 envList = append(envList, fmt.Sprintf("%s=%s", "LANG", "C.UTF-8")) // Use same locale as GitHub Actions 284 285 ext := container.LinuxContainerEnvironmentExtensions{} 286 binds, mounts := rc.GetBindsAndMounts() 287 288 // specify the network to which the container will connect when `docker create` stage. (like execute command line: docker create --network <networkName> <image>) 289 // if using service containers, will create a new network for the containers. 290 // and it will be removed after at last. 291 networkName, createAndDeleteNetwork := rc.networkName() 292 293 // add service containers 294 for serviceID, spec := range rc.Run.Job().Services { 295 // interpolate env 296 interpolatedEnvs := make(map[string]string, len(spec.Env)) 297 for k, v := range spec.Env { 298 interpolatedEnvs[k] = rc.ExprEval.Interpolate(ctx, v) 299 } 300 envs := make([]string, 0, len(interpolatedEnvs)) 301 for k, v := range interpolatedEnvs { 302 envs = append(envs, fmt.Sprintf("%s=%s", k, v)) 303 } 304 username, password, err = rc.handleServiceCredentials(ctx, spec.Credentials) 305 if err != nil { 306 return fmt.Errorf("failed to handle service %s credentials: %w", serviceID, err) 307 } 308 309 interpolatedVolumes := make([]string, 0, len(spec.Volumes)) 310 for _, volume := range spec.Volumes { 311 interpolatedVolumes = append(interpolatedVolumes, rc.ExprEval.Interpolate(ctx, volume)) 312 } 313 serviceBinds, serviceMounts := rc.GetServiceBindsAndMounts(interpolatedVolumes) 314 315 interpolatedPorts := make([]string, 0, len(spec.Ports)) 316 for _, port := range spec.Ports { 317 interpolatedPorts = append(interpolatedPorts, rc.ExprEval.Interpolate(ctx, port)) 318 } 319 exposedPorts, portBindings, err := nat.ParsePortSpecs(interpolatedPorts) 320 if err != nil { 321 return fmt.Errorf("failed to parse service %s ports: %w", serviceID, err) 322 } 323 324 imageName := rc.ExprEval.Interpolate(ctx, spec.Image) 325 if imageName == "" { 326 logger.Infof("The service '%s' will not be started because the container definition has an empty image.", serviceID) 327 continue 328 } 329 330 serviceContainerName := createContainerName(rc.jobContainerName(), serviceID) 331 c := container.NewContainer(&container.NewContainerInput{ 332 Name: serviceContainerName, 333 WorkingDir: ext.ToContainerPath(rc.Config.Workdir), 334 Image: imageName, 335 Username: username, 336 Password: password, 337 Env: envs, 338 Mounts: serviceMounts, 339 Binds: serviceBinds, 340 Stdout: logWriter, 341 Stderr: logWriter, 342 Privileged: rc.Config.Privileged, 343 UsernsMode: rc.Config.UsernsMode, 344 Platform: rc.Config.ContainerArchitecture, 345 Options: rc.ExprEval.Interpolate(ctx, spec.Options), 346 NetworkMode: networkName, 347 NetworkAliases: []string{serviceID}, 348 ExposedPorts: exposedPorts, 349 PortBindings: portBindings, 350 }) 351 rc.ServiceContainers = append(rc.ServiceContainers, c) 352 } 353 354 rc.cleanUpJobContainer = func(ctx context.Context) error { 355 reuseJobContainer := func(_ context.Context) bool { 356 return rc.Config.ReuseContainers 357 } 358 359 if rc.JobContainer != nil { 360 return rc.JobContainer.Remove().IfNot(reuseJobContainer). 361 Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName(), false)).IfNot(reuseJobContainer). 362 Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName()+"-env", false)).IfNot(reuseJobContainer). 363 Then(func(ctx context.Context) error { 364 if len(rc.ServiceContainers) > 0 { 365 logger.Infof("Cleaning up services for job %s", rc.JobName) 366 if err := rc.stopServiceContainers()(ctx); err != nil { 367 logger.Errorf("Error while cleaning services: %v", err) 368 } 369 if createAndDeleteNetwork { 370 // clean network if it has been created by act 371 // if using service containers 372 // it means that the network to which containers are connecting is created by `act_runner`, 373 // so, we should remove the network at last. 374 logger.Infof("Cleaning up network for job %s, and network name is: %s", rc.JobName, networkName) 375 if err := container.NewDockerNetworkRemoveExecutor(networkName)(ctx); err != nil { 376 logger.Errorf("Error while cleaning network: %v", err) 377 } 378 } 379 } 380 return nil 381 })(ctx) 382 } 383 return nil 384 } 385 386 jobContainerNetwork := rc.Config.ContainerNetworkMode.NetworkName() 387 if rc.containerImage(ctx) != "" { 388 jobContainerNetwork = networkName 389 } else if jobContainerNetwork == "" { 390 jobContainerNetwork = "host" 391 } 392 393 rc.JobContainer = container.NewContainer(&container.NewContainerInput{ 394 Cmd: nil, 395 Entrypoint: []string{"tail", "-f", "/dev/null"}, 396 WorkingDir: ext.ToContainerPath(rc.Config.Workdir), 397 Image: image, 398 Username: username, 399 Password: password, 400 Name: name, 401 Env: envList, 402 Mounts: mounts, 403 NetworkMode: jobContainerNetwork, 404 NetworkAliases: []string{rc.Name}, 405 Binds: binds, 406 Stdout: logWriter, 407 Stderr: logWriter, 408 Privileged: rc.Config.Privileged, 409 UsernsMode: rc.Config.UsernsMode, 410 Platform: rc.Config.ContainerArchitecture, 411 Options: rc.options(ctx), 412 }) 413 if rc.JobContainer == nil { 414 return errors.New("Failed to create job container") 415 } 416 417 return common.NewPipelineExecutor( 418 rc.pullServicesImages(rc.Config.ForcePull), 419 rc.JobContainer.Pull(rc.Config.ForcePull), 420 rc.stopJobContainer(), 421 container.NewDockerNetworkCreateExecutor(networkName).IfBool(createAndDeleteNetwork), 422 rc.startServiceContainers(networkName), 423 rc.JobContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop), 424 rc.JobContainer.Start(false), 425 rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{ 426 Name: "workflow/event.json", 427 Mode: 0o644, 428 Body: rc.EventJSON, 429 }, &container.FileEntry{ 430 Name: "workflow/envs.txt", 431 Mode: 0o666, 432 Body: "", 433 }), 434 rc.waitForServiceContainers(), 435 )(ctx) 436 } 437 } 438 439 func (rc *RunContext) execJobContainer(cmd []string, env map[string]string, user, workdir string) common.Executor { 440 return func(ctx context.Context) error { 441 return rc.JobContainer.Exec(cmd, env, user, workdir)(ctx) 442 } 443 } 444 445 func (rc *RunContext) InitializeNodeTool() common.Executor { 446 return func(ctx context.Context) error { 447 ctx, cancel := common.EarlyCancelContext(ctx) 448 defer cancel() 449 rc.GetNodeToolFullPath(ctx) 450 return nil 451 } 452 } 453 454 func (rc *RunContext) GetNodeToolFullPath(ctx context.Context) string { 455 if rc.nodeToolFullPath == "" { 456 timeed, cancel := context.WithTimeout(ctx, time.Minute) 457 defer cancel() 458 path := rc.JobContainer.GetPathVariableName() 459 cenv := map[string]string{} 460 var cpath string 461 if err := rc.JobContainer.UpdateFromImageEnv(&cenv)(ctx); err == nil { 462 if p, ok := cenv[path]; ok { 463 cpath = p 464 } 465 } 466 if len(cpath) == 0 { 467 cpath = rc.JobContainer.DefaultPathVariable() 468 } 469 cenv[path] = cpath 470 hout := &bytes.Buffer{} 471 herr := &bytes.Buffer{} 472 stdout, stderr := rc.JobContainer.ReplaceLogWriter(hout, herr) 473 err := rc.execJobContainer([]string{"node", "--no-warnings", "-e", "console.log(process.execPath)"}, 474 cenv, "", ""). 475 Finally(func(context.Context) error { 476 rc.JobContainer.ReplaceLogWriter(stdout, stderr) 477 return nil 478 })(timeed) 479 rawStr := strings.Trim(hout.String(), "\r\n") 480 if err == nil && !strings.ContainsAny(rawStr, "\r\n") { 481 rc.nodeToolFullPath = rawStr 482 } else { 483 rc.nodeToolFullPath = "node" 484 } 485 } 486 return rc.nodeToolFullPath 487 } 488 489 func (rc *RunContext) ApplyExtraPath(ctx context.Context, env *map[string]string) { 490 if len(rc.ExtraPath) > 0 { 491 path := rc.JobContainer.GetPathVariableName() 492 if rc.JobContainer.IsEnvironmentCaseInsensitive() { 493 // On windows system Path and PATH could also be in the map 494 for k := range *env { 495 if strings.EqualFold(path, k) { 496 path = k 497 break 498 } 499 } 500 } 501 if (*env)[path] == "" { 502 cenv := map[string]string{} 503 var cpath string 504 if err := rc.JobContainer.UpdateFromImageEnv(&cenv)(ctx); err == nil { 505 if p, ok := cenv[path]; ok { 506 cpath = p 507 } 508 } 509 if len(cpath) == 0 { 510 cpath = rc.JobContainer.DefaultPathVariable() 511 } 512 (*env)[path] = cpath 513 } 514 (*env)[path] = rc.JobContainer.JoinPathVariable(append(rc.ExtraPath, (*env)[path])...) 515 } 516 } 517 518 func (rc *RunContext) UpdateExtraPath(ctx context.Context, githubEnvPath string) error { 519 if common.Dryrun(ctx) { 520 return nil 521 } 522 pathTar, err := rc.JobContainer.GetContainerArchive(ctx, githubEnvPath) 523 if err != nil { 524 return err 525 } 526 defer pathTar.Close() 527 528 reader := tar.NewReader(pathTar) 529 _, err = reader.Next() 530 if err != nil && err != io.EOF { 531 return err 532 } 533 s := bufio.NewScanner(reader) 534 s.Buffer(nil, 1024*1024*1024) // increase buffer to 1GB to avoid scanner buffer overflow 535 firstLine := true 536 for s.Scan() { 537 line := s.Text() 538 if firstLine { 539 firstLine = false 540 // skip utf8 bom, powershell 5 legacy uses it for utf8 541 if len(line) >= 3 && line[0] == 239 && line[1] == 187 && line[2] == 191 { 542 line = line[3:] 543 } 544 } 545 if len(line) > 0 { 546 rc.addPath(ctx, line) 547 } 548 } 549 return s.Err() 550 } 551 552 // stopJobContainer removes the job container (if it exists) and its volume (if it exists) 553 func (rc *RunContext) stopJobContainer() common.Executor { 554 return func(ctx context.Context) error { 555 if rc.cleanUpJobContainer != nil { 556 return rc.cleanUpJobContainer(ctx) 557 } 558 return nil 559 } 560 } 561 562 func (rc *RunContext) pullServicesImages(forcePull bool) common.Executor { 563 return func(ctx context.Context) error { 564 execs := []common.Executor{} 565 for _, c := range rc.ServiceContainers { 566 execs = append(execs, c.Pull(forcePull)) 567 } 568 return common.NewParallelExecutor(len(execs), execs...)(ctx) 569 } 570 } 571 572 func (rc *RunContext) startServiceContainers(_ string) common.Executor { 573 return func(ctx context.Context) error { 574 execs := []common.Executor{} 575 for _, c := range rc.ServiceContainers { 576 execs = append(execs, common.NewPipelineExecutor( 577 c.Pull(false), 578 c.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop), 579 c.Start(false), 580 )) 581 } 582 return common.NewParallelExecutor(len(execs), execs...)(ctx) 583 } 584 } 585 586 func (rc *RunContext) waitForServiceContainer(c container.ExecutionsEnvironment) common.Executor { 587 return func(ctx context.Context) error { 588 sctx, cancel := context.WithTimeout(ctx, time.Minute*5) 589 defer cancel() 590 health := container.HealthStarting 591 delay := time.Second 592 for i := 0; ; i++ { 593 health = c.GetHealth(sctx) 594 if health != container.HealthStarting || i > 30 { 595 break 596 } 597 time.Sleep(delay) 598 delay *= 2 599 if delay > 10*time.Second { 600 delay = 10 * time.Second 601 } 602 } 603 if health == container.HealthHealthy { 604 return nil 605 } 606 return fmt.Errorf("service container failed to start") 607 } 608 } 609 610 func (rc *RunContext) waitForServiceContainers() common.Executor { 611 return func(ctx context.Context) error { 612 execs := []common.Executor{} 613 for _, c := range rc.ServiceContainers { 614 execs = append(execs, rc.waitForServiceContainer(c)) 615 } 616 return common.NewParallelExecutor(len(execs), execs...)(ctx) 617 } 618 } 619 620 func (rc *RunContext) stopServiceContainers() common.Executor { 621 return func(ctx context.Context) error { 622 execs := []common.Executor{} 623 for _, c := range rc.ServiceContainers { 624 execs = append(execs, c.Remove().Finally(c.Close())) 625 } 626 return common.NewParallelExecutor(len(execs), execs...)(ctx) 627 } 628 } 629 630 // Prepare the mounts and binds for the worker 631 632 // ActionCacheDir is for rc 633 func (rc *RunContext) ActionCacheDir() string { 634 if rc.Config.ActionCacheDir != "" { 635 return rc.Config.ActionCacheDir 636 } 637 var xdgCache string 638 var ok bool 639 if xdgCache, ok = os.LookupEnv("XDG_CACHE_HOME"); !ok || xdgCache == "" { 640 if home, err := os.UserHomeDir(); err == nil { 641 xdgCache = filepath.Join(home, ".cache") 642 } else if xdgCache, err = filepath.Abs("."); err != nil { 643 // It's almost impossible to get here, so the temp dir is a good fallback 644 xdgCache = os.TempDir() 645 } 646 } 647 return filepath.Join(xdgCache, "act") 648 } 649 650 // Interpolate outputs after a job is done 651 func (rc *RunContext) interpolateOutputs() common.Executor { 652 return func(ctx context.Context) error { 653 ee := rc.NewExpressionEvaluator(ctx) 654 for k, v := range rc.Run.Job().Outputs { 655 interpolated := ee.Interpolate(ctx, v) 656 if v != interpolated { 657 rc.Run.Job().Outputs[k] = interpolated 658 } 659 } 660 return nil 661 } 662 } 663 664 func (rc *RunContext) startContainer() common.Executor { 665 return func(ctx context.Context) error { 666 ctx, cancel := common.EarlyCancelContext(ctx) 667 defer cancel() 668 if rc.IsHostEnv(ctx) { 669 return rc.startHostEnvironment()(ctx) 670 } 671 return rc.startJobContainer()(ctx) 672 } 673 } 674 675 func (rc *RunContext) IsHostEnv(ctx context.Context) bool { 676 platform := rc.runsOnImage(ctx) 677 image := rc.containerImage(ctx) 678 return image == "" && strings.EqualFold(platform, "-self-hosted") 679 } 680 681 func (rc *RunContext) stopContainer() common.Executor { 682 return rc.stopJobContainer() 683 } 684 685 func (rc *RunContext) closeContainer() common.Executor { 686 return func(ctx context.Context) error { 687 if rc.JobContainer != nil { 688 return rc.JobContainer.Close()(ctx) 689 } 690 return nil 691 } 692 } 693 694 func (rc *RunContext) matrix() map[string]interface{} { 695 return rc.Matrix 696 } 697 698 func (rc *RunContext) result(result string) { 699 rc.Run.Job().Result = result 700 } 701 702 func (rc *RunContext) steps() []*model.Step { 703 return rc.Run.Job().Steps 704 } 705 706 // Executor returns a pipeline executor for all the steps in the job 707 func (rc *RunContext) Executor() (common.Executor, error) { 708 var executor common.Executor 709 var jobType, err = rc.Run.Job().Type() 710 711 switch jobType { 712 case model.JobTypeDefault: 713 executor = newJobExecutor(rc, &stepFactoryImpl{}, rc) 714 case model.JobTypeReusableWorkflowLocal: 715 executor = newLocalReusableWorkflowExecutor(rc) 716 case model.JobTypeReusableWorkflowRemote: 717 executor = newRemoteReusableWorkflowExecutor(rc) 718 case model.JobTypeInvalid: 719 return nil, err 720 } 721 722 return func(ctx context.Context) error { 723 res, err := rc.isEnabled(ctx) 724 if err != nil { 725 return err 726 } 727 if res { 728 return executor(ctx) 729 } 730 return nil 731 }, nil 732 } 733 734 func (rc *RunContext) containerImage(ctx context.Context) string { 735 job := rc.Run.Job() 736 737 c := job.Container() 738 if c != nil { 739 return rc.ExprEval.Interpolate(ctx, c.Image) 740 } 741 742 return "" 743 } 744 745 func (rc *RunContext) runsOnImage(ctx context.Context) string { 746 if rc.Run.Job().RunsOn() == nil { 747 common.Logger(ctx).Errorf("'runs-on' key not defined in %s", rc.String()) 748 } 749 750 for _, platformName := range rc.runsOnPlatformNames(ctx) { 751 image := rc.Config.Platforms[strings.ToLower(platformName)] 752 if image != "" { 753 return image 754 } 755 } 756 757 return "" 758 } 759 760 func (rc *RunContext) runsOnPlatformNames(ctx context.Context) []string { 761 job := rc.Run.Job() 762 763 if job.RunsOn() == nil { 764 return []string{} 765 } 766 767 if err := rc.ExprEval.EvaluateYamlNode(ctx, &job.RawRunsOn); err != nil { 768 common.Logger(ctx).Errorf("Error while evaluating runs-on: %v", err) 769 return []string{} 770 } 771 772 return job.RunsOn() 773 } 774 775 func (rc *RunContext) platformImage(ctx context.Context) string { 776 if containerImage := rc.containerImage(ctx); containerImage != "" { 777 return containerImage 778 } 779 780 return rc.runsOnImage(ctx) 781 } 782 783 func (rc *RunContext) options(ctx context.Context) string { 784 job := rc.Run.Job() 785 c := job.Container() 786 if c != nil { 787 return rc.ExprEval.Interpolate(ctx, c.Options) 788 } 789 790 return rc.Config.ContainerOptions 791 } 792 793 func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) { 794 job := rc.Run.Job() 795 l := common.Logger(ctx) 796 runJob, runJobErr := EvalBool(ctx, rc.ExprEval, job.If.Value, exprparser.DefaultStatusCheckSuccess) 797 jobType, jobTypeErr := job.Type() 798 799 if runJobErr != nil { 800 return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", job.If.Value, runJobErr) 801 } 802 803 if jobType == model.JobTypeInvalid { 804 return false, jobTypeErr 805 } 806 807 if !runJob { 808 rc.result("skipped") 809 l.WithField("jobResult", "skipped").Debugf("Skipping job '%s' due to '%s'", job.Name, job.If.Value) 810 return false, nil 811 } 812 813 if jobType != model.JobTypeDefault { 814 return true, nil 815 } 816 817 img := rc.platformImage(ctx) 818 if img == "" { 819 for _, platformName := range rc.runsOnPlatformNames(ctx) { 820 l.Infof("\U0001F6A7 Skipping unsupported platform -- Try running with `-P %+v=...`", platformName) 821 } 822 return false, nil 823 } 824 return true, nil 825 } 826 827 func mergeMaps(maps ...map[string]string) map[string]string { 828 rtnMap := make(map[string]string) 829 for _, m := range maps { 830 for k, v := range m { 831 rtnMap[k] = v 832 } 833 } 834 return rtnMap 835 } 836 837 func createContainerName(parts ...string) string { 838 name := strings.Join(parts, "-") 839 pattern := regexp.MustCompile("[^a-zA-Z0-9]") 840 name = pattern.ReplaceAllString(name, "-") 841 name = strings.ReplaceAll(name, "--", "-") 842 hash := sha256.Sum256([]byte(name)) 843 844 // SHA256 is 64 hex characters. So trim name to 63 characters to make room for the hash and separator 845 trimmedName := strings.Trim(trimToLen(name, 63), "-") 846 847 return fmt.Sprintf("%s-%x", trimmedName, hash) 848 } 849 850 func trimToLen(s string, l int) string { 851 if l < 0 { 852 l = 0 853 } 854 if len(s) > l { 855 return s[:l] 856 } 857 return s 858 } 859 860 func (rc *RunContext) getJobContext() *model.JobContext { 861 jobStatus := "success" 862 if rc.Cancelled { 863 jobStatus = "cancelled" 864 } else { 865 for _, stepStatus := range rc.StepResults { 866 if stepStatus.Conclusion == model.StepStatusFailure { 867 jobStatus = "failure" 868 break 869 } 870 } 871 } 872 return &model.JobContext{ 873 Status: jobStatus, 874 } 875 } 876 877 func (rc *RunContext) getStepsContext() map[string]*model.StepResult { 878 return rc.StepResults 879 } 880 881 func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext { 882 logger := common.Logger(ctx) 883 ghc := &model.GithubContext{ 884 Event: make(map[string]interface{}), 885 Workflow: rc.Run.Workflow.Name, 886 RunAttempt: rc.Config.Env["GITHUB_RUN_ATTEMPT"], 887 RunID: rc.Config.Env["GITHUB_RUN_ID"], 888 RunNumber: rc.Config.Env["GITHUB_RUN_NUMBER"], 889 Actor: rc.Config.Actor, 890 EventName: rc.Config.EventName, 891 Action: rc.CurrentStep, 892 Token: rc.Config.Token, 893 Job: rc.Run.JobID, 894 ActionPath: rc.ActionPath, 895 ActionRepository: rc.Env["GITHUB_ACTION_REPOSITORY"], 896 ActionRef: rc.Env["GITHUB_ACTION_REF"], 897 RepositoryOwner: rc.Config.Env["GITHUB_REPOSITORY_OWNER"], 898 RetentionDays: rc.Config.Env["GITHUB_RETENTION_DAYS"], 899 RunnerPerflog: rc.Config.Env["RUNNER_PERFLOG"], 900 RunnerTrackingID: rc.Config.Env["RUNNER_TRACKING_ID"], 901 Repository: rc.Config.Env["GITHUB_REPOSITORY"], 902 Ref: rc.Config.Env["GITHUB_REF"], 903 Sha: rc.Config.Env["SHA_REF"], 904 RefName: rc.Config.Env["GITHUB_REF_NAME"], 905 RefType: rc.Config.Env["GITHUB_REF_TYPE"], 906 BaseRef: rc.Config.Env["GITHUB_BASE_REF"], 907 HeadRef: rc.Config.Env["GITHUB_HEAD_REF"], 908 Workspace: rc.Config.Env["GITHUB_WORKSPACE"], 909 } 910 if rc.JobContainer != nil { 911 ghc.EventPath = rc.JobContainer.GetActPath() + "/workflow/event.json" 912 ghc.Workspace = rc.JobContainer.ToContainerPath(rc.Config.Workdir) 913 } 914 915 if ghc.RunAttempt == "" { 916 ghc.RunAttempt = "1" 917 } 918 919 if ghc.RunID == "" { 920 ghc.RunID = "1" 921 } 922 923 if ghc.RunNumber == "" { 924 ghc.RunNumber = "1" 925 } 926 927 if ghc.RetentionDays == "" { 928 ghc.RetentionDays = "0" 929 } 930 931 if ghc.RunnerPerflog == "" { 932 ghc.RunnerPerflog = "/dev/null" 933 } 934 935 // Backwards compatibility for configs that require 936 // a default rather than being run as a cmd 937 if ghc.Actor == "" { 938 ghc.Actor = "nektos/act" 939 } 940 941 if rc.EventJSON != "" { 942 err := json.Unmarshal([]byte(rc.EventJSON), &ghc.Event) 943 if err != nil { 944 logger.Errorf("Unable to Unmarshal event '%s': %v", rc.EventJSON, err) 945 } 946 } 947 948 ghc.SetBaseAndHeadRef() 949 repoPath := rc.Config.Workdir 950 ghc.SetRepositoryAndOwner(ctx, rc.Config.GitHubInstance, rc.Config.RemoteName, repoPath) 951 if ghc.Ref == "" { 952 ghc.SetRef(ctx, rc.Config.DefaultBranch, repoPath) 953 } 954 if ghc.Sha == "" { 955 ghc.SetSha(ctx, repoPath) 956 } 957 958 ghc.SetRefTypeAndName() 959 960 // defaults 961 ghc.ServerURL = "https://github.com" 962 ghc.APIURL = "https://api.github.com" 963 ghc.GraphQLURL = "https://api.github.com/graphql" 964 // per GHES 965 if rc.Config.GitHubInstance != "github.com" { 966 ghc.ServerURL = fmt.Sprintf("https://%s", rc.Config.GitHubInstance) 967 ghc.APIURL = fmt.Sprintf("https://%s/api/v3", rc.Config.GitHubInstance) 968 ghc.GraphQLURL = fmt.Sprintf("https://%s/api/graphql", rc.Config.GitHubInstance) 969 } 970 // allow to be overridden by user 971 if rc.Config.Env["GITHUB_SERVER_URL"] != "" { 972 ghc.ServerURL = rc.Config.Env["GITHUB_SERVER_URL"] 973 } 974 if rc.Config.Env["GITHUB_API_URL"] != "" { 975 ghc.APIURL = rc.Config.Env["GITHUB_API_URL"] 976 } 977 if rc.Config.Env["GITHUB_GRAPHQL_URL"] != "" { 978 ghc.GraphQLURL = rc.Config.Env["GITHUB_GRAPHQL_URL"] 979 } 980 981 return ghc 982 } 983 984 func isLocalCheckout(ghc *model.GithubContext, step *model.Step) bool { 985 if step.Type() == model.StepTypeInvalid { 986 // This will be errored out by the executor later, we need this here to avoid a null panic though 987 return false 988 } 989 if step.Type() != model.StepTypeUsesActionRemote { 990 return false 991 } 992 remoteAction := newRemoteAction(step.Uses) 993 if remoteAction == nil { 994 // IsCheckout() will nil panic if we dont bail out early 995 return false 996 } 997 if !remoteAction.IsCheckout() { 998 return false 999 } 1000 1001 if repository, ok := step.With["repository"]; ok && repository != ghc.Repository { 1002 return false 1003 } 1004 if repository, ok := step.With["ref"]; ok && repository != ghc.Ref { 1005 return false 1006 } 1007 return true 1008 } 1009 1010 func nestedMapLookup(m map[string]interface{}, ks ...string) (rval interface{}) { 1011 var ok bool 1012 1013 if len(ks) == 0 { // degenerate input 1014 return nil 1015 } 1016 if rval, ok = m[ks[0]]; !ok { 1017 return nil 1018 } else if len(ks) == 1 { // we've reached the final key 1019 return rval 1020 } else if m, ok = rval.(map[string]interface{}); !ok { 1021 return nil 1022 } 1023 // 1+ more keys 1024 return nestedMapLookup(m, ks[1:]...) 1025 } 1026 1027 func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubContext, env map[string]string) map[string]string { 1028 env["CI"] = "true" 1029 env["GITHUB_WORKFLOW"] = github.Workflow 1030 env["GITHUB_RUN_ATTEMPT"] = github.RunAttempt 1031 env["GITHUB_RUN_ID"] = github.RunID 1032 env["GITHUB_RUN_NUMBER"] = github.RunNumber 1033 env["GITHUB_ACTION"] = github.Action 1034 env["GITHUB_ACTION_PATH"] = github.ActionPath 1035 env["GITHUB_ACTION_REPOSITORY"] = github.ActionRepository 1036 env["GITHUB_ACTION_REF"] = github.ActionRef 1037 env["GITHUB_ACTIONS"] = "true" 1038 env["GITHUB_ACTOR"] = github.Actor 1039 env["GITHUB_REPOSITORY"] = github.Repository 1040 env["GITHUB_EVENT_NAME"] = github.EventName 1041 env["GITHUB_EVENT_PATH"] = github.EventPath 1042 env["GITHUB_WORKSPACE"] = github.Workspace 1043 env["GITHUB_SHA"] = github.Sha 1044 env["GITHUB_REF"] = github.Ref 1045 env["GITHUB_REF_NAME"] = github.RefName 1046 env["GITHUB_REF_TYPE"] = github.RefType 1047 env["GITHUB_JOB"] = github.Job 1048 env["GITHUB_REPOSITORY_OWNER"] = github.RepositoryOwner 1049 env["GITHUB_RETENTION_DAYS"] = github.RetentionDays 1050 env["RUNNER_PERFLOG"] = github.RunnerPerflog 1051 env["RUNNER_TRACKING_ID"] = github.RunnerTrackingID 1052 env["GITHUB_BASE_REF"] = github.BaseRef 1053 env["GITHUB_HEAD_REF"] = github.HeadRef 1054 env["GITHUB_SERVER_URL"] = github.ServerURL 1055 env["GITHUB_API_URL"] = github.APIURL 1056 env["GITHUB_GRAPHQL_URL"] = github.GraphQLURL 1057 1058 if rc.Config.ArtifactServerPath != "" { 1059 setActionRuntimeVars(rc, env) 1060 } 1061 1062 for _, platformName := range rc.runsOnPlatformNames(ctx) { 1063 if platformName != "" { 1064 if platformName == "ubuntu-latest" { 1065 // hardcode current ubuntu-latest since we have no way to check that 'on the fly' 1066 env["ImageOS"] = "ubuntu20" 1067 } else { 1068 platformName = strings.SplitN(strings.Replace(platformName, `-`, ``, 1), `.`, 2)[0] 1069 env["ImageOS"] = platformName 1070 } 1071 } 1072 } 1073 1074 return env 1075 } 1076 1077 func setActionRuntimeVars(rc *RunContext, env map[string]string) { 1078 actionsRuntimeURL := os.Getenv("ACTIONS_RUNTIME_URL") 1079 if actionsRuntimeURL == "" { 1080 actionsRuntimeURL = fmt.Sprintf("http://%s:%s/", rc.Config.ArtifactServerAddr, rc.Config.ArtifactServerPort) 1081 } 1082 env["ACTIONS_RUNTIME_URL"] = actionsRuntimeURL 1083 env["ACTIONS_RESULTS_URL"] = actionsRuntimeURL 1084 1085 actionsRuntimeToken := os.Getenv("ACTIONS_RUNTIME_TOKEN") 1086 if actionsRuntimeToken == "" { 1087 runID := int64(1) 1088 if rid, ok := rc.Config.Env["GITHUB_RUN_ID"]; ok { 1089 runID, _ = strconv.ParseInt(rid, 10, 64) 1090 } 1091 actionsRuntimeToken, _ = common.CreateAuthorizationToken(runID, runID, runID) 1092 } 1093 env["ACTIONS_RUNTIME_TOKEN"] = actionsRuntimeToken 1094 } 1095 1096 func (rc *RunContext) handleCredentials(ctx context.Context) (string, string, error) { 1097 // TODO: remove below 2 lines when we can release act with breaking changes 1098 username := rc.Config.Secrets["DOCKER_USERNAME"] 1099 password := rc.Config.Secrets["DOCKER_PASSWORD"] 1100 1101 container := rc.Run.Job().Container() 1102 if container == nil || container.Credentials == nil { 1103 return username, password, nil 1104 } 1105 1106 if container.Credentials != nil && len(container.Credentials) != 2 { 1107 err := fmt.Errorf("invalid property count for key 'credentials:'") 1108 return "", "", err 1109 } 1110 1111 ee := rc.NewExpressionEvaluator(ctx) 1112 if username = ee.Interpolate(ctx, container.Credentials["username"]); username == "" { 1113 err := fmt.Errorf("failed to interpolate container.credentials.username") 1114 return "", "", err 1115 } 1116 if password = ee.Interpolate(ctx, container.Credentials["password"]); password == "" { 1117 err := fmt.Errorf("failed to interpolate container.credentials.password") 1118 return "", "", err 1119 } 1120 1121 if container.Credentials["username"] == "" || container.Credentials["password"] == "" { 1122 err := fmt.Errorf("container.credentials cannot be empty") 1123 return "", "", err 1124 } 1125 1126 return username, password, nil 1127 } 1128 1129 func (rc *RunContext) handleServiceCredentials(ctx context.Context, creds map[string]string) (username, password string, err error) { 1130 if creds == nil { 1131 return 1132 } 1133 if len(creds) != 2 { 1134 err = fmt.Errorf("invalid property count for key 'credentials:'") 1135 return 1136 } 1137 1138 ee := rc.NewExpressionEvaluator(ctx) 1139 if username = ee.Interpolate(ctx, creds["username"]); username == "" { 1140 err = fmt.Errorf("failed to interpolate credentials.username") 1141 return 1142 } 1143 1144 if password = ee.Interpolate(ctx, creds["password"]); password == "" { 1145 err = fmt.Errorf("failed to interpolate credentials.password") 1146 return 1147 } 1148 1149 return 1150 } 1151 1152 // GetServiceBindsAndMounts returns the binds and mounts for the service container, resolving paths as appropriate 1153 func (rc *RunContext) GetServiceBindsAndMounts(svcVolumes []string) ([]string, map[string]string) { 1154 if rc.Config.ContainerDaemonSocket == "" { 1155 rc.Config.ContainerDaemonSocket = "/var/run/docker.sock" 1156 } 1157 binds := []string{} 1158 if rc.Config.ContainerDaemonSocket != "-" { 1159 daemonPath := getDockerDaemonSocketMountPath(rc.Config.ContainerDaemonSocket) 1160 binds = append(binds, fmt.Sprintf("%s:%s", daemonPath, "/var/run/docker.sock")) 1161 } 1162 1163 mounts := map[string]string{} 1164 1165 for _, v := range svcVolumes { 1166 if !strings.Contains(v, ":") || filepath.IsAbs(v) { 1167 // Bind anonymous volume or host file. 1168 binds = append(binds, v) 1169 } else { 1170 // Mount existing volume. 1171 paths := strings.SplitN(v, ":", 2) 1172 mounts[paths[0]] = paths[1] 1173 } 1174 } 1175 1176 return binds, mounts 1177 }