reviewlette 0.0.9 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,42 +1,46 @@
1
- require 'yaml'
2
1
  require 'octokit'
3
2
 
4
- class GithubConnection
5
-
6
- attr_accessor :client, :repo
7
-
8
- def initialize(repo, token)
9
- @client = Octokit::Client.new(access_token: token)
10
- @repo = repo
11
- end
12
-
13
- def list_pulls
14
- @client.pull_requests(@repo)
3
+ class Reviewlette
4
+ class GithubConnection
5
+ attr_accessor :client, :repo
6
+
7
+ def initialize(repo, token)
8
+ @client = Octokit::Client.new(access_token: token)
9
+ @repo = repo
10
+ end
11
+
12
+ def pull_requests
13
+ @client.pull_requests(@repo)
14
+ end
15
+
16
+ def labels(issue)
17
+ @client.labels_for_issue(@repo, issue).map(&:name)
18
+ end
19
+
20
+ def add_assignees(number, assignees)
21
+ @client.update_issue(@repo, number, assignees: assignees)
22
+ end
23
+
24
+ def comment_reviewers(number, reviewers, trello_card)
25
+ assignees = reviewers.map { |r| "@#{r.github_handle}" }.join(' and ')
26
+
27
+ comment = <<-eos
28
+ #{assignees} will review your pull request :dancers: check #{trello_card.url}
29
+ #{assignees}: Please review this pull request using our guidelines:
30
+ * test for acceptance criteria / functionality
31
+ * check if the new code is covered with tests
32
+ * check for unintended consequences
33
+ * encourage usage of the boyscout rule
34
+ * make sure the code is architected in the best way
35
+ * check that no unnecessary technical debt got introduced
36
+ * make sure that no unnecessary FIXMEs or TODOs got added
37
+ eos
38
+
39
+ @client.add_comment(@repo, number, comment)
40
+ end
41
+
42
+ def repo_exists?
43
+ @client.repository?(@repo)
44
+ end
15
45
  end
16
-
17
- def add_assignee(number, assignee)
18
- @client.update_issue(@repo, number, assignee: assignee)
19
- end
20
-
21
- def reviewer_comment(number, assignee, trello_card)
22
- comment = "@#{assignee} is your reviewer :dancers: check #{trello_card.url} \n" \
23
- "@#{assignee}: Please review this pull request using our guidelines: \n" \
24
- "* test for acceptance criteria / functionality \n" \
25
- "* check if the new code is covered with tests \n" \
26
- "* check for unintended consequences \n" \
27
- "* encourage usage of the boyscout rule \n" \
28
- "* make sure the code is architected in the best way \n" \
29
- "* check that no unnecessary technical debt got introduced \n" \
30
- "* make sure that no unnecessary FIXMEs or TODOs got added \n"
31
- @client.add_comment(@repo, number, comment)
32
- end
33
-
34
- def unassigned_pull_requests
35
- list_pulls.select { |issue| !issue[:assignee] }
36
- end
37
-
38
- def repo_exists?
39
- @client.repository?(@repo)
40
- end
41
-
42
46
  end
@@ -1,45 +1,46 @@
1
1
  require 'yaml'
2
2
  require 'trello'
3
3
 
4
-
5
- class TrelloConnection
6
-
7
- attr_accessor :board
8
-
9
- def initialize
10
- @trello = YAML.load_file("#{File.dirname(__FILE__)}/../../config/trello.yml")
11
- Trello.configure do |config|
12
- config.developer_public_key = @trello['consumerkey']
13
- config.member_token = @trello['oauthtoken']
4
+ class Reviewlette
5
+ class TrelloConnection
6
+
7
+ attr_accessor :board
8
+
9
+ def initialize(config = nil)
10
+ config ||= YAML.load_file("#{File.dirname(__FILE__)}/../../config/trello.yml")
11
+ Trello.configure do |conf|
12
+ conf.developer_public_key = config['consumerkey']
13
+ conf.member_token = config['oauthtoken']
14
+ end
15
+ @board = Trello::Board.find(config['board_id'])
14
16
  end
15
- @board = Trello::Board.find(@trello['board_id'])
16
- end
17
17
 
18
- def add_reviewer_to_card(reviewer, card)
19
- reviewer = find_member_by_username(reviewer)
20
- card.add_member(reviewer)
21
- end
18
+ def add_reviewer_to_card(reviewer, card)
19
+ reviewer = find_member_by_username(reviewer)
20
+ card.add_member(reviewer)
21
+ end
22
22
 
