diff --git a/constants/action.go b/constants/action.go index 802ddf79..da0a4449 100644 --- a/constants/action.go +++ b/constants/action.go @@ -22,6 +22,12 @@ const ( // ActionSynchronize defines the action for the synchronizing of pull requests. ActionSynchronize = "synchronize" + // ActionLabeled defines the action for the labeling of pull requests. + ActionLabeled = "labeled" + + // ActionUnlabeled defines the action for the unlabeling of pull requests. + ActionUnlabeled = "unlabeled" + // ActionTransferred defines the action for transferring repository ownership. ActionTransferred = "transferred" diff --git a/constants/allow_events.go b/constants/allow_events.go index 4a163a73..5df53926 100644 --- a/constants/allow_events.go +++ b/constants/allow_events.go @@ -11,7 +11,7 @@ const ( AllowPullSync _ // AllowPullAssigned - Not Implemented _ // AllowPullMilestoned - Not Implemented - _ // AllowPullLabel - Not Implemented + AllowPullLabel _ // AllowPullLocked - Not Implemented _ // AllowPullReady - Not Implemented AllowPullReopen @@ -23,4 +23,5 @@ const ( AllowSchedule AllowPushDeleteBranch AllowPushDeleteTag + AllowPullUnlabel ) diff --git a/library/actions/pull.go b/library/actions/pull.go index 609c6248..8e0063b1 100644 --- a/library/actions/pull.go +++ b/library/actions/pull.go @@ -12,6 +12,8 @@ type Pull struct { Edited *bool `json:"edited"` Synchronize *bool `json:"synchronize"` Reopened *bool `json:"reopened"` + Labeled *bool `json:"labeled"` + Unlabeled *bool `json:"unlabeled"` } // FromMask returns the Pull type resulting from the provided integer mask. @@ -20,6 +22,8 @@ func (a *Pull) FromMask(mask int64) *Pull { a.SetSynchronize(mask&constants.AllowPullSync > 0) a.SetEdited(mask&constants.AllowPullEdit > 0) a.SetReopened(mask&constants.AllowPullReopen > 0) + a.SetLabeled(mask&constants.AllowPullLabel > 0) + a.SetUnlabeled(mask&constants.AllowPullUnlabel > 0) return a } @@ -44,6 +48,14 @@ func (a *Pull) ToMask() int64 { mask = mask | constants.AllowPullReopen } + if a.GetLabeled() { + mask = mask | constants.AllowPullLabel + } + + if a.GetUnlabeled() { + mask = mask | constants.AllowPullUnlabel + } + return mask } @@ -91,6 +103,28 @@ func (a *Pull) GetReopened() bool { return *a.Reopened } +// GetLabeled returns the Labeled field from the provided Pull. If the object is nil, +// or the field within the object is nil, it returns the zero value instead. +func (a *Pull) GetLabeled() bool { + // return zero value if Pull type or Labeled field is nil + if a == nil || a.Labeled == nil { + return false + } + + return *a.Labeled +} + +// GetUnlabeled returns the Unlabeled field from the provided Pull. If the object is nil, +// or the field within the object is nil, it returns the zero value instead. +func (a *Pull) GetUnlabeled() bool { + // return zero value if Pull type or Unlabeled field is nil + if a == nil || a.Unlabeled == nil { + return false + } + + return *a.Unlabeled +} + // SetOpened sets the Pull Opened field. // // When the provided Pull type is nil, it @@ -142,3 +176,29 @@ func (a *Pull) SetReopened(v bool) { a.Reopened = &v } + +// SetLabeled sets the Pull Labeled field. +// +// When the provided Pull type is nil, it +// will set nothing and immediately return. +func (a *Pull) SetLabeled(v bool) { + // return if Pull type is nil + if a == nil { + return + } + + a.Labeled = &v +} + +// SetUnlabeled sets the Pull Unlabeled field. +// +// When the provided Pull type is nil, it +// will set nothing and immediately return. +func (a *Pull) SetUnlabeled(v bool) { + // return if Pull type is nil + if a == nil { + return + } + + a.Unlabeled = &v +} diff --git a/library/actions/pull_test.go b/library/actions/pull_test.go index 30b5ed8c..f4c815b5 100644 --- a/library/actions/pull_test.go +++ b/library/actions/pull_test.go @@ -42,6 +42,14 @@ func TestLibrary_Pull_Getters(t *testing.T) { if test.actions.GetReopened() != test.want.GetReopened() { t.Errorf("GetReopened is %v, want %v", test.actions.GetReopened(), test.want.GetReopened()) } + + if test.actions.GetLabeled() != test.want.GetLabeled() { + t.Errorf("GetLabeled is %v, want %v", test.actions.GetLabeled(), test.want.GetLabeled()) + } + + if test.actions.GetUnlabeled() != test.want.GetUnlabeled() { + t.Errorf("GetUnlabeled is %v, want %v", test.actions.GetUnlabeled(), test.want.GetUnlabeled()) + } } } @@ -70,6 +78,8 @@ func TestLibrary_Pull_Setters(t *testing.T) { test.actions.SetSynchronize(test.want.GetSynchronize()) test.actions.SetEdited(test.want.GetEdited()) test.actions.SetReopened(test.want.GetReopened()) + test.actions.SetLabeled(test.want.GetLabeled()) + test.actions.SetUnlabeled(test.want.GetUnlabeled()) if test.actions.GetOpened() != test.want.GetOpened() { t.Errorf("SetOpened is %v, want %v", test.actions.GetOpened(), test.want.GetOpened()) @@ -86,6 +96,14 @@ func TestLibrary_Pull_Setters(t *testing.T) { if test.actions.GetReopened() != test.want.GetReopened() { t.Errorf("SetReopened is %v, want %v", test.actions.GetReopened(), test.want.GetReopened()) } + + if test.actions.GetLabeled() != test.want.GetLabeled() { + t.Errorf("SetLabeled is %v, want %v", test.actions.GetLabeled(), test.want.GetLabeled()) + } + + if test.actions.GetUnlabeled() != test.want.GetUnlabeled() { + t.Errorf("SetUnlabeled is %v, want %v", test.actions.GetUnlabeled(), test.want.GetUnlabeled()) + } } } @@ -107,7 +125,7 @@ func TestLibrary_Pull_ToMask(t *testing.T) { // setup types actions := testPull() - want := int64(constants.AllowPullOpen | constants.AllowPullSync | constants.AllowPullReopen) + want := int64(constants.AllowPullOpen | constants.AllowPullSync | constants.AllowPullReopen | constants.AllowPullUnlabel) // run test got := actions.ToMask() @@ -123,6 +141,8 @@ func testPull() *Pull { pr.SetSynchronize(true) pr.SetEdited(false) pr.SetReopened(true) + pr.SetLabeled(false) + pr.SetUnlabeled(true) return pr } diff --git a/library/actions/push_test.go b/library/actions/push_test.go index 330444ba..b0cfba47 100644 --- a/library/actions/push_test.go +++ b/library/actions/push_test.go @@ -128,6 +128,7 @@ func testMask() int64 { constants.AllowPullOpen | constants.AllowPullSync | constants.AllowPullReopen | + constants.AllowPullUnlabel | constants.AllowDeployCreate | constants.AllowCommentCreate | constants.AllowSchedule, diff --git a/library/events.go b/library/events.go index 45f1f908..e93acb10 100644 --- a/library/events.go +++ b/library/events.go @@ -68,6 +68,10 @@ func NewEventsFromSlice(events []string) *Events { mask = mask | constants.AllowPullSync case constants.EventPull + ":" + constants.ActionReopened: mask = mask | constants.AllowPullReopen + case constants.EventPull + ":" + constants.ActionLabeled: + mask = mask | constants.AllowPullLabel + case constants.EventPull + ":" + constants.ActionUnlabeled: + mask = mask | constants.AllowPullUnlabel // deployment actions case constants.EventDeploy, constants.EventDeployAlternate, constants.EventDeploy + ":" + constants.ActionCreated: @@ -111,6 +115,10 @@ func (e *Events) Allowed(event, action string) bool { allowed = e.GetPullRequest().GetEdited() case constants.EventPull + ":" + constants.ActionReopened: allowed = e.GetPullRequest().GetReopened() + case constants.EventPull + ":" + constants.ActionLabeled: + allowed = e.GetPullRequest().GetLabeled() + case constants.EventPull + ":" + constants.ActionUnlabeled: + allowed = e.GetPullRequest().GetUnlabeled() case constants.EventTag: allowed = e.GetPush().GetTag() case constants.EventComment + ":" + constants.ActionCreated: @@ -155,6 +163,14 @@ func (e *Events) List() []string { eventSlice = append(eventSlice, constants.EventPull+":"+constants.ActionReopened) } + if e.GetPullRequest().GetLabeled() { + eventSlice = append(eventSlice, constants.EventPull+":"+constants.ActionLabeled) + } + + if e.GetPullRequest().GetUnlabeled() { + eventSlice = append(eventSlice, constants.EventPull+":"+constants.ActionUnlabeled) + } + if e.GetPush().GetTag() { eventSlice = append(eventSlice, constants.EventTag) } diff --git a/library/events_test.go b/library/events_test.go index ef60257b..79f6ee16 100644 --- a/library/events_test.go +++ b/library/events_test.go @@ -122,6 +122,7 @@ func TestLibrary_Events_List(t *testing.T) { "pull_request:opened", "pull_request:synchronize", "pull_request:reopened", + "pull_request:unlabeled", "tag", "comment:created", "schedule", @@ -130,6 +131,7 @@ func TestLibrary_Events_List(t *testing.T) { wantTwo := []string{ "pull_request:edited", + "pull_request:labeled", "deployment", "comment:edited", "delete:tag", @@ -158,6 +160,7 @@ func TestLibrary_Events_NewEventsFromMask_ToDatabase(t *testing.T) { constants.AllowPullOpen | constants.AllowPullSync | constants.AllowPullReopen | + constants.AllowPullUnlabel | constants.AllowCommentCreate | constants.AllowSchedule, ) @@ -166,6 +169,7 @@ func TestLibrary_Events_NewEventsFromMask_ToDatabase(t *testing.T) { constants.AllowPushDeleteTag | constants.AllowPullEdit | constants.AllowCommentEdit | + constants.AllowPullLabel | constants.AllowDeployCreate, ) @@ -209,12 +213,12 @@ func Test_NewEventsFromSlice(t *testing.T) { }{ { name: "action specific events to e1", - events: []string{"push:branch", "push:tag", "delete:branch", "pull_request:opened", "pull_request:synchronize", "pull_request:reopened", "comment:created", "schedule:run"}, + events: []string{"push:branch", "push:tag", "delete:branch", "pull_request:opened", "pull_request:synchronize", "pull_request:reopened", "comment:created", "schedule:run", "pull_request:unlabeled"}, want: e1, }, { name: "action specific events to e2", - events: []string{"delete:tag", "pull_request:edited", "deployment:created", "comment:edited"}, + events: []string{"delete:tag", "pull_request:edited", "deployment:created", "comment:edited", "pull_request:labeled"}, want: e2, }, { @@ -232,6 +236,8 @@ func Test_NewEventsFromSlice(t *testing.T) { Reopened: &tBool, Edited: &fBool, Synchronize: &tBool, + Labeled: &fBool, + Unlabeled: &fBool, }, Deployment: &actions.Deploy{ Created: &tBool, @@ -260,6 +266,8 @@ func Test_NewEventsFromSlice(t *testing.T) { Reopened: &tBool, Edited: &fBool, Synchronize: &tBool, + Labeled: &fBool, + Unlabeled: &fBool, }, Deployment: &actions.Deploy{ Created: &fBool, @@ -306,6 +314,8 @@ func TestLibrary_Events_Allowed(t *testing.T) { {event: "pull_request", action: "synchronize", want: true}, {event: "pull_request", action: "edited", want: false}, {event: "pull_request", action: "reopened", want: true}, + {event: "pull_request", action: "labeled", want: false}, + {event: "pull_request", action: "unlabeled", want: true}, {event: "deployment", want: false}, {event: "comment", action: "created", want: true}, {event: "comment", action: "edited", want: false}, @@ -345,6 +355,8 @@ func testEvents() (*Events, *Events) { Synchronize: &tBool, Edited: &fBool, Reopened: &tBool, + Labeled: &fBool, + Unlabeled: &tBool, }, Deployment: &actions.Deploy{ Created: &fBool, @@ -370,6 +382,8 @@ func testEvents() (*Events, *Events) { Synchronize: &fBool, Edited: &tBool, Reopened: &fBool, + Labeled: &tBool, + Unlabeled: &fBool, }, Deployment: &actions.Deploy{ Created: &tBool, diff --git a/library/repo_test.go b/library/repo_test.go index de256a0e..f9eaba2b 100644 --- a/library/repo_test.go +++ b/library/repo_test.go @@ -15,7 +15,7 @@ func TestLibrary_Repo_Environment(t *testing.T) { // setup types want := map[string]string{ "VELA_REPO_ACTIVE": "true", - "VELA_REPO_ALLOW_EVENTS": "push,pull_request:opened,pull_request:synchronize,pull_request:reopened,tag,comment:created,schedule,delete:branch", + "VELA_REPO_ALLOW_EVENTS": "push,pull_request:opened,pull_request:synchronize,pull_request:reopened,pull_request:unlabeled,tag,comment:created,schedule,delete:branch", "VELA_REPO_BRANCH": "main", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_BUILD_LIMIT": "10", @@ -31,7 +31,7 @@ func TestLibrary_Repo_Environment(t *testing.T) { "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_APPROVE_BUILD": "never", "REPOSITORY_ACTIVE": "true", - "REPOSITORY_ALLOW_EVENTS": "push,pull_request:opened,pull_request:synchronize,pull_request:reopened,tag,comment:created,schedule,delete:branch", + "REPOSITORY_ALLOW_EVENTS": "push,pull_request:opened,pull_request:synchronize,pull_request:reopened,pull_request:unlabeled,tag,comment:created,schedule,delete:branch", "REPOSITORY_BRANCH": "main", "REPOSITORY_CLONE": "https://github.com/github/octocat.git", "REPOSITORY_FULL_NAME": "github/octocat", diff --git a/pipeline/container_test.go b/pipeline/container_test.go index ba08cd2b..1e959cb5 100644 --- a/pipeline/container_test.go +++ b/pipeline/container_test.go @@ -202,6 +202,7 @@ func TestPipeline_Container_Execute(t *testing.T) { If: Rules{ Status: []string{constants.StatusSuccess}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -222,6 +223,7 @@ func TestPipeline_Container_Execute(t *testing.T) { If: Rules{ Status: []string{constants.StatusSuccess}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -241,6 +243,7 @@ func TestPipeline_Container_Execute(t *testing.T) { If: Rules{ Status: []string{constants.StatusSuccess}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -260,6 +263,7 @@ func TestPipeline_Container_Execute(t *testing.T) { If: Rules{ Status: []string{constants.StatusSuccess, constants.StatusFailure}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -279,6 +283,7 @@ func TestPipeline_Container_Execute(t *testing.T) { If: Rules{ Status: []string{constants.StatusSuccess, constants.StatusFailure}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -298,6 +303,7 @@ func TestPipeline_Container_Execute(t *testing.T) { If: Rules{ Status: []string{constants.StatusSuccess, constants.StatusFailure}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -318,6 +324,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Branch: []string{"main"}, Event: []string{constants.EventPush}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -338,6 +345,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Branch: []string{"main"}, Event: []string{constants.EventPush}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -359,6 +367,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Event: []string{constants.EventPush}, Path: []string{"README.md"}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -380,6 +389,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Event: []string{constants.EventPush}, Path: []string{"README.md"}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -401,6 +411,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Event: []string{constants.EventPush}, Path: []string{"README.md"}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -422,6 +433,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Event: []string{constants.EventComment}, Comment: []string{"run vela"}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -443,6 +455,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Event: []string{constants.EventComment}, Comment: []string{"run vela"}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -464,6 +477,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Event: []string{constants.EventComment}, Comment: []string{"run vela"}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -485,6 +499,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Event: []string{constants.EventPush}, Status: []string{constants.StatusSuccess}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -506,6 +521,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Event: []string{constants.EventPush}, Status: []string{constants.StatusSuccess}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -527,6 +543,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Event: []string{constants.EventPush}, Status: []string{constants.StatusSuccess}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -570,6 +587,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Event: []string{constants.EventTag}, Status: []string{constants.StatusSuccess}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -592,6 +610,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Event: []string{constants.EventTag}, Status: []string{constants.StatusSuccess}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -614,6 +633,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Event: []string{constants.EventTag}, Status: []string{constants.StatusSuccess}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -634,6 +654,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Unless: Rules{ Status: []string{constants.StatusSuccess}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -653,6 +674,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Unless: Rules{ Status: []string{constants.StatusSuccess}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -672,6 +694,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Unless: Rules{ Status: []string{constants.StatusSuccess}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -693,6 +716,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Event: []string{constants.EventPush}, Status: []string{constants.StatusSuccess}, }, + Operator: "and", }, }, ruleData: &RuleData{ @@ -714,6 +738,7 @@ func TestPipeline_Container_Execute(t *testing.T) { Event: []string{constants.EventPush}, Status: []string{constants.StatusSuccess}, }, + Operator: "and", }, }, ruleData: &RuleData{ diff --git a/pipeline/ruleset.go b/pipeline/ruleset.go index 04880d57..a27c8ea6 100644 --- a/pipeline/ruleset.go +++ b/pipeline/ruleset.go @@ -37,6 +37,7 @@ type ( Status Ruletype `json:"status,omitempty" yaml:"status,omitempty"` Tag Ruletype `json:"tag,omitempty" yaml:"tag,omitempty"` Target Ruletype `json:"target,omitempty" yaml:"target,omitempty"` + Label Ruletype `json:"label,omitempty" yaml:"label,omitempty"` Parallel bool `json:"-" yaml:"-"` } @@ -57,6 +58,7 @@ type ( Status string `json:"status,omitempty" yaml:"status,omitempty"` Tag string `json:"tag,omitempty" yaml:"tag,omitempty"` Target string `json:"target,omitempty" yaml:"target,omitempty"` + Label []string `json:"label,omitempty" yaml:"label,omitempty"` Parallel bool `json:"-" yaml:"-"` } ) @@ -111,7 +113,8 @@ func (r *Rules) Empty() bool { len(r.Repo) == 0 && len(r.Status) == 0 && len(r.Tag) == 0 && - len(r.Target) == 0 { + len(r.Target) == 0 && + len(r.Label) == 0 { return true } @@ -119,152 +122,152 @@ func (r *Rules) Empty() bool { return false } -// Match returns true for the or operator when one of the +// Match returns true for the `or` operator when one of the // ruletypes from the rules match the provided ruledata. -// Match returns true for the and operator when all of the +// Match returns true for the `and` operator when all of the // ruletypes from the rules match the provided ruledata. For // both operators, when none of the ruletypes from the rules // match the provided ruledata, the function returns false. func (r *Rules) Match(from *RuleData, matcher, op string) (bool, error) { - // if the path ruletype is provided - if len(from.Path) > 0 { - // if the "or" operator is provided in the ruleset - if strings.EqualFold(op, constants.OperatorOr) { - // iterate through each path in the ruletype - for _, p := range from.Path { - matches, err := matches(r, from, matcher, p, constants.OperatorOr) - if err != nil { - return false, err - } - - // return true if any ruletype matches the ruledata - if matches { - return true, nil - } - } - - // return false if no match is found - return false, nil - } - - // iterate through each path in the ruletype - for _, p := range from.Path { - matches, err := matches(r, from, matcher, p, constants.OperatorAnd) - if err != nil { - return false, err - } - - // return true if any ruletype matches the ruledata - if matches { - return true, nil - } - } - - // return false if no match is found - return false, nil - } - - // if the "or" operator is provided in the ruleset - if strings.EqualFold(op, constants.OperatorOr) { - // return true if any ruletype matches the ruledata - return matches(r, from, matcher, "", constants.OperatorOr) - } - - return matches(r, from, matcher, "", constants.OperatorAnd) -} - -// Match returns true when the provided ruletype -// matches the provided ruledata. When the provided -// ruletype is empty, the function returns true for -// the `and` operator and false for the `or` operator. -func (r *Ruletype) Match(data, matcher, logic string) (bool, error) { - // return true for `and`, false for `or` if an empty ruletype is provided - if len(*r) == 0 { - return strings.EqualFold(logic, constants.OperatorAnd), nil - } - - // iterate through each pattern in the ruletype - for _, pattern := range *r { - // handle the pattern based off the matcher provided - switch matcher { - case constants.MatcherRegex, "regex": - regExpPattern, err := regexp.Compile(pattern) - if err != nil { - return false, fmt.Errorf("error in regex pattern %s: %w", pattern, err) - } - - // return true if the regexp pattern matches the ruledata - if regExpPattern.MatchString(data) { - return true, nil - } - case constants.MatcherFilepath: - fallthrough - default: - // return true if the pattern matches the ruledata - ok, _ := filepath.Match(pattern, data) - if ok { - return true, nil - } - } - } - - // return false if no match is found - return false, nil -} - -// matches is a helper function which leverages the Match method for all rules -// and returns `true` if the ruleset is indeed a match. -func matches(r *Rules, from *RuleData, matcher, path, logic string) (bool, error) { status := true var err error if len(from.Status) != 0 { - status, err = r.Status.Match(from.Status, matcher, logic) + status, err = r.Status.MatchSingle(from.Status, matcher, op) if err != nil { return false, err } } - matchBranch, err := r.Branch.Match(from.Branch, matcher, logic) + matchBranch, err := r.Branch.MatchSingle(from.Branch, matcher, op) if err != nil { return false, err } - matchComment, err := r.Comment.Match(from.Comment, matcher, logic) + matchComment, err := r.Comment.MatchSingle(from.Comment, matcher, op) if err != nil { return false, err } - matchEvent, err := r.Event.Match(from.Event, matcher, logic) + matchEvent, err := r.Event.MatchSingle(from.Event, matcher, op) if err != nil { return false, err } - matchPath, err := r.Path.Match(path, matcher, logic) + matchPath, err := r.Path.MatchMultiple(from.Path, matcher, op) if err != nil { return false, err } - matchRepo, err := r.Repo.Match(from.Repo, matcher, logic) + matchRepo, err := r.Repo.MatchSingle(from.Repo, matcher, op) if err != nil { return false, err } - matchTag, err := r.Tag.Match(from.Tag, matcher, logic) + matchTag, err := r.Tag.MatchSingle(from.Tag, matcher, op) if err != nil { return false, err } - matchTarget, err := r.Target.Match(from.Target, matcher, logic) + matchTarget, err := r.Target.MatchSingle(from.Target, matcher, op) if err != nil { return false, err } - switch logic { - case constants.OperatorAnd: - return (matchBranch && matchComment && matchEvent && matchPath && matchRepo && matchTag && matchTarget && status), nil + matchLabel, err := r.Label.MatchMultiple(from.Label, matcher, op) + if err != nil { + return false, err + } + + switch op { + case constants.OperatorOr: + return (matchBranch || matchComment || matchEvent || matchPath || matchRepo || matchTag || matchTarget || matchLabel || status), nil + default: + return (matchBranch && matchComment && matchEvent && matchPath && matchRepo && matchTag && matchTarget && matchLabel && status), nil + } +} + +// MatchSingle returns true when the provided ruletype +// matches the provided ruledata. When the provided +// ruletype is empty, the function returns true for +// the `and` operator and false for the `or` operator. +func (r *Ruletype) MatchSingle(data, matcher, logic string) (bool, error) { + // return true for `and`, false for `or` if an empty ruletype is provided + if len(*r) == 0 { + return strings.EqualFold(logic, constants.OperatorAnd), nil + } + + // iterate through each pattern in the ruletype + for _, pattern := range *r { + match, err := match(data, matcher, pattern) + if err != nil { + return false, err + } + + if match { + return true, nil + } + } + + // return false if no match is found + return false, nil +} + +// MatchMultiple returns true when the provided ruletype +// matches the provided ruledata. When the provided +// ruletype is empty, the function returns true for +// the `and` operator and false for the `or` operator. +func (r *Ruletype) MatchMultiple(data []string, matcher, logic string) (bool, error) { + // return true for `and`, false for `or` if an empty ruletype is provided + if len(*r) == 0 { + return strings.EqualFold(logic, constants.OperatorAnd), nil + } + + // iterate through each pattern in the ruletype + for _, pattern := range *r { + for _, value := range data { + match, err := match(value, matcher, pattern) + if err != nil { + return false, err + } + + if match { + return true, nil + } + } + } + + // return false if no match is found + return false, nil +} + +// match is a helper function that compares data against a pattern +// and returns true if the data matches the pattern, depending on +// matcher specified. +func match(data, matcher, pattern string) (bool, error) { + // handle the pattern based off the matcher provided + switch matcher { + case constants.MatcherRegex, "regex": + regExpPattern, err := regexp.Compile(pattern) + if err != nil { + return false, fmt.Errorf("error in regex pattern %s: %w", pattern, err) + } + + // return true if the regexp pattern matches the ruledata + if regExpPattern.MatchString(data) { + return true, nil + } + case constants.MatcherFilepath: + fallthrough default: - return (matchBranch || matchComment || matchEvent || matchPath || matchRepo || matchTag || matchTarget || status), nil + // return true if the pattern matches the ruledata + ok, _ := filepath.Match(pattern, data) + if ok { + return true, nil + } } + + // return false if no match is found + return false, nil } diff --git a/pipeline/ruleset_test.go b/pipeline/ruleset_test.go index a310ed71..7ea87a31 100644 --- a/pipeline/ruleset_test.go +++ b/pipeline/ruleset_test.go @@ -20,62 +20,62 @@ func TestPipeline_Ruleset_Match(t *testing.T) { {ruleset: &Ruleset{}, data: &RuleData{Branch: "main"}, want: true}, // If with and operator { - ruleset: &Ruleset{If: Rules{Branch: []string{"main"}}}, + ruleset: &Ruleset{If: Rules{Branch: []string{"main"}}, Operator: "and"}, data: &RuleData{Branch: "main", Comment: "rerun", Event: "push", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: ""}, want: true, }, { - ruleset: &Ruleset{If: Rules{Branch: []string{"main"}}}, + ruleset: &Ruleset{If: Rules{Branch: []string{"main"}}, Operator: "and"}, data: &RuleData{Branch: "dev", Comment: "rerun", Event: "push", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: ""}, want: false, }, { - ruleset: &Ruleset{If: Rules{Branch: []string{"main"}, Event: []string{"push"}}}, + ruleset: &Ruleset{If: Rules{Branch: []string{"main"}, Event: []string{"push"}}, Operator: "and"}, data: &RuleData{Branch: "main", Comment: "rerun", Event: "push", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: ""}, want: true, }, { - ruleset: &Ruleset{If: Rules{Branch: []string{"main"}, Event: []string{"push"}}}, + ruleset: &Ruleset{If: Rules{Branch: []string{"main"}, Event: []string{"push"}}, Operator: "and"}, data: &RuleData{Branch: "main", Comment: "rerun", Event: "pull_request", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: ""}, want: false, }, { - ruleset: &Ruleset{If: Rules{Path: []string{"foo.txt", "/foo/bar.txt"}}}, + ruleset: &Ruleset{If: Rules{Path: []string{"foo.txt", "/foo/bar.txt"}}, Operator: "and"}, data: &RuleData{Branch: "main", Comment: "rerun", Event: "pull_request", Path: []string{}, Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: ""}, want: false, }, { - ruleset: &Ruleset{If: Rules{Comment: []string{"rerun"}}}, + ruleset: &Ruleset{If: Rules{Comment: []string{"rerun"}}, Operator: "and"}, data: &RuleData{Branch: "dev", Comment: "rerun", Event: "push", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: ""}, want: true, }, { - ruleset: &Ruleset{If: Rules{Comment: []string{"rerun"}}}, + ruleset: &Ruleset{If: Rules{Comment: []string{"rerun"}}, Operator: "and"}, data: &RuleData{Branch: "dev", Comment: "ok to test", Event: "push", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: ""}, want: false, }, { - ruleset: &Ruleset{If: Rules{Event: []string{"deployment"}, Target: []string{"production"}}}, + ruleset: &Ruleset{If: Rules{Event: []string{"deployment"}, Target: []string{"production"}}, Operator: "and"}, data: &RuleData{Branch: "dev", Comment: "", Event: "deployment", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: "production"}, want: true, }, { - ruleset: &Ruleset{If: Rules{Event: []string{"deployment"}, Target: []string{"production"}}}, + ruleset: &Ruleset{If: Rules{Event: []string{"deployment"}, Target: []string{"production"}}, Operator: "and"}, data: &RuleData{Branch: "dev", Comment: "", Event: "deployment", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: "stage"}, want: false, }, { - ruleset: &Ruleset{If: Rules{Event: []string{"schedule"}, Target: []string{"weekly"}}}, + ruleset: &Ruleset{If: Rules{Event: []string{"schedule"}, Target: []string{"weekly"}}, Operator: "and"}, data: &RuleData{Branch: "dev", Comment: "", Event: "schedule", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: "weekly"}, want: true, }, { - ruleset: &Ruleset{If: Rules{Event: []string{"schedule"}, Target: []string{"weekly"}}}, + ruleset: &Ruleset{If: Rules{Event: []string{"schedule"}, Target: []string{"weekly"}}, Operator: "and"}, data: &RuleData{Branch: "dev", Comment: "", Event: "schedule", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: "nightly"}, want: false, }, { - ruleset: &Ruleset{If: Rules{Status: []string{"success", "failure"}}}, + ruleset: &Ruleset{If: Rules{Status: []string{"success", "failure"}}, Operator: "and"}, data: &RuleData{Branch: "dev", Comment: "ok to test", Event: "push", Repo: "octocat/hello-world", Status: "failure", Tag: "refs/heads/main", Target: ""}, want: true, }, @@ -107,27 +107,27 @@ func TestPipeline_Ruleset_Match(t *testing.T) { }, // Unless with and operator { - ruleset: &Ruleset{Unless: Rules{Branch: []string{"main"}}}, + ruleset: &Ruleset{Unless: Rules{Branch: []string{"main"}}, Operator: "and"}, data: &RuleData{Branch: "main", Comment: "rerun", Event: "push", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: ""}, want: false, }, { - ruleset: &Ruleset{Unless: Rules{Branch: []string{"main"}}}, + ruleset: &Ruleset{Unless: Rules{Branch: []string{"main"}}, Operator: "and"}, data: &RuleData{Branch: "dev", Comment: "rerun", Event: "push", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: ""}, want: true, }, { - ruleset: &Ruleset{Unless: Rules{Branch: []string{"main"}, Event: []string{"push"}}}, + ruleset: &Ruleset{Unless: Rules{Branch: []string{"main"}, Event: []string{"push"}}, Operator: "and"}, data: &RuleData{Branch: "main", Comment: "rerun", Event: "push", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: ""}, want: false, }, { - ruleset: &Ruleset{Unless: Rules{Branch: []string{"main"}, Event: []string{"push"}}}, + ruleset: &Ruleset{Unless: Rules{Branch: []string{"main"}, Event: []string{"push"}}, Operator: "and"}, data: &RuleData{Branch: "main", Comment: "rerun", Event: "pull_request", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: ""}, want: true, }, { - ruleset: &Ruleset{Unless: Rules{Path: []string{"foo.txt", "/foo/bar.txt"}}}, + ruleset: &Ruleset{Unless: Rules{Path: []string{"foo.txt", "/foo/bar.txt"}}, Operator: "and"}, data: &RuleData{Branch: "main", Comment: "rerun", Event: "pull_request", Path: []string{}, Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: ""}, want: true, }, @@ -425,16 +425,52 @@ func TestPipeline_Rules_Match(t *testing.T) { }, { rules: &Rules{Event: []string{"push", "pull_request"}, Tag: []string{"release/*"}}, - data: &RuleData{Branch: "main", Event: "push", Repo: "octocat/hello-world", Status: "pending", Tag: "release/*", Target: ""}, + data: &RuleData{Branch: "main", Event: "tag", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: ""}, operator: "or", + want: false, + }, + { + rules: &Rules{Event: []string{"pull_request:labeled"}, Label: []string{"enhancement", "documentation"}}, + data: &RuleData{Branch: "main", Event: "pull_request:labeled", Repo: "octocat/hello-world", Status: "pending", Label: []string{"documentation"}}, + operator: "and", want: true, }, { - rules: &Rules{Event: []string{"push", "pull_request"}, Tag: []string{"release/*"}}, - data: &RuleData{Branch: "main", Event: "tag", Repo: "octocat/hello-world", Status: "pending", Tag: "refs/heads/main", Target: ""}, - operator: "or", + rules: &Rules{Event: []string{"pull_request:labeled"}, Label: []string{"enhancement", "documentation"}}, + data: &RuleData{Branch: "main", Event: "pull_request:labeled", Repo: "octocat/hello-world", Status: "pending", Label: []string{"support"}}, + operator: "and", + want: false, + }, + { + rules: &Rules{Event: []string{"pull_request:unlabeled"}, Label: []string{"enhancement", "documentation"}}, + data: &RuleData{Branch: "main", Event: "pull_request:unlabeled", Repo: "octocat/hello-world", Status: "pending", Label: []string{"documentation"}}, + operator: "and", + want: true, + }, + { + rules: &Rules{Event: []string{"pull_request:unlabeled"}, Label: []string{"enhancement"}}, + data: &RuleData{Branch: "main", Event: "pull_request:unlabeled", Repo: "octocat/hello-world", Status: "pending", Label: []string{"documentation"}}, + operator: "and", want: false, }, + { + rules: &Rules{Event: []string{"push"}, Label: []string{"enhancement", "documentation"}}, + data: &RuleData{Branch: "main", Event: "push", Repo: "octocat/hello-world", Status: "pending", Label: []string{"documentation"}}, + operator: "and", + want: true, + }, + { + rules: &Rules{Event: []string{"push"}, Label: []string{"enhancement"}}, + data: &RuleData{Branch: "main", Event: "push", Repo: "octocat/hello-world", Status: "pending", Label: []string{"documentation"}}, + operator: "and", + want: false, + }, + { + rules: &Rules{Event: []string{"push"}, Label: []string{"enhancement"}}, + data: &RuleData{Branch: "main", Event: "push", Repo: "octocat/hello-world", Status: "pending", Label: []string{"documentation"}}, + operator: "or", + want: true, + }, } // run test @@ -490,6 +526,9 @@ func TestPipeline_Ruletype_MatchAnd(t *testing.T) { // Target with filepath matcher {matcher: "filepath", rule: []string{"production"}, pattern: "production", want: true}, {matcher: "filepath", rule: []string{"stage"}, pattern: "production", want: false}, + // Label with filepath matcher + {matcher: "filepath", rule: []string{"enhancement", "documentation"}, pattern: "documentation", want: true}, + {matcher: "filepath", rule: []string{"enhancement", "documentation"}, pattern: "question", want: false}, // Empty with regex matcher {matcher: "regexp", rule: []string{}, pattern: "main", want: true}, {matcher: "regexp", rule: []string{}, pattern: "push", want: true}, @@ -525,11 +564,14 @@ func TestPipeline_Ruletype_MatchAnd(t *testing.T) { // Target with regex matcher {matcher: "regexp", rule: []string{"production"}, pattern: "production", want: true}, {matcher: "regexp", rule: []string{"stage"}, pattern: "production", want: false}, + // Label with regexp matcher + {matcher: "regexp", rule: []string{"enhancement", "documentation"}, pattern: "documentation", want: true}, + {matcher: "regexp", rule: []string{"enhancement", "documentation"}, pattern: "question", want: false}, } // run test for _, test := range tests { - got, _ := test.rule.Match(test.pattern, test.matcher, constants.OperatorAnd) + got, _ := test.rule.MatchSingle(test.pattern, test.matcher, constants.OperatorAnd) if got != test.want { t.Errorf("MatchAnd for %s matcher is %v, want %v", test.matcher, got, test.want) @@ -572,6 +614,9 @@ func TestPipeline_Ruletype_MatchOr(t *testing.T) { // Target with filepath matcher {matcher: "filepath", rule: []string{"production"}, pattern: "production", want: true}, {matcher: "filepath", rule: []string{"stage"}, pattern: "production", want: false}, + // Label with filepath matcher + {matcher: "filepath", rule: []string{"enhancement", "documentation"}, pattern: "documentation", want: true}, + {matcher: "filepath", rule: []string{"enhancement", "documentation"}, pattern: "question", want: false}, // Empty with regexp matcher {matcher: "regexp", rule: []string{}, pattern: "main", want: false}, {matcher: "regexp", rule: []string{}, pattern: "push", want: false}, @@ -599,11 +644,14 @@ func TestPipeline_Ruletype_MatchOr(t *testing.T) { // Target with regexp matcher {matcher: "regexp", rule: []string{"production"}, pattern: "production", want: true}, {matcher: "regexp", rule: []string{"stage"}, pattern: "production", want: false}, + // Label with regexp matcher + {matcher: "regexp", rule: []string{"enhancement", "documentation"}, pattern: "documentation", want: true}, + {matcher: "regexp", rule: []string{"enhancement", "documentation"}, pattern: "question", want: false}, } // run test for _, test := range tests { - got, _ := test.rule.Match(test.pattern, test.matcher, constants.OperatorOr) + got, _ := test.rule.MatchSingle(test.pattern, test.matcher, constants.OperatorOr) if got != test.want { t.Errorf("MatchOr for %s matcher is %v, want %v", test.matcher, got, test.want) diff --git a/webhook.go b/webhook.go index 1941e38f..579e8230 100644 --- a/webhook.go +++ b/webhook.go @@ -19,6 +19,7 @@ type PullRequest struct { Comment string Number int IsFromFork bool + Labels []string } // Webhook defines a struct that is used to return diff --git a/yaml/ruleset.go b/yaml/ruleset.go index 7b8fc615..2fe23e16 100644 --- a/yaml/ruleset.go +++ b/yaml/ruleset.go @@ -30,6 +30,7 @@ type ( Status []string `yaml:"status,omitempty,flow" json:"status,omitempty" jsonschema:"enum=[failure],enum=[success],description=Limits the execution of a step to matching build statuses.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-ruleset-tag"` Tag []string `yaml:"tag,omitempty,flow" json:"tag,omitempty" jsonschema:"description=Limits the execution of a step to matching build tag references.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-ruleset-tag"` Target []string `yaml:"target,omitempty,flow" json:"target,omitempty" jsonschema:"description=Limits the execution of a step to matching build deployment targets.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-ruleset-tag"` + Label []string `yaml:"label,omitempty,flow" json:"label,omitempty" jsonschema:"description=Limits step execution to match on pull requests labels.\nReference: https://go-vela.github.io/docs/reference/yaml/steps/#the-ruleset-tag"` } ) @@ -84,6 +85,7 @@ func (r *Ruleset) UnmarshalYAML(unmarshal func(interface{}) error) error { advanced.If.Status = append(advanced.If.Status, simple.Status...) advanced.If.Tag = append(advanced.If.Tag, simple.Tag...) advanced.If.Target = append(advanced.If.Target, simple.Target...) + advanced.If.Label = append(advanced.If.Label, simple.Label...) // set ruleset `if` to advanced `if` rules r.If = advanced.If @@ -113,6 +115,7 @@ func (r *Rules) ToPipeline() *pipeline.Rules { Status: r.Status, Tag: r.Tag, Target: r.Target, + Label: r.Label, } } @@ -128,6 +131,7 @@ func (r *Rules) UnmarshalYAML(unmarshal func(interface{}) error) error { Status raw.StringSlice Tag raw.StringSlice Target raw.StringSlice + Label raw.StringSlice }) // attempt to unmarshal rules @@ -140,6 +144,7 @@ func (r *Rules) UnmarshalYAML(unmarshal func(interface{}) error) error { r.Status = rules.Status r.Tag = rules.Tag r.Target = rules.Target + r.Label = rules.Label // account for users who use non-scoped pull_request event events := []string{} diff --git a/yaml/ruleset_test.go b/yaml/ruleset_test.go index 762bb904..559395ba 100644 --- a/yaml/ruleset_test.go +++ b/yaml/ruleset_test.go @@ -22,12 +22,13 @@ func TestYaml_Ruleset_ToPipeline(t *testing.T) { If: Rules{ Branch: []string{"main"}, Comment: []string{"test comment"}, - Event: []string{"push"}, + Event: []string{"push", "pull_request:labeled"}, Path: []string{"foo.txt"}, Repo: []string{"github/octocat"}, Status: []string{"success"}, Tag: []string{"v0.1.0"}, Target: []string{"production"}, + Label: []string{"enhancement"}, }, Unless: Rules{ Branch: []string{"main"}, @@ -47,12 +48,13 @@ func TestYaml_Ruleset_ToPipeline(t *testing.T) { If: pipeline.Rules{ Branch: []string{"main"}, Comment: []string{"test comment"}, - Event: []string{"push"}, + Event: []string{"push", "pull_request:labeled"}, Path: []string{"foo.txt"}, Repo: []string{"github/octocat"}, Status: []string{"success"}, Tag: []string{"v0.1.0"}, Target: []string{"production"}, + Label: []string{"enhancement"}, }, Unless: pipeline.Rules{ Branch: []string{"main"}, @@ -167,22 +169,24 @@ func TestYaml_Rules_ToPipeline(t *testing.T) { rules: &Rules{ Branch: []string{"main"}, Comment: []string{"test comment"}, - Event: []string{"push"}, + Event: []string{"push", "pull_request:labeled"}, Path: []string{"foo.txt"}, Repo: []string{"github/octocat"}, Status: []string{"success"}, Tag: []string{"v0.1.0"}, Target: []string{"production"}, + Label: []string{"enhancement"}, }, want: &pipeline.Rules{ Branch: []string{"main"}, Comment: []string{"test comment"}, - Event: []string{"push"}, + Event: []string{"push", "pull_request:labeled"}, Path: []string{"foo.txt"}, Repo: []string{"github/octocat"}, Status: []string{"success"}, Tag: []string{"v0.1.0"}, Target: []string{"production"}, + Label: []string{"enhancement"}, }, }, }