refactor: NewWorkflowPlanner
(#648)
feat: add flag `--no-recurse` to disable recursion when reading workflows from directories feat: added more tests to `TestPlanner`, renamed `TestJobFileInfo` to more appropriate name `WorkflowPlanTest` style: changed error message to lowercase, added single quotes for better visibility Co-authored-by: Casey Lee <cplee@nektos.com>
This commit is contained in:
parent
b04d762614
commit
806bc4d999
11 changed files with 117 additions and 31 deletions
|
@ -27,6 +27,7 @@ type Input struct {
|
||||||
privileged bool
|
privileged bool
|
||||||
usernsMode string
|
usernsMode string
|
||||||
containerArchitecture string
|
containerArchitecture string
|
||||||
|
noWorkflowRecurse bool
|
||||||
useGitIgnore bool
|
useGitIgnore bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.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.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().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().StringVarP(&input.workdir, "directory", "C", ".", "working directory")
|
||||||
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
|
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&input.noOutput, "quiet", "q", false, "disable logging of output from steps")
|
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 {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func configLocations() []string {
|
func configLocations() []string {
|
||||||
|
@ -164,7 +164,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
|
||||||
secrets := newSecrets(input.secrets)
|
secrets := newSecrets(input.secrets)
|
||||||
_ = readEnvs(input.Secretfile(), secrets)
|
_ = readEnvs(input.Secretfile(), secrets)
|
||||||
|
|
||||||
planner, err := model.NewWorkflowPlanner(input.WorkflowsPath())
|
planner, err := model.NewWorkflowPlanner(input.WorkflowsPath(), input.noWorkflowRecurse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,34 +50,78 @@ func (r *Run) Job() *Job {
|
||||||
return r.Workflow.GetJob(r.JobID)
|
return r.Workflow.GetJob(r.JobID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWorkflowPlanner will load a specific workflow or all workflows from a directory
|
type WorkflowFiles struct {
|
||||||
func NewWorkflowPlanner(path string) (WorkflowPlanner, error) {
|
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)
|
fi, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var files []os.FileInfo
|
var workflows []WorkflowFiles
|
||||||
var dirname string
|
|
||||||
|
|
||||||
if fi.IsDir() {
|
if fi.IsDir() {
|
||||||
log.Debugf("Loading workflows from '%s'", path)
|
log.Debugf("Loading workflows from '%s'", path)
|
||||||
dirname = path
|
if noWorkflowRecurse {
|
||||||
files, err = ioutil.ReadDir(path)
|
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 {
|
} else {
|
||||||
log.Debugf("Loading workflow '%s'", path)
|
log.Debugf("Loading workflow '%s'", path)
|
||||||
dirname, err = filepath.Abs(filepath.Dir(path))
|
dirname := filepath.Dir(path)
|
||||||
files = []os.FileInfo{fi}
|
|
||||||
|
workflows = append(workflows, WorkflowFiles{
|
||||||
|
dirPath: dirname,
|
||||||
|
workflowFileInfo: fi,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
wp := new(workflowPlanner)
|
wp := new(workflowPlanner)
|
||||||
for _, file := range files {
|
for _, wf := range workflows {
|
||||||
ext := filepath.Ext(file.Name())
|
ext := filepath.Ext(wf.workflowFileInfo.Name())
|
||||||
if ext == ".yml" || ext == ".yaml" {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -87,19 +131,22 @@ func NewWorkflowPlanner(path string) (WorkflowPlanner, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.Close()
|
f.Close()
|
||||||
if err == io.EOF {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if workflow.Name == "" {
|
if workflow.Name == "" {
|
||||||
workflow.Name = file.Name()
|
workflow.Name = wf.workflowFileInfo.Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
jobNameRegex := regexp.MustCompile(`^([[:alpha:]_][[:alnum:]_\-]*)$`)
|
jobNameRegex := regexp.MustCompile(`^([[:alpha:]_][[:alnum:]_\-]*)$`)
|
||||||
for k := range workflow.Jobs {
|
for k := range workflow.Jobs {
|
||||||
if ok := jobNameRegex.MatchString(k); !ok {
|
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)
|
wp.workflows = append(wp.workflows, workflow)
|
||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,25 +8,30 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TestJobFileInfo struct {
|
type WorkflowPlanTest struct {
|
||||||
workflowPath string
|
workflowPath string
|
||||||
errorMessage string
|
errorMessage string
|
||||||
|
noWorkflowRecurse bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPlanner(t *testing.T) {
|
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)
|
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")
|
workdir, err := filepath.Abs("testdata")
|
||||||
assert.NoError(t, err, workdir)
|
assert.NoError(t, err, workdir)
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
fullWorkflowPath := filepath.Join(workdir, table.workflowPath)
|
fullWorkflowPath := filepath.Join(workdir, table.workflowPath)
|
||||||
_, err = NewWorkflowPlanner(fullWorkflowPath)
|
_, err = NewWorkflowPlanner(fullWorkflowPath, table.noWorkflowRecurse)
|
||||||
if table.errorMessage == "" {
|
if table.errorMessage == "" {
|
||||||
assert.NoError(t, err, "WorkflowPlanner should exit without any error")
|
assert.NoError(t, err, "WorkflowPlanner should exit without any error")
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
name: invalid-job-name
|
name: invalid-job-name-1
|
||||||
on: push
|
on: push
|
||||||
|
|
||||||
jobs:
|
jobs:
|
8
pkg/model/testdata/invalid-job-name/invalid-2.yml
vendored
Normal file
8
pkg/model/testdata/invalid-job-name/invalid-2.yml
vendored
Normal file
|
@ -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
|
8
pkg/model/testdata/invalid-job-name/valid-1.yml
vendored
Normal file
8
pkg/model/testdata/invalid-job-name/valid-1.yml
vendored
Normal file
|
@ -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
|
8
pkg/model/testdata/invalid-job-name/valid-2.yml
vendored
Normal file
8
pkg/model/testdata/invalid-job-name/valid-2.yml
vendored
Normal file
|
@ -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
|
9
pkg/model/testdata/nested/success.yml
vendored
Normal file
9
pkg/model/testdata/nested/success.yml
vendored
Normal file
|
@ -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!"
|
0
pkg/model/testdata/nested/workflows/fail.yml
vendored
Normal file
0
pkg/model/testdata/nested/workflows/fail.yml
vendored
Normal file
|
@ -14,7 +14,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGraphEvent(t *testing.T) {
|
func TestGraphEvent(t *testing.T) {
|
||||||
planner, err := model.NewWorkflowPlanner("testdata/basic")
|
planner, err := model.NewWorkflowPlanner("testdata/basic", true)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
plan := planner.PlanEvent("push")
|
plan := planner.PlanEvent("push")
|
||||||
|
@ -56,7 +56,7 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
|
||||||
runner, err := New(runnerConfig)
|
runner, err := New(runnerConfig)
|
||||||
assert.NilError(t, err, tjfi.workflowPath)
|
assert.NilError(t, err, tjfi.workflowPath)
|
||||||
|
|
||||||
planner, err := model.NewWorkflowPlanner(fullWorkflowPath)
|
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
|
||||||
assert.NilError(t, err, fullWorkflowPath)
|
assert.NilError(t, err, fullWorkflowPath)
|
||||||
|
|
||||||
plan := planner.PlanEvent(tjfi.eventName)
|
plan := planner.PlanEvent(tjfi.eventName)
|
||||||
|
@ -143,7 +143,7 @@ func TestRunEventSecrets(t *testing.T) {
|
||||||
runner, err := New(runnerConfig)
|
runner, err := New(runnerConfig)
|
||||||
assert.NilError(t, err, workflowPath)
|
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)
|
assert.NilError(t, err, workflowPath)
|
||||||
|
|
||||||
plan := planner.PlanEvent(eventName)
|
plan := planner.PlanEvent(eventName)
|
||||||
|
@ -180,7 +180,7 @@ func TestRunEventPullRequest(t *testing.T) {
|
||||||
runner, err := New(runnerConfig)
|
runner, err := New(runnerConfig)
|
||||||
assert.NilError(t, err, workflowPath)
|
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)
|
assert.NilError(t, err, workflowPath)
|
||||||
|
|
||||||
plan := planner.PlanEvent(eventName)
|
plan := planner.PlanEvent(eventName)
|
||||||
|
|
Loading…
Reference in a new issue