Skip to content

Commit

Permalink
Set minimum approvals (#1)
Browse files Browse the repository at this point in the history
* Allow approval of a worflow with a set number of approvals

* update action interface

* Added test for when not enough approvals have been registered and minimumApprovals is set

* Warning raised by invalid minimumApprovals value should be an error

* update new input name to use hyphens

* Input env variable does in fact contain hyphens
  • Loading branch information
Edmund Dipple committed Apr 5, 2022
1 parent 31a4889 commit 4d12fc4
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 22 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ steps:
with:
secret: ${{ github.TOKEN }}
approvers: user1,user2
minimum-approvals: 1
```

`approvers` is a comma-delimited list of all required approvers.
- `approvers` is a comma-delimited list of all required approvers.
- `minimum-approvals` is an integer that sets the minimum number of approvals required to progress the workflow. Defaults to ALL approvers.
3 changes: 3 additions & 0 deletions action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ inputs:
secret:
description: Secret
required: true
minimum-approvals:
description: Minimum number of approvals to progress workflow
required: false
runs:
using: docker
image: docker://ghcr.io/trstringer/manual-approval:1.1.3
24 changes: 15 additions & 9 deletions approval.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,26 @@ type approvalEnvironment struct {
repoOwner string
runID int
approvers []string
minimumApprovals int
approvalIssue *github.Issue
approvalIssueNumber int
}

func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner string, runID int, approvers []string) (*approvalEnvironment, error) {
func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner string, runID int, approvers []string, minimumApprovals int) (*approvalEnvironment, error) {
repoOwnerAndName := strings.Split(repoFullName, "/")
if len(repoOwnerAndName) != 2 {
return nil, fmt.Errorf("repo owner and name in unexpected format: %s", repoFullName)
}
repo := repoOwnerAndName[1]

return &approvalEnvironment{
client: client,
repoFullName: repoFullName,
repo: repo,
repoOwner: repoOwner,
runID: runID,
approvers: approvers,
client: client,
repoFullName: repoFullName,
repo: repo,
repoOwner: repoOwner,
runID: runID,
approvers: approvers,
minimumApprovals: minimumApprovals,
}, nil
}

Expand Down Expand Up @@ -64,10 +66,14 @@ Respond %s to continue workflow or %s to cancel.`,
return err
}

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

if minimumApprovals == 0 {
minimumApprovals = len(approvers)
}

for _, comment := range comments {
commentUser := comment.User.GetLogin()
approverIdx := approversIndex(remainingApprovers, commentUser)
Expand All @@ -81,7 +87,7 @@ func approvalFromComments(comments []*github.IssueComment, approvers []string) (
return approvalStatusPending, err
}
if isApprovalComment {
if len(remainingApprovers) == 1 {
if len(remainingApprovers) == len(approvers)-minimumApprovals+1 {
return approvalStatusApproved, nil
}
remainingApprovers[approverIdx] = remainingApprovers[len(remainingApprovers)-1]
Expand Down
56 changes: 51 additions & 5 deletions approval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ import (
func TestApprovalFromComments(t *testing.T) {
login1 := "login1"
login2 := "login2"
login3 := "login3"
bodyApproved := "Approved"
bodyDenied := "Denied"
bodyPending := "not approval or denial"

testCases := []struct {
name string
comments []*github.IssueComment
approvers []string
expectedStatus approvalStatus
name string
comments []*github.IssueComment
approvers []string
minimumApprovals int
expectedStatus approvalStatus
}{
{
name: "single_approver_single_comment_approved",
Expand Down Expand Up @@ -112,11 +114,55 @@ func TestApprovalFromComments(t *testing.T) {
approvers: []string{login1, login2},
expectedStatus: approvalStatusDenied,
},
{
name: "multi_approver_minimum_one_approval",
comments: []*github.IssueComment{
{
User: &github.User{Login: &login1},
Body: &bodyPending,
},
{
User: &github.User{Login: &login2},
Body: &bodyApproved,
},
},
approvers: []string{login1, login2},
expectedStatus: approvalStatusApproved,
minimumApprovals: 1,
},
{
name: "multi_approver_minimum_two_approvals",
comments: []*github.IssueComment{
{
User: &github.User{Login: &login1},
Body: &bodyApproved,
},
{
User: &github.User{Login: &login2},
Body: &bodyApproved,
},
},
approvers: []string{login1, login2, login3},
expectedStatus: approvalStatusApproved,
minimumApprovals: 2,
},
{
name: "multi_approver_approvals_less_than_minimum",
comments: []*github.IssueComment{
{
User: &github.User{Login: &login1},
Body: &bodyApproved,
},
},
approvers: []string{login1, login2, login3},
expectedStatus: approvalStatusPending,
minimumApprovals: 2,
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
actual, err := approvalFromComments(testCase.comments, testCase.approvers)
actual, err := approvalFromComments(testCase.comments, testCase.approvers, testCase.minimumApprovals)
if err != nil {
t.Fatalf("error getting approval from comments: %v", err)
}
Expand Down
11 changes: 6 additions & 5 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import "time"
const (
pollingInterval time.Duration = 10 * time.Second

envVarRepoFullName string = "GITHUB_REPOSITORY"
envVarRunID string = "GITHUB_RUN_ID"
envVarRepoOwner string = "GITHUB_REPOSITORY_OWNER"
envVarToken string = "INPUT_SECRET"
envVarApprovers string = "INPUT_APPROVERS"
envVarRepoFullName string = "GITHUB_REPOSITORY"
envVarRunID string = "GITHUB_RUN_ID"
envVarRepoOwner string = "GITHUB_REPOSITORY_OWNER"
envVarToken string = "INPUT_SECRET"
envVarApprovers string = "INPUT_APPROVERS"
envVarMinimumApprovals string = "INPUT_MINIMUM-APPROVALS"
)

var (
Expand Down
19 changes: 17 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,22 @@ func main() {
fmt.Printf("Required approvers: %s\n", requiredApproversRaw)
approvers := strings.Split(requiredApproversRaw, ",")

apprv, err := newApprovalEnvironment(client, repoFullName, repoOwner, runID, approvers)
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)
}

apprv, err := newApprovalEnvironment(client, repoFullName, repoOwner, runID, approvers, minimumApprovals)
if err != nil {
fmt.Printf("error creating approval environment: %v\n", err)
os.Exit(1)
Expand All @@ -57,7 +72,7 @@ commentLoop:
os.Exit(1)
}

approved, err := approvalFromComments(comments, approvers)
approved, err := approvalFromComments(comments, approvers, minimumApprovals)
if err != nil {
fmt.Printf("error getting approval from comments: %v\n", err)
os.Exit(1)
Expand Down

0 comments on commit 4d12fc4

Please sign in to comment.