1
0
mirror of https://github.com/woodpecker-ci/woodpecker.git synced 2025-11-23 21:44:44 +02:00
Files
woodpecker/pipeline/frontend/yaml/constraint/constraint.go

204 lines
5.4 KiB
Go

// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package constraint
import (
"fmt"
"maps"
"path"
"slices"
"github.com/expr-lang/expr"
"gopkg.in/yaml.v3"
"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata"
yamlBaseTypes "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types/base"
"go.woodpecker-ci.org/woodpecker/v3/shared/optional"
)
type (
// When defines a set of runtime constraints.
When struct {
// If true then read from a list of constraint
Constraints []Constraint
}
Constraint struct {
Ref List `yaml:"ref,omitempty"`
Repo List `yaml:"repo,omitempty"`
Instance List `yaml:"instance,omitempty"`
Platform List `yaml:"platform,omitempty"`
Branch List `yaml:"branch,omitempty"`
Cron List `yaml:"cron,omitempty"`
Status List `yaml:"status,omitempty"`
Matrix Map `yaml:"matrix,omitempty"`
Local optional.Option[bool] `yaml:"local,omitempty"`
Path Path `yaml:"path,omitempty"`
Evaluate string `yaml:"evaluate,omitempty"`
Event yamlBaseTypes.StringOrSlice `yaml:"event,omitempty"`
}
)
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 metadata.Metadata, global bool, env map[string]string) (bool, error) {
for _, c := range when.Constraints {
match, err := c.Match(metadata, global, env)
if err != nil {
return false, err
}
if match {
return true, nil
}
}
if when.IsEmpty() {
// test against default Constraints
empty := &Constraint{}
return empty.Match(metadata, global, env)
}
return false, nil
}
func (when *When) IncludesStatusFailure() bool {
for _, c := range when.Constraints {
if c.Status.Includes("failure") {
return true
}
}
return false
}
func (when *When) IncludesStatusSuccess() bool {
// "success" acts differently than "failure" in that it's
// presumed to be included unless it's specifically not part
// of the list
if when.IsEmpty() {
return true
}
for _, c := range when.Constraints {
if len(c.Status.Include) == 0 || c.Status.Includes("success") {
return true
}
}
return false
}
// False if (any) non local.
func (when *When) IsLocal() bool {
for _, c := range when.Constraints {
if !c.Local.ValueOrDefault(true) {
return false
}
}
return true
}
func (when *When) UnmarshalYAML(value *yaml.Node) error {
switch value.Kind {
case yaml.SequenceNode:
if err := value.Decode(&when.Constraints); err != nil {
return err
}
case yaml.MappingNode:
c := Constraint{}
if err := value.Decode(&c); err != nil {
return err
}
when.Constraints = append(when.Constraints, c)
default:
return fmt.Errorf("not supported yaml kind: %v", value.Kind)
}
return nil
}
// MarshalYAML implements custom Yaml marshaling.
func (when When) MarshalYAML() (any, error) {
// clean up local if true make it none as we will default to true
for i := range when.Constraints {
if when.Constraints[i].Local.ValueOrDefault(true) {
when.Constraints[i].Local = optional.None[bool]()
}
}
switch len(when.Constraints) {
case 0:
return nil, nil
case 1:
return when.Constraints[0], nil
default:
return when.Constraints, nil
}
}
// Match returns true if all constraints match the given input. If a single
// constraint fails a false value is returned.
func (c *Constraint) Match(m metadata.Metadata, global bool, env map[string]string) (bool, error) {
match := true
if !global {
// apply step only filters
match = c.Matrix.Match(m.Workflow.Matrix)
}
match = match && c.Platform.Match(m.Sys.Platform) &&
(len(c.Event) == 0 || slices.Contains(c.Event, m.Curr.Event)) &&
c.Repo.Match(path.Join(m.Repo.Owner, m.Repo.Name)) &&
c.Ref.Match(m.Curr.Commit.Ref) &&
c.Instance.Match(m.Sys.Host)
// changed files filter apply only for pull-request and push events
if metadata.EventIsPull(m.Curr.Event) || m.Curr.Event == metadata.EventPush {
match = match && c.Path.Match(m.Curr.Commit.ChangedFiles, m.Curr.Commit.Message)
}
if m.Curr.Event != metadata.EventTag {
match = match && c.Branch.Match(m.Curr.Commit.Branch)
}
if m.Curr.Event == metadata.EventCron {
match = match && c.Cron.Match(m.Curr.Cron)
}
if c.Evaluate != "" {
if env == nil {
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 {
return false, err
}
result, err := expr.Run(out, env)
if err != nil {
return false, err
}
bResult, ok := result.(bool)
if !ok {
return false, fmt.Errorf("could not parse result: %v", result)
}
match = match && bResult
}
return match, nil
}