Add ability to specify a team as an approver (#32)
Prior to this change, the approvers could only be explicit users. With this change, you can now specify an org team and this will be expanded out with a user list for approvers. Closes #14.
This commit is contained in:
parent
d8f25cb80e
commit
78b4c148f1
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
|
||||
with:
|
||||
secret: ${{ github.TOKEN }}
|
||||
approvers: user1,user2
|
||||
approvers: user1,user2,org-team1
|
||||
minimum-approvals: 1
|
||||
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.
|
||||
- `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
|
||||
repoOwner string
|
||||
runID int
|
||||
approvers []string
|
||||
minimumApprovals int
|
||||
approvalIssue *github.Issue
|
||||
approvalIssueNumber int
|
||||
issueTitle string
|
||||
issueApprovers []string
|
||||
minimumApprovals int
|
||||
}
|
||||
|
||||
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,
|
||||
repoOwner: repoOwner,
|
||||
runID: runID,
|
||||
approvers: approvers,
|
||||
issueApprovers: approvers,
|
||||
minimumApprovals: minimumApprovals,
|
||||
issueTitle: issueTitle,
|
||||
}, nil
|
||||
|
|
@ -59,7 +59,7 @@ Required approvers: %s
|
|||
|
||||
Respond %s to continue workflow or %s to cancel.`,
|
||||
a.runURL(),
|
||||
a.approvers,
|
||||
a.issueApprovers,
|
||||
formatAcceptedWords(approvedWords),
|
||||
formatAcceptedWords(deniedWords),
|
||||
)
|
||||
|
|
@ -69,13 +69,13 @@ Respond %s to continue workflow or %s to cancel.`,
|
|||
a.repoOwner,
|
||||
a.repo,
|
||||
issueTitle,
|
||||
a.approvers,
|
||||
a.issueApprovers,
|
||||
issueBody,
|
||||
)
|
||||
a.approvalIssue, _, err = a.client.Issues.Create(ctx, a.repoOwner, a.repo, &github.IssueRequest{
|
||||
Title: &issueTitle,
|
||||
Body: &issueBody,
|
||||
Assignees: &a.approvers,
|
||||
Assignees: &a.issueApprovers,
|
||||
})
|
||||
a.approvalIssueNumber = a.approvalIssue.GetNumber()
|
||||
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/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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)
|
||||
go func() {
|
||||
for {
|
||||
|
|
@ -42,7 +41,7 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie
|
|||
close(channel)
|
||||
}
|
||||
|
||||
approved, err := approvalFromComments(comments, approvers, minimumApprovals)
|
||||
approved, err := approvalFromComments(comments, apprv.issueApprovers, apprv.minimumApprovals)
|
||||
if err != nil {
|
||||
fmt.Printf("error getting approval from comments: %v\n", err)
|
||||
channel <- 1
|
||||
|
|
@ -106,7 +105,40 @@ func newGithubClient(ctx context.Context) *github.Client {
|
|||
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() {
|
||||
if err := validateInput(); err != nil {
|
||||
fmt.Printf("%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
repoFullName := os.Getenv(envVarRepoFullName)
|
||||
runID, err := strconv.Atoi(os.Getenv(envVarRunID))
|
||||
if err != nil {
|
||||
|
|
@ -118,26 +150,22 @@ func main() {
|
|||
ctx := context.Background()
|
||||
client := newGithubClient(ctx)
|
||||
|
||||
requiredApproversRaw := os.Getenv(envVarApprovers)
|
||||
fmt.Printf("Required approvers: %s\n", requiredApproversRaw)
|
||||
approvers := strings.Split(requiredApproversRaw, ",")
|
||||
|
||||
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))
|
||||
approvers, err := retrieveApprovers(client, repoOwner)
|
||||
if err != nil {
|
||||
fmt.Printf("error retrieving approvers: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
fmt.Printf("error creating approval environment: %v\n", err)
|
||||
|
|
@ -153,7 +181,7 @@ func main() {
|
|||
killSignalChannel := make(chan os.Signal, 1)
|
||||
signal.Notify(killSignalChannel, os.Interrupt)
|
||||
|
||||
commentLoopChannel := newCommentLoopChannel(ctx, apprv, client, approvers, minimumApprovals)
|
||||
commentLoopChannel := newCommentLoopChannel(ctx, apprv, client)
|
||||
|
||||
select {
|
||||
case exitCode := <-commentLoopChannel:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue