act/pkg/runner/run_context.go

578 lines
15 KiB
Go
Raw Normal View History

package runner
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
2020-02-23 17:01:25 -06:00
"path/filepath"
"regexp"
2020-02-24 12:56:49 -06:00
"runtime"
"strings"
"github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/model"
log "github.com/sirupsen/logrus"
)
// RunContext contains info about current job
type RunContext struct {
Name string
2020-02-23 17:01:25 -06:00
Config *Config
Matrix map[string]interface{}
Run *model.Run
EventJSON string
Env map[string]string
ExtraPath []string
CurrentStep string
StepResults map[string]*stepResult
ExprEval ExpressionEvaluator
JobContainer container.Container
}
func (rc *RunContext) String() string {
return fmt.Sprintf("%s/%s", rc.Run.Workflow.Name, rc.Name)
}
type stepResult struct {
Success bool `json:"success"`
Outputs map[string]string `json:"outputs"`
}
// GetEnv returns the env for the context
func (rc *RunContext) GetEnv() map[string]string {
if rc.Env == nil {
rc.Env = mergeMaps(rc.Config.Env, rc.Run.Workflow.Env, rc.Run.Job().Env)
}
rc.Env["ACT"] = "true"
return rc.Env
}
2020-02-23 17:01:25 -06:00
func (rc *RunContext) jobContainerName() string {
return createContainerName("act", rc.String())
}
2020-02-23 17:01:25 -06:00
func (rc *RunContext) startJobContainer() common.Executor {
image := rc.platformImage()
2020-02-23 17:01:25 -06:00
return func(ctx context.Context) error {
rawLogger := common.Logger(ctx).WithField("raw_output", true)
2020-02-24 14:48:12 -06:00
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
2020-02-23 17:01:25 -06:00
if rc.Config.LogOutput {
rawLogger.Infof("%s", s)
2020-02-23 17:01:25 -06:00
} else {
rawLogger.Debugf("%s", s)
2020-02-23 17:01:25 -06:00
}
2020-02-24 14:48:12 -06:00
return true
2020-02-23 17:01:25 -06:00
})
common.Logger(ctx).Infof("\U0001f680 Start image=%s", image)
name := rc.jobContainerName()
2020-02-24 12:56:49 -06:00
envList := make([]string, 0)
bindModifiers := ""
if runtime.GOOS == "darwin" {
bindModifiers = ":delegated"
}
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TOOL_CACHE", "/opt/hostedtoolcache"))
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_OS", "Linux"))
2020-04-23 10:18:36 -05:00
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
2020-02-24 12:56:49 -06:00
2020-02-24 19:48:21 -06:00
binds := []string{
fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"),
}
if rc.Config.BindWorkdir {
binds = append(binds, fmt.Sprintf("%s:%s%s", rc.Config.Workdir, "/github/workspace", bindModifiers))
}
2020-02-23 17:01:25 -06:00
rc.JobContainer = container.NewContainer(&container.NewContainerInput{
Cmd: nil,
2020-02-24 00:34:48 -06:00
Entrypoint: []string{"/usr/bin/tail", "-f", "/dev/null"},
2020-02-23 17:01:25 -06:00
WorkingDir: "/github/workspace",
Image: image,
Name: name,
2020-02-24 12:56:49 -06:00
Env: envList,
2020-02-23 17:01:25 -06:00
Mounts: map[string]string{
2020-02-24 14:48:12 -06:00
name: "/github",
"act-toolcache": "/toolcache",
2020-02-24 18:38:49 -06:00
"act-actions": "/actions",
2020-02-23 17:01:25 -06:00
},
NetworkMode: "host",
Binds: binds,
Stdout: logWriter,
Stderr: logWriter,
2020-08-01 15:21:49 -05:00
Privileged: rc.Config.Privileged,
2020-02-23 17:01:25 -06:00
})
var copyWorkspace bool
var copyToPath string
if !rc.Config.BindWorkdir {
copyToPath, copyWorkspace = rc.localCheckoutPath()
copyToPath = filepath.Join("/github/workspace", copyToPath)
}
2020-02-23 17:01:25 -06:00
return common.NewPipelineExecutor(
rc.JobContainer.Pull(rc.Config.ForcePull),
rc.stopJobContainer(),
2020-02-23 17:01:25 -06:00
rc.JobContainer.Create(),
rc.JobContainer.Start(false),
rc.JobContainer.CopyDir(copyToPath, rc.Config.Workdir+string(filepath.Separator)+".").IfBool(copyWorkspace),
2020-02-23 17:01:25 -06:00
rc.JobContainer.Copy("/github/", &container.FileEntry{
Name: "workflow/event.json",
Mode: 0644,
2020-02-23 17:01:25 -06:00
Body: rc.EventJSON,
2020-02-24 18:38:49 -06:00
}, &container.FileEntry{
Name: "home/.act",
Mode: 0644,
2020-02-24 18:38:49 -06:00
Body: "",
2020-02-23 17:01:25 -06:00
}),
)(ctx)
}
}
func (rc *RunContext) execJobContainer(cmd []string, env map[string]string) common.Executor {
return func(ctx context.Context) error {
return rc.JobContainer.Exec(cmd, env)(ctx)
}
}
// stopJobContainer removes the job container (if it exists) and its volume (if it exists) if !rc.Config.ReuseContainers
2020-02-23 17:01:25 -06:00
func (rc *RunContext) stopJobContainer() common.Executor {
return func(ctx context.Context) error {
if rc.JobContainer != nil && !rc.Config.ReuseContainers {
return rc.JobContainer.Remove().
Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName(), false))(ctx)
}
return nil
}
}
2020-02-24 12:56:49 -06:00
// ActionCacheDir is for rc
func (rc *RunContext) ActionCacheDir() string {
2020-02-24 00:34:48 -06:00
var xdgCache string
var ok bool
if xdgCache, ok = os.LookupEnv("XDG_CACHE_HOME"); !ok {
if home, ok := os.LookupEnv("HOME"); ok {
xdgCache = fmt.Sprintf("%s/.cache", home)
}
}
return filepath.Join(xdgCache, "act")
}
2020-02-23 17:01:25 -06:00
// Executor returns a pipeline executor for all the steps in the job
func (rc *RunContext) Executor() common.Executor {
steps := make([]common.Executor, 0)
steps = append(steps, func(ctx context.Context) error {
if len(rc.Matrix) > 0 {
common.Logger(ctx).Infof("\U0001F9EA Matrix: %v", rc.Matrix)
}
return nil
})
2020-02-23 17:01:25 -06:00
steps = append(steps, rc.startJobContainer())
for i, step := range rc.Run.Job().Steps {
if step.ID == "" {
step.ID = fmt.Sprintf("%d", i)
}
2020-02-23 17:01:25 -06:00
steps = append(steps, rc.newStepExecutor(step))
}
steps = append(steps, rc.stopJobContainer())
2020-02-23 17:01:25 -06:00
return common.NewPipelineExecutor(steps...).If(rc.isEnabled)
}
2020-02-23 17:01:25 -06:00
func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
sc := &StepContext{
RunContext: rc,
Step: step,
}
return func(ctx context.Context) error {
2020-02-23 17:01:25 -06:00
rc.CurrentStep = sc.Step.ID
rc.StepResults[rc.CurrentStep] = &stepResult{
Success: true,
Outputs: make(map[string]string),
}
_ = sc.setupEnv()(ctx)
2020-02-23 17:01:25 -06:00
rc.ExprEval = sc.NewExpressionEvaluator()
runStep, err := rc.EvalBool(sc.Step.If)
if err != nil {
common.Logger(ctx).Errorf(" \u274C Error in if: expression - %s", sc.Step)
rc.StepResults[rc.CurrentStep].Success = false
return err
}
if !runStep {
2020-02-23 17:01:25 -06:00
log.Debugf("Skipping step '%s' due to '%s'", sc.Step.String(), sc.Step.If)
return nil
}
2020-02-23 17:01:25 -06:00
common.Logger(ctx).Infof("\u2B50 Run %s", sc.Step)
err = sc.Executor()(ctx)
2020-02-23 17:01:25 -06:00
if err == nil {
common.Logger(ctx).Infof(" \u2705 Success - %s", sc.Step)
} else {
common.Logger(ctx).Errorf(" \u274C Failure - %s", sc.Step)
rc.StepResults[rc.CurrentStep].Success = false
2020-02-20 21:43:20 -06:00
}
2020-02-23 17:01:25 -06:00
return err
}
}
2020-02-20 21:43:20 -06:00
func (rc *RunContext) platformImage() string {
job := rc.Run.Job()
c := job.Container()
if c != nil {
return rc.ExprEval.Interpolate(c.Image)
}
for _, runnerLabel := range job.RunsOn() {
platformName := rc.ExprEval.Interpolate(runnerLabel)
image := rc.Config.Platforms[strings.ToLower(platformName)]
if image != "" {
return image
}
}
return ""
}
2020-02-23 17:01:25 -06:00
func (rc *RunContext) isEnabled(ctx context.Context) bool {
job := rc.Run.Job()
l := common.Logger(ctx)
runJob, err := rc.EvalBool(job.If)
if err != nil {
common.Logger(ctx).Errorf(" \u274C Error in if: expression - %s", job.Name)
return false
}
if !runJob {
l.Debugf("Skipping job '%s' due to '%s'", job.Name, job.If)
2020-02-23 17:01:25 -06:00
return false
}
2020-02-20 21:43:20 -06:00
img := rc.platformImage()
if img == "" {
for _, runnerLabel := range job.RunsOn() {
platformName := rc.ExprEval.Interpolate(runnerLabel)
l.Infof("\U0001F6A7 Skipping unsupported platform '%+v'", platformName)
}
2020-02-23 17:01:25 -06:00
return false
}
2020-02-23 17:01:25 -06:00
return true
}
// EvalBool evaluates an expression against current run context
func (rc *RunContext) EvalBool(expr string) (bool, error) {
if strings.HasPrefix(strings.TrimSpace(expr), "!") {
return false, errors.New("expressions starting with ! must be wrapped in ${{ }}")
}
if expr != "" {
splitPattern := regexp.MustCompile(fmt.Sprintf(`%s|%s|\S+`, expressionPattern.String(), operatorPattern.String()))
parts := splitPattern.FindAllString(expr, -1)
var evaluatedParts []string
for i, part := range parts {
if operatorPattern.MatchString(part) {
evaluatedParts = append(evaluatedParts, part)
continue
}
interpolatedPart, isString := rc.ExprEval.InterpolateWithStringCheck(part)
// This peculiar transformation has to be done because the Github parser
// treats false retured from contexts as a string, not a boolean.
// Hence env.SOMETHING will be evaluated to true in an if: expression
// regardless if SOMETHING is set to false, true or any other string.
// It also handles some other weirdness that I found by trial and error.
if (expressionPattern.MatchString(part) && // it is an expression
!strings.Contains(part, "!")) && // but it's not negated
interpolatedPart == "false" && // and the interpolated string is false
(isString || previousOrNextPartIsAnOperator(i, parts)) { // and it's of type string or has an logical operator before or after
interpolatedPart = fmt.Sprintf("'%s'", interpolatedPart) // then we have to quote the false expression
}
evaluatedParts = append(evaluatedParts, interpolatedPart)
}
joined := strings.Join(evaluatedParts, " ")
v, _, err := rc.ExprEval.Evaluate(fmt.Sprintf("Boolean(%s)", joined))
if err != nil {
return false, nil
}
log.Debugf("expression '%s' evaluated to '%s'", expr, v)
return v == "true", nil
}
return true, nil
}
func previousOrNextPartIsAnOperator(i int, parts []string) bool {
operator := false
if i > 0 {
operator = operatorPattern.MatchString(parts[i-1])
}
if i+1 < len(parts) {
operator = operator || operatorPattern.MatchString(parts[i+1])
}
return operator
}
func mergeMaps(maps ...map[string]string) map[string]string {
rtnMap := make(map[string]string)
for _, m := range maps {
for k, v := range m {
rtnMap[k] = v
}
}
return rtnMap
}
2020-02-23 17:01:25 -06:00
func createContainerName(parts ...string) string {
name := make([]string, 0)
pattern := regexp.MustCompile("[^a-zA-Z0-9]")
2020-02-23 18:36:44 -06:00
partLen := (30 / len(parts)) - 1
2020-02-24 00:34:48 -06:00
for i, part := range parts {
if i == len(parts)-1 {
name = append(name, pattern.ReplaceAllString(part, "-"))
} else {
name = append(name, trimToLen(pattern.ReplaceAllString(part, "-"), partLen))
}
2020-02-23 17:01:25 -06:00
}
2020-04-16 18:05:00 -05:00
return strings.Trim(strings.Join(name, "-"), "-")
}
func trimToLen(s string, l int) string {
2020-02-20 21:43:20 -06:00
if l < 0 {
l = 0
}
if len(s) > l {
return s[:l]
}
return s
}
type jobContext struct {
Status string `json:"status"`
Container struct {
ID string `json:"id"`
Network string `json:"network"`
} `json:"container"`
Services map[string]struct {
ID string `json:"id"`
} `json:"services"`
}
func (rc *RunContext) getJobContext() *jobContext {
jobStatus := "success"
for _, stepStatus := range rc.StepResults {
if !stepStatus.Success {
jobStatus = "failure"
break
}
}
return &jobContext{
Status: jobStatus,
}
}
func (rc *RunContext) getStepsContext() map[string]*stepResult {
return rc.StepResults
}
type githubContext struct {
Event map[string]interface{} `json:"event"`
EventPath string `json:"event_path"`
Workflow string `json:"workflow"`
RunID string `json:"run_id"`
RunNumber string `json:"run_number"`
Actor string `json:"actor"`
Repository string `json:"repository"`
EventName string `json:"event_name"`
Sha string `json:"sha"`
Ref string `json:"ref"`
HeadRef string `json:"head_ref"`
BaseRef string `json:"base_ref"`
Token string `json:"token"`
Workspace string `json:"workspace"`
Action string `json:"action"`
}
func (rc *RunContext) getGithubContext() *githubContext {
token, ok := rc.Config.Secrets["GITHUB_TOKEN"]
if !ok {
token = os.Getenv("GITHUB_TOKEN")
}
runID := rc.Config.Env["GITHUB_RUN_ID"]
if runID == "" {
runID = "1"
}
runNumber := rc.Config.Env["GITHUB_RUN_NUMBER"]
if runNumber == "" {
runNumber = "1"
}
ghc := &githubContext{
Event: make(map[string]interface{}),
EventPath: "/github/workflow/event.json",
Workflow: rc.Run.Workflow.Name,
RunID: runID,
RunNumber: runNumber,
Actor: rc.Config.Actor,
EventName: rc.Config.EventName,
Token: token,
Workspace: "/github/workspace",
Action: rc.CurrentStep,
}
// Backwards compatibility for configs that require
// a default rather than being run as a cmd
if ghc.Actor == "" {
ghc.Actor = "nektos/act"
}
repoPath := rc.Config.Workdir
repo, err := common.FindGithubRepo(repoPath)
if err != nil {
log.Warningf("unable to get git repo: %v", err)
} else {
ghc.Repository = repo
}
_, sha, err := common.FindGitRevision(repoPath)
if err != nil {
log.Warningf("unable to get git revision: %v", err)
} else {
ghc.Sha = sha
}
ref, err := common.FindGitRef(repoPath)
if err != nil {
log.Warningf("unable to get git ref: %v", err)
} else {
log.Debugf("using github ref: %s", ref)
ghc.Ref = ref
}
if rc.EventJSON != "" {
err = json.Unmarshal([]byte(rc.EventJSON), &ghc.Event)
if err != nil {
log.Errorf("Unable to Unmarshal event '%s': %v", rc.EventJSON, err)
}
}
// set the branch in the event data
if rc.Config.DefaultBranch != "" {
ghc.Event = withDefaultBranch(rc.Config.DefaultBranch, ghc.Event)
} else {
ghc.Event = withDefaultBranch("master", ghc.Event)
}
if ghc.EventName == "pull_request" {
ghc.BaseRef = asString(nestedMapLookup(ghc.Event, "pull_request", "base", "ref"))
ghc.HeadRef = asString(nestedMapLookup(ghc.Event, "pull_request", "head", "ref"))
}
return ghc
}
func (ghc *githubContext) isLocalCheckout(step *model.Step) bool {
if step.Type() != model.StepTypeUsesActionRemote {
return false
}
remoteAction := newRemoteAction(step.Uses)
if !remoteAction.IsCheckout() {
return false
}
if repository, ok := step.With["repository"]; ok && repository != ghc.Repository {
return false
}
if repository, ok := step.With["ref"]; ok && repository != ghc.Ref {
return false
}
return true
}
func asString(v interface{}) string {
if v == nil {
return ""
} else if s, ok := v.(string); ok {
return s
}
return ""
}
func nestedMapLookup(m map[string]interface{}, ks ...string) (rval interface{}) {
var ok bool
if len(ks) == 0 { // degenerate input
return nil
}
if rval, ok = m[ks[0]]; !ok {
return nil
} else if len(ks) == 1 { // we've reached the final key
return rval
} else if m, ok = rval.(map[string]interface{}); !ok {
return nil
} else { // 1+ more keys
return nestedMapLookup(m, ks[1:]...)
}
}
func withDefaultBranch(b string, event map[string]interface{}) map[string]interface{} {
repoI, ok := event["repository"]
if !ok {
repoI = make(map[string]interface{})
}
repo, ok := repoI.(map[string]interface{})
if !ok {
log.Warnf("unable to set default branch to %v", b)
return event
}
// if the branch is already there return with no changes
if _, ok = repo["default_branch"]; ok {
return event
}
repo["default_branch"] = b
event["repository"] = repo
return event
}
func (rc *RunContext) withGithubEnv(env map[string]string) map[string]string {
github := rc.getGithubContext()
env["CI"] = "true"
env["HOME"] = "/github/home"
env["GITHUB_WORKFLOW"] = github.Workflow
env["GITHUB_RUN_ID"] = github.RunID
env["GITHUB_RUN_NUMBER"] = github.RunNumber
env["GITHUB_ACTION"] = github.Action
2020-02-24 12:56:49 -06:00
env["GITHUB_ACTIONS"] = "true"
env["GITHUB_ACTOR"] = github.Actor
env["GITHUB_REPOSITORY"] = github.Repository
env["GITHUB_EVENT_NAME"] = github.EventName
env["GITHUB_EVENT_PATH"] = github.EventPath
env["GITHUB_WORKSPACE"] = github.Workspace
env["GITHUB_SHA"] = github.Sha
env["GITHUB_REF"] = github.Ref
env["GITHUB_TOKEN"] = github.Token
return env
}
func (rc *RunContext) localCheckoutPath() (string, bool) {
ghContext := rc.getGithubContext()
for _, step := range rc.Run.Job().Steps {
if ghContext.isLocalCheckout(step) {
return step.With["path"], true
}
}
return "", false
}