From fbab49c68dbc33b9a9871862cb0ba03b4449ebfa Mon Sep 17 00:00:00 2001 From: Casey Lee Date: Tue, 4 Feb 2020 16:38:41 -0800 Subject: [PATCH] initial load of yaml working --- .github/workflows/basic.yml | 8 + {common => act/common}/draw.go | 0 {common => act/common}/executor.go | 0 {common => act/common}/executor_test.go | 0 {common => act/common}/file.go | 0 {common => act/common}/git.go | 0 {common => act/common}/git_test.go | 0 {container => act/container}/docker_build.go | 2 +- {container => act/container}/docker_common.go | 0 {container => act/container}/docker_images.go | 0 .../container}/docker_images_test.go | 0 {container => act/container}/docker_pull.go | 2 +- .../container}/docker_pull_test.go | 0 {container => act/container}/docker_run.go | 2 +- .../container}/docker_run_test.go | 7 +- act/model/planner.go | 196 ++++++++++++++++++ act/model/workflow.go | 67 ++++++ act/runner/api.go | 5 + act/runner/runner.go | 88 ++++++++ {actions => act/runner}/runner_exec.go | 17 +- {actions => act/runner}/runner_test.go | 2 +- actions/action.go | 7 +- actions/api.go | 51 ----- actions/graph.go | 64 ------ actions/runner.go | 160 -------------- cmd/graph.go | 40 ++++ cmd/input.go | 40 ++++ cmd/root.go | 157 +++++--------- 28 files changed, 522 insertions(+), 393 deletions(-) create mode 100644 .github/workflows/basic.yml rename {common => act/common}/draw.go (100%) rename {common => act/common}/executor.go (100%) rename {common => act/common}/executor_test.go (100%) rename {common => act/common}/file.go (100%) rename {common => act/common}/git.go (100%) rename {common => act/common}/git_test.go (100%) rename {container => act/container}/docker_build.go (98%) rename {container => act/container}/docker_common.go (100%) rename {container => act/container}/docker_images.go (100%) rename {container => act/container}/docker_images_test.go (100%) rename {container => act/container}/docker_pull.go (97%) rename {container => act/container}/docker_pull_test.go (100%) rename {container => act/container}/docker_run.go (99%) rename {container => act/container}/docker_run_test.go (97%) create mode 100644 act/model/planner.go create mode 100644 act/model/workflow.go create mode 100644 act/runner/api.go create mode 100644 act/runner/runner.go rename {actions => act/runner}/runner_exec.go (93%) rename {actions => act/runner}/runner_test.go (99%) delete mode 100644 actions/api.go delete mode 100644 actions/graph.go delete mode 100644 actions/runner.go create mode 100644 cmd/graph.go create mode 100644 cmd/input.go diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml new file mode 100644 index 00000000..7cdb3b4b --- /dev/null +++ b/.github/workflows/basic.yml @@ -0,0 +1,8 @@ +name: basic +on: push + +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo hello world! diff --git a/common/draw.go b/act/common/draw.go similarity index 100% rename from common/draw.go rename to act/common/draw.go diff --git a/common/executor.go b/act/common/executor.go similarity index 100% rename from common/executor.go rename to act/common/executor.go diff --git a/common/executor_test.go b/act/common/executor_test.go similarity index 100% rename from common/executor_test.go rename to act/common/executor_test.go diff --git a/common/file.go b/act/common/file.go similarity index 100% rename from common/file.go rename to act/common/file.go diff --git a/common/git.go b/act/common/git.go similarity index 100% rename from common/git.go rename to act/common/git.go diff --git a/common/git_test.go b/act/common/git_test.go similarity index 100% rename from common/git_test.go rename to act/common/git_test.go diff --git a/container/docker_build.go b/act/container/docker_build.go similarity index 98% rename from container/docker_build.go rename to act/container/docker_build.go index 8fac5f4d..9d3c6218 100644 --- a/container/docker_build.go +++ b/act/container/docker_build.go @@ -10,7 +10,7 @@ import ( "github.com/docker/docker/client" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/fileutils" - "github.com/nektos/act/common" + "github.com/nektos/act/pkg/common" log "github.com/sirupsen/logrus" ) diff --git a/container/docker_common.go b/act/container/docker_common.go similarity index 100% rename from container/docker_common.go rename to act/container/docker_common.go diff --git a/container/docker_images.go b/act/container/docker_images.go similarity index 100% rename from container/docker_images.go rename to act/container/docker_images.go diff --git a/container/docker_images_test.go b/act/container/docker_images_test.go similarity index 100% rename from container/docker_images_test.go rename to act/container/docker_images_test.go diff --git a/container/docker_pull.go b/act/container/docker_pull.go similarity index 97% rename from container/docker_pull.go rename to act/container/docker_pull.go index 99fcd945..c324598a 100644 --- a/container/docker_pull.go +++ b/act/container/docker_pull.go @@ -6,7 +6,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/client" - "github.com/nektos/act/common" + "github.com/nektos/act/pkg/common" ) // NewDockerPullExecutorInput the input for the NewDockerPullExecutor function diff --git a/container/docker_pull_test.go b/act/container/docker_pull_test.go similarity index 100% rename from container/docker_pull_test.go rename to act/container/docker_pull_test.go diff --git a/container/docker_run.go b/act/container/docker_run.go similarity index 99% rename from container/docker_run.go rename to act/container/docker_run.go index 246fca23..0397ec0b 100644 --- a/container/docker_run.go +++ b/act/container/docker_run.go @@ -9,7 +9,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" - "github.com/nektos/act/common" + "github.com/nektos/act/pkg/common" "golang.org/x/crypto/ssh/terminal" ) diff --git a/container/docker_run_test.go b/act/container/docker_run_test.go similarity index 97% rename from container/docker_run_test.go rename to act/container/docker_run_test.go index a1dfd9c4..9d92b22b 100644 --- a/container/docker_run_test.go +++ b/act/container/docker_run_test.go @@ -3,11 +3,12 @@ package container import ( "bytes" "context" - "github.com/nektos/act/common" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" "io/ioutil" "testing" + + "github.com/nektos/act/pkg/common" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" ) type rawFormatter struct{} diff --git a/act/model/planner.go b/act/model/planner.go new file mode 100644 index 00000000..854bf812 --- /dev/null +++ b/act/model/planner.go @@ -0,0 +1,196 @@ +package model + +import ( + "io/ioutil" + "math" + "os" + "path/filepath" + "sort" + + log "github.com/sirupsen/logrus" +) + +// WorkflowPlanner contains methods for creating plans +type WorkflowPlanner interface { + PlanEvent(eventName string) *Plan + PlanJob(jobName string) *Plan + GetEvents() []string +} + +// Plan contains a list of stages to run in series +type Plan struct { + Stages []*Stage +} + +// Stage contains a list of runs to execute in parallel +type Stage struct { + Runs []*Run +} + +// Run represents a job from a workflow that needs to be run +type Run struct { + Workflow *Workflow + JobID string +} + +// NewWorkflowPlanner will load all workflows from a directory +func NewWorkflowPlanner(dirname string) (WorkflowPlanner, error) { + log.Debugf("Loading workflows from '%s'", dirname) + files, err := ioutil.ReadDir(dirname) + if err != nil { + return nil, err + } + + wp := new(workflowPlanner) + for _, file := range files { + ext := filepath.Ext(file.Name()) + if ext == ".yml" || ext == ".yaml" { + f, err := os.Open(filepath.Join(dirname, file.Name())) + if err != nil { + return nil, err + } + + workflow, err := ReadWorkflow(f) + if err != nil { + f.Close() + return nil, err + } + wp.workflows = append(wp.workflows, workflow) + f.Close() + } + } + + return wp, nil +} + +type workflowPlanner struct { + workflows []*Workflow +} + +// PlanEvent builds a new list of runs to execute in parallel for an event name +func (wp *workflowPlanner) PlanEvent(eventName string) *Plan { + plan := new(Plan) + for _, w := range wp.workflows { + if w.On == eventName { + plan.mergeStages(createStages(w, w.GetJobIDs()...)) + } + } + return plan +} + +// PlanJob builds a new run to execute in parallel for a job name +func (wp *workflowPlanner) PlanJob(jobName string) *Plan { + plan := new(Plan) + for _, w := range wp.workflows { + plan.mergeStages(createStages(w, jobName)) + } + return plan +} + +// GetEvents gets all the events in the workflows file +func (wp *workflowPlanner) GetEvents() []string { + events := make([]string, 0) + for _, w := range wp.workflows { + found := false + for _, e := range events { + if e == w.On { + found = true + break + } + } + + if !found { + events = append(events, w.On) + } + } + + // sort the list based on depth of dependencies + sort.Slice(events, func(i, j int) bool { + return events[i] < events[j] + }) + + return events +} + +// GetJobIDs will get all the job names in the stage +func (s *Stage) GetJobIDs() []string { + names := make([]string, 0) + for _, r := range s.Runs { + names = append(names, r.JobID) + } + return names +} + +// Merge stages with existing stages in plan +func (p *Plan) mergeStages(stages []*Stage) { + newStages := make([]*Stage, int(math.Max(float64(len(p.Stages)), float64(len(stages))))) + for i := 0; i < len(newStages); i++ { + newStages[i] = new(Stage) + if i >= len(p.Stages) { + newStages[i].Runs = append(stages[i].Runs) + } else if i >= len(stages) { + newStages[i].Runs = append(p.Stages[i].Runs) + } else { + newStages[i].Runs = append(p.Stages[i].Runs, stages[i].Runs...) + } + } + p.Stages = newStages +} + +func createStages(w *Workflow, jobIDs ...string) []*Stage { + // first, build a list of all the necessary jobs to run, and their dependencies + jobDependencies := make(map[string][]string) + for len(jobIDs) > 0 { + newJobIDs := make([]string, 0) + for _, jID := range jobIDs { + // make sure we haven't visited this job yet + if _, ok := jobDependencies[jID]; !ok { + if job := w.GetJob(jID); job != nil { + jobDependencies[jID] = job.Needs + newJobIDs = append(newJobIDs, job.Needs...) + } + } + } + jobIDs = newJobIDs + } + + // next, build an execution graph + stages := make([]*Stage, 0) + for len(jobDependencies) > 0 { + stage := new(Stage) + for jID, jDeps := range jobDependencies { + // make sure all deps are in the graph already + if listInStages(jDeps, stages...) { + stage.Runs = append(stage.Runs, &Run{ + Workflow: w, + JobID: jID, + }) + delete(jobDependencies, jID) + } + } + if len(stage.Runs) == 0 { + log.Fatalf("Unable to build dependency graph!") + } + stages = append(stages, stage) + } + + return stages +} + +// return true iff all strings in srcList exist in at least one of the stages +func listInStages(srcList []string, stages ...*Stage) bool { + for _, src := range srcList { + found := false + for _, stage := range stages { + for _, search := range stage.GetJobIDs() { + if src == search { + found = true + } + } + } + if !found { + return false + } + } + return true +} diff --git a/act/model/workflow.go b/act/model/workflow.go new file mode 100644 index 00000000..9be0dfa4 --- /dev/null +++ b/act/model/workflow.go @@ -0,0 +1,67 @@ +package model + +import ( + "io" + + "gopkg.in/yaml.v2" +) + +// Workflow is the structure of the files in .github/workflows +type Workflow struct { + Name string `yaml:"name"` + On string `yaml:"on"` + Env map[string]string `yaml:"env"` + Jobs map[string]*Job `yaml:"jobs"` +} + +// Job is the structure of one job in a workflow +type Job struct { + Name string `yaml:"name"` + Needs []string `yaml:"needs"` + RunsOn string `yaml:"runs-on"` + Env map[string]string `yaml:"env"` + If string `yaml:"if"` + Steps []*Step `yaml:"steps"` + TimeoutMinutes int64 `yaml:"timeout-minutes"` +} + +// Step is the structure of one step in a job +type Step struct { + ID string `yaml:"id"` + If string `yaml:"if"` + Name string `yaml:"name"` + Uses string `yaml:"uses"` + Run string `yaml:"run"` + WorkingDirectory string `yaml:"working-directory"` + Shell string `yaml:"shell"` + Env map[string]string `yaml:"env"` + With map[string]string `yaml:"with"` + ContinueOnError bool `yaml:"continue-on-error"` + TimeoutMinutes int64 `yaml:"timeout-minutes"` +} + +// ReadWorkflow returns a list of jobs for a given workflow file reader +func ReadWorkflow(in io.Reader) (*Workflow, error) { + w := new(Workflow) + err := yaml.NewDecoder(in).Decode(w) + return w, err +} + +// GetJob will get a job by name in the workflow +func (w *Workflow) GetJob(jobID string) *Job { + for id, j := range w.Jobs { + if jobID == id { + return j + } + } + return nil +} + +// GetJobIDs will get all the job names in the workflow +func (w *Workflow) GetJobIDs() []string { + ids := make([]string, 0) + for id := range w.Jobs { + ids = append(ids, id) + } + return ids +} diff --git a/act/runner/api.go b/act/runner/api.go new file mode 100644 index 00000000..b9f342b9 --- /dev/null +++ b/act/runner/api.go @@ -0,0 +1,5 @@ +package runner + +type environmentApplier interface { + applyEnvironment(map[string]string) +} diff --git a/act/runner/runner.go b/act/runner/runner.go new file mode 100644 index 00000000..c242e674 --- /dev/null +++ b/act/runner/runner.go @@ -0,0 +1,88 @@ +package runner + +import ( + "io" + "io/ioutil" + "os" + + "github.com/nektos/act/pkg/common" + "github.com/nektos/act/pkg/model" + log "github.com/sirupsen/logrus" +) + +// Runner provides capabilities to run GitHub actions +type Runner interface { + PlanRunner + io.Closer +} + +// PlanRunner to run a specific actions +type PlanRunner interface { + RunPlan(plan *model.Plan) error +} + +// Config contains the config for a new runner +type Config struct { + Dryrun bool // don't start any of the containers + EventName string // name of event to run + EventPath string // path to JSON file to use for event.json in containers + ReuseContainers bool // reuse containers to maintain state + ForcePull bool // force pulling of the image, if already present +} + +type runnerImpl struct { + config *Config + tempDir string + eventJSON string +} + +// NewRunner Creates a new Runner +func NewRunner(runnerConfig *Config) (Runner, error) { + runner := &runnerImpl{ + config: runnerConfig, + } + + init := common.NewPipelineExecutor( + runner.setupTempDir, + runner.setupEvent, + ) + + return runner, init() +} + +func (runner *runnerImpl) setupTempDir() error { + var err error + runner.tempDir, err = ioutil.TempDir("", "act-") + return err +} + +func (runner *runnerImpl) setupEvent() error { + runner.eventJSON = "{}" + if runner.config.EventPath != "" { + log.Debugf("Reading event.json from %s", runner.config.EventPath) + eventJSONBytes, err := ioutil.ReadFile(runner.config.EventPath) + if err != nil { + return err + } + runner.eventJSON = string(eventJSONBytes) + } + return nil +} + +func (runner *runnerImpl) RunPlan(plan *model.Plan) error { + pipeline := make([]common.Executor, 0) + for _, stage := range plan.Stages { + stageExecutor := make([]common.Executor, 0) + for _, run := range stage.Runs { + stageExecutor = append(stageExecutor, runner.newRunExecutor(run)) + } + pipeline = append(pipeline, common.NewParallelExecutor(stageExecutor...)) + } + + executor := common.NewPipelineExecutor(pipeline...) + return executor() +} + +func (runner *runnerImpl) Close() error { + return os.RemoveAll(runner.tempDir) +} diff --git a/actions/runner_exec.go b/act/runner/runner_exec.go similarity index 93% rename from actions/runner_exec.go rename to act/runner/runner_exec.go index aec5151e..0a52fab5 100644 --- a/actions/runner_exec.go +++ b/act/runner/runner_exec.go @@ -1,21 +1,20 @@ -package actions +package runner import ( "archive/tar" "bytes" "fmt" "io" - "os" "path/filepath" "regexp" - "github.com/actions/workflow-parser/model" - "github.com/nektos/act/common" - "github.com/nektos/act/container" + "github.com/nektos/act/pkg/common" + "github.com/nektos/act/pkg/container" + "github.com/nektos/act/pkg/model" log "github.com/sirupsen/logrus" ) -func (runner *runnerImpl) newActionExecutor(actionName string) common.Executor { +func (runner *runnerImpl) newRunExecutor(run *model.Run) common.Executor { action := runner.workflowConfig.GetAction(actionName) if action == nil { return common.NewErrorExecutor(fmt.Errorf("Unable to find action named '%s'", actionName)) @@ -35,7 +34,8 @@ func (runner *runnerImpl) newActionExecutor(actionName string) common.Executor { return common.NewPipelineExecutor(executors...) } -func (runner *runnerImpl) addImageExecutor(action *model.Action, executors *[]common.Executor) (string, error) { +/* +func (runner *runnerImpl) addImageExecutor(action *Action, executors *[]common.Executor) (string, error) { var image string logger := newActionLogger(action.Identifier, runner.config.Dryrun) log.Debugf("Using '%s' for action '%s'", action.Uses, action.Identifier) @@ -111,8 +111,9 @@ func (runner *runnerImpl) addImageExecutor(action *model.Action, executors *[]co return image, nil } +*/ -func (runner *runnerImpl) addRunExecutor(action *model.Action, image string, executors *[]common.Executor) error { +func (runner *runnerImpl) addRunExecutor(action *Action, image string, executors *[]common.Executor) error { logger := newActionLogger(action.Identifier, runner.config.Dryrun) log.Debugf("Using '%s' for action '%s'", action.Uses, action.Identifier) diff --git a/actions/runner_test.go b/act/runner/runner_test.go similarity index 99% rename from actions/runner_test.go rename to act/runner/runner_test.go index cd96aae2..5ca1d0fb 100644 --- a/actions/runner_test.go +++ b/act/runner/runner_test.go @@ -1,4 +1,4 @@ -package actions +package runner import ( "context" diff --git a/actions/action.go b/actions/action.go index 2a3136ac..4b329163 100644 --- a/actions/action.go +++ b/actions/action.go @@ -5,17 +5,18 @@ import ( "log" "os" - "github.com/actions/workflow-parser/model" "github.com/howeyc/gopass" ) var secretCache map[string]string type actionEnvironmentApplier struct { - *model.Action + *Action } -func newActionEnvironmentApplier(action *model.Action) environmentApplier { +type Action struct{} + +func newActionEnvironmentApplier(action *Action) environmentApplier { return &actionEnvironmentApplier{action} } diff --git a/actions/api.go b/actions/api.go deleted file mode 100644 index 0ebdc2d3..00000000 --- a/actions/api.go +++ /dev/null @@ -1,51 +0,0 @@ -package actions - -import ( - "context" - "io" -) - -// Runner provides capabilities to run GitHub actions -type Runner interface { - EventGrapher - EventLister - EventRunner - ActionRunner - io.Closer -} - -// EventGrapher to list the actions -type EventGrapher interface { - GraphEvent(eventName string) ([][]string, error) -} - -// EventLister to list the events -type EventLister interface { - ListEvents() []string -} - -// EventRunner to run the actions for a given event -type EventRunner interface { - RunEvent() error -} - -// ActionRunner to run a specific actions -type ActionRunner interface { - RunActions(actionNames ...string) error -} - -// RunnerConfig contains the config for a new runner -type RunnerConfig struct { - Ctx context.Context // context to use for the run - Dryrun bool // don't start any of the containers - WorkingDir string // base directory to use - WorkflowPath string // path to load main.workflow file, relative to WorkingDir - EventName string // name of event to run - EventPath string // path to JSON file to use for event.json in containers, relative to WorkingDir - ReuseContainers bool // reuse containers to maintain state - ForcePull bool // force pulling of the image, if already present -} - -type environmentApplier interface { - applyEnvironment(map[string]string) -} diff --git a/actions/graph.go b/actions/graph.go deleted file mode 100644 index 1e1bbc95..00000000 --- a/actions/graph.go +++ /dev/null @@ -1,64 +0,0 @@ -package actions - -import ( - "log" - - "github.com/actions/workflow-parser/model" -) - -// return a pipeline that is run in series. pipeline is a list of steps to run in parallel -func newExecutionGraph(workflowConfig *model.Configuration, actionNames ...string) [][]string { - // first, build a list of all the necessary actions to run, and their dependencies - actionDependencies := make(map[string][]string) - for len(actionNames) > 0 { - newActionNames := make([]string, 0) - for _, aName := range actionNames { - // make sure we haven't visited this action yet - if _, ok := actionDependencies[aName]; !ok { - action := workflowConfig.GetAction(aName) - if action != nil { - actionDependencies[aName] = action.Needs - newActionNames = append(newActionNames, action.Needs...) - } - } - } - actionNames = newActionNames - } - - // next, build an execution graph - graph := make([][]string, 0) - for len(actionDependencies) > 0 { - stage := make([]string, 0) - for aName, aDeps := range actionDependencies { - // make sure all deps are in the graph already - if listInLists(aDeps, graph...) { - stage = append(stage, aName) - delete(actionDependencies, aName) - } - } - if len(stage) == 0 { - log.Fatalf("Unable to build dependency graph!") - } - graph = append(graph, stage) - } - - return graph -} - -// return true iff all strings in srcList exist in at least one of the searchLists -func listInLists(srcList []string, searchLists ...[]string) bool { - for _, src := range srcList { - found := false - for _, searchList := range searchLists { - for _, search := range searchList { - if src == search { - found = true - } - } - } - if !found { - return false - } - } - return true -} diff --git a/actions/runner.go b/actions/runner.go deleted file mode 100644 index 05097471..00000000 --- a/actions/runner.go +++ /dev/null @@ -1,160 +0,0 @@ -package actions - -import ( - "io/ioutil" - "os" - "path/filepath" - "sort" - - "github.com/actions/workflow-parser/model" - "github.com/actions/workflow-parser/parser" - "github.com/nektos/act/common" - log "github.com/sirupsen/logrus" -) - -type runnerImpl struct { - config *RunnerConfig - workflowConfig *model.Configuration - tempDir string - eventJSON string -} - -// NewRunner Creates a new Runner -func NewRunner(runnerConfig *RunnerConfig) (Runner, error) { - runner := &runnerImpl{ - config: runnerConfig, - } - - init := common.NewPipelineExecutor( - runner.setupTempDir, - runner.setupWorkingDir, - runner.setupWorkflows, - runner.setupEvent, - ) - - return runner, init() -} - -func (runner *runnerImpl) setupTempDir() error { - var err error - runner.tempDir, err = ioutil.TempDir("", "act-") - return err -} - -func (runner *runnerImpl) setupWorkingDir() error { - var err error - runner.config.WorkingDir, err = filepath.Abs(runner.config.WorkingDir) - log.Debugf("Setting working dir to %s", runner.config.WorkingDir) - return err -} - -func (runner *runnerImpl) setupWorkflows() error { - runner.config.WorkflowPath = runner.resolvePath(runner.config.WorkflowPath) - log.Debugf("Loading workflow config from %s", runner.config.WorkflowPath) - workflowReader, err := os.Open(runner.config.WorkflowPath) - if err != nil { - return err - } - defer workflowReader.Close() - - runner.workflowConfig, err = parser.Parse(workflowReader) - return err -} - -func (runner *runnerImpl) setupEvent() error { - runner.eventJSON = "{}" - if runner.config.EventPath != "" { - runner.config.EventPath = runner.resolvePath(runner.config.EventPath) - log.Debugf("Reading event.json from %s", runner.config.EventPath) - eventJSONBytes, err := ioutil.ReadFile(runner.config.EventPath) - if err != nil { - return err - } - runner.eventJSON = string(eventJSONBytes) - } - return nil -} - -func (runner *runnerImpl) resolvePath(path string) string { - if path == "" { - return path - } - if !filepath.IsAbs(path) { - path = filepath.Join(runner.config.WorkingDir, path) - } - return path -} - -// ListEvents gets all the events in the workflows file -func (runner *runnerImpl) ListEvents() []string { - log.Debugf("Listing all events") - events := make([]string, 0) - for _, w := range runner.workflowConfig.Workflows { - events = append(events, w.On) - } - - // sort the list based on depth of dependencies - sort.Slice(events, func(i, j int) bool { - return events[i] < events[j] - }) - - return events -} - -// GraphEvent builds an execution path -func (runner *runnerImpl) GraphEvent(eventName string) ([][]string, error) { - log.Debugf("Listing actions for event '%s'", eventName) - resolves := runner.resolveEvent(eventName) - return newExecutionGraph(runner.workflowConfig, resolves...), nil -} - -// RunAction runs a set of actions in parallel, and their dependencies -func (runner *runnerImpl) RunActions(actionNames ...string) error { - log.Debugf("Running actions %+q", actionNames) - graph := newExecutionGraph(runner.workflowConfig, actionNames...) - - pipeline := make([]common.Executor, 0) - for _, actions := range graph { - stage := make([]common.Executor, 0) - for _, actionName := range actions { - stage = append(stage, runner.newActionExecutor(actionName)) - } - pipeline = append(pipeline, common.NewParallelExecutor(stage...)) - } - - executor := common.NewPipelineExecutor(pipeline...) - return executor() -} - -// RunEvent runs the actions for a single event -func (runner *runnerImpl) RunEvent() error { - log.Debugf("Running event '%s'", runner.config.EventName) - resolves := runner.resolveEvent(runner.config.EventName) - log.Debugf("Running actions %s -> %s", runner.config.EventName, resolves) - return runner.RunActions(resolves...) -} - -func (runner *runnerImpl) Close() error { - return os.RemoveAll(runner.tempDir) -} - -// get list of resolves for an event -func (runner *runnerImpl) resolveEvent(eventName string) []string { - workflows := runner.workflowConfig.GetWorkflows(eventName) - resolves := make([]string, 0) - for _, workflow := range workflows { - for _, resolve := range workflow.Resolves { - found := false - for _, r := range resolves { - if r == resolve { - found = true - break - } - } - if !found { - resolves = append(resolves, resolve) - } - } - } - return resolves -} diff --git a/cmd/graph.go b/cmd/graph.go new file mode 100644 index 00000000..e8d0204d --- /dev/null +++ b/cmd/graph.go @@ -0,0 +1,40 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/nektos/act/pkg/common" + "github.com/nektos/act/pkg/model" +) + +func drawGraph(plan *model.Plan) error { + + drawings := make([]*common.Drawing, 0) + + jobPen := common.NewPen(common.StyleSingleLine, 96) + arrowPen := common.NewPen(common.StyleNoLine, 97) + for i, stage := range plan.Stages { + if i > 0 { + drawings = append(drawings, arrowPen.DrawArrow()) + } + + ids := make([]string, 0) + for _, r := range stage.Runs { + ids = append(ids, fmt.Sprintf("%s/%s", r.Workflow.Name, r.JobID)) + } + drawings = append(drawings, jobPen.DrawBoxes(ids...)) + } + + maxWidth := 0 + for _, d := range drawings { + if d.GetWidth() > maxWidth { + maxWidth = d.GetWidth() + } + } + + for _, d := range drawings { + d.Draw(os.Stdout, maxWidth) + } + return nil +} diff --git a/cmd/input.go b/cmd/input.go new file mode 100644 index 00000000..9f6ea5c2 --- /dev/null +++ b/cmd/input.go @@ -0,0 +1,40 @@ +package cmd + +import ( + "log" + "path/filepath" +) + +// Input contains the input for the root command +type Input struct { + workingDir string + workflowsPath string + eventPath string + reuseContainers bool + dryrun bool + forcePull bool +} + +func (i *Input) resolve(path string) string { + basedir, err := filepath.Abs(i.workingDir) + if err != nil { + log.Fatal(err) + } + if path == "" { + return path + } + if !filepath.IsAbs(path) { + path = filepath.Join(basedir, path) + } + return path +} + +// WorkflowsPath returns path to workflows +func (i *Input) WorkflowsPath() string { + return i.resolve(i.workflowsPath) +} + +// EventPath returns the path to events file +func (i *Input) EventPath() string { + return i.resolve(i.eventPath) +} diff --git a/cmd/root.go b/cmd/root.go index dc6ea165..afe9a730 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,13 +2,11 @@ package cmd import ( "context" - "fmt" "os" "path/filepath" fswatch "github.com/andreaskoch/go-fswatch" - "github.com/nektos/act/actions" - "github.com/nektos/act/common" + "github.com/nektos/act/pkg/model" gitignore "github.com/sabhiram/go-gitignore" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -16,26 +14,26 @@ import ( // Execute is the entry point to running the CLI func Execute(ctx context.Context, version string) { - runnerConfig := &actions.RunnerConfig{Ctx: ctx} + input := new(Input) var rootCmd = &cobra.Command{ Use: "act [event name to run]", Short: "Run Github actions locally by specifying the event name (e.g. `push`) or an action name directly.", Args: cobra.MaximumNArgs(1), - RunE: newRunCommand(runnerConfig), + RunE: newRunCommand(ctx, input), PersistentPreRun: setupLogging, Version: version, SilenceUsage: true, } rootCmd.Flags().BoolP("watch", "w", false, "watch the contents of the local repo and run when files change") - rootCmd.Flags().BoolP("list", "l", false, "list actions") - rootCmd.Flags().StringP("action", "a", "", "run action") - rootCmd.Flags().BoolVarP(&runnerConfig.ReuseContainers, "reuse", "r", false, "reuse action containers to maintain state") - rootCmd.Flags().StringVarP(&runnerConfig.EventPath, "event", "e", "", "path to event JSON file") - rootCmd.Flags().BoolVarP(&runnerConfig.ForcePull, "pull", "p", false, "pull docker image(s) if already present") + rootCmd.Flags().BoolP("list", "l", false, "list workflows") + rootCmd.Flags().StringP("job", "j", "", "run job") + rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "reuse action containers to maintain state") + rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", false, "pull docker image(s) if already present") + rootCmd.Flags().StringVarP(&input.eventPath, "event", "e", "", "path to event JSON file") + rootCmd.PersistentFlags().StringVarP(&input.workflowsPath, "workflows", "W", "./.github/workflows/", "path to workflow files") + rootCmd.PersistentFlags().StringVarP(&input.workingDir, "directory", "C", ".", "working directory") rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output") - rootCmd.PersistentFlags().BoolVarP(&runnerConfig.Dryrun, "dryrun", "n", false, "dryrun mode") - rootCmd.PersistentFlags().StringVarP(&runnerConfig.WorkflowPath, "file", "f", "./.github/main.workflow", "path to workflow file") - rootCmd.PersistentFlags().StringVarP(&runnerConfig.WorkingDir, "directory", "C", ".", "working directory") + rootCmd.PersistentFlags().BoolVarP(&input.dryrun, "dryrun", "n", false, "dryrun mode") if err := rootCmd.Execute(); err != nil { os.Exit(1) } @@ -49,67 +47,63 @@ func setupLogging(cmd *cobra.Command, args []string) { } } -func newRunCommand(runnerConfig *actions.RunnerConfig) func(*cobra.Command, []string) error { +func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { - if len(args) > 0 { - runnerConfig.EventName = args[0] - } - - watch, err := cmd.Flags().GetBool("watch") + planner, err := model.NewWorkflowPlanner(input.WorkflowsPath()) if err != nil { return err } - if watch { - return watchAndRun(runnerConfig.Ctx, func() error { - return parseAndRun(cmd, runnerConfig) - }) + + // Determine the event name + var eventName string + if len(args) > 0 { + eventName = args[0] + } else { + // set default event type if we only have a single workflow in the file. + // this way user dont have to specify the event. + if events := planner.GetEvents(); len(events) == 1 { + log.Debugf("Using detected workflow event: %s", events[0]) + eventName = events[0] + } } - return parseAndRun(cmd, runnerConfig) - } -} -func parseAndRun(cmd *cobra.Command, runnerConfig *actions.RunnerConfig) error { - // create the runner - runner, err := actions.NewRunner(runnerConfig) - if err != nil { - return err - } - defer runner.Close() - - // set default event type if we only have a single workflow in the file. - // this way user dont have to specify the event. - if runnerConfig.EventName == "" { - if events := runner.ListEvents(); len(events) == 1 { - log.Debugf("Using detected workflow event: %s", events[0]) - runnerConfig.EventName = events[0] + // build the plan for this run + var plan *model.Plan + if jobID, err := cmd.Flags().GetString("job"); err != nil { + return err + } else if jobID != "" { + log.Debugf("Planning job: %s", jobID) + plan = planner.PlanJob(jobID) + } else { + log.Debugf("Planning event: %s", eventName) + plan = planner.PlanEvent(eventName) } - } - // fall back to default event name if we could not detect one. - if runnerConfig.EventName == "" { - runnerConfig.EventName = "push" - } + // check if we should just print the graph + if list, err := cmd.Flags().GetBool("list"); err != nil { + return err + } else if list { + return drawGraph(plan) + } - // check if we should just print the graph - list, err := cmd.Flags().GetBool("list") - if err != nil { - return err - } - if list { - return drawGraph(runner) - } + // run the plan + // runner, err := runner.New(config) + // if err != nil { + // return err + // } + // defer runner.Close() - // check if we are running just a single action - actionName, err := cmd.Flags().GetString("action") - if err != nil { - return err - } - if actionName != "" { - return runner.RunActions(actionName) - } + // if watch, err := cmd.Flags().GetBool("watch"); err != nil { + // return err + // } else if watch { + // return watchAndRun(ctx, func() error { + // return runner.RunPlan(plan) + // }) + // } - // run the event in the RunnerRonfig - return runner.RunEvent() + // return runner.RunPlan(plan) + return nil + } } func watchAndRun(ctx context.Context, fn func() error) error { @@ -155,40 +149,3 @@ func watchAndRun(ctx context.Context, fn func() error) error { folderWatcher.Stop() return err } - -func drawGraph(runner actions.Runner) error { - eventNames := runner.ListEvents() - for _, eventName := range eventNames { - graph, err := runner.GraphEvent(eventName) - if err != nil { - return err - } - - drawings := make([]*common.Drawing, 0) - eventPen := common.NewPen(common.StyleDoubleLine, 91 /*34*/) - - drawings = append(drawings, eventPen.DrawBoxes(fmt.Sprintf("EVENT: %s", eventName))) - - actionPen := common.NewPen(common.StyleSingleLine, 96) - arrowPen := common.NewPen(common.StyleNoLine, 97) - drawings = append(drawings, arrowPen.DrawArrow()) - for i, stage := range graph { - if i > 0 { - drawings = append(drawings, arrowPen.DrawArrow()) - } - drawings = append(drawings, actionPen.DrawBoxes(stage...)) - } - - maxWidth := 0 - for _, d := range drawings { - if d.GetWidth() > maxWidth { - maxWidth = d.GetWidth() - } - } - - for _, d := range drawings { - d.Draw(os.Stdout, maxWidth) - } - } - return nil -}