Skip to content

Commit

Permalink
Merge pull request #20 from cw-kajiwara/support-pull-request-review
Browse files Browse the repository at this point in the history
Support pull request review (only approval!)
  • Loading branch information
k-kinzal authored Apr 8, 2021
2 parents 3da0e2f + 3e32a71 commit 0ab5565
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 0 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ Usage:
pr [command]

Available Commands:
assignee Manipulate assignees that match a rule
check Check if PR matches the rule and change PR status
help Help about any command
label Manipulate labels that match a rule
merge Merge PR that matches a rule
review Add review to PRs that match rules
show Show PR that matches a rule
validate Validate the rules

Expand All @@ -48,6 +51,17 @@ $ pr merge [owner]/[repo] --with-statuses -l 'state == `"open"`' -l 'length(stat
[...]
```
### Review
Add a review to PRs that match the rule.
```bash
$ pr review [owner]/[repo] --action "approve" --with-statuses -l 'state == `"open"`' -l 'length(statuses[?state == `"success"`]) > `3`'
[...]
```
`--action "approve"` adds approval to the PR that matches the rule.
### Label
Append/Remove/Replace labels to PRs that match the rule.
Expand Down
57 changes: 57 additions & 0 deletions cmd/review.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package cmd

import (
"encoding/json"
"fmt"
"os"

"github.com/k-kinzal/pr/pkg/pr"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
)

func ReviewRun(cmd *cobra.Command, args []string) error {
pulls, err := pr.Review(owner, repo, reviewOption)
if err != nil {
if _, ok := err.(*pr.NoMatchError); ok {
fmt.Fprintln(os.Stderr, err.Error())
fmt.Fprintln(os.Stdout, "[]")
if exitCode {
os.Exit(127)
}
return nil
}
return err
}

out, err := json.Marshal(pulls)
if err != nil {
return xerrors.Errorf("review: %s", err)
}
fmt.Fprintln(os.Stdout, string(out))

return nil
}

var (
reviewOption *pr.ReviewOption
reviewCmd = &cobra.Command{
Use: "review owner/repo",
Short: "Add review to PRs that match rules",
RunE: ReviewRun,
SilenceErrors: true,
SilenceUsage: true,
}
)

func setReviewFrags(cmd *cobra.Command) *pr.ReviewOption {
opt := &pr.ReviewOption{}
cmd.Flags().StringVar(&opt.Action, "action", "approve", "review action to take. currently, approve is only supported")
return opt
}

func init() {
reviewOption = setReviewFrags(reviewCmd)
reviewOption.ListOption = setListFrags(reviewCmd)
rootCmd.AddCommand(reviewCmd)
}
34 changes: 34 additions & 0 deletions pkg/api/review.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package api

import (
"context"
"github.com/google/go-github/v28/github"
"golang.org/x/sync/errgroup"
)

type ReviewOption struct {
Action string
}

func (c *Client) AddApproval(ctx context.Context, pulls []*PullRequest, opt *ReviewOption) ([]*PullRequest, error) {
eg, ctx := errgroup.WithContext(ctx)

for _, pull := range pulls {
eg.Go(func(pull *PullRequest) func() error {
return func() error {
pullRequestReviewRequest := &github.PullRequestReviewRequest{Event: github.String(opt.Action)}
pullRequestReview, _, err := c.github.PullRequests.CreateReview(ctx, pull.Owner, pull.Repo, int(pull.Number), pullRequestReviewRequest)
if err != nil {
return err
}
pull.Reviews = append(pull.Reviews, newPullRequestReview(pullRequestReview))
return nil
}
}(pull))
}
if err := eg.Wait(); err != nil {
return nil, err
}

return pulls, nil
}
98 changes: 98 additions & 0 deletions pkg/api/review_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package api_test

import (
"context"
"encoding/json"
"fmt"
"github.com/google/go-github/v28/github"
"math"
"net/http"
"testing"
"time"

"github.com/jarcoal/httpmock"
"github.com/k-kinzal/pr/pkg/api"
"github.com/k-kinzal/pr/test/gen"
)

func TestClient_AddApproval(t *testing.T) {
gen.Reset()
httpmock.Activate()
defer httpmock.DeactivateAndReset()

httpmock.RegisterResponder("POST", "=~^https://api.github.com/repos/octocat/Hello-World/pulls/\\d+/reviews", func(request *http.Request) (response *http.Response, e error) {
var req struct {
Action *string `json:"event"`
}
if err := json.NewDecoder(request.Body).Decode(&req); err != nil {
return nil, err
}

pullRequestReview, err := gen.PullRequestReview()
if err != nil {
return nil, err
}

pullRequestReview.State = github.String("APPROVED")

resp, err := httpmock.NewJsonResponse(200, pullRequestReview)
if err != nil {
return nil, err
}
resp.Header.Add("X-RateLimit-Limit", "5000")
resp.Header.Add("X-RateLimit-Remaining", "4999")
resp.Header.Add("X-RateLimit-Reset", fmt.Sprint(time.Now().Unix()))
resp.Request = request

return resp, nil
})

pulls := []*api.PullRequest{
{
Id: 1,
Number: 1,
State: "open",
Head: &api.PullRequestBranch{
Sha: "6dcb09b5b57875f334f61aebed695e2e4193db5e",
},
Owner: "octocat",
Repo: "Hello-World",
},
{
Id: 2,
Number: 2,
State: "open",
Head: &api.PullRequestBranch{
Sha: "6dcb09b5b57875f334f61aebed695e2e4193db5e",
},
Owner: "octocat",
Repo: "Hello-World",
},
}

ctx := context.Background()
client := api.NewClient(ctx, &api.Options{
Token: "xxxx",
RateLimit: math.MaxInt32,
})
opt := &api.ReviewOption{
Action: "approve",
}

pulls, err := client.AddApproval(ctx, pulls, opt)
if err != nil {
t.Fatal(err)
}
for i, pull := range pulls {
for j, review := range pull.Reviews {
if review.State != "APPROVED" {
t.Fatalf("pull[`%d`].reviews[`%d`]): expect APPROVED, but actual `%s`", i, j, review.State)
}
}
}

info := httpmock.GetCallCountInfo()
if info["POST =~^https://api.github.com/repos/octocat/Hello-World/pulls/\\d+/reviews"] != 2 {
t.Fatalf("expect `2`, but actual `%d`: %#v", info["POST =~^https://api.github.com/repos/octocat/Hello-World/pulls/\\d+/reviews"], info)
}
}
57 changes: 57 additions & 0 deletions pkg/pr/review.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package pr

import (
"context"
"fmt"
"github.com/k-kinzal/pr/pkg/api"
"strings"
)

const (
ReviewActionAddApproval = "APPROVE"
)

type ReviewOption struct {
Action string
*ListOption
}

func Review(owner string, repo string, opt *ReviewOption) ([]*api.PullRequest, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

clientOption := &api.Options{
Token: token,
RateLimit: opt.Rate,
}
client := api.NewClient(ctx, clientOption)

pullOption := api.PullsOption{
EnableComments: opt.EnableComments,
EnableReviews: opt.EnableReviews,
EnableCommits: opt.EnableCommits,
EnableStatuses: opt.EnableStatuses,
EnableChecks: opt.EnableChecks,
Rules: api.NewPullRequestRules(opt.Rules, opt.Limit),
}
pulls, err := client.GetPulls(ctx, owner, repo, pullOption)
if err != nil {
return nil, err
}
if len(pulls) == 0 {
return nil, &NoMatchError{pullOption.Rules}
}

// GitHub Pull Request Reviews require `event` parameter to be uppercase
// https://docs.github.com/en/rest/reference/pulls#create-a-review-for-a-pull-request
action := strings.ToUpper(opt.Action)

switch action {
case ReviewActionAddApproval:
reviewOption := &api.ReviewOption{
Action: action,
}
return client.AddApproval(ctx, pulls, reviewOption)
}
return nil, fmt.Errorf("currently, `approve` is only supported action, but %s was passed", opt.Action)
}

0 comments on commit 0ab5565

Please sign in to comment.