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