Compare commits
1 commit
main
...
trstringer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
879aad8338 |
6 changed files with 166 additions and 28 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
.env
|
||||||
|
|
@ -29,12 +29,12 @@ steps:
|
||||||
- uses: trstringer/manual-approval@v1
|
- uses: trstringer/manual-approval@v1
|
||||||
with:
|
with:
|
||||||
secret: ${{ github.TOKEN }}
|
secret: ${{ github.TOKEN }}
|
||||||
approvers: user1,user2
|
approvers: user1,user2,org-team1
|
||||||
minimum-approvals: 1
|
minimum-approvals: 1
|
||||||
issue-title: "Deploying v1.3.5 to prod from staging"
|
issue-title: "Deploying v1.3.5 to prod from staging"
|
||||||
```
|
```
|
||||||
|
|
||||||
- `approvers` is a comma-delimited list of all required approvers. (*Note: Required approvers must have the ability to be set as approvers in the repository. If you add an approver that doesn't have this permission then you would receive an HTTP/402 Validation Failed error when running this action*)
|
- `approvers` is a comma-delimited list of all required approvers. An approver can either be a user or an org team. (*Note: Required approvers must have the ability to be set as approvers in the repository. If you add an approver that doesn't have this permission then you would receive an HTTP/402 Validation Failed error when running this action*)
|
||||||
- `minimum-approvals` is an integer that sets the minimum number of approvals required to progress the workflow. Defaults to ALL approvers.
|
- `minimum-approvals` is an integer that sets the minimum number of approvals required to progress the workflow. Defaults to ALL approvers.
|
||||||
- `issue-title` is a string that will be appened to the title of the issue.
|
- `issue-title` is a string that will be appened to the title of the issue.
|
||||||
|
|
||||||
|
|
|
||||||
12
approval.go
12
approval.go
|
|
@ -15,11 +15,11 @@ type approvalEnvironment struct {
|
||||||
repo string
|
repo string
|
||||||
repoOwner string
|
repoOwner string
|
||||||
runID int
|
runID int
|
||||||
approvers []string
|
|
||||||
minimumApprovals int
|
|
||||||
approvalIssue *github.Issue
|
approvalIssue *github.Issue
|
||||||
approvalIssueNumber int
|
approvalIssueNumber int
|
||||||
issueTitle string
|
issueTitle string
|
||||||
|
issueApprovers []string
|
||||||
|
minimumApprovals int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner string, runID int, approvers []string, minimumApprovals int, issueTitle string) (*approvalEnvironment, error) {
|
func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner string, runID int, approvers []string, minimumApprovals int, issueTitle string) (*approvalEnvironment, error) {
|
||||||
|
|
@ -35,7 +35,7 @@ func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner strin
|
||||||
repo: repo,
|
repo: repo,
|
||||||
repoOwner: repoOwner,
|
repoOwner: repoOwner,
|
||||||
runID: runID,
|
runID: runID,
|
||||||
approvers: approvers,
|
issueApprovers: approvers,
|
||||||
minimumApprovals: minimumApprovals,
|
minimumApprovals: minimumApprovals,
|
||||||
issueTitle: issueTitle,
|
issueTitle: issueTitle,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
@ -59,7 +59,7 @@ Required approvers: %s
|
||||||
|
|
||||||
Respond %s to continue workflow or %s to cancel.`,
|
Respond %s to continue workflow or %s to cancel.`,
|
||||||
a.runURL(),
|
a.runURL(),
|
||||||
a.approvers,
|
a.issueApprovers,
|
||||||
formatAcceptedWords(approvedWords),
|
formatAcceptedWords(approvedWords),
|
||||||
formatAcceptedWords(deniedWords),
|
formatAcceptedWords(deniedWords),
|
||||||
)
|
)
|
||||||
|
|
@ -69,13 +69,13 @@ Respond %s to continue workflow or %s to cancel.`,
|
||||||
a.repoOwner,
|
a.repoOwner,
|
||||||
a.repo,
|
a.repo,
|
||||||
issueTitle,
|
issueTitle,
|
||||||
a.approvers,
|
a.issueApprovers,
|
||||||
issueBody,
|
issueBody,
|
||||||
)
|
)
|
||||||
a.approvalIssue, _, err = a.client.Issues.Create(ctx, a.repoOwner, a.repo, &github.IssueRequest{
|
a.approvalIssue, _, err = a.client.Issues.Create(ctx, a.repoOwner, a.repo, &github.IssueRequest{
|
||||||
Title: &issueTitle,
|
Title: &issueTitle,
|
||||||
Body: &issueBody,
|
Body: &issueBody,
|
||||||
Assignees: &a.approvers,
|
Assignees: &a.issueApprovers,
|
||||||
})
|
})
|
||||||
a.approvalIssueNumber = a.approvalIssue.GetNumber()
|
a.approvalIssueNumber = a.approvalIssue.GetNumber()
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
71
approvers.go
Normal file
71
approvers.go
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/go-github/v43/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
func retrieveApprovers(client *github.Client, repoOwner string) ([]string, error) {
|
||||||
|
approvers := []string{}
|
||||||
|
|
||||||
|
requiredApproversRaw := os.Getenv(envVarApprovers)
|
||||||
|
requiredApprovers := strings.Split(requiredApproversRaw, ",")
|
||||||
|
|
||||||
|
for _, approverUser := range requiredApprovers {
|
||||||
|
expandedUsers := expandGroupFromUser(client, repoOwner, approverUser)
|
||||||
|
if expandedUsers != nil {
|
||||||
|
approvers = append(approvers, expandedUsers...)
|
||||||
|
} else {
|
||||||
|
approvers = append(approvers, approverUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
approvers = deduplicateUsers(approvers)
|
||||||
|
|
||||||
|
minimumApprovalsRaw := os.Getenv(envVarMinimumApprovals)
|
||||||
|
minimumApprovals := len(approvers)
|
||||||
|
var err error
|
||||||
|
if minimumApprovalsRaw != "" {
|
||||||
|
minimumApprovals, err = strconv.Atoi(minimumApprovalsRaw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing minimum number of approvals: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if minimumApprovals > len(approvers) {
|
||||||
|
return nil, fmt.Errorf("error: minimum required approvals (%d) is greater than the total number of approvers (%d)", minimumApprovals, len(approvers))
|
||||||
|
}
|
||||||
|
|
||||||
|
return approvers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandGroupFromUser(client *github.Client, org, userOrTeam string) []string {
|
||||||
|
users, _, err := client.Teams.ListTeamMembersBySlug(context.Background(), org, userOrTeam, &github.TeamListTeamMembersOptions{})
|
||||||
|
if err != nil || len(users) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
userNames := make([]string, 0, len(users))
|
||||||
|
for _, user := range users {
|
||||||
|
userNames = append(userNames, user.GetLogin())
|
||||||
|
}
|
||||||
|
|
||||||
|
return userNames
|
||||||
|
}
|
||||||
|
|
||||||
|
func deduplicateUsers(users []string) []string {
|
||||||
|
uniqValuesByKey := make(map[string]bool)
|
||||||
|
uniqUsers := []string{}
|
||||||
|
for _, user := range users {
|
||||||
|
if _, ok := uniqValuesByKey[user]; !ok {
|
||||||
|
uniqValuesByKey[user] = true
|
||||||
|
uniqUsers = append(uniqUsers, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqUsers
|
||||||
|
}
|
||||||
38
approvers_test.go
Normal file
38
approvers_test.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeduplicateUsers(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
input []string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with_duplicate_user",
|
||||||
|
input: []string{"first", "second", "first"},
|
||||||
|
expected: []string{"first", "second"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "without_duplicate_user",
|
||||||
|
input: []string{"first", "second"},
|
||||||
|
expected: []string{"first", "second"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
actual := deduplicateUsers(testCase.input)
|
||||||
|
if !reflect.DeepEqual(testCase.expected, actual) {
|
||||||
|
t.Fatalf(
|
||||||
|
"unequal depulicated: expected %v actual %v",
|
||||||
|
testCase.expected,
|
||||||
|
actual,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
68
main.go
68
main.go
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-github/v43/github"
|
"github.com/google/go-github/v43/github"
|
||||||
|
|
@ -31,7 +30,7 @@ func handleInterrupt(ctx context.Context, client *github.Client, apprv *approval
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, client *github.Client, approvers []string, minimumApprovals int) chan int {
|
func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, client *github.Client) chan int {
|
||||||
channel := make(chan int)
|
channel := make(chan int)
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
|
|
@ -42,7 +41,7 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie
|
||||||
close(channel)
|
close(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
approved, err := approvalFromComments(comments, approvers, minimumApprovals)
|
approved, err := approvalFromComments(comments, apprv.issueApprovers, apprv.minimumApprovals)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("error getting approval from comments: %v\n", err)
|
fmt.Printf("error getting approval from comments: %v\n", err)
|
||||||
channel <- 1
|
channel <- 1
|
||||||
|
|
@ -106,7 +105,40 @@ func newGithubClient(ctx context.Context) *github.Client {
|
||||||
return github.NewClient(tc)
|
return github.NewClient(tc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateInput() error {
|
||||||
|
missingEnvVars := []string{}
|
||||||
|
if os.Getenv(envVarRepoFullName) == "" {
|
||||||
|
missingEnvVars = append(missingEnvVars, envVarRepoFullName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv(envVarRunID) == "" {
|
||||||
|
missingEnvVars = append(missingEnvVars, envVarRunID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv(envVarRepoOwner) == "" {
|
||||||
|
missingEnvVars = append(missingEnvVars, envVarRepoOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv(envVarToken) == "" {
|
||||||
|
missingEnvVars = append(missingEnvVars, envVarToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv(envVarApprovers) == "" {
|
||||||
|
missingEnvVars = append(missingEnvVars, envVarApprovers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missingEnvVars) > 0 {
|
||||||
|
return fmt.Errorf("missing env vars: %v", missingEnvVars)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
if err := validateInput(); err != nil {
|
||||||
|
fmt.Printf("%v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
repoFullName := os.Getenv(envVarRepoFullName)
|
repoFullName := os.Getenv(envVarRepoFullName)
|
||||||
runID, err := strconv.Atoi(os.Getenv(envVarRunID))
|
runID, err := strconv.Atoi(os.Getenv(envVarRunID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -118,26 +150,22 @@ func main() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
client := newGithubClient(ctx)
|
client := newGithubClient(ctx)
|
||||||
|
|
||||||
requiredApproversRaw := os.Getenv(envVarApprovers)
|
approvers, err := retrieveApprovers(client, repoOwner)
|
||||||
fmt.Printf("Required approvers: %s\n", requiredApproversRaw)
|
if err != nil {
|
||||||
approvers := strings.Split(requiredApproversRaw, ",")
|
fmt.Printf("error retrieving approvers: %v\n", err)
|
||||||
|
|
||||||
minimumApprovalsRaw := os.Getenv(envVarMinimumApprovals)
|
|
||||||
minimumApprovals := len(approvers)
|
|
||||||
if minimumApprovalsRaw != "" {
|
|
||||||
minimumApprovals, err = strconv.Atoi(minimumApprovalsRaw)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("error parsing minimum number of approvals: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if minimumApprovals > len(approvers) {
|
|
||||||
fmt.Printf("error: minimum required approvals (%v) is greater than the total number of approvers (%v)\n", minimumApprovals, len(approvers))
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
issueTitle := os.Getenv(envVarIssueTitle)
|
issueTitle := os.Getenv(envVarIssueTitle)
|
||||||
|
minimumApprovalsRaw := os.Getenv(envVarMinimumApprovals)
|
||||||
|
minimumApprovals := 0
|
||||||
|
if minimumApprovalsRaw != "" {
|
||||||
|
minimumApprovals, err = strconv.Atoi(minimumApprovalsRaw)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error parsing minimum approvals: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
apprv, err := newApprovalEnvironment(client, repoFullName, repoOwner, runID, approvers, minimumApprovals, issueTitle)
|
apprv, err := newApprovalEnvironment(client, repoFullName, repoOwner, runID, approvers, minimumApprovals, issueTitle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("error creating approval environment: %v\n", err)
|
fmt.Printf("error creating approval environment: %v\n", err)
|
||||||
|
|
@ -153,7 +181,7 @@ func main() {
|
||||||
killSignalChannel := make(chan os.Signal, 1)
|
killSignalChannel := make(chan os.Signal, 1)
|
||||||
signal.Notify(killSignalChannel, os.Interrupt)
|
signal.Notify(killSignalChannel, os.Interrupt)
|
||||||
|
|
||||||
commentLoopChannel := newCommentLoopChannel(ctx, apprv, client, approvers, minimumApprovals)
|
commentLoopChannel := newCommentLoopChannel(ctx, apprv, client)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case exitCode := <-commentLoopChannel:
|
case exitCode := <-commentLoopChannel:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue