diff --git a/README.md b/README.md index bbc1028c..1bf1fdfb 100644 --- a/README.md +++ b/README.md @@ -248,8 +248,20 @@ if: - "label-1" - "label-2" + # "repository" is satisfied if the pull request repository matches any one of the + # patterns within the "matches" list or does not match all of the patterns + # within the "not_matches" list. + # + # Note: Double-quote strings must escape backslashes while single/plain do not. + # See the Notes on YAML Syntax section of this README for more information. + repository: + matches: + - "palantir/policy.*" + not_matches: + - "palantir/.*docs" + # "title" is satisfied if the pull request title matches any one of the - # patterns within the "matches" list, and does not match all of the patterns + # patterns within the "matches" list or does not match all of the patterns # within the "not_matches" list. # e.g. this predicate triggers for titles including "BREAKING CHANGE" or titles # that are not marked as docs/style/chore changes (using conventional commits diff --git a/policy/predicate/predicates.go b/policy/predicate/predicates.go index 45a21d6a..806d7c00 100644 --- a/policy/predicate/predicates.go +++ b/policy/predicate/predicates.go @@ -32,7 +32,8 @@ type Predicates struct { HasLabels *HasLabels `yaml:"has_labels"` - Title *Title `yaml:"title"` + Repository *Repository `yaml:"repository"` + Title *Title `yaml:"title"` HasValidSignatures *HasValidSignatures `yaml:"has_valid_signatures"` HasValidSignaturesBy *HasValidSignaturesBy `yaml:"has_valid_signatures_by"` @@ -81,6 +82,10 @@ func (p *Predicates) Predicates() []Predicate { ps = append(ps, Predicate(p.HasLabels)) } + if p.Repository != nil { + ps = append(ps, Predicate(p.Repository)) + } + if p.Title != nil { ps = append(ps, Predicate(p.Title)) } diff --git a/policy/predicate/repository.go b/policy/predicate/repository.go new file mode 100644 index 00000000..da8716d1 --- /dev/null +++ b/policy/predicate/repository.go @@ -0,0 +1,76 @@ +// Copyright 2022 Palantir Technologies, Inc. +// +// 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 predicate + +import ( + "context" + + "github.com/palantir/policy-bot/policy/common" + "github.com/palantir/policy-bot/pull" +) + +type Repository struct { + Matches []common.Regexp `yaml:"matches"` + NotMatches []common.Regexp `yaml:"not_matches"` +} + +var _ Predicate = Repository{} + +func (pred Repository) Evaluate(ctx context.Context, prctx pull.Context) (*common.PredicateResult, error) { + owner := prctx.RepositoryOwner() + repo := prctx.RepositoryName() + repoFullName := owner + "/" + repo + + predicateResult := common.PredicateResult{ + ValuePhrase: "repositories", + Values: []string{repoFullName}, + ConditionPhrase: "meet the pattern requirement", + } + + var matchPatterns, notMatchPatterns []string + + for _, reg := range pred.Matches { + matchPatterns = append(matchPatterns, reg.String()) + } + + for _, reg := range pred.NotMatches { + notMatchPatterns = append(notMatchPatterns, reg.String()) + } + + if len(pred.Matches) > 0 { + if anyMatches(pred.Matches, repoFullName) { + predicateResult.ConditionsMap = map[string][]string{"match": matchPatterns} + predicateResult.Description = "PR Repository matches a Match pattern" + predicateResult.Satisfied = true + return &predicateResult, nil + } + } + + if len(pred.NotMatches) > 0 { + if !anyMatches(pred.NotMatches, repoFullName) { + predicateResult.ConditionsMap = map[string][]string{"not match": notMatchPatterns} + predicateResult.Description = "PR Repository doesn't match a NotMatch pattern" + predicateResult.Satisfied = true + return &predicateResult, nil + } + } + predicateResult.Satisfied = false + predicateResult.ConditionsMap = map[string][]string{"match": matchPatterns, "not match": notMatchPatterns} + return &predicateResult, nil +} + +func (pred Repository) Trigger() common.Trigger { + return common.TriggerPullRequest +} diff --git a/policy/predicate/repository_test.go b/policy/predicate/repository_test.go new file mode 100644 index 00000000..b098fad6 --- /dev/null +++ b/policy/predicate/repository_test.go @@ -0,0 +1,199 @@ +// Copyright 2022 Palantir Technologies, Inc. +// +// 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 predicate + +import ( + "context" + "regexp" + "testing" + + "github.com/palantir/policy-bot/policy/common" + "github.com/palantir/policy-bot/pull" + "github.com/palantir/policy-bot/pull/pulltest" + "github.com/stretchr/testify/assert" +) + +func TestRepositoryWithNotMatchRule(t *testing.T) { + p := &Repository{ + NotMatches: []common.Regexp{ + common.NewCompiledRegexp(regexp.MustCompile("palantir/docs")), + }, + Matches: []common.Regexp{}, + } + + runRepositoryTestCase(t, p, []RepositoryTestCase{ + { + "matches pattern", + &pulltest.Context{ + OwnerValue: "palantir", + RepoValue: "docs", + }, + &common.PredicateResult{ + Satisfied: false, + Values: []string{"palantir/docs"}, + ConditionsMap: map[string][]string{ + "not match": {"palantir/docs"}, + "match": nil, + }, + }, + }, + { + "does not match pattern", + &pulltest.Context{ + OwnerValue: "palantir", + RepoValue: "policy-bot", + }, + &common.PredicateResult{ + Satisfied: true, + Values: []string{"palantir/policy-bot"}, + ConditionsMap: map[string][]string{ + "not match": {"palantir/docs"}, + }, + }, + }, + }) +} + +func TestRepositoryWithMatchRule(t *testing.T) { + p := &Repository{ + NotMatches: []common.Regexp{}, + Matches: []common.Regexp{ + common.NewCompiledRegexp(regexp.MustCompile("palantir/policy.*")), + }, + } + + runRepositoryTestCase(t, p, []RepositoryTestCase{ + { + "matches pattern", + &pulltest.Context{ + OwnerValue: "palantir", + RepoValue: "policy-bot", + }, + &common.PredicateResult{ + Satisfied: true, + Values: []string{"palantir/policy-bot"}, + ConditionsMap: map[string][]string{ + "match": {"palantir/policy.*"}, + }, + }, + }, + { + "does not match pattern", + &pulltest.Context{ + OwnerValue: "palantir", + RepoValue: "nanda-bot", + }, + &common.PredicateResult{ + Satisfied: false, + Values: []string{"palantir/nanda-bot"}, + ConditionsMap: map[string][]string{ + "not match": nil, + "match": {"palantir/policy.*"}, + }, + }, + }, + }) +} + +func TestRepositoryWithMixedRules(t *testing.T) { + p := &Repository{ + NotMatches: []common.Regexp{ + common.NewCompiledRegexp(regexp.MustCompile("palantir/.*docs")), + common.NewCompiledRegexp(regexp.MustCompile("palantir/special-repo")), + }, + Matches: []common.Regexp{ + common.NewCompiledRegexp(regexp.MustCompile("palantir/policy.*")), + }, + } + + runRepositoryTestCase(t, p, []RepositoryTestCase{ + { + "matches pattern in match list", + &pulltest.Context{ + OwnerValue: "palantir", + RepoValue: "policy-bot", + }, + &common.PredicateResult{ + Satisfied: true, + Values: []string{"palantir/policy-bot"}, + ConditionsMap: map[string][]string{ + "match": {"palantir/policy.*"}, + }, + }, + }, + { + "matches pattern in not_match list", + &pulltest.Context{ + OwnerValue: "palantir", + RepoValue: "docs", + }, + &common.PredicateResult{ + Satisfied: false, + Values: []string{"palantir/docs"}, + ConditionsMap: map[string][]string{ + "not match": {"palantir/.*docs", "palantir/special-repo"}, + "match": {"palantir/policy.*"}, + }, + }, + }, + { + "matches pattern in both lists", + &pulltest.Context{ + OwnerValue: "palantir", + RepoValue: "policy-bot-docs", + }, + &common.PredicateResult{ + Satisfied: true, + Values: []string{"palantir/policy-bot-docs"}, + ConditionsMap: map[string][]string{ + "match": {"palantir/policy.*"}, + }, + }, + }, + { + "does not match any pattern", + &pulltest.Context{ + OwnerValue: "palantir", + RepoValue: "some-other-repo", + }, + &common.PredicateResult{ + Satisfied: true, + Values: []string{"palantir/some-other-repo"}, + ConditionsMap: map[string][]string{ + "not match": {"palantir/.*docs", "palantir/special-repo"}, + }, + }, + }, + }) +} + +type RepositoryTestCase struct { + name string + context pull.Context + ExpectedPredicateResult *common.PredicateResult +} + +func runRepositoryTestCase(t *testing.T, p Predicate, cases []RepositoryTestCase) { + ctx := context.Background() + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + predicateResult, err := p.Evaluate(ctx, tc.context) + if assert.NoError(t, err, "evaluation failed") { + assertPredicateResult(t, tc.ExpectedPredicateResult, predicateResult) + } + }) + } +}