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
This commit is contained in:
parent
d8289abf87
commit
bac46d08b3
7 changed files with 66 additions and 27 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
.env
|
||||
.idea/
|
||||
go.sum
|
||||
output.txt
|
||||
5
Makefile
5
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
13
approval.go
13
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
47
main.go
47
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue