Skip to content

Commit bac46d0

Browse files
committed
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
1 parent d8289ab commit bac46d0

7 files changed

Lines changed: 66 additions & 27 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.env
22
.idea/
33
go.sum
4+
output.txt

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
IMAGE_REPO=ghcr.io/trstringer/manual-approval
22
TARGET_PLATFORM=linux/amd64,linux/arm64,linux/arm/v8
3+
GO_DOCKER_IMAGE=golang:1.24
34

45
.PHONY: tidy
56
tidy:
@@ -28,6 +29,10 @@ build_push:
2829
test:
2930
go test -v .
3031

32+
.PHONY: test_docker
33+
test_docker:
34+
docker run --rm -v $$(pwd):/app -w /app $(GO_DOCKER_IMAGE) sh -c "go mod tidy && go test -v ."
35+
3136
.PHONY: lint
3237
lint:
3338
docker run --rm -v $$(pwd):/app -w /app golangci/golangci-lint:v2.1.6 golangci-lint run -v

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ The file method works unless the file itself is so big that after breaking it in
8282

8383
### Outputs
8484

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

8790
### Creating Issues in a different repository
8891

action.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ outputs:
5555
description: The URL of the issue created
5656
approval-status:
5757
description: The status of the approval ("approved" or "denied")
58+
issue-responder:
59+
description: The GitHub username that approved or denied the request
5860
runs:
5961
using: docker
6062
image: docker://ghcr.io/trstringer/manual-approval:1.12.0

approval.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func (a *approvalEnvironment) SetActionOutputs(outputs map[string]string) (bool,
160160
return true, nil
161161
}
162162

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

@@ -178,11 +178,11 @@ func approvalFromComments(comments []*github.IssueComment, approvers []string, m
178178
commentBody := comment.GetBody()
179179
isApprovalComment, err := isApproved(commentBody)
180180
if err != nil {
181-
return approvalStatusPending, err
181+
return approvalStatusPending, "", err
182182
}
183183
if isApprovalComment {
184184
if len(remainingApprovers) == len(approvers)-minimumApprovals+1 {
185-
return approvalStatusApproved, nil
185+
return approvalStatusApproved, commentUser, nil
186186
}
187187
remainingApprovers[approverIdx] = remainingApprovers[len(remainingApprovers)-1]
188188
remainingApprovers = remainingApprovers[:len(remainingApprovers)-1]
@@ -191,14 +191,14 @@ func approvalFromComments(comments []*github.IssueComment, approvers []string, m
191191

192192
isDenialComment, err := isDenied(commentBody)
193193
if err != nil {
194-
return approvalStatusPending, err
194+
return approvalStatusPending, "", err
195195
}
196196
if isDenialComment {
197-
return approvalStatusDenied, nil
197+
return approvalStatusDenied, commentUser, nil
198198
}
199199
}
200200

201-
return approvalStatusPending, nil
201+
return approvalStatusPending, "", nil
202202
}
203203

204204
func approversIndex(approvers []string, name string) int {
@@ -325,4 +325,3 @@ func splitLongString(input string) []string {
325325
}
326326
return result
327327
}
328-

approval_test.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func TestApprovalFromComments(t *testing.T) {
2525
approvers []string
2626
minimumApprovals int
2727
expectedStatus approvalStatus
28+
expectedResponder string
2829
}{
2930
{
3031
name: "single_approver_single_comment_approved",
@@ -36,6 +37,7 @@ func TestApprovalFromComments(t *testing.T) {
3637
},
3738
approvers: []string{login1},
3839
expectedStatus: approvalStatusApproved,
40+
expectedResponder: login1,
3941
},
4042
{
4143
name: "single_approver_single_comment_denied",
@@ -47,6 +49,7 @@ func TestApprovalFromComments(t *testing.T) {
4749
},
4850
approvers: []string{login1},
4951
expectedStatus: approvalStatusDenied,
52+
expectedResponder: login1,
5053
},
5154
{
5255
name: "single_approver_single_comment_pending",
@@ -58,6 +61,7 @@ func TestApprovalFromComments(t *testing.T) {
5861
},
5962
approvers: []string{login1},
6063
expectedStatus: approvalStatusPending,
64+
expectedResponder: "",
6165
},
6266
{
6367
name: "single_approver_multi_comment_approved",
@@ -73,6 +77,7 @@ func TestApprovalFromComments(t *testing.T) {
7377
},
7478
approvers: []string{login1},
7579
expectedStatus: approvalStatusApproved,
80+
expectedResponder: login1,
7681
},
7782
{
7883
name: "multi_approver_approved",
@@ -88,6 +93,7 @@ func TestApprovalFromComments(t *testing.T) {
8893
},
8994
approvers: []string{login1, login2},
9095
expectedStatus: approvalStatusApproved,
96+
expectedResponder: login2,
9197
},
9298
{
9399
name: "multi_approver_mixed",
@@ -103,6 +109,7 @@ func TestApprovalFromComments(t *testing.T) {
103109
},
104110
approvers: []string{login1, login2},
105111
expectedStatus: approvalStatusPending,
112+
expectedResponder: "",
106113
},
107114
{
108115
name: "multi_approver_denied",
@@ -118,6 +125,7 @@ func TestApprovalFromComments(t *testing.T) {
118125
},
119126
approvers: []string{login1, login2},
120127
expectedStatus: approvalStatusDenied,
128+
expectedResponder: login1,
121129
},
122130
{
123131
name: "multi_approver_minimum_one_approval",
@@ -134,6 +142,7 @@ func TestApprovalFromComments(t *testing.T) {
134142
approvers: []string{login1, login2},
135143
expectedStatus: approvalStatusApproved,
136144
minimumApprovals: 1,
145+
expectedResponder: login2,
137146
},
138147
{
139148
name: "multi_approver_minimum_two_approvals",
@@ -150,6 +159,7 @@ func TestApprovalFromComments(t *testing.T) {
150159
approvers: []string{login1, login2, login3},
151160
expectedStatus: approvalStatusApproved,
152161
minimumApprovals: 2,
162+
expectedResponder: login2,
153163
},
154164
{
155165
name: "multi_approver_approvals_less_than_minimum",
@@ -162,6 +172,7 @@ func TestApprovalFromComments(t *testing.T) {
162172
approvers: []string{login1, login2, login3},
163173
expectedStatus: approvalStatusPending,
164174
minimumApprovals: 2,
175+
expectedResponder: "",
165176
},
166177
{
167178
name: "single_approver_single_comment_approved_case_insensitive",
@@ -173,18 +184,23 @@ func TestApprovalFromComments(t *testing.T) {
173184
},
174185
approvers: []string{login1},
175186
expectedStatus: approvalStatusApproved,
187+
expectedResponder: login1u,
176188
},
177189
}
178190

179191
for _, testCase := range testCases {
180192
t.Run(testCase.name, func(t *testing.T) {
181-
actual, err := approvalFromComments(testCase.comments, testCase.approvers, testCase.minimumApprovals)
193+
actualStatus, actualResponder, err := approvalFromComments(testCase.comments, testCase.approvers, testCase.minimumApprovals)
182194
if err != nil {
183195
t.Fatalf("error getting approval from comments: %v", err)
184196
}
185197

186-
if actual != testCase.expectedStatus {
187-
t.Fatalf("actual %s, expected %s", actual, testCase.expectedStatus)
198+
if actualStatus != testCase.expectedStatus {
199+
t.Fatalf("actual %s, expected %s", actualStatus, testCase.expectedStatus)
200+
}
201+
202+
if actualResponder != testCase.expectedResponder {
203+
t.Fatalf("actual responder %s, expected %s", actualResponder, testCase.expectedResponder)
188204
}
189205
})
190206
}

main.go

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,27 @@ func handleInterrupt(ctx context.Context, client *github.Client, apprv *approval
3232
}
3333
}
3434

35-
func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, client *github.Client, pollingInterval time.Duration) chan int {
36-
channel := make(chan int)
35+
type commentLoopResult struct {
36+
exitCode int
37+
responder string
38+
status approvalStatus
39+
}
40+
41+
func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, client *github.Client, pollingInterval time.Duration) chan commentLoopResult {
42+
channel := make(chan commentLoopResult)
3743
go func() {
3844
for {
3945
comments, _, err := client.Issues.ListComments(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueListCommentsOptions{})
4046
if err != nil {
4147
fmt.Printf("error getting comments: %v\n", err)
42-
channel <- 1
48+
channel <- commentLoopResult{exitCode: 1}
4349
close(channel)
4450
}
4551

46-
approved, err := approvalFromComments(comments, apprv.issueApprovers, apprv.minimumApprovals)
52+
approved, responder, err := approvalFromComments(comments, apprv.issueApprovers, apprv.minimumApprovals)
4753
if err != nil {
4854
fmt.Printf("error getting approval from comments: %v\n", err)
49-
channel <- 1
55+
channel <- commentLoopResult{exitCode: 1}
5056
close(channel)
5157
}
5258
fmt.Printf("Workflow status: %s\n", approved)
@@ -59,16 +65,16 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie
5965
})
6066
if err != nil {
6167
fmt.Printf("error commenting on issue: %v\n", err)
62-
channel <- 1
68+
channel <- commentLoopResult{exitCode: 1}
6369
close(channel)
6470
}
6571
_, _, err = client.Issues.Edit(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueRequest{State: &newState})
6672
if err != nil {
6773
fmt.Printf("error closing issue: %v\n", err)
68-
channel <- 1
74+
channel <- commentLoopResult{exitCode: 1}
6975
close(channel)
7076
}
71-
channel <- 0
77+
channel <- commentLoopResult{exitCode: 0, responder: responder, status: approvalStatusApproved}
7278
fmt.Println("Workflow manual approval completed")
7379
close(channel)
7480
case approvalStatusDenied:
@@ -86,16 +92,16 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie
8692
})
8793
if err != nil {
8894
fmt.Printf("error commenting on issue: %v\n", err)
89-
channel <- 1
95+
channel <- commentLoopResult{exitCode: 1}
9096
close(channel)
9197
}
9298
_, _, err = client.Issues.Edit(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueRequest{State: &newState})
9399
if err != nil {
94100
fmt.Printf("error closing issue: %v\n", err)
95-
channel <- 1
101+
channel <- commentLoopResult{exitCode: 1}
96102
close(channel)
97103
}
98-
channel <- 1
104+
channel <- commentLoopResult{exitCode: 1, responder: responder, status: approvalStatusDenied}
99105
close(channel)
100106
}
101107

@@ -263,25 +269,32 @@ func main() {
263269
commentLoopChannel := newCommentLoopChannel(ctx, apprv, client, pollingInterval)
264270

265271
select {
266-
case exitCode := <-commentLoopChannel:
272+
case result := <-commentLoopChannel:
267273
approvalStatus := ""
268274

269-
if (!failOnDenial && exitCode == 1) {
275+
if result.status == approvalStatusApproved {
276+
approvalStatus = "approved"
277+
} else if result.status == approvalStatusDenied {
270278
approvalStatus = "denied"
271-
exitCode = 0
272-
} else if (exitCode == 1) {
279+
} else if (!failOnDenial && result.exitCode == 1) {
280+
approvalStatus = "denied"
281+
} else if (result.exitCode == 1) {
273282
approvalStatus = "denied"
274283
} else {
275284
approvalStatus = "approved"
276285
}
277286
outputs := map[string]string {
278287
"approval-status": approvalStatus,
288+
"issue-responder": result.responder,
279289
}
280290
if _, err := apprv.SetActionOutputs(outputs); err != nil {
281291
fmt.Printf("error setting action output: %v\n", err)
282-
exitCode = 1
292+
result.exitCode = 1
293+
}
294+
if !failOnDenial && result.exitCode == 1 {
295+
result.exitCode = 0
283296
}
284-
os.Exit(exitCode)
297+
os.Exit(result.exitCode)
285298
case <-killSignalChannel:
286299
handleInterrupt(ctx, client, apprv)
287300
os.Exit(1)

0 commit comments

Comments
 (0)