Allow multiple when conditions (#1087)

Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: LamaAni <zshotan@bloomberg.net>
This commit is contained in:
Anbraten 2022-08-14 19:32:49 +02:00 committed by GitHub
parent 98e6396e3e
commit e269890643
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 215 additions and 58 deletions

View file

@ -264,7 +264,20 @@ For more details check the [secrets docs](/docs/usage/secrets/).
### `when` - Conditional Execution ### `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` #### `repo`
@ -277,7 +290,7 @@ Example conditional execution by repository:
settings: settings:
channel: dev channel: dev
+ when: + when:
+ repo: test/test + - repo: test/test
``` ```
#### `branch` #### `branch`
@ -295,7 +308,7 @@ pipeline:
settings: settings:
channel: dev channel: dev
+ when: + 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. > 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 ```diff
when: when:
branch: [master, develop] - branch: [master, develop]
``` ```
Execute a step if the branch starts with `prefix/*`: Execute a step if the branch starts with `prefix/*`:
```diff ```diff
when: when:
branch: prefix/* - branch: prefix/*
``` ```
Execute a step using custom include and exclude logic: Execute a step using custom include and exclude logic:
```diff ```diff
when: when:
branch: - branch:
include: [ master, release/* ] include: [ master, release/* ]
exclude: [ release/1.0.0, release/1.1.* ] exclude: [ release/1.0.0, release/1.1.* ]
``` ```
#### `event` #### `event`
@ -329,29 +342,29 @@ Execute a step if the build event is a `tag`:
```diff ```diff
when: when:
event: tag - event: tag
``` ```
Execute a step if the pipeline event is a `push` to a specified branch: Execute a step if the pipeline event is a `push` to a specified branch:
```diff ```diff
when: when:
event: push - event: push
+ branch: main + branch: main
``` ```
Execute a step for all non-pull request events: Execute a step for all non-pull request events:
```diff ```diff
when: when:
event: [push, tag, deployment] - event: [push, tag, deployment]
``` ```
Execute a step for all build events: Execute a step for all build events:
```diff ```diff
when: when:
event: [push, pull_request, tag, deployment] - event: [push, pull_request, tag, deployment]
``` ```
#### `tag` #### `tag`
@ -361,8 +374,8 @@ Use glob expression to execute a step if the tag name starts with `v`:
```diff ```diff
when: when:
event: tag - event: tag
tag: v* tag: v*
``` ```
#### `status` #### `status`
@ -376,7 +389,7 @@ pipeline:
settings: settings:
channel: dev channel: dev
+ when: + when:
+ status: [ success, failure ] + - status: [ success, failure ]
``` ```
#### `platform` #### `platform`
@ -389,14 +402,14 @@ Execute a step for a specific platform:
```diff ```diff
when: when:
platform: linux/amd64 - platform: linux/amd64
``` ```
Execute a step for a specific platform using wildcards: Execute a step for a specific platform using wildcards:
```diff ```diff
when: when:
platform: [ linux/*, windows/amd64 ] - platform: [ linux/*, windows/amd64 ]
``` ```
#### `environment` #### `environment`
@ -405,8 +418,8 @@ Execute a step for deployment events matching the target deployment environment:
```diff ```diff
when: when:
environment: production - environment: production
event: deployment - event: deployment
``` ```
#### `matrix` #### `matrix`
@ -415,9 +428,9 @@ Execute a step for a single matrix permutation:
```diff ```diff
when: when:
matrix: - matrix:
GO_VERSION: 1.5 GO_VERSION: 1.5
REDIS_VERSION: 2.8 REDIS_VERSION: 2.8
``` ```
#### `instance` #### `instance`
@ -426,7 +439,7 @@ Execute a step only on a certain Woodpecker instance matching the specified host
```diff ```diff
when: when:
instance: stage.woodpecker.company.com - instance: stage.woodpecker.company.com
``` ```
#### `path` #### `path`
@ -441,17 +454,17 @@ Execute a step only on a pipeline with certain files being changed:
```diff ```diff
when: 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`. 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 ```diff
when: when:
path: - path:
include: [ '.woodpecker/*.yml', '*.ini' ] include: [ '.woodpecker/*.yml', '*.ini' ]
exclude: [ '*.md', 'docs/**' ] exclude: [ '*.md', 'docs/**' ]
ignore_message: "[ALL]" ignore_message: "[ALL]"
``` ```
**Hint:** Passing a defined ignore-message like `[ALL]` inside the commit message will ignore all path conditions. **Hint:** Passing a defined ignore-message like `[ALL]` inside the commit message will ignore all path conditions.

View file

@ -145,7 +145,7 @@ func (c *Compiler) Compile(conf *yaml.Config) *backend.Config {
config.Stages = append(config.Stages, stage) config.Stages = append(config.Stages, stage)
} else if !c.local && !conf.SkipClone { } else if !c.local && !conf.SkipClone {
for i, container := range conf.Clone.Containers { for i, container := range conf.Clone.Containers {
if !container.Constraints.Match(c.metadata) { if !container.When.Match(c.metadata) {
continue continue
} }
stage := new(backend.Stage) stage := new(backend.Stage)
@ -172,7 +172,7 @@ func (c *Compiler) Compile(conf *yaml.Config) *backend.Config {
stage.Alias = nameServices stage.Alias = nameServices
for i, container := range conf.Services.Containers { for i, container := range conf.Services.Containers {
if !container.Constraints.Match(c.metadata) { if !container.When.Match(c.metadata) {
continue continue
} }
@ -188,11 +188,11 @@ func (c *Compiler) Compile(conf *yaml.Config) *backend.Config {
var group string var group string
for i, container := range conf.Pipeline.Containers { for i, container := range conf.Pipeline.Containers {
// Skip if local and should not run local // Skip if local and should not run local
if c.local && !container.Constraints.Local.Bool() { if c.local && !container.When.IsLocal() {
continue continue
} }
if !container.Constraints.Match(c.metadata) { if !container.When.Match(c.metadata) {
continue continue
} }

View file

@ -145,6 +145,12 @@ func (c *Compiler) createProcess(name string, container *yaml.Container, section
cpuSet = c.reslimit.CPUSet 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{ return &backend.Step{
Name: name, Name: name,
Alias: container.Name, Alias: container.Name,
@ -172,11 +178,9 @@ func (c *Compiler) createProcess(name string, container *yaml.Container, section
CPUShares: cpuShares, CPUShares: cpuShares,
CPUSet: cpuSet, CPUSet: cpuSet,
AuthConfig: authConfig, AuthConfig: authConfig,
OnSuccess: container.Constraints.Status.Match("success"), OnSuccess: onSuccess,
OnFailure: (len(container.Constraints.Status.Include)+ OnFailure: onFailure,
len(container.Constraints.Status.Exclude) != 0) && NetworkMode: networkMode,
container.Constraints.Status.Match("failure"), IpcMode: ipcMode,
NetworkMode: networkMode,
IpcMode: ipcMode,
} }
} }

View file

@ -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].Name).Equal("notify_fail")
g.Assert(out.Pipeline.Containers[0].Image).Equal("plugins/slack") 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].Name).Equal("notify_success")
g.Assert(out.Pipeline.Containers[1].Image).Equal("plugins/slack") 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"})
}) })
}) })
}) })

View file

@ -12,8 +12,13 @@ import (
) )
type ( type (
// Constraints defines a set of runtime constraints. // When defines a set of runtime constraints.
Constraints struct { When struct {
// If true then read from a list of constraint
Constraints []Constraint
}
Constraint struct {
Ref List Ref List
Repo List Repo List
Instance 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 // Match returns true if all constraints match the given input. If a single
// constraint fails a false value is returned. // 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) && match := c.Platform.Match(metadata.Sys.Platform) &&
c.Environment.Match(metadata.Curr.Target) && c.Environment.Match(metadata.Curr.Target) &&
c.Event.Match(metadata.Curr.Event) && 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.Instance.Match(metadata.Sys.Host) &&
c.Matrix.Match(metadata.Job.Matrix) 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 { if metadata.Curr.Event == frontend.EventPull || metadata.Curr.Event == frontend.EventPush {
match = match && c.Path.Match(metadata.Curr.Commit.ChangedFiles, metadata.Curr.Commit.Message) match = match && c.Path.Match(metadata.Curr.Commit.ChangedFiles, metadata.Curr.Commit.Message)
} }
if metadata.Curr.Event != frontend.EventTag { if metadata.Curr.Event != frontend.EventTag {
match = match && c.Branch.Match(metadata.Curr.Commit.Branch) 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 { if len(c.Include) == 0 && len(c.Exclude) == 0 {
return true return true
} }
// exclusions are processed first. So we can include everything and then // exclusions are processed first. So we can include everything and then
// selectively include others. // selectively include others.
if len(c.Exclude) != 0 { 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 // 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 { func (c *Path) Match(v []string, message string) bool {
// ignore file pattern matches if the commit message contains a pattern // 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)) { if len(c.IgnoreMessage) > 0 && strings.Contains(strings.ToLower(message), strings.ToLower(c.IgnoreMessage)) {
return true return true
} }
// always match if there are no commit files (empty commit) // always match if there are no commit files (empty commit)
if len(v) == 0 { if len(v) == 0 {
return true return true

View file

@ -469,8 +469,8 @@ func TestConstraints(t *testing.T) {
} }
} }
func parseConstraints(t *testing.T, s string) *Constraints { func parseConstraints(t *testing.T, s string) *When {
c := &Constraints{} c := &When{}
assert.NoError(t, yaml.Unmarshal([]byte(s), c)) assert.NoError(t, yaml.Unmarshal([]byte(s), c))
return c return c
} }

View file

@ -58,7 +58,7 @@ type (
Volumes types.Volumes `yaml:"volumes,omitempty"` Volumes types.Volumes `yaml:"volumes,omitempty"`
Secrets Secrets `yaml:"secrets,omitempty"` Secrets Secrets `yaml:"secrets,omitempty"`
Sysctls types.SliceorMap `yaml:"sysctls,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"` Settings map[string]interface{} `yaml:"settings"`
} }
) )

View file

@ -109,9 +109,13 @@ func TestUnmarshalContainer(t *testing.T) {
{Source: "/etc/configs", Destination: "/etc/configs/", AccessMode: "ro"}, {Source: "/etc/configs", Destination: "/etc/configs/", AccessMode: "ro"},
}, },
}, },
Constraints: constraint.Constraints{ When: constraint.When{
Branch: constraint.List{ Constraints: []constraint.Constraint{
Include: []string{"master"}, {
Branch: constraint.List{
Include: []string{"master"},
},
},
}, },
}, },
Settings: map[string]interface{}{ Settings: map[string]interface{}{
@ -185,9 +189,13 @@ func TestUnmarshalContainers(t *testing.T) {
"tag": stringsToInterface("next", "latest"), "tag": stringsToInterface("next", "latest"),
"dry_run": true, "dry_run": true,
}, },
Constraints: constraint.Constraints{ When: constraint.When{
Event: constraint.List{Include: []string{"push"}}, Constraints: []constraint.Constraint{
Branch: constraint.List{Include: []string{"${CI_REPO_DEFAULT_BRANCH}"}}, {
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", "dockerfile": "docker/Dockerfile.cli",
"tag": stringsToInterface("next"), "tag": stringsToInterface("next"),
}, },
Constraints: constraint.Constraints{ When: constraint.When{
Event: constraint.List{Include: []string{"push"}}, Constraints: []constraint.Constraint{
Branch: constraint.List{Include: []string{"${CI_REPO_DEFAULT_BRANCH}"}}, {
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"}},
},
},
}, },
}, },
}, },

View file

@ -111,3 +111,13 @@ pipeline:
- echo "test" - echo "test"
when: when:
repo: test/test repo: test/test
when-multi:
image: alpine
commands:
- echo "test"
when:
- event: pull_request
repo: test/test
- event: push
branch: main

View file

@ -140,6 +140,18 @@
}, },
"step_when": { "step_when": {
"description": "Steps can be skipped based on conditions. Read more: https://woodpecker-ci.org/docs/usage/pipeline-syntax#when---conditional-execution", "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", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {