This is an automated email from the ASF dual-hosted git repository.

lhotari pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar-test-infra.git


The following commit(s) were added to refs/heads/master by this push:
     new f7f525d  Refactor docbot and add tests (#69)
f7f525d is described below

commit f7f525d26656c2a6dd23f281a8452262e273c3f2
Author: Zixuan Liu <[email protected]>
AuthorDate: Tue Sep 6 23:21:01 2022 +0800

    Refactor docbot and add tests (#69)
---
 docbot/action.go        | 316 +++++++++++++++++++++++++
 docbot/action.yml       |   2 +-
 docbot/action_config.go | 123 ++++++++++
 docbot/action_test.go   | 320 ++++++++++++++++++++++++++
 docbot/go.mod           |   5 +-
 docbot/go.sum           |  15 +-
 docbot/main.go          | 600 +-----------------------------------------------
 7 files changed, 780 insertions(+), 601 deletions(-)

diff --git a/docbot/action.go b/docbot/action.go
new file mode 100644
index 0000000..026a9f7
--- /dev/null
+++ b/docbot/action.go
@@ -0,0 +1,316 @@
+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.checkLabels()
+       }
+       return nil
+}
+
+func (a *Action) checkLabels() 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)
+       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) 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) deleteComment(commentID int64) error {
+       _, err := a.client.Issues.DeleteComment(a.globalContext, 
a.config.GetOwner(), a.config.GetRepo(),
+               commentID)
+       return err
+}
+
+func (a *Action) addLabels(labels []string) error {
+       _, _, err := a.client.Issues.AddLabelsToIssue(a.globalContext, 
a.config.GetOwner(), a.config.GetRepo(),
+               a.prNumber, labels)
+       return err
+}
+
+func (a *Action) removeLabel(label string) error {
+       _, err := a.client.Issues.RemoveLabelForIssue(a.globalContext, 
a.config.GetOwner(), a.config.GetRepo(),
+               a.prNumber, label)
+       return err
+}
+
+// addAndCleanupHelpComment adds a help comment when no help comment on the PR.
+func (a *Action) addAndCleanupHelpComment(login, body string) error {
+       commentIDs, err := a.getLabelInvalidCommentIDs(body)
+       if err != nil {
+               logger.Errorf("Failed to get the comment list: %v", err)
+               return err
+       }
+       if len(commentIDs) == 0 {
+               err = a.createComment(fmt.Sprintf("@%s %s", login, body))
+               if err != nil {
+                       logger.Errorf("Failed to create %s comment: %v", body, 
err)
+                       return err
+               }
+               return nil
+       } else {
+               // cleanup
+               if len(commentIDs) > 1 {
+                       for index, id := range commentIDs {
+                               if index == 0 {
+                                       continue
+                               }
+                               err := a.deleteComment(id)
+                               if err != nil {
+                                       logger.Errorf("Failed to delete %v 
comment: %v", id, err)
+                                       return err
+                               }
+                       }
+               }
+       }
+
+       return nil
+}
diff --git a/docbot/action.yml b/docbot/action.yml
index 9ecddf7..8a5dce6 100644
--- a/docbot/action.yml
+++ b/docbot/action.yml
@@ -17,6 +17,6 @@ runs:
       with:
         go-version: 1.18
     - name: Execute
-      run: go run main.go
+      run: go run .
       shell: bash
       working-directory: docbot
diff --git a/docbot/action_config.go b/docbot/action_config.go
new file mode 100644
index 0000000..34a810e
--- /dev/null
+++ b/docbot/action_config.go
@@ -0,0 +1,123 @@
+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(labelWatchListSlug, ",")
+       labelWatchSet := make(map[string]struct{})
+       for _, l := range labelWatchList {
+               key := strings.TrimSpace(l)
+               if key == "" {
+                       continue
+               }
+               labelWatchSet[key] = struct{}{}
+       }
+
+       enableLabelMissingSlug := os.Getenv("ENABLE_LABEL_MISSING")
+       enableLabelMissing := true
+       if enableLabelMissingSlug == "false" {
+               enableLabelMissing = false
+       }
+
+       labelMissing := os.Getenv("LABEL_MISSING")
+       if len(labelMissing) == 0 {
+               labelMissing = "label-missing"
+       }
+
+       enableLabelMultipleSlug := os.Getenv("ENABLE_LABEL_MULTIPLE")
+       enableLabelMultiple := false
+       if enableLabelMultipleSlug == "true" {
+               enableLabelMultiple = true
+       }
+
+       return &ActionConfig{
+               token:               &token,
+               repo:                &repo,
+               owner:               &owner,
+               labelPattern:        &labelPattern,
+               labelWatchSet:       labelWatchSet,
+               labelMissing:        &labelMissing,
+               enableLabelMissing:  &enableLabelMissing,
+               enableLabelMultiple: &enableLabelMultiple,
+       }, nil
+}
+
+func (ac *ActionConfig) GetToken() string {
+       if ac == nil || ac.token == nil {
+               return ""
+       }
+       return *ac.token
+}
+
+func (ac *ActionConfig) GetOwner() string {
+       if ac == nil || ac.owner == nil {
+               return ""
+       }
+       return *ac.owner
+}
+
+func (ac *ActionConfig) GetRepo() string {
+       if ac == nil || ac.repo == nil {
+               return ""
+       }
+       return *ac.repo
+}
+
+func (ac *ActionConfig) GetLabelPattern() string {
+       if ac == nil || ac.labelPattern == nil {
+               return ""
+       }
+       return *ac.labelPattern
+}
+
+func (ac *ActionConfig) GetLabelMissing() string {
+       if ac == nil || ac.labelMissing == nil {
+               return ""
+       }
+       return *ac.labelMissing
+}
+
+func (ac *ActionConfig) GetEnableLabelMissing() bool {
+       if ac == nil || ac.enableLabelMissing == nil {
+               return false
+       }
+       return *ac.enableLabelMissing
+}
+
+func (ac *ActionConfig) GetEnableLabelMultiple() bool {
+       if ac == nil || ac.enableLabelMultiple == nil {
+               return false
+       }
+       return *ac.enableLabelMultiple
+}
diff --git a/docbot/action_test.go b/docbot/action_test.go
new file mode 100644
index 0000000..a2af4f6
--- /dev/null
+++ b/docbot/action_test.go
@@ -0,0 +1,320 @@
+package main
+
+import (
+       "context"
+       "fmt"
+       "os"
+       "testing"
+
+       "github.com/google/go-github/v45/github"
+       "github.com/migueleliasweb/go-github-mock/src/mock"
+)
+
+func repoLabels() []*github.Label {
+       labels := []string{"doc-required", "doc-not-needed", "doc", 
"doc-complete", "doc-label-missing"}
+
+       result := make([]*github.Label, 0)
+       for _, label := range labels {
+               name := label
+               result = append(result, &github.Label{Name: &name})
+       }
+
+       return result
+}
+
+func mustNewActionConfig() *ActionConfig {
+       _ = os.Setenv("GITHUB_REPOSITORY", "apache/pulsar")
+       _ = os.Setenv("LABEL_WATCH_LIST", 
"doc,doc-required,doc-not-needed,doc-complete")
+       _ = os.Setenv("LABEL_MISSING", "doc-label-missing")
+
+       config, err := NewActionConfig()
+       if err != nil {
+               panic(err)
+       }
+
+       return config
+}
+
+func assertMessageLabel(t *testing.T, err error, message string) {
+       t.Helper()
+
+       if err == nil {
+               t.Fatal("Expect err not nil")
+       }
+
+       if err.Error() != message {
+               t.Fatal("Expect err equals " + message)
+       }
+}
+
+func TestSingleChecked(t *testing.T) {
+       id := int64(1)
+       body := fmt.Sprintf(`
+Check the box below or label this PR directly.
+
+Need to update docs?
+
+- [ ] %s
+(Your PR needs to update docs and you will update later)
+
+- [x] %s
+(Please explain why)
+
+- [ ] %s
+(Your PR contains doc changes)
+
+- [ ] %s
+(Docs have been already added)
+`, "`doc-required`", "`doc-not-needed`", "`doc`", "`doc-complete`")
+
+       mockedHTTPClient := mock.NewMockedHTTPClient(
+               mock.WithRequestMatch(
+                       mock.GetReposPullsByOwnerByRepoByPullNumber,
+                       github.PullRequest{
+                               ID:     &id,
+                               Body:   &body,
+                               Labels: nil,
+                       },
+               ), mock.WithRequestMatch(
+                       mock.GetReposLabelsByOwnerByRepo,
+                       repoLabels(),
+               ),
+               
mock.WithRequestMatch(mock.PostReposIssuesLabelsByOwnerByRepoByIssueNumber, 
nil),
+       )
+
+       config := mustNewActionConfig()
+       action := NewActionWithClient(context.Background(), config, 
github.NewClient(mockedHTTPClient))
+
+       err := action.Run(1, "opened")
+       if err != nil {
+               t.Fatal(err)
+       }
+}
+
+func TestMultipleChecked(t *testing.T) {
+       id := int64(1)
+       body := fmt.Sprintf(`
+Check the box below or label this PR directly.
+
+Need to update docs?
+
+- [ ] %s
+(Your PR needs to update docs and you will update later)
+
+- [x] %s
+(Please explain why)
+
+- [x] %s
+(Your PR contains doc changes)
+
+- [ ] %s
+(Docs have been already added)
+`, "`doc-required`", "`doc-not-needed`", "`doc`", "`doc-complete`")
+
+       mockedHTTPClient := mock.NewMockedHTTPClient(
+               mock.WithRequestMatch(
+                       mock.GetReposPullsByOwnerByRepoByPullNumber,
+                       github.PullRequest{
+                               ID:     &id,
+                               Body:   &body,
+                               Labels: nil,
+                       },
+               ), mock.WithRequestMatch(
+                       mock.GetReposLabelsByOwnerByRepo,
+                       repoLabels(),
+               ),
+               
mock.WithRequestMatch(mock.PostReposIssuesLabelsByOwnerByRepoByIssueNumber, 
nil),
+       )
+
+       const key = "ENABLE_LABEL_MULTIPLE"
+       value := os.Getenv(key)
+       defer func() {
+               // reset
+               _ = os.Setenv(key, value)
+       }()
+       _ = os.Setenv("ENABLE_LABEL_MULTIPLE", "true")
+
+       config := mustNewActionConfig()
+       action := NewActionWithClient(context.Background(), config, 
github.NewClient(mockedHTTPClient))
+
+       err := action.Run(1, "opened")
+       if err != nil {
+               t.Fatal(err)
+       }
+}
+
+func TestUnchecked(t *testing.T) {
+       id := int64(1)
+       body := fmt.Sprintf(`
+Check the box below or label this PR directly.
+
+Need to update docs?
+
+- [ ] %s
+(Your PR needs to update docs and you will update later)
+
+- [ ] %s
+(Please explain why)
+
+- [ ] %s
+(Your PR contains doc changes)
+
+- [ ] %s
+(Docs have been already added)
+`, "`doc-required`", "`doc-not-needed`", "`doc`", "`doc-complete`")
+
+       mockedHTTPClient := mock.NewMockedHTTPClient(
+               mock.WithRequestMatch(
+                       mock.GetReposPullsByOwnerByRepoByPullNumber,
+                       github.PullRequest{
+                               ID:     &id,
+                               Body:   &body,
+                               Labels: nil,
+                       },
+               ), mock.WithRequestMatch(
+                       mock.GetReposLabelsByOwnerByRepo,
+                       repoLabels(),
+               ),
+               
mock.WithRequestMatch(mock.PostReposIssuesLabelsByOwnerByRepoByIssueNumber, 
nil),
+               
mock.WithRequestMatch(mock.GetReposIssuesCommentsByOwnerByRepoByIssueNumber, 
nil),
+               
mock.WithRequestMatch(mock.PostReposIssuesCommentsByOwnerByRepoByIssueNumber, 
nil),
+       )
+
+       config := mustNewActionConfig()
+       action := NewActionWithClient(context.Background(), config, 
github.NewClient(mockedHTTPClient))
+
+       err := action.Run(1, "opened")
+       assertMessageLabel(t, err, MessageLabelMissing)
+}
+
+func TestMultipleChecked_WhenMultipleLabelsNotEnabled(t *testing.T) {
+       id := int64(1)
+       body := fmt.Sprintf(`
+Check the box below or label this PR directly.
+
+Need to update docs?
+
+- [ ] %s
+(Your PR needs to update docs and you will update later)
+
+- [x] %s
+(Please explain why)
+
+- [x] %s
+(Your PR contains doc changes)
+
+- [ ] %s
+(Docs have been already added)
+`, "`doc-required`", "`doc-not-needed`", "`doc`", "`doc-complete`")
+
+       mockedHTTPClient := mock.NewMockedHTTPClient(
+               mock.WithRequestMatch(
+                       mock.GetReposPullsByOwnerByRepoByPullNumber,
+                       github.PullRequest{
+                               ID:     &id,
+                               Body:   &body,
+                               Labels: nil,
+                       },
+               ), mock.WithRequestMatch(
+                       mock.GetReposLabelsByOwnerByRepo,
+                       repoLabels(),
+               ),
+               
mock.WithRequestMatch(mock.PostReposIssuesLabelsByOwnerByRepoByIssueNumber, 
nil),
+               
mock.WithRequestMatch(mock.GetReposIssuesCommentsByOwnerByRepoByIssueNumber, 
nil),
+               
mock.WithRequestMatch(mock.PostReposIssuesCommentsByOwnerByRepoByIssueNumber, 
nil),
+       )
+
+       config := mustNewActionConfig()
+       action := NewActionWithClient(context.Background(), config, 
github.NewClient(mockedHTTPClient))
+
+       err := action.Run(1, "opened")
+       assertMessageLabel(t, err, MessageLabelMultiple)
+}
+
+func TestSingleChecked_WhenLabelMissingExist(t *testing.T) {
+       id := int64(1)
+       body := fmt.Sprintf(`
+Check the box below or label this PR directly.
+
+Need to update docs?
+
+- [ ] %s
+(Your PR needs to update docs and you will update later)
+
+- [x] %s
+(Please explain why)
+
+- [ ] %s
+(Your PR contains doc changes)
+
+- [ ] %s
+(Docs have been already added)
+`, "`doc-required`", "`doc-not-needed`", "`doc`", "`doc-complete`")
+
+       labelMissing := "doc-label-missing"
+       mockedHTTPClient := mock.NewMockedHTTPClient(
+               mock.WithRequestMatch(
+                       mock.GetReposPullsByOwnerByRepoByPullNumber,
+                       github.PullRequest{
+                               ID:     &id,
+                               Body:   &body,
+                               Labels: []*github.Label{{Name: &labelMissing}},
+                       },
+               ), mock.WithRequestMatch(
+                       mock.GetReposLabelsByOwnerByRepo,
+                       repoLabels(),
+               ),
+               
mock.WithRequestMatch(mock.PostReposIssuesLabelsByOwnerByRepoByIssueNumber, 
nil),
+               
mock.WithRequestMatch(mock.DeleteReposIssuesLabelsByOwnerByRepoByIssueNumberByName,
 nil),
+       )
+
+       config := mustNewActionConfig()
+       action := NewActionWithClient(context.Background(), config, 
github.NewClient(mockedHTTPClient))
+
+       err := action.Run(1, "opened")
+       if err != nil {
+               t.Fatal(err)
+       }
+}
+
+func TestUnchecked_WhenLabelMissingExist(t *testing.T) {
+       id := int64(1)
+       body := fmt.Sprintf(`
+Check the box below or label this PR directly.
+
+Need to update docs?
+
+- [ ] %s
+(Your PR needs to update docs and you will update later)
+
+- [ ] %s
+(Please explain why)
+
+- [ ] %s
+(Your PR contains doc changes)
+
+- [ ] %s
+(Docs have been already added)
+`, "`doc-required`", "`doc-not-needed`", "`doc`", "`doc-complete`")
+
+       labelMissing := "doc-label-missing"
+       mockedHTTPClient := mock.NewMockedHTTPClient(
+               mock.WithRequestMatch(
+                       mock.GetReposPullsByOwnerByRepoByPullNumber,
+                       github.PullRequest{
+                               ID:     &id,
+                               Body:   &body,
+                               Labels: []*github.Label{{Name: &labelMissing}},
+                       },
+               ), mock.WithRequestMatch(
+                       mock.GetReposLabelsByOwnerByRepo,
+                       repoLabels(),
+               ),
+       )
+
+       config := mustNewActionConfig()
+       action := NewActionWithClient(context.Background(), config, 
github.NewClient(mockedHTTPClient))
+
+       err := action.Run(1, "opened")
+       assertMessageLabel(t, err, MessageLabelMissing)
+}
diff --git a/docbot/go.mod b/docbot/go.mod
index 273fc97..6fa874b 100644
--- a/docbot/go.mod
+++ b/docbot/go.mod
@@ -4,15 +4,18 @@ go 1.18
 
 require (
        github.com/google/go-github/v45 v45.0.0
+       github.com/migueleliasweb/go-github-mock v0.0.10
        github.com/sethvargo/go-githubactions v1.0.0
        golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
 )
 
 require (
        github.com/golang/protobuf v1.3.2 // indirect
+       github.com/google/go-github/v41 v41.0.0 // indirect
        github.com/google/go-querystring v1.1.0 // indirect
+       github.com/gorilla/mux v1.8.0 // indirect
        github.com/sethvargo/go-envconfig v0.6.0 // indirect
        golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
-       golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
+       golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect
        google.golang.org/appengine v1.6.7 // indirect
 )