23
- def comment_on_card(comment, card)
24
- card.add_comment(comment)
25
- end
23
+ def comment_reviewers(card, repo_name, issue_id, reviewers)
24
+ comment = reviewers.map { |r| "@#{r.trello_handle}" }.join(' and ')
25
+ comment += " will review https://github.com/#{repo_name}/issues/#{issue_id}"
26
+ card.add_comment(comment)
27
+ end
26
28
 
27
- def move_card_to_list(card, column_name)
28
- column = find_column(column_name)
29
- card.move_to_list(column)
30
- end
29
+ def move_card_to_list(card, column_name)
30
+ column = find_column(column_name)
31
+ card.move_to_list(column)
32
+ end
31
33
 
32
- def find_column(column_name)
33
- @board.lists.find { |x| x.name == column_name }
34
- end
34
+ def find_column(column_name)
35
+ @board.lists.find { |x| x.name == column_name }
36
+ end
35
37
 
36
- def find_member_by_username(username)
37
- @board.members.find { |m| m.username == username }
38
- end
38
+ def find_member_by_username(username)
39
+ @board.members.find { |m| m.username == username }
40
+ end
39
41
 
40
- def find_card_by_id(id)
41
- @board.cards.find { |c| c.short_id == id.to_i }
42
+ def find_card_by_id(id)
43
+ @board.cards.find { |c| c.short_id == id.to_i }
44
+ end
42
45
  end
43
-
44
46
  end
45
-
@@ -0,0 +1,3 @@
1
+ class Reviewlette
2
+ VERSION = '1.0.0'
3
+ end
@@ -1,11 +1,11 @@
1
1
  # coding: utf-8
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'reviewlette'
4
+ require 'reviewlette/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "reviewlette"
8
- spec.version = VERSION
8
+ spec.version = Reviewlette::VERSION
9
9
  spec.authors = ["jschmid1"]
10
10
  spec.email = ["jschmid@suse.de"]
11
11
  spec.summary = %q{Randomly assignes a reviewer to your Pullrequest and corresponding Trello Card.}
@@ -18,9 +18,6 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_runtime_dependency 'ruby-trello', '=1.1.1'
22
- spec.add_runtime_dependency 'octokit', '>=3.8.0'
23
- spec.add_runtime_dependency 'sequel', '=4.13.0'
24
- spec.add_runtime_dependency 'sqlite3', '=1.3.9'
21
+ spec.add_runtime_dependency 'ruby-trello'
22
+ spec.add_runtime_dependency 'octokit'
25
23
  end
26
-
@@ -1,46 +1,49 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe GithubConnection do
4
-
5
- subject { GithubConnection }
6
- let( :connection ) { subject.new(repo, token) }
7
- let( :repo ) { "test" }
8
- let( :token ) { "foo" }
3
+ describe Reviewlette::GithubConnection do
4
+ subject { described_class.new(repo, token) }
5
+ let(:repo) { 'test' }
6
+ let(:token) { 'foo' }
7
+ let(:member1) { double(name: 'test1', github_handle: 'githubtest1') }
8
+ let(:member2) { double(name: 'test2', github_handle: 'githubtest2') }
9
9
 
10
10
  describe '.new' do
11
11
  it 'initializes octokit client and repo' do
12
12
  expect(Octokit::Client).to receive(:new).with(:access_token => token)
13
- expect(connection.repo).to eq(repo)
13
+ expect(subject.repo).to eq(repo)
14
14
  end
15
15
  end
16
16
 
17
- describe '#add_assignee' do
18
- it 'adds an assignee to the gh issue' do
19
- expect(connection.client).to receive(:update_issue).with(repo, 11, :assignee => 'test')
20
- connection.add_assignee(11, 'test')
17
+ describe '#add_assignees' do
18
+ it 'adds assignees to the GitHub issue' do
19
+ expect(subject.client).to receive(:update_issue).with(repo, 11, assignees: ['test'])
20
+ subject.add_assignees(11, ['test'])
21
21
  end
22
22
  end
23
23
 
24
- describe '#reviewer_comment' do
24
+ describe '#comment_reviewers' do
25
25
  it 'comments on a given issue' do
26
- card = Trello::Card.new
26
+ card = Trello::Card.new
27
+ reviewers = [member1, member2]
28
+
27
29
  allow(card).to receive(:url).and_return('url')
28
- expect(connection.client).to receive(:add_comment).with(repo, 11, anything)
29
- connection.reviewer_comment(11, 'test', card)
30
+ expect(subject.client).to receive(:add_comment).with(repo, 11, anything)
31
+
32
+ subject.comment_reviewers(11, reviewers, card)
30
33
  end
31
34
  end
32
35
 
