diff --git a/cli/exec/exec.go b/cli/exec/exec.go index 53b36dc29..587a25cf4 100644 --- a/cli/exec/exec.go +++ b/cli/exec/exec.go @@ -157,7 +157,7 @@ func execWithAxis(c *cli.Context, file, repoPath string, axis matrix.Axis) error } // compiles the yaml file - compiled := compiler.New( + compiled, err := compiler.New( compiler.WithEscalated( c.StringSlice("privileged")..., ), @@ -185,6 +185,9 @@ func execWithAxis(c *cli.Context, file, repoPath string, axis matrix.Axis) error compiler.WithSecret(secrets...), compiler.WithEnviron(droneEnv), ).Compile(conf) + if err != nil { + return err + } backend.Init(context.WithValue(c.Context, types.CliContext, c)) diff --git a/docs/docs/20-usage/20-pipeline-syntax.md b/docs/docs/20-usage/20-pipeline-syntax.md index 155fbaa52..67da3fe67 100644 --- a/docs/docs/20-usage/20-pipeline-syntax.md +++ b/docs/docs/20-usage/20-pipeline-syntax.md @@ -435,6 +435,33 @@ when: **Hint:** Passing a defined ignore-message like `[ALL]` inside the commit message will ignore all path conditions. +#### `evaluate` + +Execute a step only if the provided evaluate expression is equal to true. Each [`CI_` variable](./50-environment.md#built-in-environment-variables) can be used inside the expression. + +The expression syntax can be found in [the docs](https://github.com/antonmedv/expr/blob/master/docs/Language-Definition.md) of the underlying library. + +Run on pushes to the default branch for the repository `owner/repo`: + +```yaml +when: + - evaluate: 'CI_BUILD_EVENT == "push" && CI_REPO == "owner/repo" && CI_COMMIT_BRANCH == CI_REPO_DEFAULT_BRANCH' +``` + +Run on commits created by user `woodpecker-ci`: + +```yaml +when: + - evaluate: 'CI_COMMIT_AUTHOR == "woodpecker-ci"' +``` + +Skip all commits containing `please ignore me` in the commit message: + +```yaml +when: + - evaluate: 'not (CI_COMMIT_MESSAGE contains "please ignore me")' +``` + ### `group` - Parallel execution Woodpecker supports parallel step execution for same-machine fan-in and fan-out. Parallel steps are configured using the `group` attribute. This instructs the pipeline runner to execute the named group in parallel. diff --git a/go.mod b/go.mod index 36fb7e5ac..54ec6c583 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( code.gitea.io/sdk/gitea v0.15.1-0.20220831004139-a0127ed0e7fe codeberg.org/6543/go-yaml2json v0.2.1 + github.com/antonmedv/expr v1.9.0 github.com/bmatcuk/doublestar/v4 v4.2.0 github.com/caddyserver/certmagic v0.17.1-0.20220901172127-2e22c6fa8c47 github.com/docker/cli v20.10.17+incompatible diff --git a/go.sum b/go.sum index 3bcc5a33d..c4c5c8410 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,7 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= @@ -85,6 +86,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU= +github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -143,6 +146,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -195,6 +199,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -506,6 +512,8 @@ github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -532,6 +540,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= @@ -624,6 +634,7 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg= github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -663,6 +674,8 @@ github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0ua github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= @@ -683,6 +696,7 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= @@ -716,6 +730,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -962,6 +977,7 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pipeline/frontend/yaml/compiler/compiler.go b/pipeline/frontend/yaml/compiler/compiler.go index c6b6f3002..2143547c4 100644 --- a/pipeline/frontend/yaml/compiler/compiler.go +++ b/pipeline/frontend/yaml/compiler/compiler.go @@ -93,13 +93,15 @@ func New(opts ...Option) *Compiler { // Compile compiles the YAML configuration to the pipeline intermediate // representation configuration format. -func (c *Compiler) Compile(conf *yaml.Config) *backend.Config { +func (c *Compiler) Compile(conf *yaml.Config) (*backend.Config, error) { config := new(backend.Config) - if !conf.When.Match(c.metadata, true) { + if match, err := conf.When.Match(c.metadata, true); !match && err == nil { // This pipeline does not match the configured filter so return an empty config and stop further compilation. // An empty pipeline will just be skipped completely. - return config + return config, nil + } else if err != nil { + return nil, err } // create a default volume @@ -166,9 +168,12 @@ 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.When.Match(c.metadata, false) { + if match, err := container.When.Match(c.metadata, false); !match && err == nil { continue + } else if err != nil { + return nil, err } + stage := new(backend.Stage) stage.Name = fmt.Sprintf("%s_clone_%v", c.prefix, i) stage.Alias = container.Name @@ -193,8 +198,10 @@ func (c *Compiler) Compile(conf *yaml.Config) *backend.Config { stage.Alias = nameServices for i, container := range conf.Services.Containers { - if !container.When.Match(c.metadata, false) { + if match, err := container.When.Match(c.metadata, false); !match && err == nil { continue + } else if err != nil { + return nil, err } name := fmt.Sprintf("%s_%s_%d", c.prefix, nameServices, i) @@ -213,8 +220,10 @@ func (c *Compiler) Compile(conf *yaml.Config) *backend.Config { continue } - if !container.When.Match(c.metadata, false) { + if match, err := container.When.Match(c.metadata, false); !match && err == nil { continue + } else if err != nil { + return nil, err } if stage == nil || group != container.Group || container.Group == "" { @@ -233,7 +242,7 @@ func (c *Compiler) Compile(conf *yaml.Config) *backend.Config { c.setupCacheRebuild(conf, config) - return config + return config, nil } func (c *Compiler) setupCache(conf *yaml.Config, ir *backend.Config) { diff --git a/pipeline/frontend/yaml/config_test.go b/pipeline/frontend/yaml/config_test.go index 79fce715c..cb69c8865 100644 --- a/pipeline/frontend/yaml/config_test.go +++ b/pipeline/frontend/yaml/config_test.go @@ -79,37 +79,45 @@ func TestParse(t *testing.T) { } g.It("Should match event tester", func() { - g.Assert(matchConfig.When.Match(frontend.Metadata{ + match, err := matchConfig.When.Match(frontend.Metadata{ Curr: frontend.Build{ Event: "tester", }, - }, false)).Equal(true) + }, false) + g.Assert(match).Equal(true) + g.Assert(err).IsNil() }) g.It("Should match event tester2", func() { - g.Assert(matchConfig.When.Match(frontend.Metadata{ + match, err := matchConfig.When.Match(frontend.Metadata{ Curr: frontend.Build{ Event: "tester2", }, - }, false)).Equal(true) + }, false) + g.Assert(match).Equal(true) + g.Assert(err).IsNil() }) g.It("Should match branch tester", func() { - g.Assert(matchConfig.When.Match(frontend.Metadata{ + match, err := matchConfig.When.Match(frontend.Metadata{ Curr: frontend.Build{ Commit: frontend.Commit{ Branch: "tester", }, }, - }, true)).Equal(true) + }, true) + g.Assert(match).Equal(true) + g.Assert(err).IsNil() }) g.It("Should not match event push", func() { - g.Assert(matchConfig.When.Match(frontend.Metadata{ + match, err := matchConfig.When.Match(frontend.Metadata{ Curr: frontend.Build{ Event: "push", }, - }, false)).Equal(false) + }, false) + g.Assert(match).Equal(false) + g.Assert(err).IsNil() }) }) }) diff --git a/pipeline/frontend/yaml/constraint/constraint.go b/pipeline/frontend/yaml/constraint/constraint.go index dbab2e7a4..462677b0b 100644 --- a/pipeline/frontend/yaml/constraint/constraint.go +++ b/pipeline/frontend/yaml/constraint/constraint.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/antonmedv/expr" "github.com/bmatcuk/doublestar/v4" "gopkg.in/yaml.v3" @@ -31,6 +32,7 @@ type ( Matrix Map Local types.BoolTrue Path Path + Evaluate string `yaml:"evaluate,omitempty"` } // List defines a runtime constraint for exclude & include string slices. @@ -58,10 +60,14 @@ func (when *When) IsEmpty() bool { } // Returns true if at least one of the internal constraints is true. -func (when *When) Match(metadata frontend.Metadata, global bool) bool { +func (when *When) Match(metadata frontend.Metadata, global bool) (bool, error) { for _, c := range when.Constraints { - if c.Match(metadata, global) { - return true + match, err := c.Match(metadata, global) + if err != nil { + return false, err + } + if match { + return true, nil } } @@ -70,7 +76,7 @@ func (when *When) Match(metadata frontend.Metadata, global bool) bool { empty := &Constraint{} return empty.Match(metadata, global) } - return false + return false, nil } func (when *When) IncludesStatus(status string) bool { @@ -126,7 +132,7 @@ func (when *When) UnmarshalYAML(value *yaml.Node) error { // Match returns true if all constraints match the given input. If a single // constraint fails a false value is returned. -func (c *Constraint) Match(metadata frontend.Metadata, global bool) bool { +func (c *Constraint) Match(metadata frontend.Metadata, global bool) (bool, error) { match := true if !global { c.SetDefaultEventFilter() @@ -155,7 +161,20 @@ func (c *Constraint) Match(metadata frontend.Metadata, global bool) bool { match = match && c.Cron.Match(metadata.Curr.Cron) } - return match + if c.Evaluate != "" { + env := metadata.Environ() + out, err := expr.Compile(c.Evaluate, expr.Env(env), expr.AsBool()) + if err != nil { + return false, err + } + result, err := expr.Run(out, env) + if err != nil { + return false, err + } + match = match && result.(bool) + } + + return match, nil } // SetDefaultEventFilter set default e event filter if not event filter is already set diff --git a/pipeline/frontend/yaml/constraint/constraint_test.go b/pipeline/frontend/yaml/constraint/constraint_test.go index ee4e20779..416fb1912 100644 --- a/pipeline/frontend/yaml/constraint/constraint_test.go +++ b/pipeline/frontend/yaml/constraint/constraint_test.go @@ -484,14 +484,29 @@ func TestConstraints(t *testing.T) { }, want: false, }, + { + desc: "filter by eval based on event", + conf: `{ evaluate: 'CI_BUILD_EVENT == "push"' }`, + with: frontend.Metadata{Curr: frontend.Build{Event: frontend.EventPush}}, + want: true, + }, + { + desc: "filter by eval based on event and repo", + conf: `{ evaluate: 'CI_BUILD_EVENT == "push" && CI_REPO == "owner/repo"' }`, + with: frontend.Metadata{Curr: frontend.Build{Event: frontend.EventPush}, Repo: frontend.Repo{Name: "owner/repo"}}, + want: true, + }, } for _, test := range testdata { t.Run(test.desc, func(t *testing.T) { c := parseConstraints(t, test.conf) - got, want := c.Match(test.with, false), test.want - if got != want { - t.Errorf("Expect %+v matches %q is %v", test.with, test.conf, want) + got, err := c.Match(test.with, false) + if err != nil { + t.Errorf("Match returned error: %v", err) + } + if got != test.want { + t.Errorf("Expect %+v matches %q is %v", test.with, test.conf, test.want) } }) } diff --git a/pipeline/schema/.woodpecker/test-when.yml b/pipeline/schema/.woodpecker/test-when.yml index d0159b6c5..3ad6ca7b5 100644 --- a/pipeline/schema/.woodpecker/test-when.yml +++ b/pipeline/schema/.woodpecker/test-when.yml @@ -134,9 +134,16 @@ pipeline: image: alpine commands: echo "test" when: - event: cron - cron: - include: - - test - - hello - exclude: hi + - event: cron + cron: + include: + - test + - hello + exclude: hi + + when-evaluate: + image: alpine + commands: echo "test" + when: + - event: push + evaluate: 'CI_BUILD_EVENT == "push" && CI_REPO == "owner/repo"' diff --git a/pipeline/schema/schema.json b/pipeline/schema/schema.json index ab31f0cb0..18bc9c247 100644 --- a/pipeline/schema/schema.json +++ b/pipeline/schema/schema.json @@ -189,6 +189,10 @@ "additionalProperties": false } ] + }, + "evaluate": { + "description": "Execute a step only if the expression evaluates to true. Read more: https://woodpecker-ci.org/docs/usage/pipeline-syntax#evaluate", + "type": "string" } } }, @@ -344,6 +348,10 @@ "additionalProperties": false } ] + }, + "evaluate": { + "description": "Execute a step only if the expression evaluates to true. Read more: https://woodpecker-ci.org/docs/usage/pipeline-syntax#evaluate", + "type": "string" } } }, @@ -499,10 +507,7 @@ "description": "expose ports to which other steps can connect to", "type": "array", "items": { - "oneOf": [ - { "type": "number" }, - { "type": "string" } - ] + "oneOf": [{ "type": "number" }, { "type": "string" }] }, "minLength": 1 } diff --git a/server/pipeline/filter.go b/server/pipeline/filter.go index 7491886c3..83afd123d 100644 --- a/server/pipeline/filter.go +++ b/server/pipeline/filter.go @@ -72,8 +72,10 @@ func checkIfFiltered(build *model.Build, remoteYamlConfigs []*remote.FileMeta) ( log.Trace().Msgf("config '%s': %#v", remoteYamlConfig.Name, parsedPipelineConfig) // ignore if the pipeline was filtered by matched constraints - if !parsedPipelineConfig.When.Match(matchMetadata, true) { + if match, err := parsedPipelineConfig.When.Match(matchMetadata, true); !match && err == nil { continue + } else if err != nil { + return false, err } // ignore if the pipeline was filtered by the branch (legacy) diff --git a/server/shared/procBuilder.go b/server/shared/procBuilder.go index ae43f3495..a855a2634 100644 --- a/server/shared/procBuilder.go +++ b/server/shared/procBuilder.go @@ -119,11 +119,16 @@ func (b *ProcBuilder) Build() ([]*BuildItem, error) { } // checking if filtered. - if !parsed.When.Match(metadata, true) { + if match, err := parsed.When.Match(metadata, true); !match && err == nil { log.Debug().Str("pipeline", proc.Name).Msg( "Marked as skipped, dose not match metadata", ) proc.State = model.StatusSkipped + } else if err != nil { + log.Debug().Str("pipeline", proc.Name).Msg( + "Pipeline config could not be parsed", + ) + return nil, err } // TODO: deprecated branches filter => remove after some time @@ -134,7 +139,10 @@ func (b *ProcBuilder) Build() ([]*BuildItem, error) { proc.State = model.StatusSkipped } - ir := b.toInternalRepresentation(parsed, environ, metadata, proc.ID) + ir, err := b.toInternalRepresentation(parsed, environ, metadata, proc.ID) + if err != nil { + return nil, err + } if len(ir.Stages) == 0 { continue @@ -229,7 +237,7 @@ func (b *ProcBuilder) environmentVariables(metadata frontend.Metadata, axis matr return environ } -func (b *ProcBuilder) toInternalRepresentation(parsed *yaml.Config, environ map[string]string, metadata frontend.Metadata, procID int64) *backend.Config { +func (b *ProcBuilder) toInternalRepresentation(parsed *yaml.Config, environ map[string]string, metadata frontend.Metadata, procID int64) (*backend.Config, error) { var secrets []compiler.Secret for _, sec := range b.Secs { if !sec.Match(b.Curr.Event) {