Skip to content

Commit

Permalink
Merge pull request #1 from Delphia/switch-to-delphia-fork
Browse files Browse the repository at this point in the history
Update libraries, merge changes into main, publish package
  • Loading branch information
philn-delphia authored Apr 15, 2024
2 parents dd1555b + 1782c9b commit 9f2b88b
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 460 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Build
run: make build
env:
VERSION: latest
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '^1.22'
- name: Test
run: make test
- name: Lint
Expand Down
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
FROM golang:1.17 AS builder
FROM golang:1.22-alpine AS builder
COPY . /var/app
WORKDIR /var/app
RUN CGO_ENABLED=0 go build -o app .

FROM alpine:3.14
LABEL org.opencontainers.image.source https://github.com/trstringer/manual-approval
FROM alpine:3.19
LABEL org.opencontainers.image.source https://github.com/Delphia/manual-approval
RUN apk update && apk add ca-certificates
COPY --from=builder /var/app/app /var/app/app
CMD ["/var/app/app"]
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
IMAGE_REPO=ghcr.io/trstringer/manual-approval
IMAGE_REPO=ghcr.io/delphia/manual-approval

.PHONY: build
build:
Expand Down
33 changes: 19 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Manual Workflow Approval

[![ci](https://github.com/trstringer/manual-approval/actions/workflows/ci.yaml/badge.svg)](https://github.com/trstringer/manual-approval/actions/workflows/ci.yaml)
This is a fork of [trstringer/manual-approval](https://github.com/trstringer/manual-approval) with the following changes:

- Leaving the approvers field blank allows anyone with access to the repo to approve.
- Golang version and library updates.

[![ci](https://github.com/Delphia/manual-approval/actions/workflows/ci.yaml/badge.svg)](https://github.com/Delphia/manual-approval/actions/workflows/ci.yaml)

Pause a GitHub Actions workflow and require manual approval from one or more approvers before continuing.

Expand All @@ -11,8 +16,8 @@ This is a very common feature for a deployment or release pipeline, and while [t
The way this action works is the following:

1. Workflow comes to the `manual-approval` action.
1. `manual-approval` will create an issue in the containing repository and assign it to the `approvers`.
1. If and once all approvers respond with an approved keyword, the workflow will continue.
1. `manual-approval` will create an issue in the containing repository and assign it to the `approvers` (if there are any).
1. If and once all required approvers respond with an approved keyword, the workflow will continue.
1. If any of the approvers responds with a denied keyword, then the workflow will exit with a failed status.

* Approval keywords - "approve", "approved", "lgtm", "yes"
Expand All @@ -26,7 +31,7 @@ In all cases, `manual-approval` will close the initial GitHub issue.

```yaml
steps:
- uses: trstringer/manual-approval@v1
- uses: Delphia/manual-approval@v1
with:
secret: ${{ github.TOKEN }}
approvers: user1,user2,org-team1
Expand Down Expand Up @@ -71,13 +76,17 @@ jobs:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Wait for approval
uses: trstringer/manual-approval@v1
uses: Delphia/manual-approval@v1
with:
secret: ${{ steps.generate_token.outputs.token }}
approvers: myteam
minimum-approvals: 1
```

## Allow anyone to approve

Leave the approvers field blank to allow anyone to approve.

## Timeout

If you'd like to force a timeout of your workflow pause, you can specify `timeout-minutes` at either the [step](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepstimeout-minutes) level or the [job](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes) level.
Expand All @@ -86,7 +95,7 @@ For instance, if you want your manual approval step to timeout after an hour you

```yaml
steps:
- uses: trstringer/manual-approval@v1
- uses: Delphia/manual-approval@v1
timeout-minutes: 60
...
```
Expand All @@ -112,30 +121,26 @@ For more information on permissions, please look at the [GitHub documentation](h

### Running test code

To test out your code in an action, you need to build the image and push it to a different container registry repository. For instance, if I want to test some code I won't build the image with the main image repository. Prior to this, comment out the label binding the image to a repo:

```dockerfile
# LABEL org.opencontainers.image.source https://github.com/trstringer/manual-approval
```
To test out your code in an action, you need to build the image and push it to a different container registry repository. For instance, if I want to test some code I won't build the image with the main image repository.

Build the image:

```
$ VERSION=1.7.1-rc.1 make IMAGE_REPO=ghcr.io/trstringer/manual-approval-test build
$ VERSION=1.7.1-rc.1 make IMAGE_REPO=ghcr.io/Delphia/manual-approval-test build
```

*Note: The image version can be whatever you want, as this image wouldn't be pushed to production. It is only for testing.*

Push the image to your container registry:

```
$ VERSION=1.7.1-rc.1 make IMAGE_REPO=ghcr.io/trstringer/manual-approval-test push
$ VERSION=1.7.1-rc.1 make IMAGE_REPO=ghcr.io/Delphia/manual-approval-test push
```

To test out the image you will need to modify `action.yaml` so that it points to your new image that you're testing:

```yaml
image: docker://ghcr.io/trstringer/manual-approval-test:1.7.0-rc.1
image: docker://ghcr.io/Delphia/manual-approval-test:1.7.0-rc.1
```

Then to test out the image, run a workflow specifying your dev branch:
Expand Down
6 changes: 3 additions & 3 deletions action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ branding:
inputs:
approvers:
description: Required approvers
required: true
required: false
secret:
description: Secret
required: true
Expand All @@ -21,7 +21,7 @@ inputs:
required: false
exclude-workflow-initiator-as-approver:
description: Whether or not to filter out the user who initiated the workflow as an approver if they are in the approvers list
default: false
default: 'false'
additional-approved-words:
description: Comma separated list of words that can be used to approve beyond the defaults.
default: ''
Expand All @@ -30,4 +30,4 @@ inputs:
default: ''
runs:
using: docker
image: docker://ghcr.io/trstringer/manual-approval:1.9.0
image: docker://ghcr.io/delphia/manual-approval:main
69 changes: 41 additions & 28 deletions approval.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"regexp"
"strings"

"github.com/google/go-github/v43/github"
"github.com/google/go-github/v61/github"
)

type approvalEnvironment struct {
Expand All @@ -20,26 +20,30 @@ type approvalEnvironment struct {
issueTitle string
issueBody string
issueApprovers []string
disallowedUsers []string
minimumApprovals int
workflowInitiator string
}

func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner string, runID int, approvers []string, minimumApprovals int, issueTitle, issueBody string) (*approvalEnvironment, error) {
func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner string, runID int, approvers []string, minimumApprovals int, issueTitle, issueBody string, disallowedUsers []string, workflowInitiator string) (*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,
issueApprovers: approvers,
minimumApprovals: minimumApprovals,
issueTitle: issueTitle,
issueBody: issueBody,
client: client,
repoFullName: repoFullName,
repo: repo,
repoOwner: repoOwner,
runID: runID,
issueApprovers: approvers,
disallowedUsers: disallowedUsers,
minimumApprovals: minimumApprovals,
issueTitle: fmt.Sprintf("Manual approval required for: %s (run %d)", issueTitle, runID),
issueBody: issueBody,
workflowInitiator: workflowInitiator,
}, nil
}

Expand All @@ -48,10 +52,11 @@ func (a approvalEnvironment) runURL() string {
}

func (a *approvalEnvironment) createApprovalIssue(ctx context.Context) error {
issueTitle := fmt.Sprintf("Manual approval required for workflow run %d", a.runID)

if a.issueTitle != "" {
issueTitle = fmt.Sprintf("%s: %s", issueTitle, a.issueTitle)
issueApproversText := "Anyone can approve."
assignees := []string{a.workflowInitiator}
if len(a.issueApprovers) > 0 {
issueApproversText = fmt.Sprintf("%s", a.issueApprovers)
assignees = a.issueApprovers
}

issueBody := fmt.Sprintf(`Workflow is pending manual review.
Expand All @@ -61,7 +66,7 @@ Required approvers: %s
Respond %s to continue workflow or %s to cancel.`,
a.runURL(),
a.issueApprovers,
issueApproversText,
formatAcceptedWords(approvedWords),
formatAcceptedWords(deniedWords),
)
Expand All @@ -75,14 +80,14 @@ Respond %s to continue workflow or %s to cancel.`,
"Creating issue in repo %s/%s with the following content:\nTitle: %s\nApprovers: %s\nBody:\n%s\n",
a.repoOwner,
a.repo,
issueTitle,
a.issueApprovers,
a.issueTitle,
assignees,
issueBody,
)
a.approvalIssue, _, err = a.client.Issues.Create(ctx, a.repoOwner, a.repo, &github.IssueRequest{
Title: &issueTitle,
Title: &a.issueTitle,
Body: &issueBody,
Assignees: &a.issueApprovers,
Assignees: &assignees,
})
if err != nil {
return err
Expand All @@ -93,18 +98,27 @@ Respond %s to continue workflow or %s to cancel.`,
return nil
}

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

approvals := []string{}

if minimumApprovals == 0 {
if len(approvers) == 0 {
return "", fmt.Errorf("error: no required approvers or minimum approvals set")
}
minimumApprovals = len(approvers)
}

for _, comment := range comments {
commentUser := comment.User.GetLogin()
approverIdx := approversIndex(remainingApprovers, commentUser)
if approverIdx < 0 {

if approversIndex(disallowedUsers, commentUser) >= 0 {
continue
}
if approversIndex(approvals, commentUser) >= 0 {
continue
}
if len(approvers) > 0 && approversIndex(approvers, commentUser) < 0 {
continue
}

Expand All @@ -114,11 +128,10 @@ func approvalFromComments(comments []*github.IssueComment, approvers []string, m
return approvalStatusPending, err
}
if isApprovalComment {
if len(remainingApprovers) == len(approvers)-minimumApprovals+1 {
approvals = append(approvals, commentUser)
if len(approvals) >= minimumApprovals {
return approvalStatusApproved, nil
}
remainingApprovers[approverIdx] = remainingApprovers[len(remainingApprovers)-1]
remainingApprovers = remainingApprovers[:len(remainingApprovers)-1]
continue
}

Expand Down
5 changes: 3 additions & 2 deletions approval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package main
import (
"testing"

"github.com/google/go-github/v43/github"
"github.com/google/go-github/v61/github"
)

func TestApprovalFromComments(t *testing.T) {
Expand All @@ -18,6 +18,7 @@ func TestApprovalFromComments(t *testing.T) {
name string
comments []*github.IssueComment
approvers []string
disallowedUsers []string
minimumApprovals int
expectedStatus approvalStatus
}{
Expand Down Expand Up @@ -162,7 +163,7 @@ func TestApprovalFromComments(t *testing.T) {

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
actual, err := approvalFromComments(testCase.comments, testCase.approvers, testCase.minimumApprovals)
actual, err := approvalFromComments(testCase.comments, testCase.approvers, testCase.minimumApprovals, testCase.disallowedUsers)
if err != nil {
t.Fatalf("error getting approval from comments: %v", err)
}
Expand Down
33 changes: 24 additions & 9 deletions approvers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,37 @@ import (
"strconv"
"strings"

"github.com/google/go-github/v43/github"
"github.com/google/go-github/v61/github"
)

func retrieveApprovers(client *github.Client, repoOwner string) ([]string, error) {
func retrieveApprovers(client *github.Client, repoOwner string) ([]string, []string, error) {
workflowInitiator := os.Getenv(envVarWorkflowInitiator)
shouldExcludeWorkflowInitiatorRaw := os.Getenv(envVarExcludeWorkflowInitiatorAsApprover)
shouldExcludeWorkflowInitiator, parseBoolErr := strconv.ParseBool(shouldExcludeWorkflowInitiatorRaw)
if parseBoolErr != nil {
return nil, fmt.Errorf("error parsing exclude-workflow-initiator-as-approver flag: %w", parseBoolErr)
return nil, nil, fmt.Errorf("error parsing exclude-workflow-initiator-as-approver flag: %w", parseBoolErr)
}

approvers := []string{}
requiredApproversRaw := os.Getenv(envVarApprovers)
requiredApprovers := strings.Split(requiredApproversRaw, ",")
requiredApprovers := []string{}
if requiredApproversRaw != "" {
requiredApprovers = strings.Split(requiredApproversRaw, ",")
}

minimumApprovalsRaw := os.Getenv(envVarMinimumApprovals)
minimumApprovals := len(approvers)

var disallowedUsers []string
if shouldExcludeWorkflowInitiator {
disallowedUsers = []string{workflowInitiator}
} else {
disallowedUsers = []string{}
}

if len(requiredApprovers) == 0 {
return []string{}, disallowedUsers, nil
}

for i := range requiredApprovers {
requiredApprovers[i] = strings.TrimSpace(requiredApprovers[i])
Expand All @@ -39,21 +56,19 @@ func retrieveApprovers(client *github.Client, repoOwner string) ([]string, error

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)
return nil, 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 nil, nil, fmt.Errorf("error: minimum required approvals (%d) is greater than the total number of approvers (%d)", minimumApprovals, len(approvers))
}

return approvers, nil
return approvers, disallowedUsers, nil
}

func expandGroupFromUser(client *github.Client, org, userOrTeam string, workflowInitiator string, shouldExcludeWorkflowInitiator bool) []string {
Expand Down
Loading

0 comments on commit 9f2b88b

Please sign in to comment.