You've already forked woodpecker
							
							
				mirror of
				https://github.com/woodpecker-ci/woodpecker.git
				synced 2025-10-30 23:27:39 +02:00 
			
		
		
		
	Make pipeline/frontend/yaml/* types able to be marshaled back to YAML (#1835)
This commit is contained in:
		| @@ -280,7 +280,7 @@ func TestCompilerCompile(t *testing.T) { | ||||
| 				Name:     "step", | ||||
| 				Image:    "bash", | ||||
| 				Commands: []string{"env"}, | ||||
| 				Environment: yaml_base_types.EnvironmentMap{ | ||||
| 				Environment: map[string]any{ | ||||
| 					"MISSING": map[string]any{"from_secret": "missing"}, | ||||
| 				}, | ||||
| 			}}}}, | ||||
| @@ -375,7 +375,7 @@ func TestCompilerCompileWithFromSecret(t *testing.T) { | ||||
| 				Name:     "step", | ||||
| 				Image:    "bash", | ||||
| 				Commands: []string{"env"}, | ||||
| 				Environment: yaml_base_types.EnvironmentMap{ | ||||
| 				Environment: map[string]any{ | ||||
| 					"SECRET": map[string]any{"from_secret": "secret_name"}, | ||||
| 				}, | ||||
| 			}}}}, | ||||
|   | ||||
| @@ -19,11 +19,8 @@ import ( | ||||
| 	"maps" | ||||
| 	"path" | ||||
| 	"slices" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/bmatcuk/doublestar/v4" | ||||
| 	"github.com/expr-lang/expr" | ||||
| 	"go.uber.org/multierr" | ||||
| 	"gopkg.in/yaml.v3" | ||||
|  | ||||
| 	"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata" | ||||
| @@ -38,38 +35,18 @@ type ( | ||||
| 	} | ||||
|  | ||||
| 	Constraint struct { | ||||
| 		Ref      List | ||||
| 		Repo     List | ||||
| 		Instance List | ||||
| 		Platform List | ||||
| 		Branch   List | ||||
| 		Cron     List | ||||
| 		Status   List | ||||
| 		Matrix   Map | ||||
| 		Local    yamlBaseTypes.BoolTrue | ||||
| 		Path     Path | ||||
| 		Evaluate string `yaml:"evaluate,omitempty"` | ||||
| 		Event    yamlBaseTypes.StringOrSlice | ||||
| 	} | ||||
|  | ||||
| 	// List defines a runtime constraint for exclude & include string slices. | ||||
| 	List struct { | ||||
| 		Include []string | ||||
| 		Exclude []string | ||||
| 	} | ||||
|  | ||||
| 	// Map defines a runtime constraint for exclude & include map strings. | ||||
| 	Map struct { | ||||
| 		Include map[string]string | ||||
| 		Exclude map[string]string | ||||
| 	} | ||||
|  | ||||
| 	// Path defines a runtime constrain for exclude & include paths. | ||||
| 	Path struct { | ||||
| 		Include       []string | ||||
| 		Exclude       []string | ||||
| 		IgnoreMessage string                 `yaml:"ignore_message,omitempty"` | ||||
| 		OnEmpty       yamlBaseTypes.BoolTrue `yaml:"on_empty,omitempty"` | ||||
| 		Ref      List                        `yaml:"ref,omitempty"` | ||||
| 		Repo     List                        `yaml:"repo,omitempty"` | ||||
| 		Instance List                        `yaml:"instance,omitempty"` | ||||
| 		Platform List                        `yaml:"platform,omitempty"` | ||||
| 		Branch   List                        `yaml:"branch,omitempty"` | ||||
| 		Cron     List                        `yaml:"cron,omitempty"` | ||||
| 		Status   List                        `yaml:"status,omitempty"` | ||||
| 		Matrix   Map                         `yaml:"matrix,omitempty"` | ||||
| 		Local    yamlBaseTypes.BoolTrue      `yaml:"local,omitempty"` | ||||
| 		Path     Path                        `yaml:"path,omitempty"` | ||||
| 		Evaluate string                      `yaml:"evaluate,omitempty"` | ||||
| 		Event    yamlBaseTypes.StringOrSlice `yaml:"event,omitempty"` | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| @@ -153,6 +130,18 @@ func (when *When) UnmarshalYAML(value *yaml.Node) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // MarshalYAML implements custom Yaml marshaling. | ||||
| func (when When) MarshalYAML() (any, error) { | ||||
| 	switch len(when.Constraints) { | ||||
| 	case 0: | ||||
| 		return nil, nil | ||||
| 	case 1: | ||||
| 		return when.Constraints[0], nil | ||||
| 	default: | ||||
| 		return when.Constraints, nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Match returns true if all constraints match the given input. If a single | ||||
| // constraint fails a false value is returned. | ||||
| func (c *Constraint) Match(m metadata.Metadata, global bool, env map[string]string) (bool, error) { | ||||
| @@ -204,203 +193,3 @@ func (c *Constraint) Match(m metadata.Metadata, global bool, env map[string]stri | ||||
|  | ||||
| 	return match, nil | ||||
| } | ||||
|  | ||||
| // IsEmpty return true if a constraint has no conditions. | ||||
| func (c List) IsEmpty() bool { | ||||
| 	return len(c.Include) == 0 && len(c.Exclude) == 0 | ||||
| } | ||||
|  | ||||
| // Match returns true if the string matches the include patterns and does not | ||||
| // match any of the exclude patterns. | ||||
| func (c *List) Match(v string) bool { | ||||
| 	if c.Excludes(v) { | ||||
| 		return false | ||||
| 	} | ||||
| 	if c.Includes(v) { | ||||
| 		return true | ||||
| 	} | ||||
| 	if len(c.Include) == 0 { | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // Includes returns true if the string matches the include patterns. | ||||
| func (c *List) Includes(v string) bool { | ||||
| 	for _, pattern := range c.Include { | ||||
| 		if ok, _ := doublestar.Match(pattern, v); ok { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // Excludes returns true if the string matches the exclude patterns. | ||||
| func (c *List) Excludes(v string) bool { | ||||
| 	for _, pattern := range c.Exclude { | ||||
| 		if ok, _ := doublestar.Match(pattern, v); ok { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // UnmarshalYAML unmarshal the constraint. | ||||
| func (c *List) UnmarshalYAML(value *yaml.Node) error { | ||||
| 	out1 := struct { | ||||
| 		Include yamlBaseTypes.StringOrSlice | ||||
| 		Exclude yamlBaseTypes.StringOrSlice | ||||
| 	}{} | ||||
|  | ||||
| 	var out2 yamlBaseTypes.StringOrSlice | ||||
|  | ||||
| 	err1 := value.Decode(&out1) | ||||
| 	err2 := value.Decode(&out2) | ||||
|  | ||||
| 	c.Exclude = out1.Exclude | ||||
| 	c.Include = append( //nolint:gocritic | ||||
| 		out1.Include, | ||||
| 		out2..., | ||||
| 	) | ||||
|  | ||||
| 	if err1 != nil && err2 != nil { | ||||
| 		y, _ := yaml.Marshal(value) | ||||
| 		return fmt.Errorf("could not parse condition: %s: %w", y, multierr.Append(err1, err2)) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Match returns true if the params matches the include key values and does not | ||||
| // match any of the exclude key values. | ||||
| func (c *Map) Match(params map[string]string) bool { | ||||
| 	// when no includes or excludes automatically match | ||||
| 	if len(c.Include) == 0 && len(c.Exclude) == 0 { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	// Exclusions are processed first. So we can include everything and then | ||||
| 	// selectively include others. | ||||
| 	if len(c.Exclude) != 0 { | ||||
| 		var matches int | ||||
|  | ||||
| 		for key, val := range c.Exclude { | ||||
| 			if ok, _ := doublestar.Match(val, params[key]); ok { | ||||
| 				matches++ | ||||
| 			} | ||||
| 		} | ||||
| 		if matches == len(c.Exclude) { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	for key, val := range c.Include { | ||||
| 		if ok, _ := doublestar.Match(val, params[key]); !ok { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // UnmarshalYAML unmarshal the constraint map. | ||||
| func (c *Map) UnmarshalYAML(unmarshal func(any) error) error { | ||||
| 	out1 := struct { | ||||
| 		Include map[string]string | ||||
| 		Exclude map[string]string | ||||
| 	}{ | ||||
| 		Include: map[string]string{}, | ||||
| 		Exclude: map[string]string{}, | ||||
| 	} | ||||
|  | ||||
| 	out2 := map[string]string{} | ||||
|  | ||||
| 	_ = unmarshal(&out1) // it contains include and exclude statement | ||||
| 	_ = unmarshal(&out2) // it contains no include/exclude statement, assume include as default | ||||
|  | ||||
| 	c.Include = out1.Include | ||||
| 	c.Exclude = out1.Exclude | ||||
| 	for k, v := range out2 { | ||||
| 		c.Include[k] = v | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // UnmarshalYAML unmarshal the constraint. | ||||
| func (c *Path) UnmarshalYAML(value *yaml.Node) error { | ||||
| 	out1 := struct { | ||||
| 		Include       yamlBaseTypes.StringOrSlice `yaml:"include,omitempty"` | ||||
| 		Exclude       yamlBaseTypes.StringOrSlice `yaml:"exclude,omitempty"` | ||||
| 		IgnoreMessage string                      `yaml:"ignore_message,omitempty"` | ||||
| 		OnEmpty       yamlBaseTypes.BoolTrue      `yaml:"on_empty,omitempty"` | ||||
| 	}{} | ||||
|  | ||||
| 	var out2 yamlBaseTypes.StringOrSlice | ||||
|  | ||||
| 	err1 := value.Decode(&out1) | ||||
| 	err2 := value.Decode(&out2) | ||||
|  | ||||
| 	c.Exclude = out1.Exclude | ||||
| 	c.IgnoreMessage = out1.IgnoreMessage | ||||
| 	c.OnEmpty = out1.OnEmpty | ||||
| 	c.Include = append( //nolint:gocritic | ||||
| 		out1.Include, | ||||
| 		out2..., | ||||
| 	) | ||||
|  | ||||
| 	if err1 != nil && err2 != nil { | ||||
| 		y, _ := yaml.Marshal(value) | ||||
| 		return fmt.Errorf("could not parse condition: %s", y) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Match returns true if file paths in string slice matches the include and not exclude patterns | ||||
| // or if commit message contains ignore message. | ||||
| func (c *Path) Match(v []string, message string) bool { | ||||
| 	// ignore file pattern matches if the commit message contains a pattern | ||||
| 	if len(c.IgnoreMessage) > 0 && strings.Contains(strings.ToLower(message), strings.ToLower(c.IgnoreMessage)) { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	// return value based on 'on_empty', if there are no commit files (empty commit) | ||||
| 	if len(v) == 0 { | ||||
| 		return c.OnEmpty.Bool() | ||||
| 	} | ||||
|  | ||||
| 	if len(c.Exclude) > 0 && c.Excludes(v) { | ||||
| 		return false | ||||
| 	} | ||||
| 	if len(c.Include) > 0 && !c.Includes(v) { | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // Includes returns true if the string matches any of the include patterns. | ||||
| func (c *Path) Includes(v []string) bool { | ||||
| 	for _, pattern := range c.Include { | ||||
| 		for _, file := range v { | ||||
| 			if ok, _ := doublestar.Match(pattern, file); ok { | ||||
| 				return true | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // Excludes returns true if all of the strings match any of the exclude patterns. | ||||
| func (c *Path) Excludes(v []string) bool { | ||||
| 	for _, file := range v { | ||||
| 		matched := false | ||||
| 		for _, pattern := range c.Exclude { | ||||
| 			if ok, _ := doublestar.Match(pattern, file); ok { | ||||
| 				matched = true | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		if !matched { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|   | ||||
| @@ -23,397 +23,6 @@ import ( | ||||
| 	"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata" | ||||
| ) | ||||
|  | ||||
| func TestConstraint(t *testing.T) { | ||||
| 	testdata := []struct { | ||||
| 		conf string | ||||
| 		with string | ||||
| 		want bool | ||||
| 	}{ | ||||
| 		// string value | ||||
| 		{ | ||||
| 			conf: "main", | ||||
| 			with: "develop", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "main", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "feature/*", | ||||
| 			with: "feature/foo", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		// slice value | ||||
| 		{ | ||||
| 			conf: "[ main, feature/* ]", | ||||
| 			with: "develop", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "[ main, feature/* ]", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "[ main, feature/* ]", | ||||
| 			with: "feature/foo", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		// includes block | ||||
| 		{ | ||||
| 			conf: "include: main", | ||||
| 			with: "develop", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: main", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: feature/*", | ||||
| 			with: "main", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: feature/*", | ||||
| 			with: "feature/foo", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: [ main, feature/* ]", | ||||
| 			with: "develop", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: [ main, feature/* ]", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: [ main, feature/* ]", | ||||
| 			with: "feature/foo", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		// excludes block | ||||
| 		{ | ||||
| 			conf: "exclude: main", | ||||
| 			with: "develop", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: main", | ||||
| 			with: "main", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: feature/*", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: feature/*", | ||||
| 			with: "feature/foo", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: [ main, develop ]", | ||||
| 			with: "main", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: [ feature/*, bar ]", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: [ feature/*, bar ]", | ||||
| 			with: "feature/foo", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		// include and exclude blocks | ||||
| 		{ | ||||
| 			conf: "{ include: [ main, feature/* ], exclude: [ develop ] }", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ include: [ main, feature/* ], exclude: [ feature/bar ] }", | ||||
| 			with: "feature/bar", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ include: [ main, feature/* ], exclude: [ main, develop ] }", | ||||
| 			with: "main", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		// empty blocks | ||||
| 		{ | ||||
| 			conf: "", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range testdata { | ||||
| 		c := parseConstraint(t, test.conf) | ||||
| 		assert.Equal(t, test.want, c.Match(test.with)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestConstraintList(t *testing.T) { | ||||
| 	testdata := []struct { | ||||
| 		conf    string | ||||
| 		with    []string | ||||
| 		message string | ||||
| 		want    bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			conf: "", | ||||
| 			with: []string{"CHANGELOG.md", "README.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "CHANGELOG.md", | ||||
| 			with: []string{"CHANGELOG.md", "README.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "'*.md'", | ||||
| 			with: []string{"CHANGELOG.md", "README.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "['*.md']", | ||||
| 			with: []string{"CHANGELOG.md", "README.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "'docs/*'", | ||||
| 			with: []string{"docs/README.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "'docs/*'", | ||||
| 			with: []string{"docs/sub/README.md"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "'docs/**'", | ||||
| 			with: []string{"docs/README.md", "docs/sub/README.md", "docs/sub-sub/README.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "'docs/**'", | ||||
| 			with: []string{"README.md"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ include: [ README.md ] }", | ||||
| 			with: []string{"CHANGELOG.md"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ exclude: [ README.md ] }", | ||||
| 			with: []string{"design.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		// include and exclude blocks | ||||
| 		{ | ||||
| 			conf: "{ include: [ '*.md', '*.ini' ], exclude: [ CHANGELOG.md ] }", | ||||
| 			with: []string{"README.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ include: [ '*.md' ], exclude: [ CHANGELOG.md ] }", | ||||
| 			with: []string{"CHANGELOG.md"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			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]' }", | ||||
| 			with:    []string{"CHANGELOG.md"}, | ||||
| 			message: "Build them [ALL]", | ||||
| 			want:    true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf:    "{ exclude: [ '*.php' ], ignore_message: '[ALL]' }", | ||||
| 			with:    []string{"myfile.php"}, | ||||
| 			message: "Build them [ALL]", | ||||
| 			want:    true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf:    "{ ignore_message: '[ALL]' }", | ||||
| 			with:    []string{}, | ||||
| 			message: "Build them [ALL]", | ||||
| 			want:    true, | ||||
| 		}, | ||||
| 		// empty commit | ||||
| 		{ | ||||
| 			conf: "{ include: [ README.md ] }", | ||||
| 			with: []string{}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ include: [ README.md ], on_empty: false }", | ||||
| 			with: []string{}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ include: [ README.md ], on_empty: true }", | ||||
| 			with: []string{}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range testdata { | ||||
| 		c := parseConstraintPath(t, test.conf) | ||||
| 		assert.Equal(t, test.want, c.Match(test.with, test.message)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestConstraintMap(t *testing.T) { | ||||
| 	testdata := []struct { | ||||
| 		conf string | ||||
| 		with map[string]string | ||||
| 		want bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			conf: "GOLANG: 1.7", | ||||
| 			with: map[string]string{"GOLANG": "1.7"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "GOLANG: tip", | ||||
| 			with: map[string]string{"GOLANG": "1.7"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.1", "MYSQL": "5.6"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ GOLANG: 1.7, REDIS: 3.* }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ GOLANG: 1.7, BRANCH: release/**/test }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "BRANCH": "release/v1.12.1//test"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ GOLANG: 1.7, BRANCH: release/**/test }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "BRANCH": "release/v1.12.1/qest"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		// include syntax | ||||
| 		{ | ||||
| 			conf: "include: { GOLANG: 1.7 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: { GOLANG: tip }", | ||||
| 			with: map[string]string{"GOLANG": "1.7"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: { GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.1", "MYSQL": "5.6"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: { GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		// exclude syntax | ||||
| 		{ | ||||
| 			conf: "exclude: { GOLANG: 1.7 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: { GOLANG: tip }", | ||||
| 			with: map[string]string{"GOLANG": "1.7"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: { GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.1", "MYSQL": "5.6"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: { GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		// exclude AND include values | ||||
| 		{ | ||||
| 			conf: "{ include: { GOLANG: 1.7 }, exclude: { GOLANG: 1.7 } }", | ||||
| 			with: map[string]string{"GOLANG": "1.7"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		// blanks | ||||
| 		{ | ||||
| 			conf: "", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "GOLANG: 1.7", | ||||
| 			with: map[string]string{}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ GOLANG: 1.7, REDIS: 3.0 }", | ||||
| 			with: map[string]string{}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: { GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: { GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range testdata { | ||||
| 		c := parseConstraintMap(t, test.conf) | ||||
| 		assert.Equal(t, test.want, c.Match(test.with), "config: '%s', with: '%s'", test.conf, test.with) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestConstraintStatusSuccess(t *testing.T) { | ||||
| 	testdata := []struct { | ||||
| 		conf string | ||||
| @@ -576,21 +185,3 @@ func parseConstraints(t *testing.T, s string) *When { | ||||
| 	assert.NoError(t, yaml.Unmarshal([]byte(s), c)) | ||||
| 	return c | ||||
| } | ||||
|  | ||||
| func parseConstraint(t *testing.T, s string) *List { | ||||
| 	c := &List{} | ||||
| 	assert.NoError(t, yaml.Unmarshal([]byte(s), c)) | ||||
| 	return c | ||||
| } | ||||
|  | ||||
| func parseConstraintMap(t *testing.T, s string) *Map { | ||||
| 	c := &Map{} | ||||
| 	assert.NoError(t, yaml.Unmarshal([]byte(s), c)) | ||||
| 	return c | ||||
| } | ||||
|  | ||||
| func parseConstraintPath(t *testing.T, s string) *Path { | ||||
| 	c := &Path{} | ||||
| 	assert.NoError(t, yaml.Unmarshal([]byte(s), c)) | ||||
| 	return c | ||||
| } | ||||
|   | ||||
							
								
								
									
										119
									
								
								pipeline/frontend/yaml/constraint/list.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								pipeline/frontend/yaml/constraint/list.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| // Copyright 2025 Woodpecker Authors | ||||
| // | ||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| // you may not use this file except in compliance with the License. | ||||
| // You may obtain a copy of the License at | ||||
| // | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| // | ||||
| // Unless required by applicable law or agreed to in writing, software | ||||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| // See the License for the specific language governing permissions and | ||||
| // limitations under the License. | ||||
|  | ||||
| package constraint | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/bmatcuk/doublestar/v4" | ||||
| 	"go.uber.org/multierr" | ||||
| 	"gopkg.in/yaml.v3" | ||||
|  | ||||
| 	yamlBaseTypes "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types/base" | ||||
| ) | ||||
|  | ||||
| // List defines a runtime constraint for exclude & include string slices. | ||||
| type List struct { | ||||
| 	Include []string | ||||
| 	Exclude []string | ||||
| } | ||||
|  | ||||
| // IsEmpty return true if a constraint has no conditions. | ||||
| func (c List) IsEmpty() bool { | ||||
| 	return len(c.Include) == 0 && len(c.Exclude) == 0 | ||||
| } | ||||
|  | ||||
| // Match returns true if the string matches the include patterns and does not | ||||
| // match any of the exclude patterns. | ||||
| func (c *List) Match(v string) bool { | ||||
| 	if c == nil { | ||||
| 		return true | ||||
| 	} | ||||
| 	if c.Excludes(v) { | ||||
| 		return false | ||||
| 	} | ||||
| 	if c.Includes(v) { | ||||
| 		return true | ||||
| 	} | ||||
| 	if len(c.Include) == 0 { | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // Includes returns true if the string matches the include patterns. | ||||
| func (c *List) Includes(v string) bool { | ||||
| 	for _, pattern := range c.Include { | ||||
| 		if ok, _ := doublestar.Match(pattern, v); ok { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // Excludes returns true if the string matches the exclude patterns. | ||||
| func (c *List) Excludes(v string) bool { | ||||
| 	for _, pattern := range c.Exclude { | ||||
| 		if ok, _ := doublestar.Match(pattern, v); ok { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // UnmarshalYAML unmarshal the constraint. | ||||
| func (c *List) UnmarshalYAML(value *yaml.Node) error { | ||||
| 	out1 := struct { | ||||
| 		Include yamlBaseTypes.StringOrSlice | ||||
| 		Exclude yamlBaseTypes.StringOrSlice | ||||
| 	}{} | ||||
|  | ||||
| 	var out2 yamlBaseTypes.StringOrSlice | ||||
|  | ||||
| 	err1 := value.Decode(&out1) | ||||
| 	err2 := value.Decode(&out2) | ||||
|  | ||||
| 	c.Exclude = out1.Exclude | ||||
| 	c.Include = append( //nolint:gocritic | ||||
| 		out1.Include, | ||||
| 		out2..., | ||||
| 	) | ||||
|  | ||||
| 	if err1 != nil && err2 != nil { | ||||
| 		y, _ := yaml.Marshal(value) | ||||
| 		return fmt.Errorf("could not parse condition: %s: %w", y, multierr.Append(err1, err2)) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // MarshalYAML implements custom Yaml marshaling. | ||||
| func (c List) MarshalYAML() (any, error) { | ||||
| 	switch { | ||||
| 	case len(c.Include) == 0 && len(c.Exclude) == 0: | ||||
| 		return nil, nil | ||||
| 	case len(c.Exclude) == 0: | ||||
| 		return yamlBaseTypes.StringOrSlice(c.Include), nil | ||||
| 	default: | ||||
| 		// we can not return type List as it would lead to infinite recursion :/ | ||||
| 		return struct { | ||||
| 			Include yamlBaseTypes.StringOrSlice `yaml:"include,omitempty"` | ||||
| 			Exclude yamlBaseTypes.StringOrSlice `yaml:"exclude,omitempty"` | ||||
| 		}{ | ||||
| 			Include: c.Include, | ||||
| 			Exclude: c.Exclude, | ||||
| 		}, nil | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										167
									
								
								pipeline/frontend/yaml/constraint/list_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								pipeline/frontend/yaml/constraint/list_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | ||||
| // Copyright 2025 Woodpecker Authors | ||||
| // | ||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| // you may not use this file except in compliance with the License. | ||||
| // You may obtain a copy of the License at | ||||
| // | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| // | ||||
| // Unless required by applicable law or agreed to in writing, software | ||||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| // See the License for the specific language governing permissions and | ||||
| // limitations under the License. | ||||
|  | ||||
| package constraint | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"gopkg.in/yaml.v3" | ||||
| ) | ||||
|  | ||||
| func TestConstraintList(t *testing.T) { | ||||
| 	testdata := []struct { | ||||
| 		conf string | ||||
| 		with string | ||||
| 		want bool | ||||
| 	}{ | ||||
| 		// string value | ||||
| 		{ | ||||
| 			conf: "main", | ||||
| 			with: "develop", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "main", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "feature/*", | ||||
| 			with: "feature/foo", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		// slice value | ||||
| 		{ | ||||
| 			conf: "[ main, feature/* ]", | ||||
| 			with: "develop", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "[ main, feature/* ]", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "[ main, feature/* ]", | ||||
| 			with: "feature/foo", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		// includes block | ||||
| 		{ | ||||
| 			conf: "include: main", | ||||
| 			with: "develop", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: main", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: feature/*", | ||||
| 			with: "main", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: feature/*", | ||||
| 			with: "feature/foo", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: [ main, feature/* ]", | ||||
| 			with: "develop", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: [ main, feature/* ]", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: [ main, feature/* ]", | ||||
| 			with: "feature/foo", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		// excludes block | ||||
| 		{ | ||||
| 			conf: "exclude: main", | ||||
| 			with: "develop", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: main", | ||||
| 			with: "main", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: feature/*", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: feature/*", | ||||
| 			with: "feature/foo", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: [ main, develop ]", | ||||
| 			with: "main", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: [ feature/*, bar ]", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: [ feature/*, bar ]", | ||||
| 			with: "feature/foo", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		// include and exclude blocks | ||||
| 		{ | ||||
| 			conf: "{ include: [ main, feature/* ], exclude: [ develop ] }", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ include: [ main, feature/* ], exclude: [ feature/bar ] }", | ||||
| 			with: "feature/bar", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ include: [ main, feature/* ], exclude: [ main, develop ] }", | ||||
| 			with: "main", | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		// empty blocks | ||||
| 		{ | ||||
| 			conf: "", | ||||
| 			with: "main", | ||||
| 			want: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range testdata { | ||||
| 		c := parseConstraintList(t, test.conf) | ||||
| 		assert.Equal(t, test.want, c.Match(test.with)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func parseConstraintList(t *testing.T, s string) *List { | ||||
| 	c := &List{} | ||||
| 	assert.NoError(t, yaml.Unmarshal([]byte(s), c)) | ||||
| 	return c | ||||
| } | ||||
							
								
								
									
										99
									
								
								pipeline/frontend/yaml/constraint/map.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								pipeline/frontend/yaml/constraint/map.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| // Copyright 2025 Woodpecker Authors | ||||
| // | ||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| // you may not use this file except in compliance with the License. | ||||
| // You may obtain a copy of the License at | ||||
| // | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| // | ||||
| // Unless required by applicable law or agreed to in writing, software | ||||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| // See the License for the specific language governing permissions and | ||||
| // limitations under the License. | ||||
|  | ||||
| package constraint | ||||
|  | ||||
| import "github.com/bmatcuk/doublestar/v4" | ||||
|  | ||||
| // Map defines a runtime constraint for exclude & include map strings. | ||||
| type Map struct { | ||||
| 	Include map[string]string `yaml:"include,omitempty"` | ||||
| 	Exclude map[string]string `yaml:"exclude,omitempty"` | ||||
| } | ||||
|  | ||||
| // Match returns true if the params matches the include key values and does not | ||||
| // match any of the exclude key values. | ||||
| func (c *Map) Match(params map[string]string) bool { | ||||
| 	// when no includes or excludes automatically match | ||||
| 	if c == nil || len(c.Include) == 0 && len(c.Exclude) == 0 { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	// Exclusions are processed first. So we can include everything and then | ||||
| 	// selectively include others. | ||||
| 	if len(c.Exclude) != 0 { | ||||
| 		var matches int | ||||
|  | ||||
| 		for key, val := range c.Exclude { | ||||
| 			if ok, _ := doublestar.Match(val, params[key]); ok { | ||||
| 				matches++ | ||||
| 			} | ||||
| 		} | ||||
| 		if matches == len(c.Exclude) { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	for key, val := range c.Include { | ||||
| 		if ok, _ := doublestar.Match(val, params[key]); !ok { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // UnmarshalYAML unmarshal the constraint map. | ||||
| func (c *Map) UnmarshalYAML(unmarshal func(any) error) error { | ||||
| 	out1 := struct { | ||||
| 		Include map[string]string | ||||
| 		Exclude map[string]string | ||||
| 	}{ | ||||
| 		Include: map[string]string{}, | ||||
| 		Exclude: map[string]string{}, | ||||
| 	} | ||||
|  | ||||
| 	out2 := map[string]string{} | ||||
|  | ||||
| 	_ = unmarshal(&out1) // it contains include and exclude statement | ||||
| 	_ = unmarshal(&out2) // it contains no include/exclude statement, assume include as default | ||||
|  | ||||
| 	c.Include = out1.Include | ||||
| 	c.Exclude = out1.Exclude | ||||
| 	for k, v := range out2 { | ||||
| 		c.Include[k] = v | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // MarshalYAML implements custom Yaml marshaling. | ||||
| func (c Map) MarshalYAML() (any, error) { | ||||
| 	switch { | ||||
| 	case len(c.Include) == 0 && len(c.Exclude) == 0: | ||||
| 		return nil, nil | ||||
| 	case len(c.Exclude) == 0: | ||||
| 		return c.Include, nil | ||||
| 	case len(c.Include) == 0 && len(c.Exclude) != 0: | ||||
| 		return struct { | ||||
| 			Exclude map[string]string | ||||
| 		}{Exclude: c.Exclude}, nil | ||||
| 	default: | ||||
| 		// we can not return type Map as it would lead to infinite recursion :/ | ||||
| 		return struct { | ||||
| 			Include map[string]string `yaml:"include,omitempty"` | ||||
| 			Exclude map[string]string `yaml:"exclude,omitempty"` | ||||
| 		}{ | ||||
| 			Include: c.Include, | ||||
| 			Exclude: c.Exclude, | ||||
| 		}, nil | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										150
									
								
								pipeline/frontend/yaml/constraint/map_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								pipeline/frontend/yaml/constraint/map_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,150 @@ | ||||
| // Copyright 2025 Woodpecker Authors | ||||
| // | ||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| // you may not use this file except in compliance with the License. | ||||
| // You may obtain a copy of the License at | ||||
| // | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| // | ||||
| // Unless required by applicable law or agreed to in writing, software | ||||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| // See the License for the specific language governing permissions and | ||||
| // limitations under the License. | ||||
|  | ||||
| package constraint | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"gopkg.in/yaml.v3" | ||||
| ) | ||||
|  | ||||
| func TestConstraintMap(t *testing.T) { | ||||
| 	testdata := []struct { | ||||
| 		conf string | ||||
| 		with map[string]string | ||||
| 		want bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			conf: "GOLANG: 1.7", | ||||
| 			with: map[string]string{"GOLANG": "1.7"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "GOLANG: tip", | ||||
| 			with: map[string]string{"GOLANG": "1.7"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.1", "MYSQL": "5.6"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ GOLANG: 1.7, REDIS: 3.* }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ GOLANG: 1.7, BRANCH: release/**/test }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "BRANCH": "release/v1.12.1//test"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ GOLANG: 1.7, BRANCH: release/**/test }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "BRANCH": "release/v1.12.1/qest"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		// include syntax | ||||
| 		{ | ||||
| 			conf: "include: { GOLANG: 1.7 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: { GOLANG: tip }", | ||||
| 			with: map[string]string{"GOLANG": "1.7"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: { GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.1", "MYSQL": "5.6"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: { GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		// exclude syntax | ||||
| 		{ | ||||
| 			conf: "exclude: { GOLANG: 1.7 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: { GOLANG: tip }", | ||||
| 			with: map[string]string{"GOLANG": "1.7"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: { GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.1", "MYSQL": "5.6"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: { GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		// exclude AND include values | ||||
| 		{ | ||||
| 			conf: "{ include: { GOLANG: 1.7 }, exclude: { GOLANG: 1.7 } }", | ||||
| 			with: map[string]string{"GOLANG": "1.7"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		// blanks | ||||
| 		{ | ||||
| 			conf: "", | ||||
| 			with: map[string]string{"GOLANG": "1.7", "REDIS": "3.0"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "GOLANG: 1.7", | ||||
| 			with: map[string]string{}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ GOLANG: 1.7, REDIS: 3.0 }", | ||||
| 			with: map[string]string{}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "include: { GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "exclude: { GOLANG: 1.7, REDIS: 3.1 }", | ||||
| 			with: map[string]string{}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range testdata { | ||||
| 		c := parseConstraintMap(t, test.conf) | ||||
| 		assert.Equal(t, test.want, c.Match(test.with), "config: '%s', with: '%s'", test.conf, test.with) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func parseConstraintMap(t *testing.T, s string) *Map { | ||||
| 	c := &Map{} | ||||
| 	assert.NoError(t, yaml.Unmarshal([]byte(s), c)) | ||||
| 	return c | ||||
| } | ||||
							
								
								
									
										139
									
								
								pipeline/frontend/yaml/constraint/path.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								pipeline/frontend/yaml/constraint/path.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| // Copyright 2025 Woodpecker Authors | ||||
| // | ||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| // you may not use this file except in compliance with the License. | ||||
| // You may obtain a copy of the License at | ||||
| // | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| // | ||||
| // Unless required by applicable law or agreed to in writing, software | ||||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| // See the License for the specific language governing permissions and | ||||
| // limitations under the License. | ||||
|  | ||||
| package constraint | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/bmatcuk/doublestar/v4" | ||||
| 	"gopkg.in/yaml.v3" | ||||
|  | ||||
| 	yamlBaseTypes "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types/base" | ||||
| ) | ||||
|  | ||||
| // Path defines a runtime constrain for exclude & include paths. | ||||
| type Path struct { | ||||
| 	Include       []string               `yaml:"include,omitempty"` | ||||
| 	Exclude       []string               `yaml:"exclude,omitempty"` | ||||
| 	IgnoreMessage string                 `yaml:"ignore_message,omitempty"` | ||||
| 	OnEmpty       yamlBaseTypes.BoolTrue `yaml:"on_empty,omitempty"` | ||||
| } | ||||
|  | ||||
| // UnmarshalYAML unmarshal the constraint. | ||||
| func (c *Path) UnmarshalYAML(value *yaml.Node) error { | ||||
| 	out1 := struct { | ||||
| 		Include       yamlBaseTypes.StringOrSlice `yaml:"include"` | ||||
| 		Exclude       yamlBaseTypes.StringOrSlice `yaml:"exclude"` | ||||
| 		IgnoreMessage string                      `yaml:"ignore_message"` | ||||
| 		OnEmpty       yamlBaseTypes.BoolTrue      `yaml:"on_empty"` | ||||
| 	}{} | ||||
|  | ||||
| 	var out2 yamlBaseTypes.StringOrSlice | ||||
|  | ||||
| 	err1 := value.Decode(&out1) | ||||
| 	err2 := value.Decode(&out2) | ||||
|  | ||||
| 	c.Exclude = out1.Exclude | ||||
| 	c.IgnoreMessage = out1.IgnoreMessage | ||||
| 	c.OnEmpty = out1.OnEmpty | ||||
| 	c.Include = append( //nolint:gocritic | ||||
| 		out1.Include, | ||||
| 		out2..., | ||||
| 	) | ||||
|  | ||||
| 	if err1 != nil && err2 != nil { | ||||
| 		y, _ := yaml.Marshal(value) | ||||
| 		return fmt.Errorf("could not parse condition: %s", y) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // MarshalYAML implements custom Yaml marshaling. | ||||
| func (c Path) MarshalYAML() (any, error) { | ||||
| 	// if only Include is set return simple syntax | ||||
| 	if len(c.Exclude) == 0 && | ||||
| 		len(c.IgnoreMessage) == 0 && | ||||
| 		c.OnEmpty.Bool() { | ||||
| 		if len(c.Include) == 0 { | ||||
| 			return nil, nil | ||||
| 		} | ||||
| 		return yamlBaseTypes.StringOrSlice(c.Include), nil | ||||
| 	} | ||||
| 	// we can not return type Path as it would lead to infinite recursion :/ | ||||
| 	return struct { | ||||
| 		Include       yamlBaseTypes.StringOrSlice `yaml:"include,omitempty"` | ||||
| 		Exclude       yamlBaseTypes.StringOrSlice `yaml:"exclude,omitempty"` | ||||
| 		IgnoreMessage string                      `yaml:"ignore_message,omitempty"` | ||||
| 		OnEmpty       yamlBaseTypes.BoolTrue      `yaml:"on_empty,omitempty"` | ||||
| 	}{ | ||||
| 		Include:       c.Include, | ||||
| 		Exclude:       c.Exclude, | ||||
| 		IgnoreMessage: c.IgnoreMessage, | ||||
| 		OnEmpty:       c.OnEmpty, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // Match returns true if file paths in string slice matches the include and not exclude patterns | ||||
| // or if commit message contains ignore message. | ||||
| func (c *Path) Match(v []string, message string) bool { | ||||
| 	// ignore file pattern matches if the commit message contains a pattern | ||||
| 	if len(c.IgnoreMessage) > 0 && strings.Contains(strings.ToLower(message), strings.ToLower(c.IgnoreMessage)) { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	// return value based on 'on_empty', if there are no commit files (empty commit) | ||||
| 	if len(v) == 0 { | ||||
| 		return c.OnEmpty.Bool() | ||||
| 	} | ||||
|  | ||||
| 	if len(c.Exclude) > 0 && c.Excludes(v) { | ||||
| 		return false | ||||
| 	} | ||||
| 	if len(c.Include) > 0 && !c.Includes(v) { | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // Includes returns true if the string matches any of the include patterns. | ||||
| func (c *Path) Includes(v []string) bool { | ||||
| 	for _, pattern := range c.Include { | ||||
| 		for _, file := range v { | ||||
| 			if ok, _ := doublestar.Match(pattern, file); ok { | ||||
| 				return true | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // Excludes returns true if all of the strings match any of the exclude patterns. | ||||
| func (c *Path) Excludes(v []string) bool { | ||||
| 	for _, file := range v { | ||||
| 		matched := false | ||||
| 		for _, pattern := range c.Exclude { | ||||
| 			if ok, _ := doublestar.Match(pattern, file); ok { | ||||
| 				matched = true | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		if !matched { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
							
								
								
									
										158
									
								
								pipeline/frontend/yaml/constraint/path_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								pipeline/frontend/yaml/constraint/path_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | ||||
| // Copyright 2025 Woodpecker Authors | ||||
| // | ||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| // you may not use this file except in compliance with the License. | ||||
| // You may obtain a copy of the License at | ||||
| // | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| // | ||||
| // Unless required by applicable law or agreed to in writing, software | ||||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| // See the License for the specific language governing permissions and | ||||
| // limitations under the License. | ||||
|  | ||||
| package constraint | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"gopkg.in/yaml.v3" | ||||
| ) | ||||
|  | ||||
| func TestConstraintPath(t *testing.T) { | ||||
| 	testdata := []struct { | ||||
| 		conf    string | ||||
| 		with    []string | ||||
| 		message string | ||||
| 		want    bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			conf: "", | ||||
| 			with: []string{"CHANGELOG.md", "README.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "CHANGELOG.md", | ||||
| 			with: []string{"CHANGELOG.md", "README.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "'*.md'", | ||||
| 			with: []string{"CHANGELOG.md", "README.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "['*.md']", | ||||
| 			with: []string{"CHANGELOG.md", "README.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "'docs/*'", | ||||
| 			with: []string{"docs/README.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "'docs/*'", | ||||
| 			with: []string{"docs/sub/README.md"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "'docs/**'", | ||||
| 			with: []string{"docs/README.md", "docs/sub/README.md", "docs/sub-sub/README.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "'docs/**'", | ||||
| 			with: []string{"README.md"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ include: [ README.md ] }", | ||||
| 			with: []string{"CHANGELOG.md"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ exclude: [ README.md ] }", | ||||
| 			with: []string{"design.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		// include and exclude blocks | ||||
| 		{ | ||||
| 			conf: "{ include: [ '*.md', '*.ini' ], exclude: [ CHANGELOG.md ] }", | ||||
| 			with: []string{"README.md"}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ include: [ '*.md' ], exclude: [ CHANGELOG.md ] }", | ||||
| 			with: []string{"CHANGELOG.md"}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			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]' }", | ||||
| 			with:    []string{"CHANGELOG.md"}, | ||||
| 			message: "Build them [ALL]", | ||||
| 			want:    true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf:    "{ exclude: [ '*.php' ], ignore_message: '[ALL]' }", | ||||
| 			with:    []string{"myfile.php"}, | ||||
| 			message: "Build them [ALL]", | ||||
| 			want:    true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf:    "{ ignore_message: '[ALL]' }", | ||||
| 			with:    []string{}, | ||||
| 			message: "Build them [ALL]", | ||||
| 			want:    true, | ||||
| 		}, | ||||
| 		// empty commit | ||||
| 		{ | ||||
| 			conf: "{ include: [ README.md ] }", | ||||
| 			with: []string{}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ include: [ README.md ], on_empty: false }", | ||||
| 			with: []string{}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			conf: "{ include: [ README.md ], on_empty: true }", | ||||
| 			with: []string{}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range testdata { | ||||
| 		c := parseConstraintPath(t, test.conf) | ||||
| 		assert.Equal(t, test.want, c.Match(test.with, test.message)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func parseConstraintPath(t *testing.T, s string) *Path { | ||||
| 	c := &Path{} | ||||
| 	assert.NoError(t, yaml.Unmarshal([]byte(s), c)) | ||||
| 	return c | ||||
| } | ||||
| @@ -18,6 +18,8 @@ import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 	"gopkg.in/yaml.v3" | ||||
|  | ||||
| 	"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata" | ||||
| 	yaml_base_types "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types/base" | ||||
| @@ -69,7 +71,7 @@ func TestParse(t *testing.T) { | ||||
| 		assert.Empty(t, out.Steps.ContainerList[0].When.Constraints) | ||||
| 		assert.Equal(t, "notify_success", out.Steps.ContainerList[1].Name) | ||||
| 		assert.Equal(t, "plugins/slack", out.Steps.ContainerList[1].Image) | ||||
| 		assert.Equal(t, yaml_base_types.StringOrSlice{"success"}, out.Steps.ContainerList[1].When.Constraints[0].Event) | ||||
| 		assert.Equal(t, yaml_base_types.StringOrSlice{"push"}, out.Steps.ContainerList[1].When.Constraints[0].Event) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| @@ -170,9 +172,6 @@ when: | ||||
|     - tester2 | ||||
|   - branch: | ||||
|     - tester | ||||
| build: | ||||
|   context: . | ||||
|   dockerfile: Dockerfile | ||||
| workspace: | ||||
|   path: src/github.com/octocat/hello-world | ||||
|   base: /go | ||||
| @@ -189,6 +188,7 @@ steps: | ||||
|       - go build | ||||
|     when: | ||||
|       event: push | ||||
|     depends_on: [] | ||||
|   notify: | ||||
|     image: slack | ||||
|     channel: dev | ||||
| @@ -217,38 +217,86 @@ steps: | ||||
| ` | ||||
|  | ||||
| var sampleVarYaml = ` | ||||
| _slack: &SLACK | ||||
| variables: &SLACK | ||||
|   image: plugins/slack | ||||
| steps: | ||||
|   notify_fail: *SLACK | ||||
|   notify_success: | ||||
|     << : *SLACK | ||||
|     when: | ||||
|       event: success | ||||
|       event: push | ||||
|   echo: | ||||
|     when: | ||||
|     - path: wow.sh | ||||
|       repo: "test" | ||||
|       branch: | ||||
|         exclude: main | ||||
|     - path: | ||||
|       - test.yaml | ||||
|       - test.zig | ||||
|     - path: | ||||
|         exclude: a | ||||
|         on_empty: true | ||||
|     - ref: ref/tags/v1 | ||||
|       path: | ||||
|   env: | ||||
|     image: print | ||||
|     environment: | ||||
|       DRIVER: next | ||||
|       PLATFORM: linux | ||||
| ` | ||||
|  | ||||
| var sampleSliceYaml = ` | ||||
| steps: | ||||
|   nil_slice: | ||||
|     image: plugins/slack | ||||
|   empty_slice: | ||||
|     image: plugins/slack | ||||
|     depends_on: [] | ||||
| ` | ||||
| func TestReSerialize(t *testing.T) { | ||||
| 	work1, err := ParseString(sampleVarYaml) | ||||
| 	if !assert.NoError(t, err) { | ||||
| 		t.Fail() | ||||
| 	} | ||||
|  | ||||
| 	workBin, err := yaml.Marshal(work1) | ||||
| 	if !assert.NoError(t, err) { | ||||
| 		t.Fail() | ||||
| 	} | ||||
|  | ||||
| 	assert.EqualValues(t, `steps: | ||||
|     - name: notify_fail | ||||
|       image: plugins/slack | ||||
|     - name: notify_success | ||||
|       image: plugins/slack | ||||
|       when: | ||||
|         event: push | ||||
|     - name: echo | ||||
|       when: | ||||
|         - repo: test | ||||
|           branch: | ||||
|             exclude: main | ||||
|           path: wow.sh | ||||
|         - path: | ||||
|             - test.yaml | ||||
|             - test.zig | ||||
|         - path: | ||||
|             exclude: a | ||||
|         - ref: ref/tags/v1 | ||||
|     - name: env | ||||
|       image: print | ||||
|       environment: | ||||
|         DRIVER: next | ||||
|         PLATFORM: linux | ||||
| skip_clone: false | ||||
| `, string(workBin)) | ||||
| } | ||||
|  | ||||
| func TestSlice(t *testing.T) { | ||||
| 	t.Run("should marshal a not set slice to nil", func(t *testing.T) { | ||||
| 		out, err := ParseString(sampleSliceYaml) | ||||
| 		assert.NoError(t, err) | ||||
| 	out, err := ParseString(sampleYaml) | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	t.Run("should marshal a not set slice to nil", func(t *testing.T) { | ||||
| 		assert.Equal(t, "test", out.Steps.ContainerList[0].Name) | ||||
| 		assert.Nil(t, out.Steps.ContainerList[0].DependsOn) | ||||
| 		assert.Empty(t, out.Steps.ContainerList[0].DependsOn) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("should marshal an empty slice", func(t *testing.T) { | ||||
| 		out, err := ParseString(sampleSliceYaml) | ||||
| 		assert.NoError(t, err) | ||||
|  | ||||
| 		assert.Equal(t, "build", out.Steps.ContainerList[1].Name) | ||||
| 		assert.NotNil(t, out.Steps.ContainerList[1].DependsOn) | ||||
| 		assert.Empty(t, (out.Steps.ContainerList[1].DependsOn)) | ||||
| 	}) | ||||
|   | ||||
| @@ -42,7 +42,16 @@ func (b *BoolTrue) UnmarshalYAML(value *yaml.Node) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // MarshalYAML implements custom Yaml marshaling. | ||||
| func (b BoolTrue) MarshalYAML() (any, error) { | ||||
| 	return b.Bool(), nil | ||||
| } | ||||
|  | ||||
| // Bool returns the bool value. | ||||
| func (b BoolTrue) Bool() bool { | ||||
| 	return !b.value | ||||
| } | ||||
|  | ||||
| func ToBoolTrue(v bool) BoolTrue { | ||||
| 	return BoolTrue{value: !v} | ||||
| } | ||||
|   | ||||
| @@ -52,4 +52,27 @@ func TestBoolTrue(t *testing.T) { | ||||
| 		err := yaml.Unmarshal(in, &out) | ||||
| 		assert.Error(t, err) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("marshal", func(t *testing.T) { | ||||
| 		t.Run("marshal empty", func(t *testing.T) { | ||||
| 			in := &BoolTrue{} | ||||
| 			out, err := yaml.Marshal(&in) | ||||
| 			assert.NoError(t, err) | ||||
| 			assert.EqualValues(t, "true\n", string(out)) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("marshal true", func(t *testing.T) { | ||||
| 			in := ToBoolTrue(true) | ||||
| 			out, err := yaml.Marshal(&in) | ||||
| 			assert.NoError(t, err) | ||||
| 			assert.EqualValues(t, "true\n", string(out)) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("marshal false", func(t *testing.T) { | ||||
| 			in := ToBoolTrue(false) | ||||
| 			out, err := yaml.Marshal(&in) | ||||
| 			assert.NoError(t, err) | ||||
| 			assert.EqualValues(t, "false\n", string(out)) | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -1,40 +0,0 @@ | ||||
| // Copyright 2024 Woodpecker Authors | ||||
| // | ||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| // you may not use this file except in compliance with the License. | ||||
| // You may obtain a copy of the License at | ||||
| // | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| // | ||||
| // Unless required by applicable law or agreed to in writing, software | ||||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| // See the License for the specific language governing permissions and | ||||
| // limitations under the License. | ||||
|  | ||||
| // TODO: delete file after v3.0.0 release | ||||
|  | ||||
| package base | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| ) | ||||
|  | ||||
| type EnvironmentMap map[string]any | ||||
|  | ||||
| // UnmarshalYAML implements the Unmarshaler interface. | ||||
| func (s *EnvironmentMap) UnmarshalYAML(unmarshal func(any) error) error { | ||||
| 	var mapType map[string]any | ||||
| 	err := unmarshal(&mapType) | ||||
| 	if err == nil { | ||||
| 		*s = mapType | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	var sliceType []any | ||||
| 	if err := unmarshal(&sliceType); err == nil { | ||||
| 		return fmt.Errorf("list syntax for 'environment' has been removed, use map syntax instead (https://woodpecker-ci.org/docs/usage/environment)") | ||||
| 	} | ||||
|  | ||||
| 	return err | ||||
| } | ||||
| @@ -1,51 +0,0 @@ | ||||
| // Copyright 2024 Woodpecker Authors | ||||
| // | ||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| // you may not use this file except in compliance with the License. | ||||
| // You may obtain a copy of the License at | ||||
| // | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| // | ||||
| // Unless required by applicable law or agreed to in writing, software | ||||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| // See the License for the specific language governing permissions and | ||||
| // limitations under the License. | ||||
|  | ||||
| // TODO: delete file after v3.0.0 release | ||||
|  | ||||
| package base | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"gopkg.in/yaml.v3" | ||||
| ) | ||||
|  | ||||
| type StructMap struct { | ||||
| 	Foos EnvironmentMap `yaml:"foos,omitempty"` | ||||
| } | ||||
|  | ||||
| func TestEnvironmentMapYaml(t *testing.T) { | ||||
| 	str := `{foos: [bar=baz, far=faz]}` | ||||
| 	s := StructMap{} | ||||
| 	err := yaml.Unmarshal([]byte(str), &s) | ||||
| 	if assert.Error(t, err) { | ||||
| 		assert.EqualValues(t, "list syntax for 'environment' has been removed, use map syntax instead (https://woodpecker-ci.org/docs/usage/environment)", err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	s.Foos = EnvironmentMap{"bar": "baz", "far": "faz"} | ||||
| 	d, err := yaml.Marshal(&s) | ||||
| 	assert.NoError(t, err) | ||||
| 	str = `foos: | ||||
|     bar: baz | ||||
|     far: faz | ||||
| ` | ||||
| 	assert.EqualValues(t, str, string(d)) | ||||
|  | ||||
| 	s2 := StructMap{} | ||||
| 	assert.NoError(t, yaml.Unmarshal(d, &s2)) | ||||
|  | ||||
| 	assert.Equal(t, EnvironmentMap{"bar": "baz", "far": "faz"}, s2.Foos) | ||||
| } | ||||
| @@ -44,6 +44,16 @@ func (s *StringOrSlice) UnmarshalYAML(unmarshal func(any) error) error { | ||||
| 	return errors.New("failed to unmarshal StringOrSlice") | ||||
| } | ||||
|  | ||||
| // MarshalYAML implements custom Yaml marshaling. | ||||
| func (s StringOrSlice) MarshalYAML() (any, error) { | ||||
| 	if len(s) == 0 { | ||||
| 		return nil, nil | ||||
| 	} else if len(s) == 1 { | ||||
| 		return s[0], nil | ||||
| 	} | ||||
| 	return []string(s), nil | ||||
| } | ||||
|  | ||||
| func toStrings(s []any) ([]string, error) { | ||||
| 	if s == nil { | ||||
| 		return nil, nil | ||||
|   | ||||
| @@ -22,10 +22,49 @@ import ( | ||||
| ) | ||||
|  | ||||
| type StructStringOrSlice struct { | ||||
| 	Foo StringOrSlice | ||||
| 	Foo StringOrSlice `yaml:"foo"` | ||||
| 	Bar StringOrSlice `yaml:"bar,omitempty"` | ||||
| } | ||||
|  | ||||
| func TestStringOrSliceYaml(t *testing.T) { | ||||
| 	t.Run("unmarshal", func(t *testing.T) { | ||||
| 		str := `{foo: [bar, baz]}` | ||||
|  | ||||
| 		s := StructStringOrSlice{} | ||||
| 		assert.NoError(t, yaml.Unmarshal([]byte(str), &s)) | ||||
|  | ||||
| 		assert.Equal(t, StringOrSlice{"bar", "baz"}, s.Foo) | ||||
|  | ||||
| 		d, err := yaml.Marshal(&s) | ||||
| 		assert.Nil(t, err) | ||||
|  | ||||
| 		s2 := StructStringOrSlice{} | ||||
| 		assert.NoError(t, yaml.Unmarshal(d, &s2)) | ||||
|  | ||||
| 		assert.Equal(t, StringOrSlice{"bar", "baz"}, s2.Foo) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("marshal", func(t *testing.T) { | ||||
| 		str := StructStringOrSlice{} | ||||
| 		out, err := yaml.Marshal(str) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.EqualValues(t, "foo: null\n", string(out)) | ||||
|  | ||||
| 		str = StructStringOrSlice{Foo: []string{"a\""}} | ||||
| 		out, err = yaml.Marshal(str) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.EqualValues(t, "foo: a\"\n", string(out)) | ||||
|  | ||||
| 		str = StructStringOrSlice{Foo: []string{"a", "b", "c"}} | ||||
| 		out, err = yaml.Marshal(str) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.EqualValues(t, `foo: | ||||
|     - a | ||||
|     - b | ||||
|     - c | ||||
| `, string(out)) | ||||
| 	}) | ||||
|  | ||||
| 	str := `{foo: [bar, "baz"]}` | ||||
| 	s := StructStringOrSlice{} | ||||
| 	assert.NoError(t, yaml.Unmarshal([]byte(str), &s)) | ||||
|   | ||||
| @@ -33,13 +33,14 @@ type ( | ||||
| 	// Container defines a container. | ||||
| 	Container struct { | ||||
| 		// common | ||||
| 		Name       string             `yaml:"name,omitempty"` | ||||
| 		Image      string             `yaml:"image,omitempty"` | ||||
| 		Pull       bool               `yaml:"pull,omitempty"` | ||||
| 		Commands   base.StringOrSlice `yaml:"commands,omitempty"` | ||||
| 		Entrypoint base.StringOrSlice `yaml:"entrypoint,omitempty"` | ||||
| 		Directory  string             `yaml:"directory,omitempty"` | ||||
| 		Settings   map[string]any     `yaml:"settings"` | ||||
| 		Name        string             `yaml:"name,omitempty"` | ||||
| 		Image       string             `yaml:"image,omitempty"` | ||||
| 		Pull        bool               `yaml:"pull,omitempty"` | ||||
| 		Commands    base.StringOrSlice `yaml:"commands,omitempty"` | ||||
| 		Entrypoint  base.StringOrSlice `yaml:"entrypoint,omitempty"` | ||||
| 		Directory   string             `yaml:"directory,omitempty"` | ||||
| 		Settings    map[string]any     `yaml:"settings,omitempty"` | ||||
| 		Environment map[string]any     `yaml:"environment,omitempty"` | ||||
| 		// flow control | ||||
| 		DependsOn base.StringOrSlice `yaml:"depends_on,omitempty"` | ||||
| 		When      constraint.When    `yaml:"when,omitempty"` | ||||
| @@ -56,9 +57,6 @@ type ( | ||||
|  | ||||
| 		// ACTIVE DEVELOPMENT BELOW | ||||
|  | ||||
| 		// TODO: remove base.EnvironmentMap and use map[string]any after v3.0.0 release | ||||
| 		Environment base.EnvironmentMap `yaml:"environment,omitempty"` | ||||
|  | ||||
| 		// Remove after v3.1.0 | ||||
| 		Secrets []any `yaml:"secrets,omitempty"` | ||||
|  | ||||
| @@ -121,6 +119,11 @@ func (c *ContainerList) UnmarshalYAML(value *yaml.Node) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // MarshalYAML implements custom Yaml marshaling. | ||||
| func (c ContainerList) MarshalYAML() (any, error) { | ||||
| 	return c.ContainerList, nil | ||||
| } | ||||
|  | ||||
| func (c *Container) IsPlugin() bool { | ||||
| 	return len(c.Commands) == 0 && | ||||
| 		len(c.Entrypoint) == 0 && | ||||
|   | ||||
| @@ -22,7 +22,7 @@ import ( | ||||
| 	_ "github.com/lib/pq" | ||||
| ) | ||||
|  | ||||
| // Supported database drivers | ||||
| // Supported database drivers. | ||||
| const ( | ||||
| 	DriverMysql    = "mysql" | ||||
| 	DriverPostgres = "postgres" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user