Add ability to specify a team as an approver
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
c89961879d
commit
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
|
||||
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