mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-27 19:00:35 +00:00
Allow multiple when conditions (#1087)
Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: LamaAni <zshotan@bloomberg.net>
This commit is contained in:
parent
98e6396e3e
commit
e269890643
10 changed files with 215 additions and 58 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": {
|
||||
|
|
Loading…
Reference in a new issue