diff --git a/approval.go b/approval.go index 10be90d..d4c3751 100644 --- a/approval.go +++ b/approval.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "regexp" "strings" "github.com/google/go-github/v43/github" @@ -47,8 +48,12 @@ URL: %s Required approvers: %s -Respond '%s' to continue workflow or '%s' to cancel. - `, a.runURL(), a.approvers, approvalStatusApproved, approvalStatusDenied) +Respond '%s' to continue workflow or '%s' to cancel.`, + a.runURL(), + a.approvers, + formatAcceptedWords(approvedWords), + formatAcceptedWords(deniedWords), + ) var err error a.approvalIssue, _, err = a.client.Issues.Create(ctx, a.repoOwner, a.repo, &github.IssueRequest{ Title: &issueTitle, @@ -59,7 +64,7 @@ Respond '%s' to continue workflow or '%s' to cancel. return err } -func approvalFromComments(comments []*github.IssueComment, approvers []string) approvalStatus { +func approvalFromComments(comments []*github.IssueComment, approvers []string) (approvalStatus, error) { remainingApprovers := make([]string, len(approvers)) copy(remainingApprovers, approvers) @@ -71,19 +76,29 @@ func approvalFromComments(comments []*github.IssueComment, approvers []string) a } commentBody := comment.GetBody() - if commentBody == string(approvalStatusApproved) { + isApprovalComment, err := isApproved(commentBody) + if err != nil { + return approvalStatusPending, err + } + if isApprovalComment { if len(remainingApprovers) == 1 { - return approvalStatusApproved + return approvalStatusApproved, nil } remainingApprovers[approverIdx] = remainingApprovers[len(remainingApprovers)-1] remainingApprovers = remainingApprovers[:len(remainingApprovers)-1] continue - } else if commentBody == string(approvalStatusDenied) { - return approvalStatusDenied + } + + isDenialComment, err := isDenied(commentBody) + if err != nil { + return approvalStatusPending, err + } + if isDenialComment { + return approvalStatusDenied, nil } } - return approvalStatusPending + return approvalStatusPending, nil } func approversIndex(approvers []string, name string) int { @@ -94,3 +109,41 @@ func approversIndex(approvers []string, name string) int { } return -1 } + +func isApproved(commentBody string) (bool, error) { + for _, approvedWord := range approvedWords { + matched, err := regexp.MatchString(fmt.Sprintf("(?i)^%s[.!]?$", approvedWord), commentBody) + if err != nil { + return false, err + } + if matched { + return true, nil + } + } + + return false, nil +} + +func isDenied(commentBody string) (bool, error) { + for _, deniedWord := range deniedWords { + matched, err := regexp.MatchString(fmt.Sprintf("(?i)^%s[.!]?$", deniedWord), commentBody) + if err != nil { + return false, err + } + if matched { + return true, nil + } + } + + return false, nil +} + +func formatAcceptedWords(words []string) string { + var quotedWords []string + + for _, word := range words { + quotedWords = append(quotedWords, fmt.Sprintf("\"%s\"", word)) + } + + return strings.Join(quotedWords, ",") +} diff --git a/approval_test.go b/approval_test.go index 976b90c..a62a689 100644 --- a/approval_test.go +++ b/approval_test.go @@ -116,10 +116,151 @@ func TestApprovalFromComments(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - actual := approvalFromComments(testCase.comments, testCase.approvers) + actual, err := approvalFromComments(testCase.comments, testCase.approvers) + if err != nil { + t.Fatalf("error getting approval from comments: %v", err) + } + if actual != testCase.expectedStatus { t.Fatalf("actual %s, expected %s", actual, testCase.expectedStatus) } }) } } + +func TestApprovedCommentBody(t *testing.T) { + testCases := []struct { + name string + commentBody string + isSuccess bool + }{ + { + name: "approved_lowercase_no_punctuation", + commentBody: "approved", + isSuccess: true, + }, + { + name: "approve_lowercase_no_punctuation", + commentBody: "approve", + isSuccess: true, + }, + { + name: "lgtm_lowercase_no_punctuation", + commentBody: "lgtm", + isSuccess: true, + }, + { + name: "yes_lowercase_no_punctuation", + commentBody: "yes", + isSuccess: true, + }, + { + name: "approve_uppercase_no_punctuation", + commentBody: "APPROVE", + isSuccess: true, + }, + { + name: "approved_titlecase_period", + commentBody: "Approved.", + isSuccess: true, + }, + { + name: "approved_titlecase_exclamation", + commentBody: "Approved!", + isSuccess: true, + }, + { + name: "approved_titlecase_question", + commentBody: "Approved?", + isSuccess: false, + }, + { + name: "sentence_with_keyword", + commentBody: "should i approve this", + isSuccess: false, + }, + { + name: "sentence_without_keyword", + commentBody: "this is just some random comment", + isSuccess: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + actual, err := isApproved(testCase.commentBody) + if err != nil { + t.Fatalf("error getting approval: %v", err) + } + if actual != testCase.isSuccess { + t.Fatalf("expected %v but got %v", testCase.isSuccess, actual) + } + }) + } +} + +func TestDeniedCommentBody(t *testing.T) { + testCases := []struct { + name string + commentBody string + isSuccess bool + }{ + { + name: "denied_lowercase_no_punctuation", + commentBody: "denied", + isSuccess: true, + }, + { + name: "deny_lowercase_no_punctuation", + commentBody: "deny", + isSuccess: true, + }, + { + name: "no_lowercase_no_punctuation", + commentBody: "no", + isSuccess: true, + }, + { + name: "deny_uppercase_no_punctuation", + commentBody: "DENY", + isSuccess: true, + }, + { + name: "denied_titlecase_period", + commentBody: "Denied.", + isSuccess: true, + }, + { + name: "denied_titlecase_exclamation", + commentBody: "Denied!", + isSuccess: true, + }, + { + name: "deny_titlecase_question", + commentBody: "Deny?", + isSuccess: false, + }, + { + name: "sentence_with_keyword", + commentBody: "should i deny this", + isSuccess: false, + }, + { + name: "sentence_without_keyword", + commentBody: "this is just some random comment", + isSuccess: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + actual, err := isDenied(testCase.commentBody) + if err != nil { + t.Fatalf("error getting approval: %v", err) + } + if actual != testCase.isSuccess { + t.Fatalf("expected %v but got %v", testCase.isSuccess, actual) + } + }) + } +} diff --git a/constants.go b/constants.go index f8106ac..068eb61 100644 --- a/constants.go +++ b/constants.go @@ -11,3 +11,8 @@ const ( envVarToken string = "INPUT_SECRET" envVarApprovers string = "INPUT_APPROVERS" ) + +var ( + approvedWords = []string{"approved", "approve", "lgtm", "yes"} + deniedWords = []string{"denied", "deny", "no"} +) diff --git a/main.go b/main.go index 27756ba..19f17b9 100644 --- a/main.go +++ b/main.go @@ -57,7 +57,11 @@ commentLoop: os.Exit(1) } - approved := approvalFromComments(comments, approvers) + approved, err := approvalFromComments(comments, approvers) + if err != nil { + fmt.Printf("error getting approval from comments: %v\n", err) + os.Exit(1) + } fmt.Printf("Workflow status: %s\n", approved) switch approved { case approvalStatusApproved: