This commit is contained in:
Sunny 2026-04-22 08:11:59 +00:00 committed by GitHub
commit a2f1b231f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 113 additions and 42 deletions

View file

@ -48,6 +48,12 @@ inputs:
description: Number of seconds to wait between polling GitHub API for approval status
required: false
default: '10'
close-issue-means-denial:
description: >
If true, closing the approval issue without an explicit approval
comment will be treated as a denial. Disabled by default.
required: false
default: "false"
outputs:
issue-number:
description: The number of the issue created

View file

@ -11,23 +11,24 @@ import (
)
type approvalEnvironment struct {
client *github.Client
repoFullName string
repo string
repoOwner string
runID int
approvalIssue *github.Issue
approvalIssueNumber int
issueTitle string
issueBody string
issueApprovers []string
minimumApprovals int
targetRepoOwner string
targetRepoName string
failOnDenial bool
client *github.Client
repoFullName string
repo string
repoOwner string
runID int
approvalIssue *github.Issue
approvalIssueNumber int
issueTitle string
issueBody string
issueApprovers []string
minimumApprovals int
targetRepoOwner string
targetRepoName string
failOnDenial bool
closeIssueMeansDenial bool
}
func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner string, runID int, approvers []string, minimumApprovals int, issueTitle, issueBody string, targetRepoOwner string, targetRepoName string, failOnDenial bool) (*approvalEnvironment, error) {
func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner string, runID int, approvers []string, minimumApprovals int, issueTitle, issueBody string, targetRepoOwner string, targetRepoName string, failOnDenial bool, closeIssueMeansDenial bool) (*approvalEnvironment, error) {
repoOwnerAndName := strings.Split(repoFullName, "/")
if len(repoOwnerAndName) != 2 {
return nil, fmt.Errorf("repo owner and name in unexpected format: %s", repoFullName)
@ -35,18 +36,19 @@ func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner strin
repo := repoOwnerAndName[1]
return &approvalEnvironment{
client: client,
repoFullName: repoFullName,
repo: repo,
repoOwner: repoOwner,
runID: runID,
issueApprovers: approvers,
minimumApprovals: minimumApprovals,
issueTitle: issueTitle,
issueBody: issueBody,
targetRepoOwner: targetRepoOwner,
targetRepoName: targetRepoName,
failOnDenial: failOnDenial,
client: client,
repoFullName: repoFullName,
repo: repo,
repoOwner: repoOwner,
runID: runID,
issueApprovers: approvers,
minimumApprovals: minimumApprovals,
issueTitle: issueTitle,
issueBody: issueBody,
targetRepoOwner: targetRepoOwner,
targetRepoName: targetRepoName,
failOnDenial: failOnDenial,
closeIssueMeansDenial: closeIssueMeansDenial,
}, nil
}
@ -150,9 +152,9 @@ func (a *approvalEnvironment) SetActionOutputs(outputs map[string]string) (bool,
return false, err
}
defer func() {
_ = f.Close() // Error explicitly ignored as there is nothing to handle if file close fails.
}()
defer func() {
_ = f.Close() // Error explicitly ignored as there is nothing to handle if file close fails.
}()
var pairs []string
@ -308,10 +310,10 @@ func splitLongString(input string) []string {
currentLength := 0
for i, line := range lines {
lineLength := len(line)
lineLength := len(line)
if i < len(lines)-1 {
lineLength++
}
}
if currentLength+lineLength > maxLength {
if currentChunk.Len() > 0 {
@ -344,4 +346,3 @@ func splitLongString(input string) []string {
}
return result
}

View file

@ -481,9 +481,9 @@ func TestSaveOutput(t *testing.T) {
minimumApprovals: 0,
}
if err := os.Remove(testCase.env_github_output); err != nil && !os.IsNotExist(err) {
t.Fatalf("failed to remove file: %v", err)
}
if err := os.Remove(testCase.env_github_output); err != nil && !os.IsNotExist(err) {
t.Fatalf("failed to remove file: %v", err)
}
actual, err := a.SetActionOutputs(nil)

View file

@ -26,6 +26,7 @@ const (
envVarTargetRepoOwner string = "INPUT_TARGET-REPOSITORY-OWNER"
envVarTargetRepo string = "INPUT_TARGET-REPOSITORY"
envVarPollingIntervalSeconds string = "INPUT_POLLING-INTERVAL-SECONDS"
envVarCloseIssueMeansDenial string = "INPUT_CLOSE-ISSUE-MEANS-DENIAL"
)
var (

75
main.go
View file

@ -51,6 +51,7 @@ 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)
go func() {
loop_ctr := 0
for {
comments, _, err := client.Issues.ListComments(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueListCommentsOptions{})
if err != nil {
@ -119,6 +120,58 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie
channel <- 1
close(channel)
return
case approvalStatusPending:
if apprv.closeIssueMeansDenial {
// Loop counter to make an API call only once per 10 interation, intention: avoid github rate limiting and reduce api cost and stress.
if loop_ctr < 10 {
loop_ctr += 1
continue
}
loop_ctr = 0
issue, _, err := client.Issues.Get(
ctx,
apprv.targetRepoOwner,
apprv.targetRepoName,
apprv.approvalIssueNumber,
)
if err != nil {
fmt.Printf("error fetching issue state: %v\n", err)
channel <- 1
close(channel)
return
}
if issue.GetState() == "closed" {
// Issue was closed externally without any approval/denial comment.
// Treat as denial per user configuration.
denyComment := "Issue was closed without approval. Treating closure as denial"
if !apprv.failOnDenial {
denyComment += " but continuing workflow."
} else {
denyComment += " and failing workflow."
}
fmt.Println(denyComment)
// Issue is already closed — add comment only, skip re-closing
_, _, err := client.Issues.CreateComment(
ctx,
apprv.targetRepoOwner,
apprv.targetRepoName,
apprv.approvalIssueNumber,
&github.IssueComment{Body: &denyComment},
)
if err != nil {
fmt.Printf("error commenting on closed issue: %v\n", err)
}
channel <- 1
close(channel)
return
}
}
}
time.Sleep(pollingInterval)
@ -237,6 +290,16 @@ func main() {
}
}
closeIssueMeansDenial := false
closeIssueMeansDenialRaw := os.Getenv(envVarCloseIssueMeansDenial)
if closeIssueMeansDenialRaw != "" {
closeIssueMeansDenial, err = strconv.ParseBool(closeIssueMeansDenialRaw)
if err != nil {
fmt.Printf("error parsing close-issue-means-denial: %v\n", err)
os.Exit(1)
}
}
pollingInterval := defaultPollingInterval
pollingIntervalSecondsRaw := os.Getenv(envVarPollingIntervalSeconds)
if pollingIntervalSecondsRaw != "" {
@ -274,7 +337,7 @@ func main() {
}
}
apprv, err := newApprovalEnvironment(client, repoFullName, repoOwner, runID, approvers, minimumApprovals, issueTitle, issueBody, targetRepoOwner, targetRepoName, failOnDenial)
apprv, err := newApprovalEnvironment(client, repoFullName, repoOwner, runID, approvers, minimumApprovals, issueTitle, issueBody, targetRepoOwner, targetRepoName, failOnDenial, closeIssueMeansDenial)
if err != nil {
fmt.Printf("error creating approval environment: %v\n", err)
os.Exit(1)
@ -286,9 +349,9 @@ func main() {
os.Exit(1)
}
outputs := map[string]string {
outputs := map[string]string{
"issue-number": fmt.Sprintf("%d", apprv.approvalIssueNumber),
"issue-url": apprv.approvalIssue.GetHTMLURL(),
"issue-url": apprv.approvalIssue.GetHTMLURL(),
}
_, err = apprv.SetActionOutputs(outputs)
if err != nil {
@ -305,15 +368,15 @@ func main() {
case exitCode := <-commentLoopChannel:
approvalStatus := ""
if (!failOnDenial && exitCode == 1) {
if !failOnDenial && exitCode == 1 {
approvalStatus = "denied"
exitCode = 0
} else if (exitCode == 1) {
} else if exitCode == 1 {
approvalStatus = "denied"
} else {
approvalStatus = "approved"
}
outputs := map[string]string {
outputs := map[string]string{
"approval-status": approvalStatus,
}
if _, err := apprv.SetActionOutputs(outputs); err != nil {