Repository: palantir/policy-bot
Pull request #461 (2022-08-26 21:13:30)

add repository predicate
Sometimes we find ourselves wanting to make exceptions for certain policies, but don't want to have individual policy files in the repos that are exceptional. This PR would add a new repository predicate that would perform simple matching on the repository and from that information decide whether or not a rule should be evaluated. I could see this being especially useful for organizations that want to use a centralized policy to govern all repos.

Another area where an addition like this brings value is when setting up policy bot as a github application, you're given an option to install on all repositories or on select repositories. If an organization has 100s of repositories it may be difficult to configure policy bot on all but some. On the other hand, if you select the all repos option, you end up with pending status checks on PRs that may cause confusion. This configuration would allow you to configure automatic approval for certain repos allowing orgs to put their exceptions "in code" and select the all repos option which makes sure that most new repos will receive the benefits of policy bot automatically without any additional configuration.

```yaml
policy:
  approval:
    - or:
      - normal review
      - docs review

approval_rules:
- name: normal review
   if:
     repository:
       not_matches:
        - "palantir/.*docs"
   requires:
     count: 1
- name: docs review
  if:
    repository:
      matches:
       - "palantir/.*docs"
  requires:
    count: 0
```

Let me know what you think of this idea, and if you think there's a better way of accomplishing the same. And as always, thanks for maintaining an awesome tool!



Patch:
diff --git a/README.md b/README.md
index bbc1028..1bf1fdf 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 45a21d6..806d7c0 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 0000000..da8716d
--- /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
+}


Test patch:
diff --git a/policy/predicate/repository_test.go b/policy/predicate/repository_test.go
new file mode 100644
index 0000000..b098fad
--- /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)
+			}
+		})
+	}
+}