diff --git a/docs/docs/20-usage/20-pipeline-syntax.md b/docs/docs/20-usage/20-pipeline-syntax.md index a7a64de67..64a22ca8a 100644 --- a/docs/docs/20-usage/20-pipeline-syntax.md +++ b/docs/docs/20-usage/20-pipeline-syntax.md @@ -264,7 +264,20 @@ For more details check the [secrets docs](/docs/usage/secrets/). ### `when` - Conditional Execution -Woodpecker supports defining conditions for pipeline step by a `when` block. If all conditions in the `when` block evaluate to true the step is executed, otherwise it is skipped. +Woodpecker supports defining a list of conditions for a pipeline step by using a `when` block. If at least one of the conditions in the `when` block evaluate to true the step is executed, otherwise it is skipped. A condition can be a check like: + +```diff + pipeline: + slack: + image: plugins/slack + settings: + channel: dev ++ when: ++ - event: pull_request ++ repo: test/test ++ - event: push ++ branch: main +``` #### `repo` @@ -277,7 +290,7 @@ Example conditional execution by repository: settings: channel: dev + when: -+ repo: test/test ++ - repo: test/test ``` #### `branch` @@ -295,7 +308,7 @@ pipeline: settings: channel: dev + when: -+ branch: master ++ - branch: master ``` > The step now triggers on master, but also if the target branch of a pull request is `master`. Add an event condition to limit it further to pushes on master only. @@ -304,23 +317,23 @@ Execute a step if the branch is `master` or `develop`: ```diff when: - branch: [master, develop] + - branch: [master, develop] ``` Execute a step if the branch starts with `prefix/*`: ```diff when: - branch: prefix/* + - branch: prefix/* ``` Execute a step using custom include and exclude logic: ```diff when: - branch: - include: [ master, release/* ] - exclude: [ release/1.0.0, release/1.1.* ] + - branch: + include: [ master, release/* ] + exclude: [ release/1.0.0, release/1.1.* ] ``` #### `event` @@ -329,29 +342,29 @@ Execute a step if the build event is a `tag`: ```diff when: - event: tag + - event: tag ``` Execute a step if the pipeline event is a `push` to a specified branch: ```diff when: - event: push -+ branch: main + - event: push ++ branch: main ``` Execute a step for all non-pull request events: ```diff when: - event: [push, tag, deployment] + - event: [push, tag, deployment] ``` Execute a step for all build events: ```diff when: - event: [push, pull_request, tag, deployment] + - event: [push, pull_request, tag, deployment] ``` #### `tag` @@ -361,8 +374,8 @@ Use glob expression to execute a step if the tag name starts with `v`: ```diff when: - event: tag - tag: v* + - event: tag + tag: v* ``` #### `status` @@ -376,7 +389,7 @@ pipeline: settings: channel: dev + when: -+ status: [ success, failure ] ++ - status: [ success, failure ] ``` #### `platform` @@ -389,14 +402,14 @@ Execute a step for a specific platform: ```diff when: - platform: linux/amd64 + - platform: linux/amd64 ``` Execute a step for a specific platform using wildcards: ```diff when: - platform: [ linux/*, windows/amd64 ] + - platform: [ linux/*, windows/amd64 ] ``` #### `environment` @@ -405,8 +418,8 @@ Execute a step for deployment events matching the target deployment environment: ```diff when: - environment: production - event: deployment + - environment: production + - event: deployment ``` #### `matrix` @@ -415,9 +428,9 @@ Execute a step for a single matrix permutation: ```diff when: - matrix: - GO_VERSION: 1.5 - REDIS_VERSION: 2.8 + - matrix: + GO_VERSION: 1.5 + REDIS_VERSION: 2.8 ``` #### `instance` @@ -426,7 +439,7 @@ Execute a step only on a certain Woodpecker instance matching the specified host ```diff when: - instance: stage.woodpecker.company.com + - instance: stage.woodpecker.company.com ``` #### `path` @@ -441,17 +454,17 @@ Execute a step only on a pipeline with certain files being changed: ```diff when: - path: "src/*" + - path: "src/*" ``` You can use [glob patterns](https://github.com/bmatcuk/doublestar#patterns) to match the changed files and specify if the step should run if a file matching that pattern has been changed `include` or if some files have **not** been changed `exclude`. ```diff when: - path: - include: [ '.woodpecker/*.yml', '*.ini' ] - exclude: [ '*.md', 'docs/**' ] - ignore_message: "[ALL]" + - path: + include: [ '.woodpecker/*.yml', '*.ini' ] + exclude: [ '*.md', 'docs/**' ] + ignore_message: "[ALL]" ``` **Hint:** Passing a defined ignore-message like `[ALL]` inside the commit message will ignore all path conditions. diff --git a/pipeline/frontend/yaml/compiler/compiler.go b/pipeline/frontend/yaml/compiler/compiler.go index d317d74dd..a76be917a 100644 --- a/pipeline/frontend/yaml/compiler/compiler.go +++ b/pipeline/frontend/yaml/compiler/compiler.go @@ -145,7 +145,7 @@ func (c *Compiler) Compile(conf *yaml.Config) *backend.Config { config.Stages = append(config.Stages, stage) } else if !c.local && !conf.SkipClone { for i, container := range conf.Clone.Containers { - if !container.Constraints.Match(c.metadata) { + if !container.When.Match(c.metadata) { continue } stage := new(backend.Stage) @@ -172,7 +172,7 @@ func (c *Compiler) Compile(conf *yaml.Config) *backend.Config { stage.Alias = nameServices for i, container := range conf.Services.Containers { - if !container.Constraints.Match(c.metadata) { + if !container.When.Match(c.metadata) { continue } @@ -188,11 +188,11 @@ func (c *Compiler) Compile(conf *yaml.Config) *backend.Config { var group string for i, container := range conf.Pipeline.Containers { // Skip if local and should not run local - if c.local && !container.Constraints.Local.Bool() { + if c.local && !container.When.IsLocal() { continue } - if !container.Constraints.Match(c.metadata) { + if !container.When.Match(c.metadata) { continue } diff --git a/pipeline/frontend/yaml/compiler/convert.go b/pipeline/frontend/yaml/compiler/convert.go index 3e1fb6c5b..ec426b10f 100644 --- a/pipeline/frontend/yaml/compiler/convert.go +++ b/pipeline/frontend/yaml/compiler/convert.go @@ -145,6 +145,12 @@ func (c *Compiler) createProcess(name string, container *yaml.Container, section cpuSet = c.reslimit.CPUSet } + // all constraints must exclude success. + onSuccess := container.When.IsEmpty() || + !container.When.ExcludesStatus("success") + // at least one constraint must include the status failure. + onFailure := container.When.IncludesStatus("failure") + return &backend.Step{ Name: name, Alias: container.Name, @@ -172,11 +178,9 @@ func (c *Compiler) createProcess(name string, container *yaml.Container, section CPUShares: cpuShares, CPUSet: cpuSet, AuthConfig: authConfig, - OnSuccess: container.Constraints.Status.Match("success"), - OnFailure: (len(container.Constraints.Status.Include)+ - len(container.Constraints.Status.Exclude) != 0) && - container.Constraints.Status.Match("failure"), - NetworkMode: networkMode, - IpcMode: ipcMode, + OnSuccess: onSuccess, + OnFailure: onFailure, + NetworkMode: networkMode, + IpcMode: ipcMode, } } diff --git a/pipeline/frontend/yaml/config_test.go b/pipeline/frontend/yaml/config_test.go index 92888e3f9..14deae65e 100644 --- a/pipeline/frontend/yaml/config_test.go +++ b/pipeline/frontend/yaml/config_test.go @@ -61,10 +61,10 @@ func TestParse(t *testing.T) { } g.Assert(out.Pipeline.Containers[0].Name).Equal("notify_fail") g.Assert(out.Pipeline.Containers[0].Image).Equal("plugins/slack") - g.Assert(len(out.Pipeline.Containers[0].Constraints.Event.Include)).Equal(0) + g.Assert(len(out.Pipeline.Containers[0].When.Constraints)).Equal(0) g.Assert(out.Pipeline.Containers[1].Name).Equal("notify_success") g.Assert(out.Pipeline.Containers[1].Image).Equal("plugins/slack") - g.Assert(out.Pipeline.Containers[1].Constraints.Event.Include).Equal([]string{"success"}) + g.Assert(out.Pipeline.Containers[1].When.Constraints[0].Event.Include).Equal([]string{"success"}) }) }) }) diff --git a/pipeline/frontend/yaml/constraint/constraint.go b/pipeline/frontend/yaml/constraint/constraint.go index 22c4d284f..b363112f3 100644 --- a/pipeline/frontend/yaml/constraint/constraint.go +++ b/pipeline/frontend/yaml/constraint/constraint.go @@ -12,8 +12,13 @@ import ( ) type ( - // Constraints defines a set of runtime constraints. - Constraints struct { + // When defines a set of runtime constraints. + When struct { + // If true then read from a list of constraint + Constraints []Constraint + } + + Constraint struct { Ref List Repo List Instance List @@ -47,9 +52,82 @@ type ( } ) +func (when *When) IsEmpty() bool { + return len(when.Constraints) == 0 +} + +// Returns true if at least one of the internal constraints is true. +func (when *When) Match(metadata frontend.Metadata) bool { + for _, c := range when.Constraints { + if c.Match(metadata) { + return true + } + } + return when.IsEmpty() +} + +func (when *When) IncludesStatus(status string) bool { + for _, c := range when.Constraints { + if c.Status.Includes(status) { + return true + } + } + + return false +} + +func (when *When) ExcludesStatus(status string) bool { + for _, c := range when.Constraints { + if !c.Status.Excludes(status) { + return false + } + } + + return len(when.Constraints) > 0 +} + +// False if (any) non local +func (when *When) IsLocal() bool { + for _, c := range when.Constraints { + if !c.Local.Bool() { + return false + } + } + return true +} + +func (when *When) UnmarshalYAML(value *yaml.Node) error { + unmarshelAsList := func() error { + lst := []Constraint{} + err := value.Decode(&lst) + if err != nil { + return err + } + when.Constraints = lst + return nil + } + + unmarshelAsDict := func() error { + c := Constraint{} + err := value.Decode(&c) + if err != nil { + return err + } + when.Constraints = append(when.Constraints, c) + return nil + } + + err := unmarshelAsList() + if err != nil { + err = unmarshelAsDict() + } + + return err +} + // Match returns true if all constraints match the given input. If a single // constraint fails a false value is returned. -func (c *Constraints) Match(metadata frontend.Metadata) bool { +func (c *Constraint) Match(metadata frontend.Metadata) bool { match := c.Platform.Match(metadata.Sys.Platform) && c.Environment.Match(metadata.Curr.Target) && c.Event.Match(metadata.Curr.Event) && @@ -58,10 +136,11 @@ func (c *Constraints) Match(metadata frontend.Metadata) bool { c.Instance.Match(metadata.Sys.Host) && c.Matrix.Match(metadata.Job.Matrix) - // changed files filter do only apply for pull-request and push events + // changed files filter apply only for pull-request and push events if metadata.Curr.Event == frontend.EventPull || metadata.Curr.Event == frontend.EventPush { match = match && c.Path.Match(metadata.Curr.Commit.ChangedFiles, metadata.Curr.Commit.Message) } + if metadata.Curr.Event != frontend.EventTag { match = match && c.Branch.Match(metadata.Curr.Commit.Branch) } @@ -137,6 +216,7 @@ func (c *Map) Match(params map[string]string) bool { 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 { @@ -211,12 +291,13 @@ func (c *Path) UnmarshalYAML(value *yaml.Node) error { } // Match returns true if file paths in string slice matches the include and not exclude patterns -// or if commit message contains ignore message. +// 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 } + // always match if there are no commit files (empty commit) if len(v) == 0 { return true diff --git a/pipeline/frontend/yaml/constraint/constraint_test.go b/pipeline/frontend/yaml/constraint/constraint_test.go index 6a2d3ff3b..94c364b18 100644 --- a/pipeline/frontend/yaml/constraint/constraint_test.go +++ b/pipeline/frontend/yaml/constraint/constraint_test.go @@ -469,8 +469,8 @@ func TestConstraints(t *testing.T) { } } -func parseConstraints(t *testing.T, s string) *Constraints { - c := &Constraints{} +func parseConstraints(t *testing.T, s string) *When { + c := &When{} assert.NoError(t, yaml.Unmarshal([]byte(s), c)) return c } diff --git a/pipeline/frontend/yaml/container.go b/pipeline/frontend/yaml/container.go index 5e5287f69..2e19537f6 100644 --- a/pipeline/frontend/yaml/container.go +++ b/pipeline/frontend/yaml/container.go @@ -58,7 +58,7 @@ type ( Volumes types.Volumes `yaml:"volumes,omitempty"` Secrets Secrets `yaml:"secrets,omitempty"` Sysctls types.SliceorMap `yaml:"sysctls,omitempty"` - Constraints constraint.Constraints `yaml:"when,omitempty"` + When constraint.When `yaml:"when,omitempty"` Settings map[string]interface{} `yaml:"settings"` } ) diff --git a/pipeline/frontend/yaml/container_test.go b/pipeline/frontend/yaml/container_test.go index 0ee54dd1e..85fe82cc1 100644 --- a/pipeline/frontend/yaml/container_test.go +++ b/pipeline/frontend/yaml/container_test.go @@ -109,9 +109,13 @@ func TestUnmarshalContainer(t *testing.T) { {Source: "/etc/configs", Destination: "/etc/configs/", AccessMode: "ro"}, }, }, - Constraints: constraint.Constraints{ - Branch: constraint.List{ - Include: []string{"master"}, + When: constraint.When{ + Constraints: []constraint.Constraint{ + { + Branch: constraint.List{ + Include: []string{"master"}, + }, + }, }, }, Settings: map[string]interface{}{ @@ -185,9 +189,13 @@ func TestUnmarshalContainers(t *testing.T) { "tag": stringsToInterface("next", "latest"), "dry_run": true, }, - Constraints: constraint.Constraints{ - Event: constraint.List{Include: []string{"push"}}, - Branch: constraint.List{Include: []string{"${CI_REPO_DEFAULT_BRANCH}"}}, + When: constraint.When{ + Constraints: []constraint.Constraint{ + { + Event: constraint.List{Include: []string{"push"}}, + Branch: constraint.List{Include: []string{"${CI_REPO_DEFAULT_BRANCH}"}}, + }, + }, }, }, }, @@ -213,9 +221,38 @@ func TestUnmarshalContainers(t *testing.T) { "dockerfile": "docker/Dockerfile.cli", "tag": stringsToInterface("next"), }, - Constraints: constraint.Constraints{ - Event: constraint.List{Include: []string{"push"}}, - Branch: constraint.List{Include: []string{"${CI_REPO_DEFAULT_BRANCH}"}}, + When: constraint.When{ + Constraints: []constraint.Constraint{ + { + Event: constraint.List{Include: []string{"push"}}, + Branch: constraint.List{Include: []string{"${CI_REPO_DEFAULT_BRANCH}"}}, + }, + }, + }, + }, + }, + }, + { + from: `publish-cli: + image: print/env + when: + - branch: ${CI_REPO_DEFAULT_BRANCH} + event: push + - event: pull_request`, + want: []*Container{ + { + Name: "publish-cli", + Image: "print/env", + When: constraint.When{ + Constraints: []constraint.Constraint{ + { + Event: constraint.List{Include: []string{"push"}}, + Branch: constraint.List{Include: []string{"${CI_REPO_DEFAULT_BRANCH}"}}, + }, + { + Event: constraint.List{Include: []string{"pull_request"}}, + }, + }, }, }, }, diff --git a/pipeline/schema/.woodpecker/test-when.yml b/pipeline/schema/.woodpecker/test-when.yml index c3b95859e..83f1a87ac 100644 --- a/pipeline/schema/.woodpecker/test-when.yml +++ b/pipeline/schema/.woodpecker/test-when.yml @@ -111,3 +111,13 @@ pipeline: - echo "test" when: repo: test/test + + when-multi: + image: alpine + commands: + - echo "test" + when: + - event: pull_request + repo: test/test + - event: push + branch: main diff --git a/pipeline/schema/schema.json b/pipeline/schema/schema.json index 6b3a509a5..506864769 100644 --- a/pipeline/schema/schema.json +++ b/pipeline/schema/schema.json @@ -140,6 +140,18 @@ }, "step_when": { "description": "Steps can be skipped based on conditions. Read more: https://woodpecker-ci.org/docs/usage/pipeline-syntax#when---conditional-execution", + "oneOf": [ + { + "type": "array", + "minLength": 1, + "items": { "$ref": "#/definitions/step_when_condition" } + }, + { + "$ref": "#/definitions/step_when_condition" + } + ] + }, + "step_when_condition": { "type": "object", "additionalProperties": false, "properties": {