Lint for event filter and deprecate exclude (#3222)

Closes https://github.com/woodpecker-ci/woodpecker/discussions/2174

- return bad habit error if no event filter is set
- If this is applied, it's useless to allow `exclude`s on events.
Therefore, deprecate it together with `include`s which should be
replaced by `base.StringOrSlice` later.
This commit is contained in:
qwerty287 2024-02-10 17:33:05 +01:00 committed by GitHub
parent 8700d2b300
commit f369d2c543
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 211 additions and 17 deletions

View file

@ -4423,9 +4423,11 @@ const docTemplate = `{
"linter", "linter",
"deprecation", "deprecation",
"compiler", "compiler",
"generic" "generic",
"bad_habit"
], ],
"x-enum-comments": { "x-enum-comments": {
"PipelineErrorTypeBadHabit": "some bad-habit error",
"PipelineErrorTypeCompiler": "some error with the config semantics", "PipelineErrorTypeCompiler": "some error with the config semantics",
"PipelineErrorTypeDeprecation": "using some deprecated feature", "PipelineErrorTypeDeprecation": "using some deprecated feature",
"PipelineErrorTypeGeneric": "some generic error", "PipelineErrorTypeGeneric": "some generic error",
@ -4435,7 +4437,8 @@ const docTemplate = `{
"PipelineErrorTypeLinter", "PipelineErrorTypeLinter",
"PipelineErrorTypeDeprecation", "PipelineErrorTypeDeprecation",
"PipelineErrorTypeCompiler", "PipelineErrorTypeCompiler",
"PipelineErrorTypeGeneric" "PipelineErrorTypeGeneric",
"PipelineErrorTypeBadHabit"
] ]
}, },
"model.Workflow": { "model.Workflow": {

View file

@ -7,6 +7,7 @@ Some versions need some changes to the server configuration or the pipeline conf
- Deprecated `steps.[name].group` in favor of `steps.[name].depends_on` (see [workflow syntax](./20-usage/20-workflow-syntax.md#depends_on) to learn how to set dependencies) - Deprecated `steps.[name].group` in favor of `steps.[name].depends_on` (see [workflow syntax](./20-usage/20-workflow-syntax.md#depends_on) to learn how to set dependencies)
- Removed `WOODPECKER_ROOT_PATH` and `WOODPECKER_ROOT_URL` config variables. Use `WOODPECKER_HOST` with a path instead - Removed `WOODPECKER_ROOT_PATH` and `WOODPECKER_ROOT_URL` config variables. Use `WOODPECKER_HOST` with a path instead
- Pipelines without a config file will now be skipped instead of failing - Pipelines without a config file will now be skipped instead of failing
- Deprecated `includes` and `excludes` support from **event** filter
## 2.0.0 ## 2.0.0

View file

@ -14,6 +14,7 @@ const (
PipelineErrorTypeDeprecation PipelineErrorType = "deprecation" // using some deprecated feature PipelineErrorTypeDeprecation PipelineErrorType = "deprecation" // using some deprecated feature
PipelineErrorTypeCompiler PipelineErrorType = "compiler" // some error with the config semantics PipelineErrorTypeCompiler PipelineErrorType = "compiler" // some error with the config semantics
PipelineErrorTypeGeneric PipelineErrorType = "generic" // some generic error PipelineErrorTypeGeneric PipelineErrorType = "generic" // some generic error
PipelineErrorTypeBadHabit PipelineErrorType = "bad_habit" // some bad-habit error
) )
type PipelineError struct { type PipelineError struct {

View file

@ -42,7 +42,6 @@ type (
Instance List Instance List
Platform List Platform List
Environment List Environment List
Event List
Branch List Branch List
Cron List Cron List
Status List Status List
@ -50,6 +49,8 @@ type (
Local yamlBaseTypes.BoolTrue Local yamlBaseTypes.BoolTrue
Path Path Path Path
Evaluate string `yaml:"evaluate,omitempty"` Evaluate string `yaml:"evaluate,omitempty"`
// TODO change to StringOrSlice in 3.x
Event List
} }
// List defines a runtime constraint for exclude & include string slices. // List defines a runtime constraint for exclude & include string slices.

View file

@ -263,10 +263,86 @@ func (l *Linter) lintDeprecations(config *WorkflowConfig) (err error) {
} }
} }
for i, c := range parsed.When.Constraints {
if len(c.Event.Exclude) != 0 {
err = multierr.Append(err, &errors.PipelineError{
Type: errors.PipelineErrorTypeDeprecation,
Message: "Please only use allow lists for events",
Data: errors.DeprecationErrorData{
File: config.File,
Field: fmt.Sprintf("when[%d].event", i),
Docs: "https://woodpecker-ci.org/docs/usage/workflow-syntax#event-1",
},
IsWarning: true,
})
}
}
for _, step := range parsed.Steps.ContainerList {
for i, c := range step.When.Constraints {
if len(c.Event.Exclude) != 0 {
err = multierr.Append(err, &errors.PipelineError{
Type: errors.PipelineErrorTypeDeprecation,
Message: "Please only use allow lists for events",
Data: errors.DeprecationErrorData{
File: config.File,
Field: fmt.Sprintf("steps.%s.when[%d].event", step.Name, i),
Docs: "https://woodpecker-ci.org/docs/usage/workflow-syntax#event",
},
IsWarning: true,
})
}
}
}
return err return err
} }
func (l *Linter) lintBadHabits(_ *WorkflowConfig) error { func (l *Linter) lintBadHabits(config *WorkflowConfig) (err error) {
// TODO: add bad habit warnings parsed := new(types.Workflow)
return nil err = xyaml.Unmarshal([]byte(config.RawConfig), parsed)
if err != nil {
return err
}
rootEventFilters := len(parsed.When.Constraints) > 0
for _, c := range parsed.When.Constraints {
if len(c.Event.Include) == 0 {
rootEventFilters = false
break
}
}
if !rootEventFilters {
// root whens do not necessarily have an event filter, check steps
for _, step := range parsed.Steps.ContainerList {
var field string
if len(step.When.Constraints) == 0 {
field = fmt.Sprintf("steps.%s", step.Name)
} else {
stepEventIndex := -1
for i, c := range step.When.Constraints {
if len(c.Event.Include) == 0 {
stepEventIndex = i
break
}
}
if stepEventIndex > -1 {
field = fmt.Sprintf("steps.%s.when[%d]", step.Name, stepEventIndex)
}
}
if field != "" {
err = multierr.Append(err, &errors.PipelineError{
Type: errors.PipelineErrorTypeBadHabit,
Message: "Please set an event filter on all when branches",
Data: errors.LinterErrorData{
File: config.File,
Field: field,
},
IsWarning: true,
})
}
}
}
return
} }

