From bac46d08b3bf7670e2e5ad9958e6fba5a0a01ae7 Mon Sep 17 00:00:00 2001 From: Felipe Lambert Date: Mon, 26 Jan 2026 11:19:45 -0300 Subject: [PATCH] Add issue approver output and enhance approval logic - Updated action outputs to include the GitHub username of the approver. - Modified approval logic to return the approver's username upon approval or denial. - Enhanced tests to validate the new approver output. Rename 'issue-approver' to 'issue-responder' for clarity in approval process --- .gitignore | 1 + Makefile | 5 +++++ README.md | 3 +++ action.yaml | 2 ++ approval.go | 13 ++++++------- approval_test.go | 22 +++++++++++++++++++--- main.go | 47 ++++++++++++++++++++++++++++++----------------- 7 files changed, 66 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 03ad3d9..5144618 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env .idea/ go.sum +output.txt \ No newline at end of file diff --git a/Makefile b/Makefile index 420c847..755774d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ IMAGE_REPO=ghcr.io/trstringer/manual-approval TARGET_PLATFORM=linux/amd64,linux/arm64,linux/arm/v8 +GO_DOCKER_IMAGE=golang:1.24 .PHONY: tidy tidy: @@ -28,6 +29,10 @@ build_push: test: go test -v . +.PHONY: test_docker +test_docker: + docker run --rm -v $$(pwd):/app -w /app $(GO_DOCKER_IMAGE) sh -c "go mod tidy && go test -v ." + .PHONY: lint lint: docker run --rm -v $$(pwd):/app -w /app golangci/golangci-lint:v2.1.6 golangci-lint run -v diff --git a/README.md b/README.md index b65cf17..e1f0319 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,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 diff --git a/action.yaml b/action.yaml index fdd28ae..297b695 100644 --- a/action.yaml +++ b/action.yaml @@ -55,6 +55,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 diff --git a/approval.go b/approval.go index 6391ad2..14a21f0 100644 --- a/approval.go +++ b/approval.go @@ -160,7 +160,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) @@ -178,11 +178,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] @@ -191,14 +191,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 { @@ -325,4 +325,3 @@ func splitLongString(input string) []string { } return result } - diff --git a/approval_test.go b/approval_test.go index 086a6fa..d0b49cf 100644 --- a/approval_test.go +++ b/approval_test.go @@ -25,6 +25,7 @@ func TestApprovalFromComments(t *testing.T) { approvers []string minimumApprovals int expectedStatus approvalStatus + expectedResponder string }{ { name: "single_approver_single_comment_approved", @@ -36,6 +37,7 @@ func TestApprovalFromComments(t *testing.T) { }, approvers: []string{login1}, expectedStatus: approvalStatusApproved, + expectedResponder: login1, }, { name: "single_approver_single_comment_denied", @@ -47,6 +49,7 @@ func TestApprovalFromComments(t *testing.T) { }, approvers: []string{login1}, expectedStatus: approvalStatusDenied, + expectedResponder: login1, }, { name: "single_approver_single_comment_pending", @@ -58,6 +61,7 @@ func TestApprovalFromComments(t *testing.T) { }, approvers: []string{login1}, expectedStatus: approvalStatusPending, + expectedResponder: "", }, { name: "single_approver_multi_comment_approved", @@ -73,6 +77,7 @@ func TestApprovalFromComments(t *testing.T) { }, approvers: []string{login1}, expectedStatus: approvalStatusApproved, + expectedResponder: login1, }, { name: "multi_approver_approved", @@ -88,6 +93,7 @@ func TestApprovalFromComments(t *testing.T) { }, approvers: []string{login1, login2}, expectedStatus: approvalStatusApproved, + expectedResponder: login2, }, { name: "multi_approver_mixed", @@ -103,6 +109,7 @@ func TestApprovalFromComments(t *testing.T) { }, approvers: []string{login1, login2}, expectedStatus: approvalStatusPending, + expectedResponder: "", }, { name: "multi_approver_denied", @@ -118,6 +125,7 @@ func TestApprovalFromComments(t *testing.T) { }, approvers: []string{login1, login2}, expectedStatus: approvalStatusDenied, + expectedResponder: login1, }, { name: "multi_approver_minimum_one_approval", @@ -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", @@ -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", @@ -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", @@ -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) } }) } diff --git a/main.go b/main.go index 89305fd..09ecaeb 100644 --- a/main.go +++ b/main.go @@ -32,21 +32,27 @@ 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() { 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) } - 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) } fmt.Printf("Workflow status: %s\n", approved) @@ -59,16 +65,16 @@ 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) } _, _, err = client.Issues.Edit(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueRequest{State: &newState}) if err != nil { fmt.Printf("error closing issue: %v\n", err) - channel <- 1 + channel <- commentLoopResult{exitCode: 1} close(channel) } - channel <- 0 + channel <- commentLoopResult{exitCode: 0, responder: responder, status: approvalStatusApproved} fmt.Println("Workflow manual approval completed") close(channel) case approvalStatusDenied: @@ -86,16 +92,16 @@ 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) } _, _, err = client.Issues.Edit(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueRequest{State: &newState}) if err != nil { fmt.Printf("error closing issue: %v\n", err) - channel <- 1 + channel <- commentLoopResult{exitCode: 1} close(channel) } - channel <- 1 + channel <- commentLoopResult{exitCode: 1, responder: responder, status: approvalStatusDenied} close(channel) } @@ -263,25 +269,32 @@ func main() { commentLoopChannel := newCommentLoopChannel(ctx, apprv, client, pollingInterval) select { - case exitCode := <-commentLoopChannel: + case result := <-commentLoopChannel: approvalStatus := "" - if (!failOnDenial && exitCode == 1) { + if result.status == approvalStatusApproved { + approvalStatus = "approved" + } else if result.status == approvalStatusDenied { approvalStatus = "denied" - exitCode = 0 - } else if (exitCode == 1) { + } else if (!failOnDenial && result.exitCode == 1) { + approvalStatus = "denied" + } else if (result.exitCode == 1) { approvalStatus = "denied" } 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 } - os.Exit(exitCode) + if !failOnDenial && result.exitCode == 1 { + result.exitCode = 0 + } + os.Exit(result.exitCode) case <-killSignalChannel: handleInterrupt(ctx, client, apprv) os.Exit(1)