diff --git a/docbot/go.sum b/docbot/go.sum
index cc47aa0..76374c4 100644
--- a/docbot/go.sum
+++ b/docbot/go.sum
@@ -1,12 +1,22 @@
+github.com/buger/jsonparser v1.1.1/go.mod 
h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
+github.com/go-kit/log v0.2.0/go.mod 
h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
+github.com/go-logfmt/logfmt v0.5.1/go.mod 
h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
 github.com/golang/protobuf v1.3.1/go.mod 
h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2 
h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 github.com/golang/protobuf v1.3.2/go.mod 
h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/google/go-cmp v0.5.2/go.mod 
h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod 
h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/go-github/v41 v41.0.0 
h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg=
+github.com/google/go-github/v41 v41.0.0/go.mod 
h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg=
 github.com/google/go-github/v45 v45.0.0 
h1:LU0WBjYidxIVyx7PZeWb+FP4JZJ3Wh3FQgdumnGqiLs=
 github.com/google/go-github/v45 v45.0.0/go.mod 
h1:FObaZJEDSTa/WGCzZ2Z3eoCDXWJKMenWWTrd8jrta28=
 github.com/google/go-querystring v1.1.0 
h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod 
h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod 
h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/migueleliasweb/go-github-mock v0.0.10 
h1:VTaNa4eYzkRpnZ7Fqop7Jldgh7vsUsgnKWXDOnAvm3k=
+github.com/migueleliasweb/go-github-mock v0.0.10/go.mod 
h1:mD5w+9J3oBBMLr7uD6owEYlYBAL8tZd+BA7iGjI4EU8=
 github.com/sethvargo/go-envconfig v0.6.0 
