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
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.

View file

@ -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
}

View file

@ -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,
}
}

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].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"})
})
})
})

View file

@ -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

View file

@ -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
}

View file

@ -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"`
}
)

View file

@ -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"}},
},
},
},
},
},

View file

@ -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

View file

@ -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": {