summaryrefslogtreecommitdiffstats
path: root/pkg/runner/runner.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/runner/runner.go')
-rw-r--r--pkg/runner/runner.go280
1 files changed, 280 insertions, 0 deletions
diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go
new file mode 100644
index 0000000..a4dd989
--- /dev/null
+++ b/pkg/runner/runner.go
@@ -0,0 +1,280 @@
+package runner
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "os"
+ "runtime"
+ "time"
+
+ docker_container "github.com/docker/docker/api/types/container"
+ log "github.com/sirupsen/logrus"
+
+ "github.com/nektos/act/pkg/common"
+ "github.com/nektos/act/pkg/model"
+)
+
+// Runner provides capabilities to run GitHub actions
+type Runner interface {
+ NewPlanExecutor(plan *model.Plan) common.Executor
+}
+
+// Config contains the config for a new runner
+type Config struct {
+ Actor string // the user that triggered the event
+ Workdir string // path to working directory
+ ActionCacheDir string // path used for caching action contents
+ ActionOfflineMode bool // when offline, use caching action contents
+ BindWorkdir bool // bind the workdir to the job container
+ EventName string // name of event to run
+ EventPath string // path to JSON file to use for event.json in containers
+ DefaultBranch string // name of the main branch for this repository
+ ReuseContainers bool // reuse containers to maintain state
+ ForcePull bool // force pulling of the image, even if already present
+ ForceRebuild bool // force rebuilding local docker image action
+ LogOutput bool // log the output from docker run
+ JSONLogger bool // use json or text logger
+ LogPrefixJobID bool // switches from the full job name to the job id
+ Env map[string]string // env for containers
+ Inputs map[string]string // manually passed action inputs
+ Secrets map[string]string // list of secrets
+ Vars map[string]string // list of vars
+ Token string // GitHub token
+ InsecureSecrets bool // switch hiding output when printing to terminal
+ Platforms map[string]string // list of platforms
+ Privileged bool // use privileged mode
+ UsernsMode string // user namespace to use
+ ContainerArchitecture string // Desired OS/architecture platform for running containers
+ ContainerDaemonSocket string // Path to Docker daemon socket
+ ContainerOptions string // Options for the job container
+ UseGitIgnore bool // controls if paths in .gitignore should not be copied into container, default true
+ GitHubInstance string // GitHub instance to use, default "github.com"
+ ContainerCapAdd []string // list of kernel capabilities to add to the containers
+ ContainerCapDrop []string // list of kernel capabilities to remove from the containers
+ AutoRemove bool // controls if the container is automatically removed upon workflow completion
+ ArtifactServerPath string // the path where the artifact server stores uploads
+ ArtifactServerAddr string // the address the artifact server binds to
+ ArtifactServerPort string // the port the artifact server binds to
+ NoSkipCheckout bool // do not skip actions/checkout
+ RemoteName string // remote name in local git repo config
+ ReplaceGheActionWithGithubCom []string // Use actions from GitHub Enterprise instance to GitHub
+ ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub.
+ Matrix map[string]map[string]bool // Matrix config to run
+ ContainerNetworkMode docker_container.NetworkMode // the network mode of job containers (the value of --network)
+ ActionCache ActionCache // Use a custom ActionCache Implementation
+
+ PresetGitHubContext *model.GithubContext // the preset github context, overrides some fields like DefaultBranch, Env, Secrets etc.
+ EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath
+ ContainerNamePrefix string // the prefix of container name
+ ContainerMaxLifetime time.Duration // the max lifetime of job containers
+ DefaultActionInstance string // the default actions web site
+ PlatformPicker func(labels []string) string // platform picker, it will take precedence over Platforms if isn't nil
+ JobLoggerLevel *log.Level // the level of job logger
+ ValidVolumes []string // only volumes (and bind mounts) in this slice can be mounted on the job container or service containers
+ InsecureSkipTLS bool // whether to skip verifying TLS certificate of the Gitea instance
+
+ ContainerNetworkEnableIPv6 bool // create the network with IPv6 support enabled
+}
+
+// GetToken: Adapt to Gitea
+func (c Config) GetToken() string {
+ token := c.Secrets["GITHUB_TOKEN"]
+ if c.Secrets["GITEA_TOKEN"] != "" {
+ token = c.Secrets["GITEA_TOKEN"]
+ }
+ return token
+}
+
+type caller struct {
+ runContext *RunContext
+}
+
+type runnerImpl struct {
+ config *Config
+ eventJSON string
+ caller *caller // the job calling this runner (caller of a reusable workflow)
+}
+
+// New Creates a new Runner
+func New(runnerConfig *Config) (Runner, error) {
+ runner := &runnerImpl{
+ config: runnerConfig,
+ }
+
+ return runner.configure()
+}
+
+func (runner *runnerImpl) configure() (Runner, error) {
+ runner.eventJSON = "{}"
+ if runner.config.EventJSON != "" {
+ runner.eventJSON = runner.config.EventJSON
+ } else if runner.config.EventPath != "" {
+ log.Debugf("Reading event.json from %s", runner.config.EventPath)
+ eventJSONBytes, err := os.ReadFile(runner.config.EventPath)
+ if err != nil {
+ return nil, err
+ }
+ runner.eventJSON = string(eventJSONBytes)
+ } else if len(runner.config.Inputs) != 0 {
+ eventMap := map[string]map[string]string{
+ "inputs": runner.config.Inputs,
+ }
+ eventJSON, err := json.Marshal(eventMap)
+ if err != nil {
+ return nil, err
+ }
+ runner.eventJSON = string(eventJSON)
+ }
+ return runner, nil
+}
+
+// NewPlanExecutor ...
+func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
+ maxJobNameLen := 0
+
+ stagePipeline := make([]common.Executor, 0)
+ log.Debugf("Plan Stages: %v", plan.Stages)
+
+ for i := range plan.Stages {
+ stage := plan.Stages[i]
+ stagePipeline = append(stagePipeline, func(ctx context.Context) error {
+ pipeline := make([]common.Executor, 0)
+ for _, run := range stage.Runs {
+ log.Debugf("Stages Runs: %v", stage.Runs)
+ stageExecutor := make([]common.Executor, 0)
+ job := run.Job()
+ log.Debugf("Job.Name: %v", job.Name)
+ log.Debugf("Job.RawNeeds: %v", job.RawNeeds)
+ log.Debugf("Job.RawRunsOn: %v", job.RawRunsOn)
+ log.Debugf("Job.Env: %v", job.Env)
+ log.Debugf("Job.If: %v", job.If)
+ for step := range job.Steps {
+ if nil != job.Steps[step] {
+ log.Debugf("Job.Steps: %v", job.Steps[step].String())
+ }
+ }
+ log.Debugf("Job.TimeoutMinutes: %v", job.TimeoutMinutes)
+ log.Debugf("Job.Services: %v", job.Services)
+ log.Debugf("Job.Strategy: %v", job.Strategy)
+ log.Debugf("Job.RawContainer: %v", job.RawContainer)
+ log.Debugf("Job.Defaults.Run.Shell: %v", job.Defaults.Run.Shell)
+ log.Debugf("Job.Defaults.Run.WorkingDirectory: %v", job.Defaults.Run.WorkingDirectory)
+ log.Debugf("Job.Outputs: %v", job.Outputs)
+ log.Debugf("Job.Uses: %v", job.Uses)
+ log.Debugf("Job.With: %v", job.With)
+ // log.Debugf("Job.RawSecrets: %v", job.RawSecrets)
+ log.Debugf("Job.Result: %v", job.Result)
+
+ if job.Strategy != nil {
+ log.Debugf("Job.Strategy.FailFast: %v", job.Strategy.FailFast)
+ log.Debugf("Job.Strategy.MaxParallel: %v", job.Strategy.MaxParallel)
+ log.Debugf("Job.Strategy.FailFastString: %v", job.Strategy.FailFastString)
+ log.Debugf("Job.Strategy.MaxParallelString: %v", job.Strategy.MaxParallelString)
+ log.Debugf("Job.Strategy.RawMatrix: %v", job.Strategy.RawMatrix)
+
+ strategyRc := runner.newRunContext(ctx, run, nil)
+ if err := strategyRc.NewExpressionEvaluator(ctx).EvaluateYamlNode(ctx, &job.Strategy.RawMatrix); err != nil {
+ log.Errorf("Error while evaluating matrix: %v", err)
+ }
+ }
+
+ var matrixes []map[string]interface{}
+ if m, err := job.GetMatrixes(); err != nil {
+ log.Errorf("Error while get job's matrix: %v", err)
+ } else {
+ log.Debugf("Job Matrices: %v", m)
+ log.Debugf("Runner Matrices: %v", runner.config.Matrix)
+ matrixes = selectMatrixes(m, runner.config.Matrix)
+ }
+ log.Debugf("Final matrix after applying user inclusions '%v'", matrixes)
+
+ maxParallel := 4
+ if job.Strategy != nil {
+ maxParallel = job.Strategy.MaxParallel
+ }
+
+ if len(matrixes) < maxParallel {
+ maxParallel = len(matrixes)
+ }
+
+ for i, matrix := range matrixes {
+ matrix := matrix
+ rc := runner.newRunContext(ctx, run, matrix)
+ rc.JobName = rc.Name
+ if len(matrixes) > 1 {
+ rc.Name = fmt.Sprintf("%s-%d", rc.Name, i+1)
+ }
+ if len(rc.String()) > maxJobNameLen {
+ maxJobNameLen = len(rc.String())
+ }
+ stageExecutor = append(stageExecutor, func(ctx context.Context) error {
+ jobName := fmt.Sprintf("%-*s", maxJobNameLen, rc.String())
+ executor, err := rc.Executor()
+ if err != nil {
+ return err
+ }
+
+ return executor(common.WithJobErrorContainer(WithJobLogger(ctx, rc.Run.JobID, jobName, rc.Config, &rc.Masks, matrix)))
+ })
+ }
+ pipeline = append(pipeline, common.NewParallelExecutor(maxParallel, stageExecutor...))
+ }
+ ncpu := runtime.NumCPU()
+ if 1 > ncpu {
+ ncpu = 1
+ }
+ log.Debugf("Detected CPUs: %d", ncpu)
+ return common.NewParallelExecutor(ncpu, pipeline...)(ctx)
+ })
+ }
+
+ return common.NewPipelineExecutor(stagePipeline...).Then(handleFailure(plan))
+}
+
+func handleFailure(plan *model.Plan) common.Executor {
+ return func(ctx context.Context) error {
+ for _, stage := range plan.Stages {
+ for _, run := range stage.Runs {
+ if run.Job().Result == "failure" {
+ return fmt.Errorf("Job '%s' failed", run.String())
+ }
+ }
+ }
+ return nil
+ }
+}
+
+func selectMatrixes(originalMatrixes []map[string]interface{}, targetMatrixValues map[string]map[string]bool) []map[string]interface{} {
+ matrixes := make([]map[string]interface{}, 0)
+ for _, original := range originalMatrixes {
+ flag := true
+ for key, val := range original {
+ if allowedVals, ok := targetMatrixValues[key]; ok {
+ valToString := fmt.Sprintf("%v", val)
+ if _, ok := allowedVals[valToString]; !ok {
+ flag = false
+ }
+ }
+ }
+ if flag {
+ matrixes = append(matrixes, original)
+ }
+ }
+ return matrixes
+}
+
+func (runner *runnerImpl) newRunContext(ctx context.Context, run *model.Run, matrix map[string]interface{}) *RunContext {
+ rc := &RunContext{
+ Config: runner.config,
+ Run: run,
+ EventJSON: runner.eventJSON,
+ StepResults: make(map[string]*model.StepResult),
+ Matrix: matrix,
+ caller: runner.caller,
+ }
+ rc.ExprEval = rc.NewExpressionEvaluator(ctx)
+ rc.Name = rc.ExprEval.Interpolate(ctx, run.String())
+
+ return rc
+}