33
- describe '#list_pulls' do
36
+ describe '#pull_requests' do
34
37
  it 'lists all pullrequests for a given repository' do
35
- expect(connection.client).to receive(:pull_requests).with(repo)
36
- connection.list_pulls
38
+ expect(subject.client).to receive(:pull_requests).with(repo)
39
+ subject.pull_requests
37
40
  end
38
41
  end
39
42
 
40
43
  describe '#repo_exists?' do
41
44
  it 'checks if a certain repository exists' do
42
- expect(connection.client).to receive(:repository?).with(repo)
43
- connection.repo_exists?
45
+ expect(subject.client).to receive(:repository?).with(repo)
46
+ subject.repo_exists?
44
47
  end
45
48
 
46
49
  end
@@ -1,13 +1,11 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Reviewlette do
4
- subject { Reviewlette }
4
+ let(:instance) { described_class.new members: [member1, member2] }
5
+ subject { instance }
5
6
 
6
- let(:reviewlette) { subject.new }
7
-
8
- let(:members_config) { { 'members' => [member1, member2] } }
9
- let(:member1) { { 'name' => 'test1', 'suse_username' => 'test1', 'trello_username' => 'trellotest1' } }
10
- let(:member2) { { 'name' => 'test2', 'suse_username' => 'test2', 'trello_username' => 'trellotest2' } }
7
+ let(:member1) { double(name: 'test1', github_handle: 'pinky', trello_handle: 'trellotest1') }
8
+ let(:member2) { double(name: 'test2', github_handle: 'brain', trello_handle: 'trellotest2') }
11
9
 
12
10
  let(:github_config) { { token: token, repos: [repo, repo2] } }
13
11
  let(:token) { '1234' }
@@ -15,71 +13,171 @@ describe Reviewlette do
15
13
  let(:repo2) { 'SUSE/foo' }
16
14
 
17
15
  before do
18
- allow(TrelloConnection).to receive(:new).and_return TrelloConnection
19
- allow(GithubConnection).to receive(:new).with(repo, token).and_return GithubConnection
16
+ allow(described_class::TrelloConnection).to receive(:new).and_return described_class::TrelloConnection
17
+ allow(described_class::GithubConnection).to receive(:new).with(repo, token).and_return described_class::GithubConnection
20
18
  allow(YAML).to receive(:load_file).with(/github\.yml/).and_return github_config
21
- allow(YAML).to receive(:load_file).with(/members\.yml/).and_return members_config
22
19
  end
23
20
 
24
21
  describe '.new' do
25
22
  it 'sets trello connections' do
26
- expect(TrelloConnection).to receive(:new)
27
- subject.new
23
+ expect(described_class::TrelloConnection).to receive(:new)
24
+ subject
28
25
  end
29
26
  end
30
27
 
31
28
  describe '.run' do
32
29
  it 'iterates over all open repositories and looks for unassigned pull requests' do
33
30
  github_config[:repos].each do |r|
34
- expect(reviewlette).to receive(:check_repo).with(r, token)
35
- reviewlette.check_repo(r, token)
31
+ expect(subject).to receive(:check_repo).with(r, token)
32
+ subject.check_repo(r, token)
36
33
  end
37
34
  end
38
35
  end
39
36
 
40
37
  describe '.check_repo' do
38
+ context 'invalid repo' do
39
+ it 'skips the repo' do
40
+ expect(described_class::GithubConnection).to receive(:repo_exists?).and_return false
41
+ expect { subject.check_repo(repo, token) }.to output(/does not exist/).to_stdout
42
+ end
43
+ end
44
+
41
45
  it 'iterates over all open pull requests and extracts trello ids from name' do
42
- expect(GithubConnection).to receive(:repo_exists?).and_return true
43
- expect(GithubConnection).to receive(:unassigned_pull_requests).and_return([{number: 11, title: 'test_issue_12'}])
44
- expect(TrelloConnection).to receive(:find_card_by_id).with(12)
46
+ expect(described_class::GithubConnection).to receive(:repo_exists?).and_return true
47
+ expect(described_class::GithubConnection).to receive(:pull_requests).and_return([{number: 11, title: 'test_issue_12'}])
48
+ expect(described_class::GithubConnection).to receive(:labels).and_return([])
49
+ expect(described_class::TrelloConnection).to receive(:find_card_by_id).with(12)
45
50
 
46
- reviewlette.check_repo(repo, token)
51
+ subject.check_repo(repo, token)
47
52
  end
48
53
 
