From 4d12fc4297f3a50060bc48672b7ae86aeb0e36f5 Mon Sep 17 00:00:00 2001 From: Edmund Dipple Date: Tue, 5 Apr 2022 14:05:47 +0100 Subject: [PATCH] Set minimum approvals (#1) * 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 --- README.md | 4 +++- action.yaml | 3 +++ approval.go | 24 +++++++++++++-------- approval_test.go | 56 +++++++++++++++++++++++++++++++++++++++++++----- constants.go | 11 +++++----- main.go | 19 ++++++++++++++-- 6 files changed, 95 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index f798d55..4828206 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/action.yaml b/action.yaml index 3d4ed14..e456a6c 100644 --- a/action.yaml +++ b/action.yaml @@ -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 diff --git a/approval.go b/approval.go index e54acc1..b0b385d 100644 --- a/approval.go +++ b/approval.go @@ -16,11 +16,12 @@ 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) @@ -28,12 +29,13 @@ func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner strin 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 } @@ -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) @@ -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] diff --git a/approval_test.go b/approval_test.go index a62a689..c4114ac 100644 --- a/approval_test.go +++ b/approval_test.go @@ -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", @@ -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) } diff --git a/constants.go b/constants.go index 068eb61..f1ee6e9 100644 --- a/constants.go +++ b/constants.go @@ -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 ( diff --git a/main.go b/main.go index 19f17b9..c738e60 100644 --- a/main.go +++ b/main.go @@ -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) @@ -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)