Repository: srvaroa/labeler (go)

Issue:
[Feature] Apply labels based on author's team membership
Rather than maintaining a separate list of members in this config file, it would be great to be able to reference a [Github team](https://docs.github.com/en/organizations/organizing-members-into-teams/about-teams) that is already maintained to have an accurate list of authors. This is similar to the functionality available in [Auto Request Review](https://github.com/necojackarc/auto-request-review?tab=readme-ov-file#reviewers-configuration) by using the prefix `team:`. If this is already possible, I'd love to know how!

Pull request:
Support author-in-team
Fixes #145 

Signed-off-by: Galo Navarro <anglorvaroa@gmail.com>


Patch:
diff --git a/README.md b/README.md
index 922baa1..456511e 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,7 @@ configurable matching rules. Available conditions:
 
 * [Age](#age): label based on the age of a PR or Issue.
 * [Author can merge](#author-can-merge): label based on whether the author can merge the PR
+* [Author is member of team](#author-in-team): label based on whether the author is an active member of the given team
 * [Authors](#authors): label based on the PR/Issue authors
 * [Base branch](#base-branch): label based on the PR's base branch name
 * [Body](#body): label based on the PR/Issue body
@@ -339,6 +340,17 @@ This is implemented by checking if the author is an owner of the repo.
 ```yaml
 author-can-merge: True
 ```
+
+
+### Author is member (PRs and Issues) <a name="author-in-team" />
+
+This condition is satisfied when the author of the PR is an active
+member of the given team.
+
+```yaml
+author-is-member: CoreTeam
+```
+
 ### Authors (PRs and Issues)  <a name="authors" />
 
 This condition is satisfied when the author of the PR or Issue matches
diff --git a/cmd/action.go b/cmd/action.go
index 97a1c66..137be2d 100644
--- a/cmd/action.go
+++ b/cmd/action.go
@@ -244,6 +244,13 @@ func newLabeler(gh *github.Client, config *labeler.LabelerConfigV1) *labeler.Lab
 					owner, repo, &github.PullRequestListOptions{})
 				return prs, err
 			},
+			IsUserMemberOfTeam: func(user, team string) (bool, error) {
+				membership, _, err := gh.Organizations.GetOrgMembership(ctx, user, team)
+				if err != nil {
+					return false, err
+				}
+				return membership.GetState() == "active", nil
+			},
 		},
 		Client: labeler.NewDefaultHttpClient(),
 	}
diff --git a/pkg/condition_author_in_team.go b/pkg/condition_author_in_team.go
new file mode 100644
index 0000000..96c53d9
--- /dev/null
+++ b/pkg/condition_author_in_team.go
@@ -0,0 +1,23 @@
+package labeler
+
+import (
+	"fmt"
+)
+
+func AuthorInTeamCondition(l *Labeler) Condition {
+	return Condition{
+		GetName: func() string {
+			return "Author is member of team"
+		},
+		CanEvaluate: func(target *Target) bool {
+			return true
+		},
+		Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) {
+			if len(matcher.AuthorInTeam) <= 0 {
+				return false, fmt.Errorf("author-in-team is not set in config")
+			}
+			// check if author is a member of team
+			return l.GitHubFacade.IsUserMemberOfTeam(target.Author, matcher.AuthorInTeam)
+		},
+	}
+}
diff --git a/pkg/labeler.go b/pkg/labeler.go
index 21250b5..1f61624 100644
--- a/pkg/labeler.go
+++ b/pkg/labeler.go
@@ -22,6 +22,7 @@ type LabelMatcher struct {
 	Age            string
 	AuthorCanMerge string `yaml:"author-can-merge"`
 	Authors        []string
+	AuthorInTeam   string `yaml:"author-in-team"`
 	BaseBranch     string `yaml:"base-branch"`
 	Body           string
 	Branch         string
@@ -65,9 +66,10 @@ type LabelUpdates struct {
 
 // Just to make this mockable..
 type GitHubFacade struct {
-	GetRawDiff       func(owner, repo string, prNumber int) (string, error)
-	ListIssuesByRepo func(owner, repo string) ([]*gh.Issue, error)
-	ListPRs          func(owner, repo string) ([]*gh.PullRequest, error)
+	GetRawDiff         func(owner, repo string, prNumber int) (string, error)
+	ListIssuesByRepo   func(owner, repo string) ([]*gh.Issue, error)
+	ListPRs            func(owner, repo string) ([]*gh.PullRequest, error)
+	IsUserMemberOfTeam func(user, team string) (bool, error)
 }
 
 type Labeler struct {
@@ -225,6 +227,7 @@ func (l *Labeler) findMatches(target *Target, config *LabelerConfigV1) (LabelUpd
 		AgeCondition(l),
 		AuthorCondition(),
 		AuthorCanMergeCondition(),
+		AuthorInTeamCondition(l),
 		BaseBranchCondition(),
 		BodyCondition(),
 		BranchCondition(),


Test patch:
diff --git a/pkg/labeler_test.go b/pkg/labeler_test.go
index 6a24c92..50316b6 100644
--- a/pkg/labeler_test.go
+++ b/pkg/labeler_test.go
@@ -983,6 +983,46 @@ func TestHandleEvent(t *testing.T) {
 			initialLabels:  []string{"Meh"},
 			expectedLabels: []string{"Meh", "Test"},
 		},
+		{
+			event:    "issues",
+			payloads: []string{"issue_open"},
+			name:     "Add a label to issue when author is in team",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					{
+						Label:        "ShouldAppear",
+						AuthorInTeam: "team-with-srvaroa",
+					},
+					{
+						Label:        "ShouldNotAppear",
+						AuthorInTeam: "team-with",
+					},
+				},
+			},
+			initialLabels:  []string{"Meh"},
+			expectedLabels: []string{"Meh", "ShouldAppear"},
+		},
+		{
+			event:    "pull_request",
+			payloads: []string{"create_pr"},
+			name:     "Add a label to PR when author is in team",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					{
+						Label:        "ShouldAppear",
+						AuthorInTeam: "team-with-srvaroa",
+					},
+					{
+						Label:        "ShouldNotAppear",
+						AuthorInTeam: "team-with",
+					},
+				},
+			},
+			initialLabels:  []string{"Meh"},
+			expectedLabels: []string{"Meh", "ShouldAppear"},
+		},
 	}
 
 	for _, tc := range testCases {
@@ -1034,6 +1074,10 @@ func NewTestLabeler(t *testing.T, tc TestCase) Labeler {
 				data, err := ioutil.ReadAll(file)
 				return string(data), nil
 			},
+			// Will return true whenever team contains the given user name
+			IsUserMemberOfTeam: func(user, team string) (bool, error) {
+				return strings.Contains(team, user), nil
+			},
 		},
 	}
 }