h1:GxxdoeiNpWgGiVEphNFNObgMYRN/ZvI2dN7rBwadyss=
 github.com/sethvargo/go-envconfig v0.6.0/go.mod 
h1:00S1FAhRUuTNJazWBWcJGvEHOM+NO6DhoRMAOX7FY5o=
 github.com/sethvargo/go-githubactions v1.0.0 
h1:5mYGPNxIwIXaS8MLj4uYGWM8QM8giUVqA4FuSYOZjXE=
@@ -15,12 +25,15 @@ golang.org/x/crypto 
v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 
h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod 
h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod 
h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 
h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod 
h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 
h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod 
h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be 
h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod 
h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod 
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod 
h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
diff --git a/docbot/main.go b/docbot/main.go
index f98bfff..1a98d44 100644
--- a/docbot/main.go
+++ b/docbot/main.go
@@ -1,586 +1,10 @@
 package main
 
 import (
-       "context"
-       "fmt"
-       "os"
-       "regexp"
-       "strings"
-
-       "github.com/google/go-github/v45/github"
-       "github.com/sethvargo/go-githubactions"
-       "golang.org/x/oauth2"
-
        "github.com/apache/pulsar-test-infra/docbot/pkg/logger"
+       "github.com/sethvargo/go-githubactions"
 )
 
