diff --git a/cmd/input.go b/cmd/input.go index 0e3451a..f9f36b4 100644 --- a/cmd/input.go +++ b/cmd/input.go @@ -27,6 +27,7 @@ type Input struct { privileged bool usernsMode string containerArchitecture string + noWorkflowRecurse bool useGitIgnore bool } diff --git a/cmd/root.go b/cmd/root.go index fac8f74..260aa34 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -52,6 +52,7 @@ func Execute(ctx context.Context, version string) { rootCmd.Flags().BoolVar(&input.useGitIgnore, "use-gitignore", true, "Controls whether paths specified in .gitignore should be copied into container") rootCmd.PersistentFlags().StringVarP(&input.actor, "actor", "a", "nektos/act", "user that triggered the event") rootCmd.PersistentFlags().StringVarP(&input.workflowsPath, "workflows", "W", "./.github/workflows/", "path to workflow file(s)") + rootCmd.PersistentFlags().BoolVarP(&input.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag") rootCmd.PersistentFlags().StringVarP(&input.workdir, "directory", "C", ".", "working directory") rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output") rootCmd.PersistentFlags().BoolVarP(&input.noOutput, "quiet", "q", false, "disable logging of output from steps") @@ -65,7 +66,6 @@ func Execute(ctx context.Context, version string) { if err := rootCmd.Execute(); err != nil { os.Exit(1) } - } func configLocations() []string { @@ -164,7 +164,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str secrets := newSecrets(input.secrets) _ = readEnvs(input.Secretfile(), secrets) - planner, err := model.NewWorkflowPlanner(input.WorkflowsPath()) + planner, err := model.NewWorkflowPlanner(input.WorkflowsPath(), input.noWorkflowRecurse) if err != nil { return err } diff --git a/pkg/model/planner.go b/pkg/model/planner.go index c80cb32..828132e 100644 --- a/pkg/model/planner.go +++ b/pkg/model/planner.go @@ -50,34 +50,78 @@ func (r *Run) Job() *Job { return r.Workflow.GetJob(r.JobID) } -// NewWorkflowPlanner will load a specific workflow or all workflows from a directory -func NewWorkflowPlanner(path string) (WorkflowPlanner, error) { +type WorkflowFiles struct { + workflowFileInfo os.FileInfo + dirPath string +} + +// NewWorkflowPlanner will load a specific workflow, all workflows from a directory or all workflows from a directory and its subdirectories +func NewWorkflowPlanner(path string, noWorkflowRecurse bool) (WorkflowPlanner, error) { + path, err := filepath.Abs(path) + if err != nil { + return nil, err + } + fi, err := os.Stat(path) if err != nil { return nil, err } - var files []os.FileInfo - var dirname string + var workflows []WorkflowFiles if fi.IsDir() { log.Debugf("Loading workflows from '%s'", path) - dirname = path - files, err = ioutil.ReadDir(path) + if noWorkflowRecurse { + files, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + + for _, v := range files { + workflows = append(workflows, WorkflowFiles{ + dirPath: path, + workflowFileInfo: v, + }) + } + } else { + log.Debug("Loading workflows recursively") + if err := filepath.Walk(path, + func(p string, f os.FileInfo, err error) error { + if err != nil { + return err + } + + if !f.IsDir() { + log.Debugf("Found workflow '%s' in '%s'", f.Name(), p) + workflows = append(workflows, WorkflowFiles{ + dirPath: filepath.Dir(p), + workflowFileInfo: f, + }) + } + + return nil + }); err != nil { + return nil, err + } + } } else { log.Debugf("Loading workflow '%s'", path) - dirname, err = filepath.Abs(filepath.Dir(path)) - files = []os.FileInfo{fi} + dirname := filepath.Dir(path) + + workflows = append(workflows, WorkflowFiles{ + dirPath: dirname, + workflowFileInfo: fi, + }) } if err != nil { return nil, err } wp := new(workflowPlanner) - for _, file := range files { - ext := filepath.Ext(file.Name()) + for _, wf := range workflows { + ext := filepath.Ext(wf.workflowFileInfo.Name()) if ext == ".yml" || ext == ".yaml" { - f, err := os.Open(filepath.Join(dirname, file.Name())) + f, err := os.Open(filepath.Join(wf.dirPath, wf.workflowFileInfo.Name())) if err != nil { return nil, err } @@ -87,19 +131,22 @@ func NewWorkflowPlanner(path string) (WorkflowPlanner, error) { if err != nil { f.Close() if err == io.EOF { - return nil, errors.WithMessagef(err, "unable to read workflow, %s file is empty", file.Name()) + return nil, errors.WithMessagef(err, "unable to read workflow, %s file is empty", wf.workflowFileInfo.Name()) } return nil, err } + if workflow.Name == "" { - workflow.Name = file.Name() + workflow.Name = wf.workflowFileInfo.Name() } + jobNameRegex := regexp.MustCompile(`^([[:alpha:]_][[:alnum:]_\-]*)$`) for k := range workflow.Jobs { if ok := jobNameRegex.MatchString(k); !ok { - return nil, fmt.Errorf("The workflow is not valid. %s: Job name %s is invalid. Names must start with a letter or '_' and contain only alphanumeric characters, '-', or '_'", workflow.Name, k) + return nil, fmt.Errorf("workflow is not valid. '%s': Job name '%s' is invalid. Names must start with a letter or '_' and contain only alphanumeric characters, '-', or '_'", workflow.Name, k) } } + wp.workflows = append(wp.workflows, workflow) f.Close() } diff --git a/pkg/model/planner_test.go b/pkg/model/planner_test.go index 14765be..8ba5ca7 100644 --- a/pkg/model/planner_test.go +++ b/pkg/model/planner_test.go @@ -8,25 +8,30 @@ import ( "github.com/stretchr/testify/assert" ) -type TestJobFileInfo struct { - workflowPath string - errorMessage string +type WorkflowPlanTest struct { + workflowPath string + errorMessage string + noWorkflowRecurse bool } func TestPlanner(t *testing.T) { - tables := []TestJobFileInfo{ - {"invalid-job-name", "The workflow is not valid. invalid-job-name: Job name invalid-JOB-Name-v1.2.3-docker_hub is invalid. Names must start with a letter or '_' and contain only alphanumeric characters, '-', or '_'"}, - {"empty-workflow", "unable to read workflow, push.yml file is empty: EOF"}, - - {"", ""}, // match whole directory - } log.SetLevel(log.DebugLevel) + tables := []WorkflowPlanTest{ + {"invalid-job-name/invalid-1.yml", "workflow is not valid. 'invalid-job-name-1': Job name 'invalid-JOB-Name-v1.2.3-docker_hub' is invalid. Names must start with a letter or '_' and contain only alphanumeric characters, '-', or '_'", false}, + {"invalid-job-name/invalid-2.yml", "workflow is not valid. 'invalid-job-name-2': Job name '1234invalid-JOB-Name-v123-docker_hub' is invalid. Names must start with a letter or '_' and contain only alphanumeric characters, '-', or '_'", false}, + {"invalid-job-name/valid-1.yml", "", false}, + {"invalid-job-name/valid-2.yml", "", false}, + {"empty-workflow", "unable to read workflow, push.yml file is empty: EOF", false}, + {"nested", "unable to read workflow, fail.yml file is empty: EOF", false}, + {"nested", "", true}, + } + workdir, err := filepath.Abs("testdata") assert.NoError(t, err, workdir) for _, table := range tables { fullWorkflowPath := filepath.Join(workdir, table.workflowPath) - _, err = NewWorkflowPlanner(fullWorkflowPath) + _, err = NewWorkflowPlanner(fullWorkflowPath, table.noWorkflowRecurse) if table.errorMessage == "" { assert.NoError(t, err, "WorkflowPlanner should exit without any error") } else { diff --git a/pkg/model/testdata/invalid-job-name/push.yml b/pkg/model/testdata/invalid-job-name/invalid-1.yml similarity index 89% rename from pkg/model/testdata/invalid-job-name/push.yml rename to pkg/model/testdata/invalid-job-name/invalid-1.yml index cc3951b..b7129d4 100644 --- a/pkg/model/testdata/invalid-job-name/push.yml +++ b/pkg/model/testdata/invalid-job-name/invalid-1.yml @@ -1,4 +1,4 @@ -name: invalid-job-name +name: invalid-job-name-1 on: push jobs: diff --git a/pkg/model/testdata/invalid-job-name/invalid-2.yml b/pkg/model/testdata/invalid-job-name/invalid-2.yml new file mode 100644 index 0000000..33dd31d --- /dev/null +++ b/pkg/model/testdata/invalid-job-name/invalid-2.yml @@ -0,0 +1,8 @@ +name: invalid-job-name-2 +on: push + +jobs: + 1234invalid-JOB-Name-v123-docker_hub: + runs-on: ubuntu-latest + steps: + - run: echo hi diff --git a/pkg/model/testdata/invalid-job-name/valid-1.yml b/pkg/model/testdata/invalid-job-name/valid-1.yml new file mode 100644 index 0000000..42e8293 --- /dev/null +++ b/pkg/model/testdata/invalid-job-name/valid-1.yml @@ -0,0 +1,8 @@ +name: valid-job-name-1 +on: push + +jobs: + valid-JOB-Name-v123-docker_hub: + runs-on: ubuntu-latest + steps: + - run: echo hi diff --git a/pkg/model/testdata/invalid-job-name/valid-2.yml b/pkg/model/testdata/invalid-job-name/valid-2.yml new file mode 100644 index 0000000..3e5c7bc --- /dev/null +++ b/pkg/model/testdata/invalid-job-name/valid-2.yml @@ -0,0 +1,8 @@ +name: valid-job-name-2 +on: push + +jobs: + ___valid-JOB-Name-v123-docker_hub: + runs-on: ubuntu-latest + steps: + - run: echo hi diff --git a/pkg/model/testdata/nested/success.yml b/pkg/model/testdata/nested/success.yml new file mode 100644 index 0000000..f65ee91 --- /dev/null +++ b/pkg/model/testdata/nested/success.yml @@ -0,0 +1,9 @@ +name: Hello World Workflow +on: push + +jobs: + hello-world: + name: Hello World Job + runs-on: ubuntu-latest + steps: + - run: echo "Hello World!" diff --git a/pkg/model/testdata/nested/workflows/fail.yml b/pkg/model/testdata/nested/workflows/fail.yml new file mode 100644 index 0000000..e69de29 diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index fc35f0e..f58d5a3 100755 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -14,7 +14,7 @@ import ( ) func TestGraphEvent(t *testing.T) { - planner, err := model.NewWorkflowPlanner("testdata/basic") + planner, err := model.NewWorkflowPlanner("testdata/basic", true) assert.NilError(t, err) plan := planner.PlanEvent("push") @@ -56,7 +56,7 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) { runner, err := New(runnerConfig) assert.NilError(t, err, tjfi.workflowPath) - planner, err := model.NewWorkflowPlanner(fullWorkflowPath) + planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true) assert.NilError(t, err, fullWorkflowPath) plan := planner.PlanEvent(tjfi.eventName) @@ -143,7 +143,7 @@ func TestRunEventSecrets(t *testing.T) { runner, err := New(runnerConfig) assert.NilError(t, err, workflowPath) - planner, err := model.NewWorkflowPlanner(fmt.Sprintf("testdata/%s", workflowPath)) + planner, err := model.NewWorkflowPlanner(fmt.Sprintf("testdata/%s", workflowPath), true) assert.NilError(t, err, workflowPath) plan := planner.PlanEvent(eventName) @@ -180,7 +180,7 @@ func TestRunEventPullRequest(t *testing.T) { runner, err := New(runnerConfig) assert.NilError(t, err, workflowPath) - planner, err := model.NewWorkflowPlanner(fmt.Sprintf("testdata/%s", workflowPath)) + planner, err := model.NewWorkflowPlanner(fmt.Sprintf("testdata/%s", workflowPath), true) assert.NilError(t, err, workflowPath) plan := planner.PlanEvent(eventName)