Skip to content

Commit 06c444f

Browse files
authored
use github api for automerge approvals (#11)
1 parent a087d10 commit 06c444f

File tree

6 files changed

+136
-14
lines changed

6 files changed

+136
-14
lines changed

bot/actions/automerge/automerge.go

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ type MergeableEventData interface {
1818
Merge(mergeMethod string)
1919
}
2020

21+
type HasReviewersAPIEventData interface {
22+
GetReviewers() map[string]string
23+
GetApprovals() []string
24+
}
25+
2126
func (a *action) merge(meta bot.EventData) {
2227
if a.rule.Label == "" {
2328
mergeable, ok := meta.(MergeableEventData)
@@ -32,12 +37,28 @@ func (a *action) merge(meta bot.EventData) {
3237
}
3338
}
3439

35-
func (a *action) Apply(config bot.Configuration, meta bot.EventData) {
40+
func (a *action) getApprovalsFromAPI(meta bot.EventData) (int, bool) {
3641
assigneesList := meta.GetAssignees()
37-
if len(assigneesList) == 0 {
38-
util.Logger.Debug("No assignees to issue - skipping")
39-
return
42+
approvals := 0
43+
assignees := util.StringSet{}
44+
assignees.AddAll(assigneesList)
45+
reviewApi, ok := meta.(HasReviewersAPIEventData)
46+
if !ok {
47+
util.Logger.Warning("Event data does not support reviewers API. Check your configuration")
48+
a.err = fmt.Errorf("Event data does not support reviewers API")
49+
} else {
50+
for _, approver := range reviewApi.GetApprovals() {
51+
if assignees.Contains(approver) {
52+
assignees.Remove(approver)
53+
approvals++
54+
}
55+
}
4056
}
57+
return approvals, assignees.Len() == 0
58+
}
59+
60+
func (a *action) getApprovalsFromComments(meta bot.EventData) (int, bool) {
61+
assigneesList := meta.GetAssignees()
4162
approvals := 0
4263
assignees := util.StringSet{}
4364
assignees.AddAll(assigneesList)
@@ -51,12 +72,30 @@ func (a *action) Apply(config bot.Configuration, meta bot.EventData) {
5172
approvals++
5273
}
5374
}
54-
if a.rule.Require == 0 && assignees.Len() == 0 {
55-
util.Logger.Debug("All assignees have approved the PR - merging")
56-
a.merge(meta)
57-
} else if a.rule.Require > 0 && approvals >= a.rule.Require {
58-
util.Logger.Debug("Got %d required approvals for PR - merging", a.rule.Require)
59-
a.merge(meta)
75+
return approvals, assignees.Len() == 0
76+
}
77+
78+
func (a *action) Apply(config bot.Configuration, meta bot.EventData) {
79+
assigneesList := meta.GetAssignees()
80+
if len(assigneesList) == 0 {
81+
util.Logger.Debug("No assignees to issue - skipping")
82+
return
83+
}
84+
calls := []func(bot.EventData) (int, bool){
85+
a.getApprovalsFromAPI,
86+
a.getApprovalsFromComments,
87+
}
88+
for _, call := range calls {
89+
approvals, all := call(meta)
90+
if a.rule.Require == 0 && all {
91+
util.Logger.Debug("All assignees have approved the PR - merging")
92+
a.merge(meta)
93+
return
94+
} else if a.rule.Require > 0 && approvals >= a.rule.Require {
95+
util.Logger.Debug("Got %d required approvals for PR - merging", a.rule.Require)
96+
a.merge(meta)
97+
return
98+
}
6099
}
61100
}
62101

bot/actions/automerge/automerge.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ Assignees must use one of the approval phrases (_Approve_, _Approved_, _LGTM_, _
88

99
**Note** If others comment with one of the approval phrases, it will not count as approval
1010

11+
### GitHub Users
12+
13+
Since GitHub API support for pull request reviews API - the lookup will first search the API and only then for comments.
14+
15+
**Note** Approvals can only be read by API or comments. Mixing both approvals from API and comments is not supported.
16+
1117
## Requirements
1218

1319
None

bot/actions/automerge/automerge_test.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,66 @@
11
package automerge
22

33
import (
4+
"testing"
5+
46
"github.com/bivas/rivi/bot"
57
"github.com/bivas/rivi/bot/mock"
68
"github.com/stretchr/testify/assert"
7-
"testing"
89
)
910

1011
type mockMergableEventData struct {
1112
mock.MockEventData
12-
merged bool
13-
method string
13+
merged bool
14+
method string
15+
reviewers map[string]string
16+
approvals []string
17+
}
18+
19+
func (m *mockMergableEventData) GetReviewers() map[string]string {
20+
return m.reviewers
21+
}
22+
23+
func (m *mockMergableEventData) GetApprovals() []string {
24+
return m.approvals
1425
}
1526

1627
func (m *mockMergableEventData) Merge(mergeMethod string) {
1728
m.merged = true
1829
m.method = mergeMethod
1930
}
2031

32+
func TestNoReviewersAPI(t *testing.T) {
33+
action := action{rule: &rule{}}
34+
action.rule.Defaults()
35+
config := &mock.MockConfiguration{}
36+
meta := &mock.MockEventData{Assignees: []string{"user1"}, Comments: []bot.Comment{
37+
{Commenter: "user1", Comment: "approved"},
38+
}}
39+
action.Apply(config, meta)
40+
assert.NotNil(t, action.err, "should be error on api")
41+
}
42+
43+
func TestMergeWithAPI(t *testing.T) {
44+
action := action{rule: &rule{}}
45+
action.rule.Defaults()
46+
config := &mock.MockConfiguration{}
47+
mockEventData := mock.MockEventData{Assignees: []string{"user1"}}
48+
meta := &mockMergableEventData{MockEventData: mockEventData, approvals: []string{"user1"}}
49+
action.Apply(config, meta)
50+
assert.True(t, meta.merged, "should be merged")
51+
assert.Equal(t, "merge", meta.method, "default should be merge")
52+
}
53+
54+
func TestMergeWithAPINoApprovals(t *testing.T) {
55+
action := action{rule: &rule{}}
56+
action.rule.Defaults()
57+
config := &mock.MockConfiguration{}
58+
mockEventData := mock.MockEventData{Assignees: []string{"user1"}}
59+
meta := &mockMergableEventData{MockEventData: mockEventData, approvals: []string{"user2"}}
60+
action.Apply(config, meta)
61+
assert.False(t, meta.merged, "should be merged")
62+
}
63+
2164
func TestNotCapableToMerge(t *testing.T) {
2265
action := action{rule: &rule{}}
2366
action.rule.Defaults()

bot/connector/github/builder.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ func (builder *eventDataBuilder) readFromClient(context *builderContext) {
8686
context.data.fileNames = fileNames
8787
stringSet := util.StringSet{Transformer: filepath.Ext}
8888
context.data.fileExt = stringSet.AddAll(fileNames).Values()
89+
context.data.reviewers = context.client.GetReviewers(id)
8990
context.data.locked = context.client.Locked(id)
9091
}
9192

bot/connector/github/client.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,21 @@ func (c *ghClient) AddComment(issue int, comment string) bot.Comment {
177177
}
178178
}
179179

180+
func (c *ghClient) GetReviewers(issue int) map[string]string {
181+
result := make(map[string]string)
182+
reviews, _, err := c.client.PullRequests.ListReviews(context.Background(), c.owner, c.repo, issue, nil)
183+
if err != nil {
184+
util.Logger.Error("Unable to get reviewers for issue %d. %s", issue, err)
185+
return result
186+
}
187+
for _, review := range reviews {
188+
user := strings.ToLower(*review.User.Login)
189+
state := strings.ToLower(*review.State)
190+
result[user] = state
191+
}
192+
return result
193+
}
194+
180195
func (c *ghClient) Merge(issue int, mergeMethod string) {
181196
options := &github.PullRequestOptions{MergeMethod: mergeMethod}
182197
_, _, err := c.client.PullRequests.Merge(context.Background(), c.owner, c.repo, issue, "", options)

bot/connector/github/event_data.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package github
22

3-
import "github.com/bivas/rivi/bot"
3+
import (
4+
"github.com/bivas/rivi/bot"
5+
"github.com/bivas/rivi/util"
6+
)
47

58
type eventData struct {
69
client *ghClient
@@ -21,6 +24,21 @@ type eventData struct {
2124
assignees []string
2225
comments []bot.Comment
2326
payload []byte
27+
reviewers map[string]string
28+
}
29+
30+
func (d *eventData) GetReviewers() map[string]string {
31+
return d.reviewers
32+
}
33+
34+
func (d *eventData) GetApprovals() []string {
35+
result := util.StringSet{}
36+
for reviewer, state := range d.reviewers {
37+
if state == "approve" {
38+
result.Add(reviewer)
39+
}
40+
}
41+
return result.Values()
2442
}
2543

2644
func (d *eventData) Lock() {

0 commit comments

Comments
 (0)