-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 ActionConfig struct {
-       token  *string
-       repo   *string
-       owner  *string
-       number *int
-
-       labelPattern        *string
-       labelWatchSet       map[string]struct{}
-       labelMissing        *string
-       enableLabelMissing  *bool
-       enableLabelMultiple *bool
-
-       // labels extracted from PR body
-       labels map[string]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{}{}
-       }
-
-       enableLabelMissingSlug := os.Getenv("ENABLE_LABEL_MISSING")
-       enableLabelMissing := true
-       if enableLabelMissingSlug == "false" {
-               enableLabelMissing = false
-       }
-
-       labelMissing := os.Getenv("LABEL_MISSING")
-       if len(labelMissing) == 0 {
-               labelMissing = "label-missing"
-       }
-
-       enableLabelMultipleSlug := os.Getenv("ENABLE_LABEL_MULTIPLE")
-       enableLabelMultiple := false
-       if enableLabelMultipleSlug == "true" {
-               enableLabelMultiple = true
-       }
-
-       return &ActionConfig{
-               token:               &token,
-               repo:                &repo,
-               owner:               &owner,
-               labelPattern:        &labelPattern,
-               labelWatchSet:       labelWatchSet,
-               labelMissing:        &labelMissing,
-               enableLabelMissing:  &enableLabelMissing,
-               enableLabelMultiple: &enableLabelMultiple,
-       }, nil
-}
-
-func (ac *ActionConfig) GetToken() string {
-       if ac == nil || ac.token == nil {
-               return ""
-       }
-       return *ac.token
-}
-
-func (ac *ActionConfig) GetOwner() string {
-       if ac == nil || ac.owner == nil {
-               return ""
-       }
-       return *ac.owner
-}
-
-func (ac *ActionConfig) GetRepo() string {
-       if ac == nil || ac.repo == nil {
-               return ""
-       }
-       return *ac.repo
-}
-
-func (ac *ActionConfig) GetNumber() int {
-       if ac == nil || ac.number == nil {
-               return 0
-       }
-       return *ac.number
-}
-
-func (ac *ActionConfig) GetLabelPattern() string {
-       if ac == nil || ac.labelPattern == nil {
-               return ""
-       }
-       return *ac.labelPattern
-}
-
-func (ac *ActionConfig) GetLabelMissing() string {
-       if ac == nil || ac.labelMissing == nil {
-               return ""
-       }
-       return *ac.labelMissing
-}
-
-func (ac *ActionConfig) GetEnableLabelMissing() bool {
-       if ac == nil || ac.enableLabelMissing == nil {
-               return false
-       }
-       return *ac.enableLabelMissing
-}
-
-func (ac *ActionConfig) GetEnableLabelMultiple() bool {
-       if ac == nil || ac.enableLabelMultiple == nil {
-               return false
-       }
-       return *ac.enableLabelMultiple
-}
-
-type Action struct {
-       config *ActionConfig
-
-       globalContext context.Context
-       client        *github.Client
-
-       // opened, edited, labeled, unlabeled
-       event string
-}
-
-func NewAction(ac *ActionConfig) *Action {
-       ctx := context.Background()
-       ts := oauth2.StaticTokenSource(
-               &oauth2.Token{AccessToken: ac.GetToken()},
-       )
-
-       tc := oauth2.NewClient(ctx, ts)
-
-       return &Action{
-               config:        ac,
-               globalContext: ctx,
-               client:        github.NewClient(tc),
-       }
-}
-
-func (a *Action) Run(actionType string) error {
-       a.event = actionType
-       switch actionType {
-       case "opened", "edited":
-               return a.onPullRequestOpenedOrEdited()
-       case "labeled", "unlabeled":
-               return a.onPullRequestLabeledOrUnlabeled()
-       }
-       return nil
-}
-
-func (a *Action) onPullRequestOpenedOrEdited() error {
-       pr, _, err := a.client.PullRequests.Get(a.globalContext, 
a.config.GetOwner(), a.config.GetRepo(), a.config.GetNumber())
-       if err != nil {
-               return fmt.Errorf("get PR: %v", err)
-       }
-
-       // Get repo labels
-       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", a.labelsToString(repoLabels))
-
-       repoLabelsSet := make(map[string]struct{})
-       for _, label := range repoLabels {
-               repoLabelsSet[label.GetName()] = struct{}{}
-       }
-
-       // Get current labels on this PR
-       logger.Infoln("@List issue labels")
-       issueLabels, err := a.getIssueLabels()
-       if err != nil {
-               return fmt.Errorf("list current issue labels: %v", err)
-       }
-       logger.Infof("Issue labels: %v\n", a.labelsToString(issueLabels))
-
-       // Get the intersection of issueLabels and labelWatchSet, including 
labelMissing
-       logger.Infoln("@List current labels")
-       currentLabelsSet := make(map[string]struct{})
-       for _, label := range issueLabels {
-               if _, exist := a.config.labelWatchSet[label.GetName()]; !exist 
&& label.GetName() != a.config.GetLabelMissing() {
-                       continue
-               }
-               currentLabelsSet[label.GetName()] = struct{}{}
-       }
-       logger.Infof("Current labels: %v\n", 
a.labelsSetToString(currentLabelsSet))
-
-       // Get expected labels
-       // Only handle labels already exist in repo
-       logger.Infoln("@List expected labels")
-       expectedLabelsMap := make(map[string]bool)
-       for label, checked := range a.config.labels {
-               if _, exist := repoLabelsSet[label]; !exist {
-                       logger.Infof("Found label %v not exist int repo\n", 
label)
-                       continue
-               }
-               expectedLabelsMap[label] = checked
-       }
-       logger.Infof("Expected labels: %v\n", expectedLabelsMap)
-
-       // Remove labels
-       logger.Infoln("@Remove labels")
-       labelsToRemove := make(map[string]struct{})
-       if len(expectedLabelsMap) == 0 { // Remove current labels when PR body 
is empty
-               for l := range a.config.labelWatchSet {
-                       if _, exist := currentLabelsSet[l]; exist {
-                               labelsToRemove[l] = struct{}{}
-                       }
-               }
-       } else {
-               for label := range currentLabelsSet {
-                       if label == a.config.GetLabelMissing() {
-                               continue
-                       }
-                       if checked, exist := expectedLabelsMap[label]; exist && 
checked {
-                               continue
-                       }
-                       labelsToRemove[label] = struct{}{}
-               }
-       }
-
-       // Remove missing label
-       checkedCount := 0
-       for _, checked := range expectedLabelsMap {
-               if checked {
-                       checkedCount++
-               }
-       }
-
-       if !a.config.GetEnableLabelMultiple() && checkedCount > 1 {
-               logger.Infoln("Multiple labels detected")
-               _, _, err = a.client.Issues.CreateComment(a.globalContext,
-                       a.config.GetOwner(), a.config.GetRepo(), 
a.config.GetNumber(),
-                       &github.IssueComment{
-                               Body: func(v string) *string { return &v 
}(fmt.Sprintf("@%s %s", pr.User.GetLogin(), MessageLabelMultiple))})
-               if err != nil {
-                       return fmt.Errorf("create issue comment: %v", err)
-               }
-               return fmt.Errorf("%s", MessageLabelMultiple)
-       }
-
-       if _, exist := currentLabelsSet[a.config.GetLabelMissing()]; exist && 
checkedCount > 0 {
-               labelsToRemove[a.config.GetLabelMissing()] = struct{}{}
-       }
-
-       logger.Infof("Labels to remove: %v\n", 
a.labelsSetToString(labelsToRemove))
-
-       for label := range labelsToRemove {
-               _, err := a.client.Issues.RemoveLabelForIssue(a.globalContext, 
a.config.GetOwner(), a.config.GetRepo(), a.config.GetNumber(), label)
-               if err != nil {
-                       return fmt.Errorf("remove label %v: %v", label, err)
-               }
-       }
-
-       // Add labels
-       logger.Infoln("@Add labels")
-
-       labelsToAdd := []string{}
-       for label, checked := range expectedLabelsMap {
-               if !checked {
-                       continue
-               }
-               if _, exist := currentLabelsSet[label]; !exist {
-                       labelsToAdd = append(labelsToAdd, label)
-               }
-       }
-
-       if len(labelsToAdd) == 0 {
-               logger.Infoln("No labels to add.")
-       } else {
-               logger.Infof("Labels to add: %v\n", labelsToAdd)
-
-               _, _, err = a.client.Issues.AddLabelsToIssue(a.globalContext, 
a.config.GetOwner(), a.config.GetRepo(), a.config.GetNumber(), labelsToAdd)
-               if err != nil {
-                       logger.Infof("Add labels %v: %v\n", labelsToAdd, err)
-               }
-       }
-
-       // Add missing label
-       if a.config.GetEnableLabelMissing() && checkedCount == 0 {
-               if _, exist := currentLabelsSet[a.config.GetLabelMissing()]; 
exist {
-                       logger.Infoln("Already added missing label.")
-                       return fmt.Errorf("%s", MessageLabelMissing)
-               }
-
-               logger.Infoln("@Add missing label")
-               _, _, err = a.client.Issues.AddLabelsToIssue(a.globalContext,
-                       a.config.GetOwner(), a.config.GetRepo(), 
a.config.GetNumber(),
-                       []string{a.config.GetLabelMissing()})
-               if err != nil {
-                       return fmt.Errorf("add missing label %v: %v", 
a.config.GetLabelMissing(), err)
-               }
-
-               _, _, err = a.client.Issues.CreateComment(a.globalContext,
-                       a.config.GetOwner(), a.config.GetRepo(), 
a.config.GetNumber(),
-                       &github.IssueComment{
-                               Body: func(v string) *string { return &v 
}(fmt.Sprintf("@%s %s", pr.User.GetLogin(), MessageLabelMissing))})
-               if err != nil {
-                       logger.Infof("Create issue comment: %v\n", err)
-               }
-
-               return fmt.Errorf("%s", MessageLabelMissing)
-       }
-
-       return nil
-}
-
-func (a *Action) onPullRequestLabeledOrUnlabeled() error {
-       pr, _, err := a.client.PullRequests.Get(a.globalContext, 
a.config.GetOwner(), a.config.GetRepo(), a.config.GetNumber())
-       if err != nil {
-               return fmt.Errorf("get PR: %v", err)
-       }
-
-       // Get repo labels
-       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", a.labelsToString(repoLabels))
-
-       repoLabelsSet := make(map[string]struct{})
-       for _, label := range repoLabels {
-               repoLabelsSet[label.GetName()] = struct{}{}
-       }
-
-       // Get current labels on this PR
-       logger.Infoln("@List issue labels")
-       issueLabels, err := a.getIssueLabels()
-       if err != nil {
-               return fmt.Errorf("list current issue labels: %v", err)
-       }
-       logger.Infof("Issue labels: %v\n", a.labelsToString(issueLabels))
-
-       // Get the intersection of issueLabels and labelWatchSet, including 
labelMissing
-       logger.Infoln("@List current labels")
-       currentLabelsSet := make(map[string]struct{})
-       for _, label := range issueLabels {
-               if _, exist := a.config.labelWatchSet[label.GetName()]; !exist 
&& label.GetName() != a.config.GetLabelMissing() {
-                       continue
-               }
-               currentLabelsSet[label.GetName()] = struct{}{}
-       }
-       logger.Infof("Current labels: %v\n", 
a.labelsSetToString(currentLabelsSet))
-
-       // Get expected labels
-       // Only handle labels already exist in repo
-       logger.Infoln("@List expected labels")
-       expectedLabelsMap := make(map[string]bool)
-       for label, checked := range a.config.labels {
-               if _, exist := repoLabelsSet[label]; !exist {
-                       logger.Infof("Found label %v not exist int repo\n", 
label)
-                       continue
-               }
-               expectedLabelsMap[label] = checked
-       }
-       logger.Infof("Expected labels: %v\n", expectedLabelsMap)
-
-       // Remove missing label
-       labelsToRemove := make(map[string]struct{})
-       checkedCount := 0
-       for label := range currentLabelsSet {
-               if label != a.config.GetLabelMissing() {
-                       checkedCount++
-               }
-       }
-
-       if !a.config.GetEnableLabelMultiple() && checkedCount > 1 {
-               logger.Infoln("Multiple labels detected")
-               _, _, err = a.client.Issues.CreateComment(a.globalContext,
-                       a.config.GetOwner(), a.config.GetRepo(), 
a.config.GetNumber(),
-                       &github.IssueComment{
-                               Body: func(v string) *string { return &v 
}(fmt.Sprintf("@%s %s", pr.User.GetLogin(), MessageLabelMultiple))})
-               if err != nil {
-                       return fmt.Errorf("create issue comment: %v", err)
-               }
-               return fmt.Errorf("%s", MessageLabelMultiple)
-       }
-
-       if _, exist := currentLabelsSet[a.config.GetLabelMissing()]; exist && 
checkedCount > 0 {
-               labelsToRemove[a.config.GetLabelMissing()] = struct{}{}
-       }
-
-       logger.Infof("Labels to remove: %v\n", labelsToRemove)
-
-       for label := range labelsToRemove {
-               _, err := a.client.Issues.RemoveLabelForIssue(a.globalContext, 
a.config.GetOwner(), a.config.GetRepo(), a.config.GetNumber(), label)
-               if err != nil {
-                       return fmt.Errorf("remove label %v: %v", label, err)
-               }
-       }
-
-       // Add missing label
-       if a.config.GetEnableLabelMissing() && checkedCount == 0 {
-               if _, exist := currentLabelsSet[a.config.GetLabelMissing()]; 
exist {
-                       logger.Infoln("Already added missing label.")
-                       return fmt.Errorf("%s", MessageLabelMissing)
-               }
-
-               logger.Infoln("@Add missing label")
-               _, _, err = a.client.Issues.AddLabelsToIssue(a.globalContext,
-                       a.config.GetOwner(), a.config.GetRepo(), 
a.config.GetNumber(),
-                       []string{a.config.GetLabelMissing()})
-               if err != nil {
-                       return fmt.Errorf("add missing label %v: %v", 
a.config.GetLabelMissing(), err)
-               }
-
-               _, _, err = a.client.Issues.CreateComment(a.globalContext,
-                       a.config.GetOwner(), a.config.GetRepo(), 
a.config.GetNumber(),
-                       &github.IssueComment{
-                               Body: func(v string) *string { return &v 
}(fmt.Sprintf("@%s %s", pr.User.GetLogin(), MessageLabelMissing))})
-               if err != nil {
-                       logger.Infof("Create issue comment: %v\n", err)
-               }
-
-               return fmt.Errorf("%s", MessageLabelMissing)
-       }
-
-       // Update PR Body
-       // Compare current labels and expected labels
-       if a.event == "unlabeled" {
-               return nil
-       }
-
-       changeList := make(map[string]bool)
-       for label := range currentLabelsSet {
-               if checked, exist := expectedLabelsMap[label]; exist && checked 
{
-                       continue
-               }
-
-               // If not exist, need to add
-
-               // If exist but not checked, need to update
-
-               changeList[label] = true
-       }
-
-       for label, checked := range expectedLabelsMap {
-               if _, exist := currentLabelsSet[label]; !exist && checked {
-                       changeList[label] = false
-               }
-       }
-
-       body := pr.GetBody()
-       for label, checked := range changeList {
-               src := fmt.Sprintf("- [ ] `%s`", label)
-               dst := fmt.Sprintf("- [x] `%s`", label)
-               if !checked {
-                       src = fmt.Sprintf("- [x] `%s`", label)
-                       dst = fmt.Sprintf("- [ ] `%s`", label)
-               }
-
-               if strings.Contains(body, src) { // Update the label
-                       body = strings.Replace(body, src, dst, 1)
-               } else { // Add the label
-                       body = fmt.Sprintf("%s\r\n%s\r\n", body, dst)
-               }
-       }
-
-       if len(changeList) > 0 {
-               logger.Infoln("@Update PR body")
-               logger.Infof("ChangeList: %v\n", changeList)
-
-               _, _, err = a.client.PullRequests.Edit(a.globalContext, 
a.config.GetOwner(), a.config.GetRepo(), a.config.GetNumber(),
-                       &github.PullRequest{Body: &body})
-               if err != nil {
-                       return fmt.Errorf("edit PR: %v", err)
-               }
-       }
-
-       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() ([]*github.Label, error) {
-       ctx := context.Background()
-       listOptions := &github.ListOptions{PerPage: 100}
-       repoLabels := make([]*github.Label, 0)
-       for {
-               rLabels, resp, err := a.client.Issues.ListLabels(ctx, 
a.config.GetOwner(), a.config.GetRepo(), listOptions)
-               if err != nil {
-                       return nil, err
-               }
-               repoLabels = append(repoLabels, rLabels...)
-               if resp.NextPage == 0 {
-                       break
-               }
-               listOptions.Page = resp.NextPage
-       }
-       return repoLabels, nil
-}
-
-func (a *Action) getIssueLabels() ([]*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.config.GetNumber(), 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) labelsToString(labels []*github.Label) []string {
-       result := []string{}
-       for _, label := range labels {
-               result = append(result, label.GetName())
-       }
-       return result
-}
-
-func (a *Action) labelsSetToString(labels map[string]struct{}) []string {
-       result := []string{}
-       for label := range labels {
-               result = append(result, label)
-       }
-       return result
-}
-
 func main() {
        logger.Infoln("@Start docbot")
 
@@ -597,8 +21,6 @@ func main() {
        }
 
        switch githubContext.EventName {
-       case "issues":
-               logger.Infoln("@EventName is issues")
        case "pull_request", "pull_request_target":
                logger.Infoln("@EventName is PR")
 
@@ -607,26 +29,8 @@ func main() {
                        logger.Fatalln("Action type is not string")
                }
 
-               pr := githubContext.Event["pull_request"]
-               pullRequest, ok := pr.(map[string]interface{})
-               if !ok {
-                       logger.Fatalln("PR event is not map")
-               }
-
                number := int(githubContext.Event["number"].(float64))
-
-               prBody, ok := pullRequest["body"].(string)
-               if !ok {
-                       logger.Fatalln("PR body is not string")
-               }
-
-               // Get expected labels
-               labels := action.extractLabels(prBody)
-
-               actionConfig.number = &number
-               actionConfig.labels = labels
-
-               if err := action.Run(actionType); err != nil {
+               if err := action.Run(number, actionType); err != nil {
                        logger.Fatalln(err)
                }
        }

Reply via email to