tisonkun commented on code in PR #69: URL: https://github.com/apache/pulsar-test-infra/pull/69#discussion_r962618120
########## docbot/action_config.go: ########## @@ -0,0 +1,119 @@ +package main + +import ( + "fmt" + "os" + "strings" +) + +type ActionConfig struct { + token *string + repo *string + owner *string + + labelPattern *string + labelWatchSet map[string]struct{} + labelMissing *string + enableLabelMissing *bool + enableLabelMultiple *bool +} + +func NewActionConfig() (*ActionConfig, error) { + ownerRepoSlug := os.Getenv("GITHUB_REPOSITORY") + ownerRepo := strings.Split(ownerRepoSlug, "/") + if len(ownerRepo) != 2 { + return nil, fmt.Errorf("GITHUB_REPOSITORY is not found") + } + owner, repo := ownerRepo[0], ownerRepo[1] + + token := os.Getenv("GITHUB_TOKEN") + + labelPattern := os.Getenv("LABEL_PATTERN") + if len(labelPattern) == 0 { + labelPattern = "- \\[(.*?)\\] ?`(.+?)`" + } + + labelWatchListSlug := os.Getenv("LABEL_WATCH_LIST") + labelWatchList := strings.Split(strings.TrimSpace(labelWatchListSlug), ",") + labelWatchSet := make(map[string]struct{}) + for _, l := range labelWatchList { + labelWatchSet[l] = struct{}{} Review Comment: ```suggestion if r := strings.TrimSpace(l); len(r) > 0 { labelWatchSet[r] = struct{}{} } ``` For `LABEL_WATCH_LIST == 'docs, docs-required,'`. ########## docbot/action.go: ########## @@ -0,0 +1,346 @@ +package main + +import ( + "context" + "errors" + "fmt" + "regexp" + "strings" + + "github.com/apache/pulsar-test-infra/docbot/pkg/logger" + "github.com/google/go-github/v45/github" + "golang.org/x/oauth2" +) + +const ( + MessageLabelMissing = `Please provide a correct documentation label for your PR. +Instructions see [Pulsar Documentation Label Guide](https://docs.google.com/document/d/1Qw7LHQdXWBW9t2-r-A7QdFDBwmZh6ytB4guwMoXHqc0).` + MessageLabelMultiple = `Please select only one documentation label for your PR. +Instructions see [Pulsar Documentation Label Guide](https://docs.google.com/document/d/1Qw7LHQdXWBW9t2-r-A7QdFDBwmZh6ytB4guwMoXHqc0).` +) + +type Action struct { + config *ActionConfig + + globalContext context.Context + client *github.Client + + prNumber int +} + +func NewAction(ac *ActionConfig) *Action { + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: ac.GetToken()}, + ) + + tc := oauth2.NewClient(ctx, ts) + + return NewActionWithClient(ctx, ac, github.NewClient(tc)) +} + +func NewActionWithClient(ctx context.Context, ac *ActionConfig, client *github.Client) *Action { + return &Action{ + config: ac, + globalContext: ctx, + client: client, + } +} + +func (a *Action) Run(prNumber int, actionType string) error { + a.prNumber = prNumber + + switch actionType { + case "opened", "edited", "labeled", "unlabeled": + return a.labeling() + } + return nil +} + +func (a *Action) labeling() error { + pr, _, err := a.client.PullRequests.Get(a.globalContext, a.config.GetOwner(), a.config.GetRepo(), a.prNumber) + if err != nil { + return fmt.Errorf("get PR: %v", err) + } + + var bodyLabels map[string]bool + if pr.Body != nil { + bodyLabels = a.extractLabels(*pr.Body) + } + + logger.Infoln("@List repo labels") + repoLabels, err := a.getRepoLabels() + if err != nil { + return fmt.Errorf("list repo labels: %v", err) + } + logger.Infof("Repo labels: %v\n", repoLabels) + + prLabels := a.labelsToMap(pr.Labels) + logger.Infof("PR labels: %v\n", prLabels) + + // Get expected labels + // Only handle labels already exist in repo + expectedLabelsMap := make(map[string]bool) + checkedCount := 0 + for label, checked := range bodyLabels { + if _, exist := repoLabels[label]; !exist { + logger.Infof("Found label %v not exist int repo\n", label) + continue + } + expectedLabelsMap[label] = checked + if checked { + checkedCount++ + } + } + logger.Infof("Expected labels: %v\n", expectedLabelsMap) + + labelsToRemove := make(map[string]struct{}, 0) + labelsToAdd := make(map[string]struct{}, 0) + + if checkedCount == 0 { + logger.Infoln("Label missing") + for label := range a.config.labelWatchSet { + _, found := prLabels[label] + if found { + labelsToRemove[label] = struct{}{} + } + } + _, found := prLabels[a.config.GetLabelMissing()] + if !found { + labelsToAdd[a.config.GetLabelMissing()] = struct{}{} + } else { + logger.Infoln("Already added missing label.") + return errors.New(MessageLabelMissing) + } + } else { + if !a.config.GetEnableLabelMultiple() && checkedCount > 1 { + logger.Infoln("Multiple labels not enabled") + err = a.AddAndCleanupHelpComment(pr.User.GetLogin(), MessageLabelMultiple) + if err != nil { + return err + } + return errors.New(MessageLabelMultiple) + } + + _, found := prLabels[a.config.GetLabelMissing()] + if found { + labelsToRemove[a.config.GetLabelMissing()] = struct{}{} + } + + for label, checked := range expectedLabelsMap { + _, found := prLabels[label] + if found { + continue + } + if checked { + labelsToAdd[label] = struct{}{} + } + } + } + + if len(labelsToAdd) == 0 { + logger.Infoln("No labels to add.") + } else { + labels := a.labelsSetToString(labelsToAdd) + logger.Infof("Labels to add: %v\n", labels) + err = a.AddLabels(labels) + if err != nil { + logger.Errorf("Failed add labels %v: %v\n", labelsToAdd, err) + return err + } + } + + if len(labelsToRemove) == 0 { + logger.Infoln("No labels to remove.") + } else { + labels := a.labelsSetToString(labelsToRemove) + logger.Infof("Labels to remove: %v\n", labels) + for _, label := range labels { + err = a.RemoveLabel(label) + if err != nil { + logger.Errorf("Failed remove labels %v: %v\n", labelsToRemove, err) + return err + } + } + } + + if checkedCount == 0 { + err := a.AddAndCleanupHelpComment(pr.User.GetLogin(), MessageLabelMissing) + if err != nil { + return err + } + return errors.New(MessageLabelMissing) + } + + return nil +} + +func (a *Action) extractLabels(prBody string) map[string]bool { + r := regexp.MustCompile(a.config.GetLabelPattern()) + targets := r.FindAllStringSubmatch(prBody, -1) + labels := make(map[string]bool) + + //// Init labels from watch list + //for label := range a.config.labelWatchSet { + // labels[label] = false + //} Review Comment: ```suggestion ``` ########## docbot/action.go: ########## @@ -0,0 +1,346 @@ +package main + +import ( + "context" + "errors" + "fmt" + "regexp" + "strings" + + "github.com/apache/pulsar-test-infra/docbot/pkg/logger" + "github.com/google/go-github/v45/github" + "golang.org/x/oauth2" +) + +const ( + MessageLabelMissing = `Please provide a correct documentation label for your PR. +Instructions see [Pulsar Documentation Label Guide](https://docs.google.com/document/d/1Qw7LHQdXWBW9t2-r-A7QdFDBwmZh6ytB4guwMoXHqc0).` + MessageLabelMultiple = `Please select only one documentation label for your PR. +Instructions see [Pulsar Documentation Label Guide](https://docs.google.com/document/d/1Qw7LHQdXWBW9t2-r-A7QdFDBwmZh6ytB4guwMoXHqc0).` +) + +type Action struct { + config *ActionConfig + + globalContext context.Context + client *github.Client + + prNumber int +} + +func NewAction(ac *ActionConfig) *Action { + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: ac.GetToken()}, + ) + + tc := oauth2.NewClient(ctx, ts) + + return NewActionWithClient(ctx, ac, github.NewClient(tc)) +} + +func NewActionWithClient(ctx context.Context, ac *ActionConfig, client *github.Client) *Action { + return &Action{ + config: ac, + globalContext: ctx, + client: client, + } +} + +func (a *Action) Run(prNumber int, actionType string) error { + a.prNumber = prNumber + + switch actionType { + case "opened", "edited", "labeled", "unlabeled": + return a.labeling() + } + return nil +} + +func (a *Action) labeling() error { + pr, _, err := a.client.PullRequests.Get(a.globalContext, a.config.GetOwner(), a.config.GetRepo(), a.prNumber) + if err != nil { + return fmt.Errorf("get PR: %v", err) + } + + var bodyLabels map[string]bool + if pr.Body != nil { + bodyLabels = a.extractLabels(*pr.Body) + } + + logger.Infoln("@List repo labels") + repoLabels, err := a.getRepoLabels() + if err != nil { + return fmt.Errorf("list repo labels: %v", err) + } + logger.Infof("Repo labels: %v\n", repoLabels) + + prLabels := a.labelsToMap(pr.Labels) + logger.Infof("PR labels: %v\n", prLabels) + + // Get expected labels + // Only handle labels already exist in repo + expectedLabelsMap := make(map[string]bool) + checkedCount := 0 + for label, checked := range bodyLabels { + if _, exist := repoLabels[label]; !exist { + logger.Infof("Found label %v not exist int repo\n", label) + continue + } + expectedLabelsMap[label] = checked + if checked { + checkedCount++ + } + } + logger.Infof("Expected labels: %v\n", expectedLabelsMap) + + labelsToRemove := make(map[string]struct{}, 0) + labelsToAdd := make(map[string]struct{}, 0) + + if checkedCount == 0 { + logger.Infoln("Label missing") + for label := range a.config.labelWatchSet { + _, found := prLabels[label] + if found { + labelsToRemove[label] = struct{}{} + } + } + _, found := prLabels[a.config.GetLabelMissing()] + if !found { + labelsToAdd[a.config.GetLabelMissing()] = struct{}{} + } else { + logger.Infoln("Already added missing label.") + return errors.New(MessageLabelMissing) + } + } else { + if !a.config.GetEnableLabelMultiple() && checkedCount > 1 { + logger.Infoln("Multiple labels not enabled") + err = a.AddAndCleanupHelpComment(pr.User.GetLogin(), MessageLabelMultiple) + if err != nil { + return err + } + return errors.New(MessageLabelMultiple) + } + + _, found := prLabels[a.config.GetLabelMissing()] + if found { + labelsToRemove[a.config.GetLabelMissing()] = struct{}{} + } + + for label, checked := range expectedLabelsMap { + _, found := prLabels[label] + if found { + continue + } + if checked { + labelsToAdd[label] = struct{}{} + } + } + } + + if len(labelsToAdd) == 0 { + logger.Infoln("No labels to add.") + } else { + labels := a.labelsSetToString(labelsToAdd) + logger.Infof("Labels to add: %v\n", labels) + err = a.AddLabels(labels) + if err != nil { + logger.Errorf("Failed add labels %v: %v\n", labelsToAdd, err) + return err + } + } + + if len(labelsToRemove) == 0 { + logger.Infoln("No labels to remove.") + } else { + labels := a.labelsSetToString(labelsToRemove) + logger.Infof("Labels to remove: %v\n", labels) + for _, label := range labels { + err = a.RemoveLabel(label) + if err != nil { + logger.Errorf("Failed remove labels %v: %v\n", labelsToRemove, err) + return err + } + } + } + + if checkedCount == 0 { + err := a.AddAndCleanupHelpComment(pr.User.GetLogin(), MessageLabelMissing) + if err != nil { + return err + } + return errors.New(MessageLabelMissing) + } + + return nil +} + +func (a *Action) extractLabels(prBody string) map[string]bool { + r := regexp.MustCompile(a.config.GetLabelPattern()) + targets := r.FindAllStringSubmatch(prBody, -1) + labels := make(map[string]bool) + + //// Init labels from watch list + //for label := range a.config.labelWatchSet { + // labels[label] = false + //} + + for _, v := range targets { + checked := strings.ToLower(strings.TrimSpace(v[1])) == "x" + name := strings.TrimSpace(v[2]) + + // Filter uninterested labels + if _, exist := a.config.labelWatchSet[name]; !exist { + continue + } + + labels[name] = checked + } + + return labels +} + +func (a *Action) getRepoLabels() (map[string]struct{}, error) { + ctx := context.Background() + listOptions := &github.ListOptions{PerPage: 100} + repoLabels := make(map[string]struct{}, 0) + for { + rLabels, resp, err := a.client.Issues.ListLabels(ctx, a.config.GetOwner(), a.config.GetRepo(), listOptions) + if err != nil { + return nil, err + } + + for _, label := range rLabels { + repoLabels[label.GetName()] = struct{}{} + } + if resp.NextPage == 0 { + break + } + listOptions.Page = resp.NextPage + } + return repoLabels, nil +} + +func (a *Action) getPRLabels() ([]*github.Label, error) { + ctx := context.Background() + listOptions := &github.ListOptions{PerPage: 100} + issueLabels := make([]*github.Label, 0) + for { + iLabels, resp, err := a.client.Issues.ListLabelsByIssue(ctx, a.config.GetOwner(), a.config.GetRepo(), + a.prNumber, listOptions) + if err != nil { + return nil, err + } + issueLabels = append(issueLabels, iLabels...) + if resp.NextPage == 0 { + break + } + listOptions.Page = resp.NextPage + } + return issueLabels, nil +} + +func (a *Action) labelsToMap(labels []*github.Label) map[string]struct{} { + result := make(map[string]struct{}, 0) + for _, label := range labels { + result[label.GetName()] = struct{}{} + } + return result +} + +func (a *Action) labelsSetToString(labels map[string]struct{}) []string { + result := []string{} + for label := range labels { + result = append(result, label) + } + return result +} + +func (a *Action) GetLabelInvalidCommentIDs(body string) ([]int64, error) { + ctx := context.Background() + listOptions := &github.IssueListCommentsOptions{} + listOptions.PerPage = 100 + commentIDs := make([]int64, 0) + for { + comments, resp, err := a.client.Issues.ListComments(ctx, a.config.GetOwner(), a.config.GetRepo(), + a.prNumber, listOptions) + if err != nil { + return nil, err + } + for _, item := range comments { + if strings.Contains(*item.Body, body) { + commentIDs = append(commentIDs, *item.ID) + } + } + + if resp.NextPage == 0 { + break + } + listOptions.Page = resp.NextPage + } + + return commentIDs, nil +} + +func (a *Action) CreateComment(body string) error { + _, _, err := a.client.Issues.CreateComment(a.globalContext, a.config.GetOwner(), a.config.GetRepo(), + a.prNumber, &github.IssueComment{Body: func(v string) *string { return &v }(body)}) + return err +} + +func (a *Action) EditComment(commentID int64, body string) error { + _, _, err := a.client.Issues.EditComment(a.globalContext, a.config.GetOwner(), a.config.GetRepo(), + commentID, &github.IssueComment{Body: func(v string) *string { return &v }(body)}) + return err +} Review Comment: ```suggestion ``` Unused. ########## docbot/action.go: ########## @@ -0,0 +1,346 @@ +package main + +import ( + "context" + "errors" + "fmt" + "regexp" + "strings" + + "github.com/apache/pulsar-test-infra/docbot/pkg/logger" + "github.com/google/go-github/v45/github" + "golang.org/x/oauth2" +) + +const ( + MessageLabelMissing = `Please provide a correct documentation label for your PR. +Instructions see [Pulsar Documentation Label Guide](https://docs.google.com/document/d/1Qw7LHQdXWBW9t2-r-A7QdFDBwmZh6ytB4guwMoXHqc0).` + MessageLabelMultiple = `Please select only one documentation label for your PR. +Instructions see [Pulsar Documentation Label Guide](https://docs.google.com/document/d/1Qw7LHQdXWBW9t2-r-A7QdFDBwmZh6ytB4guwMoXHqc0).` +) + +type Action struct { + config *ActionConfig + + globalContext context.Context + client *github.Client + + prNumber int +} + +func NewAction(ac *ActionConfig) *Action { + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: ac.GetToken()}, + ) + + tc := oauth2.NewClient(ctx, ts) + + return NewActionWithClient(ctx, ac, github.NewClient(tc)) +} + +func NewActionWithClient(ctx context.Context, ac *ActionConfig, client *github.Client) *Action { + return &Action{ + config: ac, + globalContext: ctx, + client: client, + } +} + +func (a *Action) Run(prNumber int, actionType string) error { + a.prNumber = prNumber + + switch actionType { + case "opened", "edited", "labeled", "unlabeled": + return a.labeling() + } + return nil +} + +func (a *Action) labeling() error { Review Comment: ```suggestion func (a *Action) checkLabels() error { ``` ########## docbot/action_test.go: ########## @@ -0,0 +1,320 @@ +package main + +import ( Review Comment: I suggest use [testify](https://github.com/stretchr/testify) to simplify assertions. ########## docbot/action.go: ########## @@ -0,0 +1,346 @@ +package main + +import ( + "context" + "errors" + "fmt" + "regexp" + "strings" + + "github.com/apache/pulsar-test-infra/docbot/pkg/logger" + "github.com/google/go-github/v45/github" + "golang.org/x/oauth2" +) + +const ( + MessageLabelMissing = `Please provide a correct documentation label for your PR. +Instructions see [Pulsar Documentation Label Guide](https://docs.google.com/document/d/1Qw7LHQdXWBW9t2-r-A7QdFDBwmZh6ytB4guwMoXHqc0).` + MessageLabelMultiple = `Please select only one documentation label for your PR. +Instructions see [Pulsar Documentation Label Guide](https://docs.google.com/document/d/1Qw7LHQdXWBW9t2-r-A7QdFDBwmZh6ytB4guwMoXHqc0).` +) + +type Action struct { + config *ActionConfig + + globalContext context.Context + client *github.Client + + prNumber int +} + +func NewAction(ac *ActionConfig) *Action { + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: ac.GetToken()}, + ) + + tc := oauth2.NewClient(ctx, ts) + + return NewActionWithClient(ctx, ac, github.NewClient(tc)) +} + +func NewActionWithClient(ctx context.Context, ac *ActionConfig, client *github.Client) *Action { + return &Action{ + config: ac, + globalContext: ctx, + client: client, + } +} + +func (a *Action) Run(prNumber int, actionType string) error { + a.prNumber = prNumber + + switch actionType { + case "opened", "edited", "labeled", "unlabeled": + return a.labeling() + } + return nil +} + +func (a *Action) labeling() error { + pr, _, err := a.client.PullRequests.Get(a.globalContext, a.config.GetOwner(), a.config.GetRepo(), a.prNumber) + if err != nil { + return fmt.Errorf("get PR: %v", err) + } + + var bodyLabels map[string]bool + if pr.Body != nil { + bodyLabels = a.extractLabels(*pr.Body) + } + + logger.Infoln("@List repo labels") + repoLabels, err := a.getRepoLabels() + if err != nil { + return fmt.Errorf("list repo labels: %v", err) + } + logger.Infof("Repo labels: %v\n", repoLabels) + + prLabels := a.labelsToMap(pr.Labels) + logger.Infof("PR labels: %v\n", prLabels) + + // Get expected labels + // Only handle labels already exist in repo + expectedLabelsMap := make(map[string]bool) + checkedCount := 0 + for label, checked := range bodyLabels { + if _, exist := repoLabels[label]; !exist { + logger.Infof("Found label %v not exist int repo\n", label) + continue + } + expectedLabelsMap[label] = checked + if checked { + checkedCount++ + } + } + logger.Infof("Expected labels: %v\n", expectedLabelsMap) + + labelsToRemove := make(map[string]struct{}, 0) + labelsToAdd := make(map[string]struct{}, 0) + + if checkedCount == 0 { + logger.Infoln("Label missing") + for label := range a.config.labelWatchSet { + _, found := prLabels[label] + if found { + labelsToRemove[label] = struct{}{} + } + } + _, found := prLabels[a.config.GetLabelMissing()] + if !found { + labelsToAdd[a.config.GetLabelMissing()] = struct{}{} + } else { + logger.Infoln("Already added missing label.") + return errors.New(MessageLabelMissing) + } + } else { + if !a.config.GetEnableLabelMultiple() && checkedCount > 1 { + logger.Infoln("Multiple labels not enabled") + err = a.AddAndCleanupHelpComment(pr.User.GetLogin(), MessageLabelMultiple) + if err != nil { + return err + } + return errors.New(MessageLabelMultiple) + } + + _, found := prLabels[a.config.GetLabelMissing()] + if found { + labelsToRemove[a.config.GetLabelMissing()] = struct{}{} + } + + for label, checked := range expectedLabelsMap { + _, found := prLabels[label] + if found { + continue + } + if checked { + labelsToAdd[label] = struct{}{} + } + } + } + + if len(labelsToAdd) == 0 { + logger.Infoln("No labels to add.") + } else { + labels := a.labelsSetToString(labelsToAdd) + logger.Infof("Labels to add: %v\n", labels) + err = a.AddLabels(labels) + if err != nil { + logger.Errorf("Failed add labels %v: %v\n", labelsToAdd, err) + return err + } + } + + if len(labelsToRemove) == 0 { + logger.Infoln("No labels to remove.") + } else { + labels := a.labelsSetToString(labelsToRemove) + logger.Infof("Labels to remove: %v\n", labels) + for _, label := range labels { + err = a.RemoveLabel(label) + if err != nil { + logger.Errorf("Failed remove labels %v: %v\n", labelsToRemove, err) + return err + } + } + } + + if checkedCount == 0 { + err := a.AddAndCleanupHelpComment(pr.User.GetLogin(), MessageLabelMissing) + if err != nil { + return err + } + return errors.New(MessageLabelMissing) + } + + return nil +} + +func (a *Action) extractLabels(prBody string) map[string]bool { + r := regexp.MustCompile(a.config.GetLabelPattern()) + targets := r.FindAllStringSubmatch(prBody, -1) + labels := make(map[string]bool) + + //// Init labels from watch list + //for label := range a.config.labelWatchSet { + // labels[label] = false + //} + + for _, v := range targets { + checked := strings.ToLower(strings.TrimSpace(v[1])) == "x" + name := strings.TrimSpace(v[2]) + + // Filter uninterested labels + if _, exist := a.config.labelWatchSet[name]; !exist { + continue + } + + labels[name] = checked + } + + return labels +} + +func (a *Action) getRepoLabels() (map[string]struct{}, error) { + ctx := context.Background() + listOptions := &github.ListOptions{PerPage: 100} + repoLabels := make(map[string]struct{}, 0) + for { + rLabels, resp, err := a.client.Issues.ListLabels(ctx, a.config.GetOwner(), a.config.GetRepo(), listOptions) + if err != nil { + return nil, err + } + + for _, label := range rLabels { + repoLabels[label.GetName()] = struct{}{} + } + if resp.NextPage == 0 { + break + } + listOptions.Page = resp.NextPage + } + return repoLabels, nil +} + +func (a *Action) getPRLabels() ([]*github.Label, error) { + ctx := context.Background() + listOptions := &github.ListOptions{PerPage: 100} + issueLabels := make([]*github.Label, 0) + for { + iLabels, resp, err := a.client.Issues.ListLabelsByIssue(ctx, a.config.GetOwner(), a.config.GetRepo(), + a.prNumber, listOptions) + if err != nil { + return nil, err + } + issueLabels = append(issueLabels, iLabels...) + if resp.NextPage == 0 { + break + } + listOptions.Page = resp.NextPage + } + return issueLabels, nil +} + Review Comment: ```suggestion ``` Unused. ########## docbot/action.go: ########## @@ -0,0 +1,346 @@ +package main + +import ( + "context" + "errors" + "fmt" + "regexp" + "strings" + + "github.com/apache/pulsar-test-infra/docbot/pkg/logger" + "github.com/google/go-github/v45/github" + "golang.org/x/oauth2" +) + +const ( + MessageLabelMissing = `Please provide a correct documentation label for your PR. +Instructions see [Pulsar Documentation Label Guide](https://docs.google.com/document/d/1Qw7LHQdXWBW9t2-r-A7QdFDBwmZh6ytB4guwMoXHqc0).` + MessageLabelMultiple = `Please select only one documentation label for your PR. +Instructions see [Pulsar Documentation Label Guide](https://docs.google.com/document/d/1Qw7LHQdXWBW9t2-r-A7QdFDBwmZh6ytB4guwMoXHqc0).` +) + +type Action struct { + config *ActionConfig + + globalContext context.Context + client *github.Client + + prNumber int +} + +func NewAction(ac *ActionConfig) *Action { + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: ac.GetToken()}, + ) + + tc := oauth2.NewClient(ctx, ts) + + return NewActionWithClient(ctx, ac, github.NewClient(tc)) +} + +func NewActionWithClient(ctx context.Context, ac *ActionConfig, client *github.Client) *Action { + return &Action{ + config: ac, + globalContext: ctx, + client: client, + } +} + +func (a *Action) Run(prNumber int, actionType string) error { + a.prNumber = prNumber + + switch actionType { + case "opened", "edited", "labeled", "unlabeled": + return a.labeling() + } + return nil +} + +func (a *Action) labeling() error { + pr, _, err := a.client.PullRequests.Get(a.globalContext, a.config.GetOwner(), a.config.GetRepo(), a.prNumber) + if err != nil { + return fmt.Errorf("get PR: %v", err) + } + + var bodyLabels map[string]bool + if pr.Body != nil { + bodyLabels = a.extractLabels(*pr.Body) + } + + logger.Infoln("@List repo labels") + repoLabels, err := a.getRepoLabels() + if err != nil { + return fmt.Errorf("list repo labels: %v", err) + } + logger.Infof("Repo labels: %v\n", repoLabels) + + prLabels := a.labelsToMap(pr.Labels) + logger.Infof("PR labels: %v\n", prLabels) + + // Get expected labels + // Only handle labels already exist in repo + expectedLabelsMap := make(map[string]bool) + checkedCount := 0 + for label, checked := range bodyLabels { + if _, exist := repoLabels[label]; !exist { + logger.Infof("Found label %v not exist int repo\n", label) + continue + } + expectedLabelsMap[label] = checked + if checked { + checkedCount++ + } + } + logger.Infof("Expected labels: %v\n", expectedLabelsMap) + + labelsToRemove := make(map[string]struct{}, 0) + labelsToAdd := make(map[string]struct{}, 0) + + if checkedCount == 0 { + logger.Infoln("Label missing") + for label := range a.config.labelWatchSet { + _, found := prLabels[label] + if found { + labelsToRemove[label] = struct{}{} + } + } + _, found := prLabels[a.config.GetLabelMissing()] + if !found { + labelsToAdd[a.config.GetLabelMissing()] = struct{}{} + } else { + logger.Infoln("Already added missing label.") + return errors.New(MessageLabelMissing) + } + } else { + if !a.config.GetEnableLabelMultiple() && checkedCount > 1 { + logger.Infoln("Multiple labels not enabled") + err = a.AddAndCleanupHelpComment(pr.User.GetLogin(), MessageLabelMultiple) + if err != nil { + return err + } + return errors.New(MessageLabelMultiple) + } + + _, found := prLabels[a.config.GetLabelMissing()] + if found { + labelsToRemove[a.config.GetLabelMissing()] = struct{}{} + } + + for label, checked := range expectedLabelsMap { + _, found := prLabels[label] + if found { + continue + } + if checked { + labelsToAdd[label] = struct{}{} + } + } + } + + if len(labelsToAdd) == 0 { + logger.Infoln("No labels to add.") + } else { + labels := a.labelsSetToString(labelsToAdd) + logger.Infof("Labels to add: %v\n", labels) + err = a.AddLabels(labels) + if err != nil { + logger.Errorf("Failed add labels %v: %v\n", labelsToAdd, err) + return err + } + } + + if len(labelsToRemove) == 0 { + logger.Infoln("No labels to remove.") + } else { + labels := a.labelsSetToString(labelsToRemove) + logger.Infof("Labels to remove: %v\n", labels) + for _, label := range labels { + err = a.RemoveLabel(label) + if err != nil { + logger.Errorf("Failed remove labels %v: %v\n", labelsToRemove, err) + return err + } + } + } + + if checkedCount == 0 { + err := a.AddAndCleanupHelpComment(pr.User.GetLogin(), MessageLabelMissing) + if err != nil { + return err + } + return errors.New(MessageLabelMissing) + } + + return nil +} + +func (a *Action) extractLabels(prBody string) map[string]bool { + r := regexp.MustCompile(a.config.GetLabelPattern()) + targets := r.FindAllStringSubmatch(prBody, -1) + labels := make(map[string]bool) + + //// Init labels from watch list + //for label := range a.config.labelWatchSet { + // labels[label] = false + //} + + for _, v := range targets { + checked := strings.ToLower(strings.TrimSpace(v[1])) == "x" + name := strings.TrimSpace(v[2]) + + // Filter uninterested labels + if _, exist := a.config.labelWatchSet[name]; !exist { + continue + } + + labels[name] = checked + } + + return labels +} + +func (a *Action) getRepoLabels() (map[string]struct{}, error) { + ctx := context.Background() + listOptions := &github.ListOptions{PerPage: 100} + repoLabels := make(map[string]struct{}, 0) + for { + rLabels, resp, err := a.client.Issues.ListLabels(ctx, a.config.GetOwner(), a.config.GetRepo(), listOptions) + if err != nil { + return nil, err + } + + for _, label := range rLabels { + repoLabels[label.GetName()] = struct{}{} + } + if resp.NextPage == 0 { + break + } + listOptions.Page = resp.NextPage + } + return repoLabels, nil +} + +func (a *Action) getPRLabels() ([]*github.Label, error) { + ctx := context.Background() + listOptions := &github.ListOptions{PerPage: 100} + issueLabels := make([]*github.Label, 0) + for { + iLabels, resp, err := a.client.Issues.ListLabelsByIssue(ctx, a.config.GetOwner(), a.config.GetRepo(), + a.prNumber, listOptions) + if err != nil { + return nil, err + } + issueLabels = append(issueLabels, iLabels...) + if resp.NextPage == 0 { + break + } + listOptions.Page = resp.NextPage + } + return issueLabels, nil +} + +func (a *Action) labelsToMap(labels []*github.Label) map[string]struct{} { + result := make(map[string]struct{}, 0) + for _, label := range labels { + result[label.GetName()] = struct{}{} + } + return result +} + +func (a *Action) labelsSetToString(labels map[string]struct{}) []string { + result := []string{} + for label := range labels { + result = append(result, label) + } + return result +} + +func (a *Action) GetLabelInvalidCommentIDs(body string) ([]int64, error) { Review Comment: ```suggestion func (a *Action) getLabelInvalidCommentIDs(body string) ([]int64, error) { ``` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: dev-unsubscr...@pulsar.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org