Repository: woodpecker-ci/woodpecker (Go)

Issue:
Weird behavior on path exclude contraints
### Component

server

### Describe the bug

We like to setup a workflow that is skipped if a change contains ONLY changes to a certain list of files. For that we tried using a `when` condition similar to this:

```yaml
when:
  - event: pull_request
    path:
      exclude: [ 'README.md' ]
      include: [ 'src/*.go' ]
```

This will however make woodpecker skip the workflow for any commit that touches files listed in `exclude`. Including commits that change other files in addition. E.g. if I have a commit changing `src/main.go` and `README.md` the workflow will not be executed.
Is that really the desired behavior? To me it looks more like a but. I could be wrong, but I would expect the more common use case for such an `exclude` constraint to be to skip execution when an change contains only files that are listed in `exclude` and to not skip as soon as at least one file is changed that is not excluded.

### Steps to reproduce

- Create a woodpecker workflow with the above `path` constraint. 
- Submit a change that touches `README.md` and `src/main.go` 
- The workflow will not be executed

### Expected behavior

_No response_

### System Info

```shell
woodpecker v3.4.x, happens also on the current main branch
```

### Additional context

I'll try to submit a PR with adjusted unit tests to clarify what I think is the more useful behavior.

### Validations

- [x] Read the [docs](https://woodpecker-ci.org/docs/intro).
- [x] Check that there isn't [already an issue](https://github.com/woodpecker-ci/woodpecker/issues) that reports the same bug to avoid creating a duplicate.
- [x] Checked that the bug isn't fixed in the `next` version already [https://woodpecker-ci.org/versions]


Discussion:
At least I would agree, this looks like a bug and I would expect the described behavior. You said you have started to work on a fix already, if you like you can also create a draft PR with the current state so someone else could continue as well if you don't have time.
Nevermind, you did that already. Thanks



Patch:
diff --git a/pipeline/frontend/yaml/constraint/constraint.go b/pipeline/frontend/yaml/constraint/constraint.go
index 891db304f83..1dee7477adf 100644
--- a/pipeline/frontend/yaml/constraint/constraint.go
+++ b/pipeline/frontend/yaml/constraint/constraint.go
@@ -388,14 +388,19 @@ func (c *Path) Includes(v []string) bool {
 	return false
 }
 
-// Excludes returns true if the string matches any of the exclude patterns.
+// Excludes returns true if all of the strings match any of the exclude patterns.
 func (c *Path) Excludes(v []string) bool {
-	for _, pattern := range c.Exclude {
-		for _, file := range v {
+	for _, file := range v {
+		matched := false
+		for _, pattern := range c.Exclude {
 			if ok, _ := doublestar.Match(pattern, file); ok {
-				return true
+				matched = true
+				break
 			}
 		}
+		if !matched {
+			return false
+		}
 	}
-	return false
+	return true
 }


Test patch:
diff --git a/pipeline/frontend/yaml/constraint/constraint_test.go b/pipeline/frontend/yaml/constraint/constraint_test.go
index a6c68dffa14..7dafd6f995b 100644
--- a/pipeline/frontend/yaml/constraint/constraint_test.go
+++ b/pipeline/frontend/yaml/constraint/constraint_test.go
@@ -233,8 +233,23 @@ func TestConstraintList(t *testing.T) {
 		{
 			conf: "{ include: [ '*.md' ], exclude: [ CHANGELOG.md ] }",
 			with: []string{"README.md", "CHANGELOG.md"},
+			want: true,
+		},
+		{
+			conf: "{ exclude: [ CHANGELOG.md ] }",
+			with: []string{"README.md", "CHANGELOG.md"},
+			want: true,
+		},
+		{
+			conf: "{ exclude: [ CHANGELOG.md, docs/**/*.md ] }",
+			with: []string{"docs/main.md", "CHANGELOG.md"},
 			want: false,
 		},
+		{
+			conf: "{ exclude: [ CHANGELOG.md, docs/**/*.md ] }",
+			with: []string{"docs/main.md", "CHANGELOG.md", "README.md"},
+			want: true,
+		},
 		// commit message ignore matches
 		{
 			conf:    "{ include: [ README.md ], ignore_message: '[ALL]' }",