1891c72ab1
* fix: continue jobs + steps after failure To allow proper if expression handling on jobs and steps (like always, success, failure, ...) we need to continue running all executors in the prepared chain. To keep the error handling intact we add an occurred error to the go context and handle it later in the pipeline/chain. Also we add the job result to the needs context to give expressions access to it. The needs object, failure and success functions are split between run context (on jobs) and step context. Closes #442 Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * style: correct linter warnings Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * fix: job if value defaults to success() As described in the documentation, a default value of "success()" is applied when no "if" value is present on the job. https://docs.github.com/en/actions/learn-github-actions/expressions#job-status-check-functions Co-authored-by: Markus Wolf <mail@markus-wolf.de> * fix: check job needs recursively Ensure job result includes results of previous jobs Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add runner test for job status check functions Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * test: add unit tests for run context if evaluation Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * refactor: move if expression evaluation Move if expression evaluation into own function (step context) to better support unit testing. Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> * test: add unit tests for step context if evaluation Co-authored-by: Markus Wolf <markus.wolf@new-work.se> * fix: handle job error more resilient The job error is not stored in a context map instead of a context added value. Since context values are immutable an added value requires to keep the new context in all cases. This is fragile since it might slip unnoticed to other parts of the code. Storing the error of a job in the context map will make it more stable, since the map is always there and the context of the pipeline is stable for the whole run. * feat: steps should use a default if expression of success() * test: add integration test for if-expressions * chore: disable editorconfig-checker for yaml multiline string Co-authored-by: Björn Brauer <zaubernerd@zaubernerd.de> Co-authored-by: Björn Brauer <bjoern.brauer@new-work.se>
849 lines
23 KiB
Go
849 lines
23 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/google/shlex"
|
|
"github.com/spf13/pflag"
|
|
|
|
"github.com/mitchellh/go-homedir"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
selinux "github.com/opencontainers/selinux/go-selinux"
|
|
|
|
"github.com/nektos/act/pkg/common"
|
|
"github.com/nektos/act/pkg/container"
|
|
"github.com/nektos/act/pkg/model"
|
|
)
|
|
|
|
const ActPath string = "/var/run/act"
|
|
|
|
// RunContext contains info about current job
|
|
type RunContext struct {
|
|
Name string
|
|
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
|
|
OutputMappings map[MappableOutput]MappableOutput
|
|
JobName string
|
|
}
|
|
|
|
type MappableOutput struct {
|
|
StepID string
|
|
OutputName string
|
|
}
|
|
|
|
func (rc *RunContext) String() string {
|
|
return fmt.Sprintf("%s/%s", rc.Run.Workflow.Name, rc.Name)
|
|
}
|
|
|
|
type stepStatus int
|
|
|
|
const (
|
|
stepStatusSuccess stepStatus = iota
|
|
stepStatusFailure
|
|
)
|
|
|
|
var stepStatusStrings = [...]string{
|
|
"success",
|
|
"failure",
|
|
}
|
|
|
|
func (s stepStatus) MarshalText() ([]byte, error) {
|
|
return []byte(s.String()), nil
|
|
}
|
|
|
|
func (s *stepStatus) UnmarshalText(b []byte) error {
|
|
str := string(b)
|
|
for i, name := range stepStatusStrings {
|
|
if name == str {
|
|
*s = stepStatus(i)
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("invalid step status %q", str)
|
|
}
|
|
|
|
func (s stepStatus) String() string {
|
|
if int(s) >= len(stepStatusStrings) {
|
|
return ""
|
|
}
|
|
return stepStatusStrings[s]
|
|
}
|
|
|
|
type stepResult struct {
|
|
Outputs map[string]string `json:"outputs"`
|
|
Conclusion stepStatus `json:"conclusion"`
|
|
Outcome stepStatus `json:"outcome"`
|
|
}
|
|
|
|
// 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().Environment())
|
|
}
|
|
rc.Env["ACT"] = "true"
|
|
return rc.Env
|
|
}
|
|
|
|
func (rc *RunContext) jobContainerName() string {
|
|
return createContainerName("act", rc.String())
|
|
}
|
|
|
|
// Returns the binds and mounts for the container, resolving paths as appopriate
|
|
func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) {
|
|
name := rc.jobContainerName()
|
|
|
|
if rc.Config.ContainerDaemonSocket == "" {
|
|
rc.Config.ContainerDaemonSocket = "/var/run/docker.sock"
|
|
}
|
|
|
|
binds := []string{
|
|
fmt.Sprintf("%s:%s", rc.Config.ContainerDaemonSocket, "/var/run/docker.sock"),
|
|
}
|
|
|
|
mounts := map[string]string{
|
|
"act-toolcache": "/toolcache",
|
|
name + "-env": ActPath,
|
|
}
|
|
|
|
if rc.Config.BindWorkdir {
|
|
bindModifiers := ""
|
|
if runtime.GOOS == "darwin" {
|
|
bindModifiers = ":delegated"
|
|
}
|
|
if selinux.GetEnabled() {
|
|
bindModifiers = ":z"
|
|
}
|
|
binds = append(binds, fmt.Sprintf("%s:%s%s", rc.Config.Workdir, rc.Config.ContainerWorkdir(), bindModifiers))
|
|
} else {
|
|
mounts[name] = rc.Config.ContainerWorkdir()
|
|
}
|
|
|
|
return binds, mounts
|
|
}
|
|
|
|
func (rc *RunContext) startJobContainer() common.Executor {
|
|
image := rc.platformImage()
|
|
hostname := rc.hostname()
|
|
|
|
return func(ctx context.Context) error {
|
|
rawLogger := common.Logger(ctx).WithField("raw_output", true)
|
|
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
|
|
if rc.Config.LogOutput {
|
|
rawLogger.Infof("%s", s)
|
|
} else {
|
|
rawLogger.Debugf("%s", s)
|
|
}
|
|
return true
|
|
})
|
|
|
|
username, password, err := rc.handleCredentials()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to handle credentials: %s", err)
|
|
}
|
|
|
|
common.Logger(ctx).Infof("\U0001f680 Start image=%s", image)
|
|
name := rc.jobContainerName()
|
|
|
|
envList := make([]string, 0)
|
|
|
|
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TOOL_CACHE", "/opt/hostedtoolcache"))
|
|
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_OS", "Linux"))
|
|
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
|
|
|
|
binds, mounts := rc.GetBindsAndMounts()
|
|
|
|
rc.JobContainer = container.NewContainer(&container.NewContainerInput{
|
|
Cmd: nil,
|
|
Entrypoint: []string{"/usr/bin/tail", "-f", "/dev/null"},
|
|
WorkingDir: rc.Config.ContainerWorkdir(),
|
|
Image: image,
|
|
Username: username,
|
|
Password: password,
|
|
Name: name,
|
|
Env: envList,
|
|
Mounts: mounts,
|
|
NetworkMode: "host",
|
|
Binds: binds,
|
|
Stdout: logWriter,
|
|
Stderr: logWriter,
|
|
Privileged: rc.Config.Privileged,
|
|
UsernsMode: rc.Config.UsernsMode,
|
|
Platform: rc.Config.ContainerArchitecture,
|
|
Hostname: hostname,
|
|
})
|
|
|
|
var copyWorkspace bool
|
|
var copyToPath string
|
|
if !rc.Config.BindWorkdir {
|
|
copyToPath, copyWorkspace = rc.localCheckoutPath()
|
|
copyToPath = filepath.Join(rc.Config.ContainerWorkdir(), copyToPath)
|
|
}
|
|
|
|
return common.NewPipelineExecutor(
|
|
rc.JobContainer.Pull(rc.Config.ForcePull),
|
|
rc.stopJobContainer(),
|
|
rc.JobContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
|
|
rc.JobContainer.Start(false),
|
|
rc.JobContainer.UpdateFromImageEnv(&rc.Env),
|
|
rc.JobContainer.UpdateFromEnv("/etc/environment", &rc.Env),
|
|
rc.JobContainer.Exec([]string{"mkdir", "-m", "0777", "-p", ActPath}, rc.Env, "root", ""),
|
|
rc.JobContainer.CopyDir(copyToPath, rc.Config.Workdir+string(filepath.Separator)+".", rc.Config.UseGitIgnore).IfBool(copyWorkspace),
|
|
rc.JobContainer.Copy(ActPath+"/", &container.FileEntry{
|
|
Name: "workflow/event.json",
|
|
Mode: 0644,
|
|
Body: rc.EventJSON,
|
|
}, &container.FileEntry{
|
|
Name: "workflow/envs.txt",
|
|
Mode: 0666,
|
|
Body: "",
|
|
}, &container.FileEntry{
|
|
Name: "workflow/paths.txt",
|
|
Mode: 0666,
|
|
Body: "",
|
|
}),
|
|
)(ctx)
|
|
}
|
|
}
|
|
|
|
func (rc *RunContext) execJobContainer(cmd []string, env map[string]string, user, workdir string) common.Executor {
|
|
return func(ctx context.Context) error {
|
|
return rc.JobContainer.Exec(cmd, env, user, workdir)(ctx)
|
|
}
|
|
}
|
|
|
|
// stopJobContainer removes the job container (if it exists) and its volume (if it exists) if !rc.Config.ReuseContainers
|
|
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
|
|
}
|
|
}
|
|
|
|
// Prepare the mounts and binds for the worker
|
|
|
|
// ActionCacheDir is for rc
|
|
func (rc *RunContext) ActionCacheDir() string {
|
|
var xdgCache string
|
|
var ok bool
|
|
if xdgCache, ok = os.LookupEnv("XDG_CACHE_HOME"); !ok || xdgCache == "" {
|
|
if home, err := homedir.Dir(); err == nil {
|
|
xdgCache = filepath.Join(home, ".cache")
|
|
} else if xdgCache, err = filepath.Abs("."); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
return filepath.Join(xdgCache, "act")
|
|
}
|
|
|
|
// Interpolate outputs after a job is done
|
|
func (rc *RunContext) interpolateOutputs() common.Executor {
|
|
return func(ctx context.Context) error {
|
|
ee := rc.NewExpressionEvaluator()
|
|
for k, v := range rc.Run.Job().Outputs {
|
|
interpolated := ee.Interpolate(v)
|
|
if v != interpolated {
|
|
rc.Run.Job().Outputs[k] = interpolated
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// 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
|
|
})
|
|
|
|
steps = append(steps, rc.startJobContainer())
|
|
|
|
for i, step := range rc.Run.Job().Steps {
|
|
if step.ID == "" {
|
|
step.ID = fmt.Sprintf("%d", i)
|
|
}
|
|
steps = append(steps, rc.newStepExecutor(step))
|
|
}
|
|
steps = append(steps, func(ctx context.Context) error {
|
|
err := rc.stopJobContainer()(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rc.Run.Job().Result = "success"
|
|
jobError := common.JobError(ctx)
|
|
if jobError != nil {
|
|
rc.Run.Job().Result = "failure"
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return common.NewPipelineExecutor(steps...).Finally(rc.interpolateOutputs()).Finally(func(ctx context.Context) error {
|
|
if rc.JobContainer != nil {
|
|
return rc.JobContainer.Close()(ctx)
|
|
}
|
|
return nil
|
|
}).If(rc.isEnabled)
|
|
}
|
|
|
|
func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor {
|
|
sc := &StepContext{
|
|
RunContext: rc,
|
|
Step: step,
|
|
}
|
|
return func(ctx context.Context) error {
|
|
rc.CurrentStep = sc.Step.ID
|
|
rc.StepResults[rc.CurrentStep] = &stepResult{
|
|
Outcome: stepStatusSuccess,
|
|
Conclusion: stepStatusSuccess,
|
|
Outputs: make(map[string]string),
|
|
}
|
|
|
|
runStep, err := sc.isEnabled(ctx)
|
|
if err != nil {
|
|
rc.StepResults[rc.CurrentStep].Conclusion = stepStatusFailure
|
|
rc.StepResults[rc.CurrentStep].Outcome = stepStatusFailure
|
|
return err
|
|
}
|
|
|
|
if !runStep {
|
|
log.Debugf("Skipping step '%s' due to '%s'", sc.Step.String(), sc.Step.If.Value)
|
|
return nil
|
|
}
|
|
|
|
exprEval, err := sc.setupEnv(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rc.ExprEval = exprEval
|
|
|
|
common.Logger(ctx).Infof("\u2B50 Run %s", sc.Step)
|
|
err = sc.Executor()(ctx)
|
|
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].Outcome = stepStatusFailure
|
|
if sc.Step.ContinueOnError {
|
|
common.Logger(ctx).Infof("Failed but continue next step")
|
|
err = nil
|
|
rc.StepResults[rc.CurrentStep].Conclusion = stepStatusSuccess
|
|
} else {
|
|
rc.StepResults[rc.CurrentStep].Conclusion = stepStatusFailure
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
func (rc *RunContext) platformImage() string {
|
|
job := rc.Run.Job()
|
|
|
|
c := job.Container()
|
|
if c != nil {
|
|
return rc.ExprEval.Interpolate(c.Image)
|
|
}
|
|
|
|
if job.RunsOn() == nil {
|
|
log.Errorf("'runs-on' key not defined in %s", rc.String())
|
|
}
|
|
|
|
for _, runnerLabel := range job.RunsOn() {
|
|
platformName := rc.ExprEval.Interpolate(runnerLabel)
|
|
image := rc.Config.Platforms[strings.ToLower(platformName)]
|
|
if image != "" {
|
|
return image
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (rc *RunContext) hostname() string {
|
|
job := rc.Run.Job()
|
|
c := job.Container()
|
|
if c == nil {
|
|
return ""
|
|
}
|
|
|
|
optionsFlags := pflag.NewFlagSet("container_options", pflag.ContinueOnError)
|
|
hostname := optionsFlags.StringP("hostname", "h", "", "")
|
|
optionsArgs, err := shlex.Split(c.Options)
|
|
if err != nil {
|
|
log.Warnf("Cannot parse container options: %s", c.Options)
|
|
return ""
|
|
}
|
|
err = optionsFlags.Parse(optionsArgs)
|
|
if err != nil {
|
|
log.Warnf("Cannot parse container options: %s", c.Options)
|
|
return ""
|
|
}
|
|
return *hostname
|
|
}
|
|
|
|
func (rc *RunContext) isEnabled(ctx context.Context) bool {
|
|
job := rc.Run.Job()
|
|
l := common.Logger(ctx)
|
|
runJob, err := EvalBool(rc.ExprEval, job.If.Value)
|
|
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.Value)
|
|
return false
|
|
}
|
|
|
|
img := rc.platformImage()
|
|
if img == "" {
|
|
if job.RunsOn() == nil {
|
|
log.Errorf("'runs-on' key not defined in %s", rc.String())
|
|
}
|
|
|
|
for _, runnerLabel := range job.RunsOn() {
|
|
platformName := rc.ExprEval.Interpolate(runnerLabel)
|
|
l.Infof("\U0001F6A7 Skipping unsupported platform -- Try running with `-P %+v=...`", platformName)
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
var splitPattern *regexp.Regexp
|
|
|
|
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
|
|
}
|
|
|
|
func createContainerName(parts ...string) string {
|
|
name := make([]string, 0)
|
|
pattern := regexp.MustCompile("[^a-zA-Z0-9]")
|
|
partLen := (30 / len(parts)) - 1
|
|
for i, part := range parts {
|
|
if i == len(parts)-1 {
|
|
name = append(name, pattern.ReplaceAllString(part, "-"))
|
|
} else {
|
|
// If any part has a '-<number>' on the end it is likely part of a matrix job.
|
|
// Let's preserve the number to prevent clashes in container names.
|
|
re := regexp.MustCompile("-[0-9]+$")
|
|
num := re.FindStringSubmatch(part)
|
|
if len(num) > 0 {
|
|
name = append(name, trimToLen(pattern.ReplaceAllString(part, "-"), partLen-len(num[0])))
|
|
name = append(name, num[0])
|
|
} else {
|
|
name = append(name, trimToLen(pattern.ReplaceAllString(part, "-"), partLen))
|
|
}
|
|
}
|
|
}
|
|
return strings.ReplaceAll(strings.Trim(strings.Join(name, "-"), "-"), "--", "-")
|
|
}
|
|
|
|
func trimToLen(s string, l int) string {
|
|
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.Conclusion == stepStatusFailure {
|
|
jobStatus = "failure"
|
|
break
|
|
}
|
|
}
|
|
return &jobContext{
|
|
Status: jobStatus,
|
|
}
|
|
}
|
|
|
|
func (rc *RunContext) getStepsContext() map[string]*stepResult {
|
|
return rc.StepResults
|
|
}
|
|
|
|
func (rc *RunContext) getNeedsTransitive(job *model.Job) []string {
|
|
needs := job.Needs()
|
|
|
|
for _, need := range needs {
|
|
parentNeeds := rc.getNeedsTransitive(rc.Run.Workflow.GetJob(need))
|
|
needs = append(needs, parentNeeds...)
|
|
}
|
|
|
|
return needs
|
|
}
|
|
|
|
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"`
|
|
ActionPath string `json:"action_path"`
|
|
ActionRef string `json:"action_ref"`
|
|
ActionRepository string `json:"action_repository"`
|
|
Job string `json:"job"`
|
|
JobName string `json:"job_name"`
|
|
RepositoryOwner string `json:"repository_owner"`
|
|
RetentionDays string `json:"retention_days"`
|
|
RunnerPerflog string `json:"runner_perflog"`
|
|
RunnerTrackingID string `json:"runner_tracking_id"`
|
|
}
|
|
|
|
func (rc *RunContext) getGithubContext() *githubContext {
|
|
ghc := &githubContext{
|
|
Event: make(map[string]interface{}),
|
|
EventPath: ActPath + "/workflow/event.json",
|
|
Workflow: rc.Run.Workflow.Name,
|
|
RunID: rc.Config.Env["GITHUB_RUN_ID"],
|
|
RunNumber: rc.Config.Env["GITHUB_RUN_NUMBER"],
|
|
Actor: rc.Config.Actor,
|
|
EventName: rc.Config.EventName,
|
|
Workspace: rc.Config.ContainerWorkdir(),
|
|
Action: rc.CurrentStep,
|
|
Token: rc.Config.Secrets["GITHUB_TOKEN"],
|
|
ActionPath: rc.Config.Env["GITHUB_ACTION_PATH"],
|
|
ActionRef: rc.Config.Env["RUNNER_ACTION_REF"],
|
|
ActionRepository: rc.Config.Env["RUNNER_ACTION_REPOSITORY"],
|
|
RepositoryOwner: rc.Config.Env["GITHUB_REPOSITORY_OWNER"],
|
|
RetentionDays: rc.Config.Env["GITHUB_RETENTION_DAYS"],
|
|
RunnerPerflog: rc.Config.Env["RUNNER_PERFLOG"],
|
|
RunnerTrackingID: rc.Config.Env["RUNNER_TRACKING_ID"],
|
|
}
|
|
|
|
if ghc.RunID == "" {
|
|
ghc.RunID = "1"
|
|
}
|
|
|
|
if ghc.RunNumber == "" {
|
|
ghc.RunNumber = "1"
|
|
}
|
|
|
|
if ghc.RetentionDays == "" {
|
|
ghc.RetentionDays = "0"
|
|
}
|
|
|
|
if ghc.RunnerPerflog == "" {
|
|
ghc.RunnerPerflog = "/dev/null"
|
|
}
|
|
|
|
// 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, rc.Config.GitHubInstance)
|
|
if err != nil {
|
|
log.Warningf("unable to get git repo: %v", err)
|
|
} else {
|
|
ghc.Repository = repo
|
|
if ghc.RepositoryOwner == "" {
|
|
ghc.RepositoryOwner = strings.Split(repo, "/")[0]
|
|
}
|
|
}
|
|
|
|
_, sha, err := common.FindGitRevision(repoPath)
|
|
if err != nil {
|
|
log.Warningf("unable to get git revision: %v", err)
|
|
} else {
|
|
ghc.Sha = sha
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
maybeRef := nestedMapLookup(ghc.Event, ghc.EventName, "ref")
|
|
if maybeRef != nil {
|
|
log.Debugf("using github ref from event: %s", maybeRef)
|
|
ghc.Ref = maybeRef.(string)
|
|
} else {
|
|
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
|
|
}
|
|
|
|
// 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.StepTypeInvalid {
|
|
// This will be errored out by the executor later, we need this here to avoid a null panic though
|
|
return false
|
|
}
|
|
if step.Type() != model.StepTypeUsesActionRemote {
|
|
return false
|
|
}
|
|
remoteAction := newRemoteAction(step.Uses)
|
|
if remoteAction == nil {
|
|
// IsCheckout() will nil panic if we dont bail out early
|
|
return false
|
|
}
|
|
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["GITHUB_ENV"] = ActPath + "/workflow/envs.txt"
|
|
env["GITHUB_PATH"] = ActPath + "/workflow/paths.txt"
|
|
env["GITHUB_WORKFLOW"] = github.Workflow
|
|
env["GITHUB_RUN_ID"] = github.RunID
|
|
env["GITHUB_RUN_NUMBER"] = github.RunNumber
|
|
env["GITHUB_ACTION"] = github.Action
|
|
if github.ActionPath != "" {
|
|
env["GITHUB_ACTION_PATH"] = github.ActionPath
|
|
}
|
|
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
|
|
env["GITHUB_SERVER_URL"] = "https://github.com"
|
|
env["GITHUB_API_URL"] = "https://api.github.com"
|
|
env["GITHUB_GRAPHQL_URL"] = "https://api.github.com/graphql"
|
|
env["GITHUB_ACTION_REF"] = github.ActionRef
|
|
env["GITHUB_ACTION_REPOSITORY"] = github.ActionRepository
|
|
env["GITHUB_BASE_REF"] = github.BaseRef
|
|
env["GITHUB_HEAD_REF"] = github.HeadRef
|
|
env["GITHUB_JOB"] = rc.JobName
|
|
env["GITHUB_REPOSITORY_OWNER"] = github.RepositoryOwner
|
|
env["GITHUB_RETENTION_DAYS"] = github.RetentionDays
|
|
env["RUNNER_PERFLOG"] = github.RunnerPerflog
|
|
env["RUNNER_TRACKING_ID"] = github.RunnerTrackingID
|
|
if rc.Config.GitHubInstance != "github.com" {
|
|
env["GITHUB_SERVER_URL"] = fmt.Sprintf("https://%s", rc.Config.GitHubInstance)
|
|
env["GITHUB_API_URL"] = fmt.Sprintf("https://%s/api/v3", rc.Config.GitHubInstance)
|
|
env["GITHUB_GRAPHQL_URL"] = fmt.Sprintf("https://%s/api/graphql", rc.Config.GitHubInstance)
|
|
}
|
|
|
|
if rc.Config.ArtifactServerPath != "" {
|
|
setActionRuntimeVars(rc, env)
|
|
}
|
|
|
|
job := rc.Run.Job()
|
|
if job.RunsOn() != nil {
|
|
for _, runnerLabel := range job.RunsOn() {
|
|
platformName := rc.ExprEval.Interpolate(runnerLabel)
|
|
if platformName != "" {
|
|
if platformName == "ubuntu-latest" {
|
|
// hardcode current ubuntu-latest since we have no way to check that 'on the fly'
|
|
env["ImageOS"] = "ubuntu20"
|
|
} else {
|
|
platformName = strings.SplitN(strings.Replace(platformName, `-`, ``, 1), `.`, 2)[0]
|
|
env["ImageOS"] = platformName
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return env
|
|
}
|
|
|
|
func setActionRuntimeVars(rc *RunContext, env map[string]string) {
|
|
actionsRuntimeURL := os.Getenv("ACTIONS_RUNTIME_URL")
|
|
if actionsRuntimeURL == "" {
|
|
actionsRuntimeURL = fmt.Sprintf("http://%s:%s/", common.GetOutboundIP().String(), rc.Config.ArtifactServerPort)
|
|
}
|
|
env["ACTIONS_RUNTIME_URL"] = actionsRuntimeURL
|
|
|
|
actionsRuntimeToken := os.Getenv("ACTIONS_RUNTIME_TOKEN")
|
|
if actionsRuntimeToken == "" {
|
|
actionsRuntimeToken = "token"
|
|
}
|
|
env["ACTIONS_RUNTIME_TOKEN"] = actionsRuntimeToken
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (rc *RunContext) handleCredentials() (username, password string, err error) {
|
|
// TODO: remove below 2 lines when we can release act with breaking changes
|
|
username = rc.Config.Secrets["DOCKER_USERNAME"]
|
|
password = rc.Config.Secrets["DOCKER_PASSWORD"]
|
|
|
|
container := rc.Run.Job().Container()
|
|
if container == nil || container.Credentials == nil {
|
|
return
|
|
}
|
|
|
|
if container.Credentials != nil && len(container.Credentials) != 2 {
|
|
err = fmt.Errorf("invalid property count for key 'credentials:'")
|
|
return
|
|
}
|
|
|
|
ee := rc.NewExpressionEvaluator()
|
|
var ok bool
|
|
if username, ok = ee.InterpolateWithStringCheck(container.Credentials["username"]); !ok {
|
|
err = fmt.Errorf("failed to interpolate container.credentials.username")
|
|
return
|
|
}
|
|
if password, ok = ee.InterpolateWithStringCheck(container.Credentials["password"]); !ok {
|
|
err = fmt.Errorf("failed to interpolate container.credentials.password")
|
|
return
|
|
}
|
|
|
|
if container.Credentials["username"] == "" || container.Credentials["password"] == "" {
|
|
err = fmt.Errorf("container.credentials cannot be empty")
|
|
return
|
|
}
|
|
|
|
return username, password, err
|
|
}
|