Skip to content

Commit 4ba1c2b

Browse files
feat: --validate and --strict (#2717)
* feat: `--validate` and `--strict` * fix missing changes * add test for strict validate --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent d9c6afc commit 4ba1c2b

File tree

9 files changed

+79
-35
lines changed

9 files changed

+79
-35
lines changed

cmd/input.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ type Input struct {
6262
useNewActionCache bool
6363
localRepository []string
6464
listOptions bool
65+
validate bool
66+
strict bool
6567
concurrentJobs int
6668
}
6769

cmd/root.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ func createRootCommand(ctx context.Context, input *Input, version string) *cobra
6666
}
6767

6868
rootCmd.Flags().BoolP("watch", "w", false, "watch the contents of the local repo and run when files change")
69+
rootCmd.Flags().BoolVar(&input.validate, "validate", false, "validate workflows")
70+
rootCmd.Flags().BoolVar(&input.strict, "strict", false, "use strict workflow schema")
6971
rootCmd.Flags().BoolP("list", "l", false, "list workflows")
7072
rootCmd.Flags().BoolP("graph", "g", false, "draw workflows")
7173
rootCmd.Flags().StringP("job", "j", "", "run a specific job ID")
@@ -446,7 +448,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
446448
matrixes := parseMatrix(input.matrix)
447449
log.Debugf("Evaluated matrix inclusions: %v", matrixes)
448450

449-
planner, err := model.NewWorkflowPlanner(input.WorkflowsPath(), input.noWorkflowRecurse)
451+
planner, err := model.NewWorkflowPlanner(input.WorkflowsPath(), input.noWorkflowRecurse, input.strict)
450452
if err != nil {
451453
return err
452454
}
@@ -462,6 +464,11 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
462464
return err
463465
}
464466

467+
// check if we should just validate the workflows
468+
if input.validate {
469+
return err
470+
}
471+
465472
// check if we should just draw the graph
466473
graph, err := cmd.Flags().GetBool("graph")
467474
if err != nil {

pkg/artifacts/server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
298298
runner, err := runner.New(runnerConfig)
299299
assert.Nil(t, err, tjfi.workflowPath)
300300

301-
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
301+
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true, false)
302302
assert.Nil(t, err, fullWorkflowPath)
303303

304304
plan, err := planner.PlanEvent(tjfi.eventName)

pkg/model/planner.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ type WorkflowFiles struct {
5656
}
5757