View file

@ -27,6 +27,9 @@ import (
func TestLint(t *testing.T) { func TestLint(t *testing.T) {
testdatas := []struct{ Title, Data string }{{ testdatas := []struct{ Title, Data string }{{
Title: "map", Data: ` Title: "map", Data: `
when:
event: push
steps: steps:
build: build:
image: docker image: docker
@ -46,6 +49,9 @@ services:
`, `,
}, { }, {
Title: "list", Data: ` Title: "list", Data: `
when:
event: push
steps: steps:
- name: build - name: build
image: docker image: docker
@ -65,6 +71,9 @@ services:
`, `,
}, { }, {
Title: "merge maps", Data: ` Title: "merge maps", Data: `
when:
event: push
variables: variables:
step_template: &base-step step_template: &base-step
image: golang:1.19 image: golang:1.19
@ -172,3 +181,41 @@ func TestLintErrors(t *testing.T) {
assert.True(t, found, "Expected error %q, got %q", test.want, lerrors) assert.True(t, found, "Expected error %q, got %q", test.want, lerrors)
} }
} }
func TestBadHabits(t *testing.T) {
testdata := []struct {
from string
want string
}{
{
from: "steps: { build: { image: golang } }",
want: "Please set an event filter on all when branches",
},
{
from: "when: [{branch: xyz}, {event: push}]\nsteps: { build: { image: golang } }",
want: "Please set an event filter on all when branches",
},
}
for _, test := range testdata {
conf, err := yaml.ParseString(test.from)
assert.NoError(t, err)
lerr := linter.New().Lint([]*linter.WorkflowConfig{{
File: test.from,
RawConfig: test.from,
Workflow: conf,
}})
assert.Error(t, lerr, "expected lint error for configuration", test.from)
lerrors := errors.GetPipelineErrors(lerr)
found := false
for _, lerr := range lerrors {
if lerr.Message == test.want {
found = true
break
}
}
assert.True(t, found, "Expected error %q, got %q", test.want, lerrors)
}
}

View file

@ -21,6 +21,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/errors"
"go.woodpecker-ci.org/woodpecker/v2/server/forge" "go.woodpecker-ci.org/woodpecker/v2/server/forge"
"go.woodpecker-ci.org/woodpecker/v2/server/forge/mocks" "go.woodpecker-ci.org/woodpecker/v2/server/forge/mocks"
forge_types "go.woodpecker-ci.org/woodpecker/v2/server/forge/types" forge_types "go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
@ -39,6 +40,7 @@ func TestGlobalEnvsubst(t *testing.T) {
Repo: &model.Repo{}, Repo: &model.Repo{},
Curr: &model.Pipeline{ Curr: &model.Pipeline{
Message: "aaa", Message: "aaa",
Event: model.EventPush,
}, },
Last: &model.Pipeline{}, Last: &model.Pipeline{},
Netrc: &model.Netrc{}, Netrc: &model.Netrc{},
@ -47,6 +49,8 @@ func TestGlobalEnvsubst(t *testing.T) {
Host: "", Host: "",
Yamls: []*forge_types.FileMeta{ Yamls: []*forge_types.FileMeta{
{Data: []byte(` {Data: []byte(`
when:
event: push
steps: steps:
build: build:
image: ${IMAGE} image: ${IMAGE}
@ -75,6 +79,7 @@ func TestMissingGlobalEnvsubst(t *testing.T) {
Repo: &model.Repo{}, Repo: &model.Repo{},
Curr: &model.Pipeline{ Curr: &model.Pipeline{
Message: "aaa", Message: "aaa",
Event: model.EventPush,
}, },
Last: &model.Pipeline{}, Last: &model.Pipeline{},
Netrc: &model.Netrc{}, Netrc: &model.Netrc{},
@ -83,6 +88,8 @@ func TestMissingGlobalEnvsubst(t *testing.T) {
Host: "", Host: "",
Yamls: []*forge_types.FileMeta{ Yamls: []*forge_types.FileMeta{
{Data: []byte(` {Data: []byte(`
when:
event: push
steps: steps:
build: build:
image: ${IMAGE} image: ${IMAGE}
@ -116,6 +123,8 @@ bbb`,
Host: "", Host: "",
Yamls: []*forge_types.FileMeta{ Yamls: []*forge_types.FileMeta{
{Data: []byte(` {Data: []byte(`
when:
event: push
steps: steps:
xxx: xxx:
image: scratch image: scratch
@ -123,6 +132,8 @@ steps:
yyy: ${CI_COMMIT_MESSAGE} yyy: ${CI_COMMIT_MESSAGE}
`)}, `)},
{Data: []byte(` {Data: []byte(`
when:
event: push
steps: steps:
build: build:
image: scratch image: scratch
@ -145,7 +156,9 @@ func TestMultiPipeline(t *testing.T) {
b := StepBuilder{ b := StepBuilder{
Forge: getMockForge(t), Forge: getMockForge(t),
Repo: &model.Repo{}, Repo: &model.Repo{},
Curr: &model.Pipeline{}, Curr: &model.Pipeline{
Event: model.EventPush,
},
Last: &model.Pipeline{}, Last: &model.Pipeline{},
Netrc: &model.Netrc{}, Netrc: &model.Netrc{},
Secs: []*model.Secret{}, Secs: []*model.Secret{},
@ -153,11 +166,15 @@ func TestMultiPipeline(t *testing.T) {
Host: "", Host: "",
Yamls: []*forge_types.FileMeta{ Yamls: []*forge_types.FileMeta{
{Data: []byte(` {Data: []byte(`
when:
event: push
steps: steps:
xxx: xxx:
image: scratch image: scratch
`)}, `)},
{Data: []byte(` {Data: []byte(`
when:
event: push
steps: steps:
build: build:
image: scratch image: scratch
@ -180,7 +197,9 @@ func TestDependsOn(t *testing.T) {
b := StepBuilder{ b := StepBuilder{
Forge: getMockForge(t), Forge: getMockForge(t),
Repo: &model.Repo{}, Repo: &model.Repo{},
Curr: &model.Pipeline{}, Curr: &model.Pipeline{
Event: model.EventPush,
},
Last: &model.Pipeline{}, Last: &model.Pipeline{},
Netrc: &model.Netrc{}, Netrc: &model.Netrc{},
Secs: []*model.Secret{}, Secs: []*model.Secret{},
@ -188,16 +207,22 @@ func TestDependsOn(t *testing.T) {
Host: "", Host: "",
Yamls: []*forge_types.FileMeta{ Yamls: []*forge_types.FileMeta{
{Name: "lint", Data: []byte(` {Name: "lint", Data: []byte(`
when:
event: push
steps: steps:
build: build:
image: scratch image: scratch
`)}, `)},
{Name: "test", Data: []byte(` {Name: "test", Data: []byte(`
when:
event: push
steps: steps:
build: build:
image: scratch image: scratch
`)}, `)},
{Data: []byte(` {Data: []byte(`
when:
event: push
steps: steps:
deploy: deploy:
image: scratch image: scratch
@ -227,7 +252,9 @@ func TestRunsOn(t *testing.T) {
b := StepBuilder{ b := StepBuilder{
Forge: getMockForge(t), Forge: getMockForge(t),
Repo: &model.Repo{}, Repo: &model.Repo{},
Curr: &model.Pipeline{}, Curr: &model.Pipeline{
Event: model.EventPush,
},
Last: &model.Pipeline{}, Last: &model.Pipeline{},
Netrc: &model.Netrc{}, Netrc: &model.Netrc{},
Secs: []*model.Secret{}, Secs: []*model.Secret{},
@ -235,6 +262,8 @@ func TestRunsOn(t *testing.T) {
Host: "", Host: "",
Yamls: []*forge_types.FileMeta{ Yamls: []*forge_types.FileMeta{
{Data: []byte(` {Data: []byte(`
when:
event: push
steps: steps:
deploy: deploy:
image: scratch image: scratch
@ -264,7 +293,9 @@ func TestPipelineName(t *testing.T) {
b := StepBuilder{ b := StepBuilder{
Forge: getMockForge(t), Forge: getMockForge(t),
Repo: &model.Repo{Config: ".woodpecker"}, Repo: &model.Repo{Config: ".woodpecker"},
Curr: &model.Pipeline{}, Curr: &model.Pipeline{
Event: model.EventPush,
},
Last: &model.Pipeline{}, Last: &model.Pipeline{},
Netrc: &model.Netrc{}, Netrc: &model.Netrc{},
Secs: []*model.Secret{}, Secs: []*model.Secret{},
@ -272,11 +303,15 @@ func TestPipelineName(t *testing.T) {
Host: "", Host: "",
Yamls: []*forge_types.FileMeta{ Yamls: []*forge_types.FileMeta{
{Name: ".woodpecker/lint.yml", Data: []byte(` {Name: ".woodpecker/lint.yml", Data: []byte(`
when:
event: push
steps: steps:
build: build:
image: scratch image: scratch
`)}, `)},
{Name: ".woodpecker/.test.yml", Data: []byte(` {Name: ".woodpecker/.test.yml", Data: []byte(`
when:
event: push
steps: steps:
build: build:
image: scratch image: scratch
@ -300,7 +335,10 @@ func TestBranchFilter(t *testing.T) {
b := StepBuilder{ b := StepBuilder{
Forge: getMockForge(t), Forge: getMockForge(t),
Repo: &model.Repo{}, Repo: &model.Repo{},
Curr: &model.Pipeline{Branch: "dev"}, Curr: &model.Pipeline{
Branch: "dev",
Event: model.EventPush,
},
Last: &model.Pipeline{}, Last: &model.Pipeline{},
Netrc: &model.Netrc{}, Netrc: &model.Netrc{},
Secs: []*model.Secret{}, Secs: []*model.Secret{},
@ -308,12 +346,16 @@ func TestBranchFilter(t *testing.T) {
Host: "", Host: "",
Yamls: []*forge_types.FileMeta{ Yamls: []*forge_types.FileMeta{
{Data: []byte(` {Data: []byte(`
when:
event: push
steps: steps:
xxx: xxx:
image: scratch image: scratch
branches: main branches: main
`)}, `)},
{Data: []byte(` {Data: []byte(`
when:
event: push
steps: steps:
build: build:
image: scratch image: scratch
@ -371,9 +413,7 @@ steps:
} }
pipelineItems, err := b.Build() pipelineItems, err := b.Build()
if err != nil { assert.False(t, errors.HasBlockingErrors(err))
t.Fatal(err)
}
if len(pipelineItems) != 2 { if len(pipelineItems) != 2 {
t.Fatal("Should have generated 2 pipelineItems") t.Fatal("Should have generated 2 pipelineItems")
@ -383,7 +423,10 @@ steps:
func TestZeroSteps(t *testing.T) { func TestZeroSteps(t *testing.T) {
t.Parallel() t.Parallel()
pipeline := &model.Pipeline{Branch: "dev"} pipeline := &model.Pipeline{
Branch: "dev",
Event: model.EventPush,
}
b := StepBuilder{ b := StepBuilder{
Forge: getMockForge(t), Forge: getMockForge(t),
@ -396,6 +439,8 @@ func TestZeroSteps(t *testing.T) {
Host: "", Host: "",
Yamls: []*forge_types.FileMeta{ Yamls: []*forge_types.FileMeta{
{Data: []byte(` {Data: []byte(`
when:
event: push
skip_clone: true skip_clone: true
steps: steps:
build: build:
@ -418,7 +463,10 @@ steps:
func TestZeroStepsAsMultiPipelineDeps(t *testing.T) { func TestZeroStepsAsMultiPipelineDeps(t *testing.T) {
t.Parallel() t.Parallel()
pipeline := &model.Pipeline{Branch: "dev"} pipeline := &model.Pipeline{
Branch: "dev",
Event: model.EventPush,
}
b := StepBuilder{ b := StepBuilder{
Forge: getMockForge(t), Forge: getMockForge(t),
@ -431,6 +479,8 @@ func TestZeroStepsAsMultiPipelineDeps(t *testing.T) {
Host: "", Host: "",
Yamls: []*forge_types.FileMeta{ Yamls: []*forge_types.FileMeta{
{Name: "zerostep", Data: []byte(` {Name: "zerostep", Data: []byte(`
when:
event: push
skip_clone: true skip_clone: true
steps: steps:
build: build:
@ -439,11 +489,15 @@ steps:
image: scratch image: scratch
`)}, `)},
{Name: "justastep", Data: []byte(` {Name: "justastep", Data: []byte(`
when:
event: push
steps: steps:
build: build:
image: scratch image: scratch
`)}, `)},
{Name: "shouldbefiltered", Data: []byte(` {Name: "shouldbefiltered", Data: []byte(`
when:
event: push
steps: steps:
build: build:
image: scratch image: scratch
@ -467,7 +521,10 @@ depends_on: [ zerostep ]
func TestZeroStepsAsMultiPipelineTransitiveDeps(t *testing.T) { func TestZeroStepsAsMultiPipelineTransitiveDeps(t *testing.T) {
t.Parallel() t.Parallel()
pipeline := &model.Pipeline{Branch: "dev"} pipeline := &model.Pipeline{
Branch: "dev",
Event: model.EventPush,
}
b := StepBuilder{ b := StepBuilder{
Forge: getMockForge(t), Forge: getMockForge(t),
@ -480,6 +537,8 @@ func TestZeroStepsAsMultiPipelineTransitiveDeps(t *testing.T) {
Host: "", Host: "",
Yamls: []*forge_types.FileMeta{ Yamls: []*forge_types.FileMeta{
{Name: "zerostep", Data: []byte(` {Name: "zerostep", Data: []byte(`
when:
event: push
skip_clone: true skip_clone: true
steps: steps:
build: build:
@ -488,17 +547,23 @@ steps:
image: scratch image: scratch
`)}, `)},
{Name: "justastep", Data: []byte(` {Name: "justastep", Data: []byte(`
when:
event: push
steps: steps:
build: build:
image: scratch image: scratch
`)}, `)},
{Name: "shouldbefiltered", Data: []byte(` {Name: "shouldbefiltered", Data: []byte(`
when:
event: push
steps: steps:
build: build:
image: scratch image: scratch
depends_on: [ zerostep ] depends_on: [ zerostep ]
`)}, `)},
{Name: "shouldbefilteredtoo", Data: []byte(` {Name: "shouldbefilteredtoo", Data: []byte(`
when:
event: push
steps: steps:
build: build:
image: scratch image: scratch