Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.env
.idea/
go.sum
output.txt
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ build_push:
test:
go test -v .


.PHONY: lint
lint:
docker run --rm -v $$(pwd):/app -w /app golangci/golangci-lint:v2.1.6 golangci-lint run -v
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ The file method works unless the file itself is so big that after breaking it in

### Outputs

* `issue-number` is a string that indicates the number of the issue created.
* `issue-url` is a string that indicates the URL of the issue created.
* `approval-status` is a string that indicates the final status of the approval. This will be either `approved` or `denied`.
* `issue-responder` is a string that indicates the GitHub username that approved or denied the request.

### Creating Issues in a different repository

Expand Down
2 changes: 2 additions & 0 deletions action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ outputs:
description: The URL of the issue created
approval-status:
description: The status of the approval ("approved" or "denied")
issue-responder:
description: The GitHub username that approved or denied the request
runs:
using: docker
image: docker://ghcr.io/trstringer/manual-approval:1.12.0
12 changes: 6 additions & 6 deletions approval.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func (a *approvalEnvironment) SetActionOutputs(outputs map[string]string) (bool,
return true, nil
}

func approvalFromComments(comments []*github.IssueComment, approvers []string, minimumApprovals int) (approvalStatus, error) {
func approvalFromComments(comments []*github.IssueComment, approvers []string, minimumApprovals int) (approvalStatus, string, error) {
remainingApprovers := make([]string, len(approvers))
copy(remainingApprovers, approvers)

Expand All @@ -202,11 +202,11 @@ func approvalFromComments(comments []*github.IssueComment, approvers []string, m
commentBody := comment.GetBody()
isApprovalComment, err := isApproved(commentBody)
if err != nil {
return approvalStatusPending, err
return approvalStatusPending, "", err
}
if isApprovalComment {
if len(remainingApprovers) == len(approvers)-minimumApprovals+1 {
return approvalStatusApproved, nil
return approvalStatusApproved, commentUser, nil
}
remainingApprovers[approverIdx] = remainingApprovers[len(remainingApprovers)-1]
remainingApprovers = remainingApprovers[:len(remainingApprovers)-1]
Expand All @@ -215,14 +215,14 @@ func approvalFromComments(comments []*github.IssueComment, approvers []string, m

isDenialComment, err := isDenied(commentBody)
if err != nil {
return approvalStatusPending, err
return approvalStatusPending, "", err
}
if isDenialComment {
return approvalStatusDenied, nil
return approvalStatusDenied, commentUser, nil
}
}

return approvalStatusPending, nil
return approvalStatusPending, "", nil
}

func approversIndex(approvers []string, name string) int {
Expand Down
22 changes: 19 additions & 3 deletions approval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func TestApprovalFromComments(t *testing.T) {
approvers []string
minimumApprovals int
expectedStatus approvalStatus
expectedResponder string
}{
{
name: "single_approver_single_comment_approved",
Expand All @@ -36,6 +37,7 @@ func TestApprovalFromComments(t *testing.T) {
},
approvers: []string{login1},
expectedStatus: approvalStatusApproved,
expectedResponder: login1,
},
{
name: "single_approver_single_comment_denied",
Expand All @@ -47,6 +49,7 @@ func TestApprovalFromComments(t *testing.T) {
},
approvers: []string{login1},
expectedStatus: approvalStatusDenied,
expectedResponder: login1,
},
{
name: "single_approver_single_comment_pending",
Expand All @@ -58,6 +61,7 @@ func TestApprovalFromComments(t *testing.T) {
},
approvers: []string{login1},
expectedStatus: approvalStatusPending,
expectedResponder: "",
},
{
name: "single_approver_multi_comment_approved",
Expand All @@ -73,6 +77,7 @@ func TestApprovalFromComments(t *testing.T) {
},
approvers: []string{login1},
expectedStatus: approvalStatusApproved,
expectedResponder: login1,
},
{
name: "multi_approver_approved",
Expand All @@ -88,6 +93,7 @@ func TestApprovalFromComments(t *testing.T) {
},
approvers: []string{login1, login2},
expectedStatus: approvalStatusApproved,
expectedResponder: login2,
},
{
name: "multi_approver_mixed",
Expand All @@ -103,6 +109,7 @@ func TestApprovalFromComments(t *testing.T) {
},
approvers: []string{login1, login2},
expectedStatus: approvalStatusPending,
expectedResponder: "",
},
{
name: "multi_approver_denied",
Expand All @@ -118,6 +125,7 @@ func TestApprovalFromComments(t *testing.T) {
},
approvers: []string{login1, login2},
expectedStatus: approvalStatusDenied,
expectedResponder: login1,
},
{
name: "multi_approver_minimum_one_approval",
Expand All @@ -134,6 +142,7 @@ func TestApprovalFromComments(t *testing.T) {
approvers: []string{login1, login2},
expectedStatus: approvalStatusApproved,
minimumApprovals: 1,
expectedResponder: login2,
},
{
name: "multi_approver_minimum_two_approvals",
Expand All @@ -150,6 +159,7 @@ func TestApprovalFromComments(t *testing.T) {
approvers: []string{login1, login2, login3},
expectedStatus: approvalStatusApproved,
minimumApprovals: 2,
expectedResponder: login2,
},
{
name: "multi_approver_approvals_less_than_minimum",
Expand All @@ -162,6 +172,7 @@ func TestApprovalFromComments(t *testing.T) {
approvers: []string{login1, login2, login3},
expectedStatus: approvalStatusPending,
minimumApprovals: 2,
expectedResponder: "",
},
{
name: "single_approver_single_comment_approved_case_insensitive",
Expand All @@ -173,18 +184,23 @@ func TestApprovalFromComments(t *testing.T) {
},
approvers: []string{login1},
expectedStatus: approvalStatusApproved,
expectedResponder: login1u,
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
actual, err := approvalFromComments(testCase.comments, testCase.approvers, testCase.minimumApprovals)
actualStatus, actualResponder, err := approvalFromComments(testCase.comments, testCase.approvers, testCase.minimumApprovals)
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)
if actualStatus != testCase.expectedStatus {
t.Fatalf("actual %s, expected %s", actualStatus, testCase.expectedStatus)
}

if actualResponder != testCase.expectedResponder {
t.Fatalf("actual responder %s, expected %s", actualResponder, testCase.expectedResponder)
}
})
}
Expand Down
39 changes: 25 additions & 14 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,29 @@ func handleInterrupt(ctx context.Context, client *github.Client, apprv *approval
}
}

func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, client *github.Client, pollingInterval time.Duration) chan int {
channel := make(chan int)
type commentLoopResult struct {
exitCode int
responder string
status approvalStatus
}

func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, client *github.Client, pollingInterval time.Duration) chan commentLoopResult {
channel := make(chan commentLoopResult)
go func() {
loop_ctr := 0
for {
comments, _, err := client.Issues.ListComments(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueListCommentsOptions{})
if err != nil {
fmt.Printf("error getting comments: %v\n", err)
channel <- 1
channel <- commentLoopResult{exitCode: 1}
close(channel)
return
}

approved, err := approvalFromComments(comments, apprv.issueApprovers, apprv.minimumApprovals)
approved, responder, err := approvalFromComments(comments, apprv.issueApprovers, apprv.minimumApprovals)
if err != nil {
fmt.Printf("error getting approval from comments: %v\n", err)
channel <- 1
channel <- commentLoopResult{exitCode: 1}
close(channel)
return
}
Expand All @@ -78,17 +84,17 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie
})
if err != nil {
fmt.Printf("error commenting on issue: %v\n", err)
channel <- 1
channel <- commentLoopResult{exitCode: 1}
close(channel)
return
}
if err = patchIssueState(ctx, client, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, newState); err != nil {
fmt.Printf("error closing issue: %v\n", err)
channel <- 1
channel <- commentLoopResult{exitCode: 1}
close(channel)
return
}
channel <- 0
channel <- commentLoopResult{exitCode: 0, responder: responder, status: approvalStatusApproved}
fmt.Println("Workflow manual approval completed")
close(channel)
return
Expand All @@ -107,17 +113,17 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie
})
if err != nil {
fmt.Printf("error commenting on issue: %v\n", err)
channel <- 1
channel <- commentLoopResult{exitCode: 1}
close(channel)
return
}
if err = patchIssueState(ctx, client, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, newState); err != nil {
fmt.Printf("error closing issue: %v\n", err)
channel <- 1
channel <- commentLoopResult{exitCode: 1}
close(channel)
return
}
channel <- 1
channel <- commentLoopResult{exitCode: 1, responder: responder, status: approvalStatusDenied}
close(channel)
return
case approvalStatusPending:
Expand Down Expand Up @@ -374,7 +380,7 @@ func main() {
commentLoopChannel := newCommentLoopChannel(ctx, apprv, client, pollingInterval)

select {
case exitCode := <-commentLoopChannel:
case result := <-commentLoopChannel:
approvalStatus := ""

if !failOnDenial && exitCode == 1 {
Expand All @@ -385,14 +391,19 @@ func main() {
} else {
approvalStatus = "approved"
}

outputs := map[string]string{
"approval-status": approvalStatus,
"issue-responder": result.responder,
}
if _, err := apprv.SetActionOutputs(outputs); err != nil {
fmt.Printf("error setting action output: %v\n", err)
exitCode = 1
result.exitCode = 1
}
if !failOnDenial && result.exitCode == 1 {
result.exitCode = 0
}
os.Exit(exitCode)
os.Exit(result.exitCode)
case <-killSignalChannel:
handleInterrupt(ctx, client, apprv)
os.Exit(1)
Expand Down
Loading