5858
// NewWorkflowPlanner will load a specific workflow, all workflows from a directory or all workflows from a directory and its subdirectories
59-
func NewWorkflowPlanner(path string, noWorkflowRecurse bool) (WorkflowPlanner, error) {
59+
func NewWorkflowPlanner(path string, noWorkflowRecurse, strict bool) (WorkflowPlanner, error) {
6060
path, err := filepath.Abs(path)
6161
if err != nil {
6262
return nil, err
@@ -124,7 +124,7 @@ func NewWorkflowPlanner(path string, noWorkflowRecurse bool) (WorkflowPlanner, e
124124
}
125125

126126
log.Debugf("Reading workflow '%s'", f.Name())
127-
workflow, err := ReadWorkflow(f)
127+
workflow, err := ReadWorkflow(f, strict)
128128
if err != nil {
129129
_ = f.Close()
130130
if err == io.EOF {
@@ -161,7 +161,7 @@ func NewSingleWorkflowPlanner(name string, f io.Reader) (WorkflowPlanner, error)
161161
wp := new(workflowPlanner)
162162

163163
log.Debugf("Reading workflow %s", name)
164-
workflow, err := ReadWorkflow(f)
164+
workflow, err := ReadWorkflow(f, false)
165165
if err != nil {
166166
if err == io.EOF {
167167
return nil, fmt.Errorf("unable to read workflow '%s': file is empty: %w", name, err)

pkg/model/planner_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestPlanner(t *testing.T) {
3131
assert.NoError(t, err, workdir)
3232
for _, table := range tables {
3333
fullWorkflowPath := filepath.Join(workdir, table.workflowPath)
34-
_, err = NewWorkflowPlanner(fullWorkflowPath, table.noWorkflowRecurse)
34+
_, err = NewWorkflowPlanner(fullWorkflowPath, table.noWorkflowRecurse, false)
3535
if table.errorMessage == "" {
3636
assert.NoError(t, err, "WorkflowPlanner should exit without any error")
3737
} else {

pkg/model/workflow.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,20 @@ func (w *Workflow) UnmarshalYAML(node *yaml.Node) error {
8080
return node.Decode((*WorkflowDefault)(w))
8181
}
8282

83+
type WorkflowStrict Workflow
84+
85+
func (w *WorkflowStrict) UnmarshalYAML(node *yaml.Node) error {
86+
// Validate the schema before deserializing it into our model
87+
if err := (&schema.Node{
88+
Definition: "workflow-root-strict",
89+
Schema: schema.GetWorkflowSchema(),
90+
}).UnmarshalYAML(node); err != nil {
91+
return errors.Join(err, fmt.Errorf("Actions YAML Strict Schema Validation Error detected:\nFor more information, see: https://nektosact.com/usage/schema.html"))
92+
}
93+
type WorkflowDefault Workflow
94+
return node.Decode((*WorkflowDefault)(w))
95+
}
96+
8397
type WorkflowDispatchInput struct {
8498
Description string `yaml:"description"`
8599
Required bool `yaml:"required"`
@@ -711,7 +725,12 @@ func (s *Step) Type() StepType {
711725
}
712726

713727
// ReadWorkflow returns a list of jobs for a given workflow file reader
714-
func ReadWorkflow(in io.Reader) (*Workflow, error) {
728+
func ReadWorkflow(in io.Reader, strict bool) (*Workflow, error) {
729+
if strict {
730+
w := new(WorkflowStrict)
731+
err := yaml.NewDecoder(in).Decode(w)
732+
return (*Workflow)(w), err
733+
}
715734
w := new(Workflow)
716735
err := yaml.NewDecoder(in).Decode(w)
717736
return w, err

pkg/model/workflow_test.go

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- uses: ./actions/docker-url
2020
`
2121

22-
workflow, err := ReadWorkflow(strings.NewReader(yaml))
22+
workflow, err := ReadWorkflow(strings.NewReader(yaml), false)
2323
assert.NoError(t, err, "read workflow should succeed")
2424

2525
assert.Len(t, workflow.On(), 1)
@@ -38,7 +38,7 @@ jobs:
3838
- uses: ./actions/docker-url
3939
`
4040

41-
workflow, err := ReadWorkflow(strings.NewReader(yaml))
41+
workflow, err := ReadWorkflow(strings.NewReader(yaml), false)
4242
assert.NoError(t, err, "read workflow should succeed")
4343

4444
assert.Len(t, workflow.On(), 2)
@@ -64,7 +64,7 @@ jobs:
6464
- uses: ./actions/docker-url
6565
`
6666

67-
workflow, err := ReadWorkflow(strings.NewReader(yaml))
67+
workflow, err := ReadWorkflow(strings.NewReader(yaml), false)
6868
assert.NoError(t, err, "read workflow should succeed")
6969
assert.Len(t, workflow.On(), 2)
7070
assert.Contains(t, workflow.On(), "push")
@@ -83,7 +83,7 @@ jobs:
8383
steps:
8484
- uses: ./actions/docker-url`
8585

86-
workflow, err := ReadWorkflow(strings.NewReader(yaml))
86+
workflow, err := ReadWorkflow(strings.NewReader(yaml), false)
8787
assert.NoError(t, err, "read workflow should succeed")
8888
assert.Equal(t, workflow.Jobs["test"].RunsOn(), []string{"ubuntu-latest"})
8989
}
@@ -101,7 +101,7 @@ jobs:
101101
steps:
102102
- uses: ./actions/docker-url`
103103

104-
workflow, err := ReadWorkflow(strings.NewReader(yaml))
104+
workflow, err := ReadWorkflow(strings.NewReader(yaml), false)
105105
assert.NoError(t, err, "read workflow should succeed")
106106
assert.Equal(t, workflow.Jobs["test"].RunsOn(), []string{"ubuntu-latest", "linux"})
107107
}
@@ -126,7 +126,7 @@ jobs:
126126
- uses: ./actions/docker-url
127127
`
128128

129-
workflow, err := ReadWorkflow(strings.NewReader(yaml))
129+
workflow, err := ReadWorkflow(strings.NewReader(yaml), false)
130130
assert.NoError(t, err, "read workflow should succeed")
131131
assert.Len(t, workflow.Jobs, 2)
132132
assert.Contains(t, workflow.Jobs["test"].Container().Image, "nginx:latest")
@@ -156,7 +156,7 @@ jobs:
156156
- uses: ./actions/docker-url
157157
`
158158

159-
workflow, err := ReadWorkflow(strings.NewReader(yaml))
159+
workflow, err := ReadWorkflow(strings.NewReader(yaml), false)
160160
assert.NoError(t, err, "read workflow should succeed")
161161
assert.Len(t, workflow.Jobs, 1)
162162

@@ -194,7 +194,7 @@ jobs:
194194
uses: ./some/path/to/workflow.yaml
195195
`
196196

197-
workflow, err := ReadWorkflow(strings.NewReader(yaml))
197+
workflow, err := ReadWorkflow(strings.NewReader(yaml), false)
198198
assert.NoError(t, err, "read workflow should succeed")
199199
assert.Len(t, workflow.Jobs, 6)
200200

@@ -238,7 +238,7 @@ jobs:
238238
uses: some/path/to/workflow.yaml
239239
`
240240

241-
workflow, err := ReadWorkflow(strings.NewReader(yaml))
241+
workflow, err := ReadWorkflow(strings.NewReader(yaml), false)
242242
assert.NoError(t, err, "read workflow should succeed")
243243
assert.Len(t, workflow.Jobs, 4)
244244

@@ -280,7 +280,7 @@ jobs:
280280
uses: ./local-action
281281
`
282282

283-
_, err := ReadWorkflow(strings.NewReader(yaml))
283+
_, err := ReadWorkflow(strings.NewReader(yaml), false)
284284
assert.Error(t, err, "read workflow should fail")
285285
}
286286

@@ -312,7 +312,7 @@ jobs:
312312
echo "${{ needs.test1.outputs.some-b-key }}"
313313
`
314314

315-
workflow, err := ReadWorkflow(strings.NewReader(yaml))
315+
workflow, err := ReadWorkflow(strings.NewReader(yaml), false)
316316
assert.NoError(t, err, "read workflow should succeed")
317317
assert.Len(t, workflow.Jobs, 2)
318318

@@ -327,7 +327,7 @@ jobs:
327327
}
328328

329329
func TestReadWorkflow_Strategy(t *testing.T) {
330-
w, err := NewWorkflowPlanner("testdata/strategy/push.yml", true)
330+
w, err := NewWorkflowPlanner("testdata/strategy/push.yml", true, false)
331331
assert.NoError(t, err)
332332

333333
p, err := w.PlanJob("strategy-only-max-parallel")
@@ -418,7 +418,7 @@ func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) {
418418
yaml := `
419419
name: local-action-docker-url
420420
`
421-
workflow, err := ReadWorkflow(strings.NewReader(yaml))
421+
workflow, err := ReadWorkflow(strings.NewReader(yaml), false)
422422
assert.NoError(t, err, "read workflow should succeed")
423423
workflowDispatch := workflow.WorkflowDispatchConfig()
424424
assert.Nil(t, workflowDispatch)
@@ -427,7 +427,7 @@ func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) {
427427
name: local-action-docker-url
428428
on: push
429429
`
430-
workflow, err = ReadWorkflow(strings.NewReader(yaml))
430+
workflow, err = ReadWorkflow(strings.NewReader(yaml), false)
431431
assert.NoError(t, err, "read workflow should succeed")
432432
workflowDispatch = workflow.WorkflowDispatchConfig()
433433
assert.Nil(t, workflowDispatch)
@@ -436,7 +436,7 @@ func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) {
436436
name: local-action-docker-url
437437
on: workflow_dispatch
438438
`
439-
workflow, err = ReadWorkflow(strings.NewReader(yaml))
439+
workflow, err = ReadWorkflow(strings.NewReader(yaml), false)
440440
assert.NoError(t, err, "read workflow should succeed")
441441
workflowDispatch = workflow.WorkflowDispatchConfig()
442442
assert.NotNil(t, workflowDispatch)
@@ -446,7 +446,7 @@ func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) {
446446
name: local-action-docker-url
447447
on: [push, pull_request]
448448
`
449-
workflow, err = ReadWorkflow(strings.NewReader(yaml))
449+
workflow, err = ReadWorkflow(strings.NewReader(yaml), false)
450450
assert.NoError(t, err, "read workflow should succeed")
451451
workflowDispatch = workflow.WorkflowDispatchConfig()
452452
assert.Nil(t, workflowDispatch)
@@ -455,7 +455,7 @@ func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) {
455455
name: local-action-docker-url
456456
on: [push, workflow_dispatch]
457457
`
458-
workflow, err = ReadWorkflow(strings.NewReader(yaml))
458+
workflow, err = ReadWorkflow(strings.NewReader(yaml), false)
459459
assert.NoError(t, err, "read workflow should succeed")
460460
workflowDispatch = workflow.WorkflowDispatchConfig()
461461
assert.NotNil(t, workflowDispatch)
@@ -467,7 +467,7 @@ func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) {
467467
- push
468468
- workflow_dispatch
469469
`
470-
workflow, err = ReadWorkflow(strings.NewReader(yaml))
470+
workflow, err = ReadWorkflow(strings.NewReader(yaml), false)
471471
assert.NoError(t, err, "read workflow should succeed")
472472
workflowDispatch = workflow.WorkflowDispatchConfig()
473473
assert.NotNil(t, workflowDispatch)
@@ -479,7 +479,7 @@ func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) {
479479
push:
480480
pull_request:
481481
`
482-
workflow, err = ReadWorkflow(strings.NewReader(yaml))
482+
workflow, err = ReadWorkflow(strings.NewReader(yaml), false)
483483
assert.NoError(t, err, "read workflow should succeed")
484484
workflowDispatch = workflow.WorkflowDispatchConfig()
485485
assert.Nil(t, workflowDispatch)
@@ -501,7 +501,7 @@ func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) {
501501
- warning
502502
- debug
503503
`
504-
workflow, err = ReadWorkflow(strings.NewReader(yaml))
504+
workflow, err = ReadWorkflow(strings.NewReader(yaml), false)
505505
assert.NoError(t, err, "read workflow should succeed")
506506
workflowDispatch = workflow.WorkflowDispatchConfig()
507507
assert.NotNil(t, workflowDispatch)
@@ -517,3 +517,19 @@ func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) {
517517
Type: "choice",
518518
}, workflowDispatch.Inputs["logLevel"])
519519
}
520+
521+
func TestReadWorkflow_InvalidStringEvent(t *testing.T) {
522+
yaml := `
523+
name: local-action-docker-url
524+
on: push2
525+
526+
jobs:
527+
test:
528+
runs-on: ubuntu-latest
529+
steps:
530+
- uses: ./actions/docker-url
531+
`
532+
533+
_, err := ReadWorkflow(strings.NewReader(yaml), true)
534+
assert.Error(t, err, "read workflow should succeed")
535+
}

pkg/runner/reusable_workflow.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func cloneIfRequired(rc *RunContext, remoteReusableWorkflow remoteReusableWorkfl
115115

116116
func newReusableWorkflowExecutor(rc *RunContext, directory string, workflow string) common.Executor {
117117
return func(ctx context.Context) error {
118-
planner, err := model.NewWorkflowPlanner(path.Join(directory, workflow), true)
118+
planner, err := model.NewWorkflowPlanner(path.Join(directory, workflow), true, false)
119119
if err != nil {
120120
return err
121121
}

pkg/runner/runner_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func init() {
5555
}
5656

5757
func TestNoWorkflowsFoundByPlanner(t *testing.T) {
58-
planner, err := model.NewWorkflowPlanner("res", true)
58+
planner, err := model.NewWorkflowPlanner("res", true, false)
5959
assert.NoError(t, err)
6060

6161
out := log.StandardLogger().Out
@@ -75,7 +75,7 @@ func TestNoWorkflowsFoundByPlanner(t *testing.T) {
7575
}
7676

7777
func TestGraphMissingEvent(t *testing.T) {
78-
planner, err := model.NewWorkflowPlanner("testdata/issue-1595/no-event.yml", true)
78+
planner, err := model.NewWorkflowPlanner("testdata/issue-1595/no-event.yml", true, false)
7979
assert.NoError(t, err)
8080

8181
out := log.StandardLogger().Out
@@ -93,7 +93,7 @@ func TestGraphMissingEvent(t *testing.T) {
9393
}
9494

9595
func TestGraphMissingFirst(t *testing.T) {
96-
planner, err := model.NewWorkflowPlanner("testdata/issue-1595/no-first.yml", true)
96+
planner, err := model.NewWorkflowPlanner("testdata/issue-1595/no-first.yml", true, false)
9797
assert.NoError(t, err)
9898

9999
plan, err := planner.PlanEvent("push")
@@ -103,7 +103,7 @@ func TestGraphMissingFirst(t *testing.T) {
103103
}
104104

105105
func TestGraphWithMissing(t *testing.T) {
106-
planner, err := model.NewWorkflowPlanner("testdata/issue-1595/missing.yml", true)
106+
planner, err := model.NewWorkflowPlanner("testdata/issue-1595/missing.yml", true, false)
107107
assert.NoError(t, err)
108108

109109
out := log.StandardLogger().Out
@@ -122,7 +122,7 @@ func TestGraphWithMissing(t *testing.T) {
122122
func TestGraphWithSomeMissing(t *testing.T) {
123123
log.SetLevel(log.DebugLevel)
124124

125-
planner, err := model.NewWorkflowPlanner("testdata/issue-1595/", true)
125+
planner, err := model.NewWorkflowPlanner("testdata/issue-1595/", true, false)
126126
assert.NoError(t, err)
127127

128128
out := log.StandardLogger().Out
@@ -140,7 +140,7 @@ func TestGraphWithSomeMissing(t *testing.T) {
140140
}
141141

142142
func TestGraphEvent(t *testing.T) {
143-
planner, err := model.NewWorkflowPlanner("testdata/basic", true)
143+
planner, err := model.NewWorkflowPlanner("testdata/basic", true, false)
144144
assert.NoError(t, err)
145145

146146
plan, err := planner.PlanEvent("push")
@@ -198,7 +198,7 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
198198
runner, err := New(runnerConfig)
199199
assert.Nil(t, err, j.workflowPath)
200200

201-
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
201+
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true, false)
202202
if j.errorMessage != "" && err != nil {
203203
assert.Error(t, err, j.errorMessage)
204204
} else if assert.Nil(t, err, fullWorkflowPath) {

0 commit comments

Comments
 (0)