Fixed when:evaluate on non-standard (non-CI*) env vars (#1907)

Makes it possible to evaluate `when` constraint on custom environment
variables.
This commit is contained in:
Thomas Anderson
2023-07-03 00:45:22 +03:00
committed by GitHub
parent 2ba64dcb7d
commit b616a822a0
8 changed files with 46 additions and 19 deletions

View File

@@ -444,7 +444,7 @@ when:
#### `evaluate` #### `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. Execute a step only if the provided evaluate expression is equal to true. Both built-in [`CI_`](./50-environment.md#built-in-environment-variables) and custom 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. The expression syntax can be found in [the docs](https://github.com/antonmedv/expr/blob/master/docs/Language-Definition.md) of the underlying library.
@@ -476,6 +476,13 @@ when:
- evaluate: 'CI_COMMIT_PULL_REQUEST_LABELS contains "deploy"' - evaluate: 'CI_COMMIT_PULL_REQUEST_LABELS contains "deploy"'
``` ```
Skip step only if `SKIP=true`, run otherwise or if undefined:
```yaml
when:
- evaluate: 'SKIP != "true"'
```
### `group` - Parallel execution ### `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. 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.

View File

@@ -1,6 +1,6 @@
# Environment variables # Environment variables
Woodpecker provides the ability to pass environment variables to individual pipeline steps. Example pipeline step with custom environment variables: Woodpecker provides the ability to pass environment variables to individual pipeline steps. Note that these can't overwrite any existing, built-in variables. Example pipeline step with custom environment variables:
```diff ```diff
steps: steps:

View File

@@ -104,7 +104,7 @@ func New(opts ...Option) *Compiler {
func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, error) { func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, error) {
config := new(backend_types.Config) config := new(backend_types.Config)
if match, err := conf.When.Match(c.metadata, true); !match && err == nil { if match, err := conf.When.Match(c.metadata, true, c.env); !match && err == nil {
// This pipeline does not match the configured filter so return an empty config and stop further compilation. // 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. // An empty pipeline will just be skipped completely.
return config, nil return config, nil
@@ -177,7 +177,7 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
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.ContainerList { for i, container := range conf.Clone.ContainerList {
if match, err := container.When.Match(c.metadata, false); !match && err == nil { if match, err := container.When.Match(c.metadata, false, c.env); !match && err == nil {
continue continue
} else if err != nil { } else if err != nil {
return nil, err return nil, err
@@ -212,7 +212,7 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
stage.Alias = nameServices stage.Alias = nameServices
for i, container := range conf.Services.ContainerList { for i, container := range conf.Services.ContainerList {
if match, err := container.When.Match(c.metadata, false); !match && err == nil { if match, err := container.When.Match(c.metadata, false, c.env); !match && err == nil {
continue continue
} else if err != nil { } else if err != nil {
return nil, err return nil, err
@@ -234,7 +234,7 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
continue continue
} }
if match, err := container.When.Match(c.metadata, false); !match && err == nil { if match, err := container.When.Match(c.metadata, false, c.env); !match && err == nil {
continue continue
} else if err != nil { } else if err != nil {
return nil, err return nil, err

View File

@@ -8,6 +8,7 @@ import (
"github.com/antonmedv/expr" "github.com/antonmedv/expr"
"github.com/bmatcuk/doublestar/v4" "github.com/bmatcuk/doublestar/v4"
"golang.org/x/exp/maps"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/metadata" "github.com/woodpecker-ci/woodpecker/pipeline/frontend/metadata"
@@ -62,9 +63,9 @@ func (when *When) IsEmpty() bool {
} }
// Returns true if at least one of the internal constraints is true. // Returns true if at least one of the internal constraints is true.
func (when *When) Match(metadata metadata.Metadata, global bool) (bool, error) { func (when *When) Match(metadata metadata.Metadata, global bool, env map[string]string) (bool, error) {
for _, c := range when.Constraints { for _, c := range when.Constraints {
match, err := c.Match(metadata, global) match, err := c.Match(metadata, global, env)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -76,7 +77,7 @@ func (when *When) Match(metadata metadata.Metadata, global bool) (bool, error) {
if when.IsEmpty() { if when.IsEmpty() {
// test against default Constraints // test against default Constraints
empty := &Constraint{} empty := &Constraint{}
return empty.Match(metadata, global) return empty.Match(metadata, global, env)
} }
return false, nil return false, nil
} }
@@ -139,7 +140,7 @@ func (when *When) UnmarshalYAML(value *yaml.Node) error {
// 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 *Constraint) Match(m metadata.Metadata, global bool) (bool, error) { func (c *Constraint) Match(m metadata.Metadata, global bool, env map[string]string) (bool, error) {
match := true match := true
if !global { if !global {
// apply step only filters // apply step only filters
@@ -167,8 +168,12 @@ func (c *Constraint) Match(m metadata.Metadata, global bool) (bool, error) {
} }
if c.Evaluate != "" { if c.Evaluate != "" {
env := m.Environ() if env == nil {
out, err := expr.Compile(c.Evaluate, expr.Env(env), expr.AsBool()) env = m.Environ()
} else {
maps.Copy(env, m.Environ())
}
out, err := expr.Compile(c.Evaluate, expr.Env(env), expr.AllowUndefinedVariables(), expr.AsBool())
if err != nil { if err != nil {
return false, err return false, err
} }

View File

@@ -405,6 +405,7 @@ func TestConstraints(t *testing.T) {
desc string desc string
conf string conf string
with metadata.Metadata with metadata.Metadata
env map[string]string
want bool want bool
}{ }{
{ {
@@ -510,6 +511,20 @@ func TestConstraints(t *testing.T) {
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventPush}, Repo: metadata.Repo{Owner: "owner", Name: "repo"}}, with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventPush}, Repo: metadata.Repo{Owner: "owner", Name: "repo"}},
want: true, want: true,
}, },
{
desc: "filter by eval based on custom variable",
conf: `{ evaluate: 'TESTVAR == "testval"' }`,
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventManual}},
env: map[string]string{"TESTVAR": "testval"},
want: true,
},
{
desc: "filter by eval based on custom variable",
conf: `{ evaluate: 'TESTVAR == "testval"' }`,
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventManual}},
env: map[string]string{"TESTVAR": "qwe"},
want: false,
},
} }
for _, test := range testdata { for _, test := range testdata {
@@ -517,7 +532,7 @@ func TestConstraints(t *testing.T) {
conf, err := frontend.EnvVarSubst(test.conf, test.with.Environ()) conf, err := frontend.EnvVarSubst(test.conf, test.with.Environ())
assert.NoError(t, err) assert.NoError(t, err)
c := parseConstraints(t, conf) c := parseConstraints(t, conf)
got, err := c.Match(test.with, false) got, err := c.Match(test.with, false, test.env)
if err != nil { if err != nil {
t.Errorf("Match returned error: %v", err) t.Errorf("Match returned error: %v", err)
} }

View File

@@ -84,7 +84,7 @@ func TestParse(t *testing.T) {
Curr: metadata.Pipeline{ Curr: metadata.Pipeline{
Event: "tester", Event: "tester",
}, },
}, false) }, false, nil)
g.Assert(match).Equal(true) g.Assert(match).Equal(true)
g.Assert(err).IsNil() g.Assert(err).IsNil()
}) })
@@ -94,7 +94,7 @@ func TestParse(t *testing.T) {
Curr: metadata.Pipeline{ Curr: metadata.Pipeline{
Event: "tester2", Event: "tester2",
}, },
}, false) }, false, nil)
g.Assert(match).Equal(true) g.Assert(match).Equal(true)
g.Assert(err).IsNil() g.Assert(err).IsNil()
}) })
@@ -106,7 +106,7 @@ func TestParse(t *testing.T) {
Branch: "tester", Branch: "tester",
}, },
}, },
}, true) }, true, nil)
g.Assert(match).Equal(true) g.Assert(match).Equal(true)
g.Assert(err).IsNil() g.Assert(err).IsNil()
}) })
@@ -116,7 +116,7 @@ func TestParse(t *testing.T) {
Curr: metadata.Pipeline{ Curr: metadata.Pipeline{
Event: "push", Event: "push",
}, },
}, false) }, false, nil)
g.Assert(match).Equal(false) g.Assert(match).Equal(false)
g.Assert(err).IsNil() g.Assert(err).IsNil()
}) })

View File

@@ -118,7 +118,7 @@ func (b *StepBuilder) Build() ([]*Item, error) {
} }
// checking if filtered. // checking if filtered.
if match, err := parsed.When.Match(workflowMetadata, true); !match && err == nil { if match, err := parsed.When.Match(workflowMetadata, true, environ); !match && err == nil {
log.Debug().Str("pipeline", workflow.Name).Msg( log.Debug().Str("pipeline", workflow.Name).Msg(
"Marked as skipped, dose not match metadata", "Marked as skipped, dose not match metadata",
) )

View File

@@ -72,7 +72,7 @@ func checkIfFiltered(repo *model.Repo, p *model.Pipeline, forgeYamlConfigs []*fo
log.Trace().Msgf("config '%s': %#v", forgeYamlConfig.Name, parsedPipelineConfig) log.Trace().Msgf("config '%s': %#v", forgeYamlConfig.Name, parsedPipelineConfig)
// ignore if the pipeline was filtered by matched constraints // ignore if the pipeline was filtered by matched constraints
if match, err := parsedPipelineConfig.When.Match(matchMetadata, true); !match && err == nil { if match, err := parsedPipelineConfig.When.Match(matchMetadata, true, p.AdditionalVariables); !match && err == nil {
continue continue
} else if err != nil { } else if err != nil {
return false, err return false, err