49
- it 'adds assignee and reviewer comment on github, adds comment on trello and moves card' do
50
- card = Trello::Card.new
54
+ it 'iterates over all pull requests and skips those with no card id' do
55
+ expect(described_class::GithubConnection).to receive(:repo_exists?).and_return true
56
+ expect(described_class::GithubConnection).to receive(:pull_requests).and_return([{ number: 11, title: 'no card id' }])
57
+ expect(described_class::GithubConnection).to receive(:labels).and_return([])
51
58
 
52
- expect(GithubConnection).to receive(:repo_exists?).and_return true
53
- expect(GithubConnection).to receive(:unassigned_pull_requests).and_return([{number: 11, title: 'test_issue_12'}])
54
- expect(TrelloConnection).to receive(:find_card_by_id).with(12).and_return(card)
55
- expect(reviewlette).to receive(:select_reviewer).and_return({'suse_username' => 'test', 'github_username' => 'testgit'})
59
+ expect { subject.check_repo(repo, token) }.to output(/Pull request not assigned to a trello card/).to_stdout
60
+ end
61
+
62
+ it 'adds assignees and reviewers comment on github, adds comment on trello and moves card' do
63
+ card = Trello::Card.new
64
+ user = double(github_handle: 'testgit', trello_handle: 'testtrello')
65
+ pullrequest = { number: 11, title: 'test_issue_12', assignees: [] }
56
66
 
57
- expect(GithubConnection).to receive(:add_assignee).with(11, 'testgit')
58
- expect(GithubConnection).to receive(:reviewer_comment).with(11, 'testgit', card)
67
+ expect(described_class::GithubConnection).to receive(:repo_exists?).and_return true
68
+ expect(described_class::GithubConnection).to receive(:pull_requests).and_return([pullrequest])
69
+ expect(described_class::GithubConnection).to receive(:labels).and_return([])
70
+ expect(described_class::TrelloConnection).to receive(:find_card_by_id).with(12).and_return(card)
71
+ expect(subject).to receive(:select_reviewers).and_return([user])
59
72
 
60
- expect(TrelloConnection).to receive(:comment_on_card).with("@ will review https://github.com/SUSE/test/issues/11", card)
61
- expect(TrelloConnection).to receive(:move_card_to_list).with(card, 'In review')
73
+ expect(described_class::GithubConnection).to receive(:add_assignees).with(11, ['testgit'])
74
+ expect(described_class::GithubConnection).to receive(:comment_reviewers).with(11, [user], card)
62
75
 
63
- reviewlette.check_repo(repo, token)
76
+ expect(described_class::TrelloConnection).to receive(:comment_reviewers).with(card, 'SUSE/test', 11, [user])
77
+ expect(described_class::TrelloConnection).to receive(:move_card_to_list).with(card, 'In review')
78
+
79
+ subject.check_repo(repo, token)
64
80
  end
65
81
 
66
- end
82
+ context 'pull request with one reviewer but two wanted' do
83
+ it 'selects a second reviewer' do
84
+ card = Trello::Card.new
85
+ pullrequest = { number: 11, title: 'pr title 42', assignees: [OpenStruct.new({login: 'pinky'})] }
86
+ expect(described_class::GithubConnection).to receive(:repo_exists?).and_return true
87
+ expect(described_class::GithubConnection).to receive(:pull_requests).and_return([pullrequest])
88
+ expect(described_class::GithubConnection).to receive(:labels).and_return(['2 reviewers'])
89
+ expect(described_class::TrelloConnection).to receive(:find_card_by_id).with(42).and_return(card)
90
+
91
+ expect(described_class::GithubConnection).to receive(:add_assignees).with(11, ['pinky', 'brain'])
92
+ expect(described_class::GithubConnection).to receive(:comment_reviewers).with(11, [member1, member2], card)
93
+ expect(described_class::TrelloConnection).to receive(:comment_reviewers).with(card, repo, 11, [member1, member2])
94
+ expect(described_class::TrelloConnection).to receive(:move_card_to_list).with(card, 'In review')
95
+ expect(subject).to receive(:select_reviewers).with(card, 2, [member1]).and_return([member1, member2])
96
+ subject.check_repo(repo, token)
97
+ end
98
+ end
67
99
 
68
- describe '.select_reviewer' do
69
- it 'excludes members on vacation' do
70
- card = Trello::Card.new
100
+ context 'pull request with two reviewers but no "2 reviewers" label' do
101
+ it 'keeps both reviewers' do
102
+ card = Trello::Card.new
103
+ pullrequest = { number: 11, title: 'pr title 42', assignees: [ OpenStruct.new({login: 'pinky'}), OpenStruct.new({login: 'brain'})] }
104
+ expect(described_class::GithubConnection).to receive(:repo_exists?).and_return true
105
+ expect(described_class::GithubConnection).to receive(:pull_requests).and_return([pullrequest])
106
+ expect(described_class::GithubConnection).to receive(:labels).and_return([])
107
+ expect(described_class::TrelloConnection).to receive(:find_card_by_id).with(42).and_return(card)
108
+
109
+ expect(described_class::GithubConnection).not_to receive(:add_assignees)
110
+ expect(described_class::TrelloConnection).not_to receive(:move_card_to_list).with(card, 'In review')
111
+ subject.check_repo(repo, token)
112
+ end
113
+ end
71
114
 
72
- allow(card).to receive(:members).and_return([])
73
- expect(Vacations).to receive(:members_on_vacation).and_return([member1['suse_username']])
74
- expect(reviewlette.select_reviewer(nil, card)).to eq(member2)
115
+ context 'pull request with already correct number of reviewers' do
116
+ it 'does not assign nor comment in GitHub or Trello' do
117
+ card = Trello::Card.new
118
+ pullrequest = { number: 11, title: 'pr title 42', assignees: [ OpenStruct.new({login: 'pinky'}), OpenStruct.new({login: 'pinky'})] }
119
+ expect(described_class::GithubConnection).to receive(:repo_exists?).and_return true
120
+ expect(described_class::GithubConnection).to receive(:pull_requests).and_return([pullrequest])
121
+ expect(described_class::GithubConnection).to receive(:labels).and_return(['2 reviewers'])
122
+ expect(described_class::TrelloConnection).to receive(:find_card_by_id).with(42).and_return(card)
123
+
124
+ expect(described_class::GithubConnection).not_to receive(:add_assignees)
125
+ expect(described_class::GithubConnection).not_to receive(:comment_reviewers)
126
+ expect(described_class::TrelloConnection).not_to receive(:comment_reviewers)
127
+ expect(described_class::TrelloConnection).not_to receive(:move_card_to_list).with(card, 'In review')
128
+ subject.check_repo(repo, token)
129
+ end
75
130
  end
76
131
 
132
+ it 'does not find a reviewer' do
133
+ card = Trello::Card.new
134
+
135
+ expect(described_class::GithubConnection).to receive(:repo_exists?).and_return true
136
+ expect(described_class::GithubConnection).to receive(:pull_requests).and_return([{ number: 11, title: 'test_issue_12', assignees: [] }])
137
+ expect(described_class::GithubConnection).to receive(:labels).and_return([])
138
+ expect(described_class::TrelloConnection).to receive(:find_card_by_id).with(12).and_return(card)
139
+ expect(subject).to receive(:select_reviewers).and_return []
140
+
141
+ expect { subject.check_repo(repo, token) }.to output(/Could not find a reviewer/).to_stdout
142
+ end
143
+
144
+ end
145
+
146
+ describe '.select_reviewers' do
77
147
  it 'excludes the owner of the trello card' do
78
148
  card = Trello::Card.new
149
+ reviewers = [member1, member2]
150
+
151
+ allow(card).to receive_message_chain(:members, :map).and_return(reviewers)
152
+ expect(subject.select_reviewers(card).size).to eq(1)
153
+ end
154
+
155
+ it 'selects n reviewers' do
156
+ card = Trello::Card.new
157
+
158
+ allow(card).to receive_message_chain(:members, :map).and_return([member1])
159
+ expect(subject.select_reviewers(card, 2)).to match_array([member1, member2])
160
+ end
161
+
162
+ it 'selects only one reviewer if no second is available' do
163
+ card = Trello::Card.new
164
+
165
+ allow(card).to receive_message_chain(:members, :map).and_return([member2.trello_handle])
166
+ expect(subject.select_reviewers(card, 2)).to eq([member1])
167
+ end
168
+ end
169
+
170
+ describe '.how_many_should_review' do
171
+ subject { instance.how_many_should_review(labels) }
172
+
173
+ context 'with "2 reviewers" label' do
174
+ let(:labels) { ['foo', '2 reviewers', 'bar'] }
175
+ it { is_expected.to eq(2) }
176
+ end
79
177
 
80
- allow(card).to receive_message_chain(:members, :map).and_return(['trellotest1'])
81
- expect(Vacations).to receive(:members_on_vacation).and_return([])
82
- expect(reviewlette.select_reviewer(nil, card)).to eq(members_config['members'].last)
178
+ context 'with no "2 reviewers" label' do
179
+ let(:labels) { ['foo', 'bar'] }
180
+ it { is_expected.to eq(1) }
83
181
  end
84
182
  end
85
183
  end