pronto-github_resolver 0.0.2 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +20 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +11 -1
- data/README.md +34 -9
- data/lib/pronto/github_resolver/github_client_ext.rb +93 -0
- data/lib/pronto/github_resolver/version.rb +1 -1
- data/lib/pronto/github_resolver.rb +43 -163
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f182d2c36206cc761770cf1a135ac78f7a345eae10b756f45da8e0410c1d68db
|
4
|
+
data.tar.gz: cf595336d02afaa1bb68ea04495d10177f9b453a5b3139b96b1201f65b72a51a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f58dacfa4acdf9e686cdee8d5cd81d07fc072f8be3d54814730f7a26d766b36cd34f08f5af3564cdbdbea8a33918031c763f3cc73d219185191be7f0e113f835
|
7
|
+
data.tar.gz: 6148af0e0865e54b6757f130cbf5a2fc9456354bfa1b4b8bb01abb5de27826ec78246ccecd07633efcfb7294042ad7768a6ac92635693146f4f690afe7fbee09
|
data/.rubocop.yml
CHANGED
@@ -1,5 +1,11 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-rake
|
3
|
+
- rubocop-rspec
|
4
|
+
- rubocop-performance
|
5
|
+
|
1
6
|
AllCops:
|
2
7
|
TargetRubyVersion: 2.6
|
8
|
+
NewCops: enable
|
3
9
|
|
4
10
|
Style/StringLiterals:
|
5
11
|
Enabled: true
|
@@ -11,3 +17,17 @@ Style/StringLiteralsInInterpolation:
|
|
11
17
|
|
12
18
|
Layout/LineLength:
|
13
19
|
Max: 120
|
20
|
+
Metrics/CyclomaticComplexity:
|
21
|
+
Max: 8
|
22
|
+
Metrics/AbcSize:
|
23
|
+
Max: 24
|
24
|
+
|
25
|
+
Metrics/BlockLength:
|
26
|
+
Exclude:
|
27
|
+
- spec/**/*.rb
|
28
|
+
Style/OpenStructUse:
|
29
|
+
Exclude:
|
30
|
+
- spec/**/*.rb
|
31
|
+
RSpec/AnyInstance: { Enabled: false }
|
32
|
+
RSpec/MultipleMemoizedHelpers:
|
33
|
+
Max: 7
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pronto-github_resolver (0.0.
|
4
|
+
pronto-github_resolver (0.0.6)
|
5
5
|
pronto (~> 0.11)
|
6
6
|
|
7
7
|
GEM
|
@@ -84,6 +84,13 @@ GEM
|
|
84
84
|
unicode-display_width (>= 1.4.0, < 3.0)
|
85
85
|
rubocop-ast (1.13.0)
|
86
86
|
parser (>= 3.0.1.1)
|
87
|
+
rubocop-performance (1.12.0)
|
88
|
+
rubocop (>= 1.7.0, < 2.0)
|
89
|
+
rubocop-ast (>= 0.4.0)
|
90
|
+
rubocop-rake (0.6.0)
|
91
|
+
rubocop (~> 1.0)
|
92
|
+
rubocop-rspec (2.6.0)
|
93
|
+
rubocop (~> 1.19)
|
87
94
|
ruby-progressbar (1.11.0)
|
88
95
|
ruby2_keywords (0.0.5)
|
89
96
|
rugged (1.0.1)
|
@@ -103,6 +110,9 @@ DEPENDENCIES
|
|
103
110
|
rake (~> 13.0)
|
104
111
|
rspec (~> 3.0)
|
105
112
|
rubocop (~> 1.21)
|
113
|
+
rubocop-performance
|
114
|
+
rubocop-rake
|
115
|
+
rubocop-rspec
|
106
116
|
|
107
117
|
BUNDLED WITH
|
108
118
|
2.2.31
|
data/README.md
CHANGED
@@ -1,28 +1,53 @@
|
|
1
1
|
# Pronto::GithubResolver
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/pronto-github_resolver.svg)](https://badge.fury.io/rb/pronto-github_resolver)
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
4
|
+
Pronto formatter to resolve old pronto messages in pull requests.
|
6
5
|
|
7
6
|
## Installation
|
8
7
|
|
9
8
|
Add this line to your application's Gemfile:
|
10
9
|
|
11
10
|
```ruby
|
12
|
-
gem 'pronto-github_resolver'
|
11
|
+
gem 'pronto-github_resolver', require: false
|
13
12
|
```
|
14
13
|
|
15
14
|
And then execute:
|
15
|
+
```sh
|
16
|
+
bundle install
|
17
|
+
```
|
16
18
|
|
17
|
-
|
19
|
+
Use pronto's `github_pr_review` formatter in your CI, for example:
|
20
|
+
```yml
|
21
|
+
- name: Run pronto
|
22
|
+
run: bundle exec pronto run -f github_status github_pr_review
|
23
|
+
env:
|
24
|
+
PRONTO_GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
25
|
+
PRONTO_PULL_REQUEST_ID: ${{ github.event.pull_request.number }}
|
26
|
+
PRONTO_GITHUB_BOT_ID: 12345678 # replace with your bot user id
|
27
|
+
```
|
18
28
|
|
19
|
-
|
29
|
+
## Usage
|
20
30
|
|
21
|
-
|
31
|
+
Pronto will pick up this from gemfile automatically.
|
22
32
|
|
23
|
-
|
33
|
+
- When any of pronto runners emits message with level `:error` or `:fatal` - generated PR review will have resolution 'REQUEST_CHANGES', and default in other cases.
|
34
|
+
- On each run comment threads where message is no longer generated will be marked as resolved.
|
35
|
+
- Set ENV['PRONTO_GITHUB_BOT_ID'] to github id of your bot user (by default it's name `github-actions[bot]`, but id is different).
|
36
|
+
This enables posting PR 'APPROVE' review by bot after all messages are resolved.
|
37
|
+
|
38
|
+
### Getting bot's id
|
39
|
+
|
40
|
+
At the time of writing, github for unknown reason does not allow bots to get own id by calling `/user`.
|
41
|
+
If you know how to do this without user's effort - please let me know in [issues](https://github.com/Vasfed/pronto-github_resolver/issues).
|
42
|
+
|
43
|
+
You can look up bot's user id by doing
|
44
|
+
|
45
|
+
```sh
|
46
|
+
curl -u "your_user:your_token" https://api.github.com/repos/[organization]/[repo]/pulls/[pull_id]/reviews
|
47
|
+
```
|
48
|
+
on a pull request where the bot has posted a review.
|
24
49
|
|
25
|
-
|
50
|
+
Personal Access Token is generated in [developer settings in your GitHub profile](https://github.com/settings/tokens).
|
26
51
|
|
27
52
|
## Development
|
28
53
|
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pronto
|
4
|
+
# extend stock pronto github client wrapper
|
5
|
+
class Github < Pronto::Client
|
6
|
+
# original, but with event param
|
7
|
+
def publish_pull_request_comments(comments, event: nil)
|
8
|
+
comments_left = comments.clone
|
9
|
+
while comments_left.any?
|
10
|
+
comments_to_publish = comments_left.slice!(0, warnings_per_review)
|
11
|
+
create_pull_request_review(comments_to_publish, event: event)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# original, but with event param
|
16
|
+
def create_pull_request_review(comments, event: nil)
|
17
|
+
options = {
|
18
|
+
event: event || @config.github_review_type,
|
19
|
+
accept: "application/vnd.github.v3.diff+json", # https://developer.github.com/v3/pulls/reviews/#create-a-pull-request-review
|
20
|
+
comments: comments.map do |comment|
|
21
|
+
{ path: comment.path, position: comment.position, body: comment.body }
|
22
|
+
end
|
23
|
+
}
|
24
|
+
client.create_pull_request_review(slug, pull_id, options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def approve_pull_request(message = "")
|
28
|
+
client.create_pull_request_review(
|
29
|
+
slug, pull_id,
|
30
|
+
{ event: "APPROVE", body: message, accept: "application/vnd.github.v3.diff+json" }
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def existing_pull_request_reviews
|
35
|
+
client.pull_request_reviews(slug, pull_id)
|
36
|
+
end
|
37
|
+
|
38
|
+
GET_REVIEW_THREADS_QUERY = <<~GQL
|
39
|
+
query getReviewThreadIds($owner: String!, $name: String!, $pull_num: Int!) {
|
40
|
+
repository(owner: $owner, name: $name) {
|
41
|
+
pullRequest: issueOrPullRequest(number: $pull_num) {
|
42
|
+
... on PullRequest {
|
43
|
+
reviewThreads(last:100) {
|
44
|
+
totalCount
|
45
|
+
nodes {
|
46
|
+
id
|
47
|
+
comments(last: 10) {
|
48
|
+
nodes {
|
49
|
+
viewerDidAuthor
|
50
|
+
path position body
|
51
|
+
}
|
52
|
+
}
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
GQL
|
60
|
+
|
61
|
+
def fetch_review_threads # rubocop:disable Metrics/MethodLength
|
62
|
+
owner, repo_name = slug.split("/")
|
63
|
+
res = client.post :graphql, {
|
64
|
+
query: GET_REVIEW_THREADS_QUERY,
|
65
|
+
variables: { owner: owner, name: repo_name, pull_num: pull_id }
|
66
|
+
}.to_json
|
67
|
+
|
68
|
+
return [] if res.errors || !res.data # TODO: handle errors
|
69
|
+
|
70
|
+
res.data.repository.pullRequest.reviewThreads.nodes.to_h do |node|
|
71
|
+
[
|
72
|
+
node.id,
|
73
|
+
node.comments.nodes.map do |comment|
|
74
|
+
{ authored: comment.viewerDidAuthor, path: comment.path, position: comment.position, body: comment.body }
|
75
|
+
end
|
76
|
+
]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def resolve_review_threads(node_ids)
|
81
|
+
return unless node_ids.any?
|
82
|
+
|
83
|
+
query = <<~GQL
|
84
|
+
mutation {
|
85
|
+
#{node_ids.each_with_index.map do |id, index|
|
86
|
+
"q#{index}: resolveReviewThread(input: { threadId: \"#{id}\" }){ thread { id } } "
|
87
|
+
end.join("\n")}
|
88
|
+
}
|
89
|
+
GQL
|
90
|
+
client.post :graphql, { query: query }.to_json
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -1,197 +1,77 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "pronto"
|
3
4
|
require_relative "github_resolver/version"
|
4
|
-
|
5
|
+
require_relative "github_resolver/github_client_ext"
|
5
6
|
|
6
7
|
module Pronto
|
7
|
-
|
8
|
-
class Github < Client
|
9
|
-
# pronto messes up relative paths and does not have tests for this, patch to add repo.path.join
|
10
|
-
def pull_comments(sha)
|
11
|
-
@comment_cache["#{pull_id}/#{sha}"] ||= begin
|
12
|
-
client.pull_comments(slug, pull_id).map do |comment|
|
13
|
-
Comment.new(sha, comment.body, @repo.path.join(comment.path),
|
14
|
-
comment.position || comment.original_position)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
rescue Octokit::NotFound => e
|
18
|
-
@config.logger.log("Error raised and rescued: #{e}")
|
19
|
-
msg = "Pull request for sha #{sha} with id #{pull_id} was not found."
|
20
|
-
raise Pronto::Error, msg
|
21
|
-
end
|
22
|
-
|
23
|
-
def publish_pull_request_comments(comments, event: nil)
|
24
|
-
comments_left = comments.clone
|
25
|
-
while comments_left.any?
|
26
|
-
comments_to_publish = comments_left.slice!(0, warnings_per_review)
|
27
|
-
create_pull_request_review(comments_to_publish, event: event)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def create_pull_request_review(comments, event: nil)
|
32
|
-
options = {
|
33
|
-
event: event || @config.github_review_type,
|
34
|
-
accept: 'application/vnd.github.v3.diff+json', # https://developer.github.com/v3/pulls/reviews/#create-a-pull-request-review
|
35
|
-
comments: comments.map do |comment|
|
36
|
-
{
|
37
|
-
path: comment.path,
|
38
|
-
position: comment.position,
|
39
|
-
body: comment.body
|
40
|
-
}
|
41
|
-
end
|
42
|
-
}
|
43
|
-
client.create_pull_request_review(slug, pull_id, options)
|
44
|
-
end
|
45
|
-
|
46
|
-
def approve_pull_request(message=nil)
|
47
|
-
client.create_pull_request_review(slug, pull_id, {
|
48
|
-
event: 'APPROVE', body: message, accept: 'application/vnd.github.v3.diff+json'
|
49
|
-
})
|
50
|
-
end
|
51
|
-
|
52
|
-
def existing_pull_request_reviews
|
53
|
-
client.pull_request_reviews(slug, pull_id)
|
54
|
-
end
|
55
|
-
|
56
|
-
def bot_user_id
|
57
|
-
bot_user.id
|
58
|
-
end
|
59
|
-
|
60
|
-
def bot_user
|
61
|
-
@bot_user ||= client.user
|
62
|
-
end
|
63
|
-
|
64
|
-
def get_review_threads
|
65
|
-
owner, repo_name = (slug || "").split('/')
|
66
|
-
res = client.post :graphql, { query: <<~GQL }.to_json
|
67
|
-
query getUserId {
|
68
|
-
repository(owner: "#{owner}", name: "#{repo_name}") {
|
69
|
-
pullRequest: issueOrPullRequest(number: #{pull_id}) {
|
70
|
-
... on PullRequest {
|
71
|
-
reviewThreads(last:100) {
|
72
|
-
totalCount
|
73
|
-
nodes {
|
74
|
-
id
|
75
|
-
comments(last: 10) {
|
76
|
-
nodes {
|
77
|
-
author { ... on Node { id } }
|
78
|
-
viewerDidAuthor
|
79
|
-
path position body
|
80
|
-
}
|
81
|
-
}
|
82
|
-
}
|
83
|
-
}
|
84
|
-
}
|
85
|
-
}
|
86
|
-
}
|
87
|
-
}
|
88
|
-
GQL
|
89
|
-
|
90
|
-
if res.errors || !res.data
|
91
|
-
# ex: [{:message=>"Parse error on \"11\" (INT) at [1, 22]", :locations=>[{:line=>1, :column=>22}]}]
|
92
|
-
# TODO: handle errors
|
93
|
-
return []
|
94
|
-
end
|
95
|
-
|
96
|
-
res.data.repository.pullRequest.reviewThreads.nodes.to_h { |node|
|
97
|
-
[
|
98
|
-
node.id,
|
99
|
-
node.comments.nodes.map{ |comment|
|
100
|
-
{
|
101
|
-
author_id: comment.author.id,
|
102
|
-
path: comment.path, position: comment.position, body: comment.body
|
103
|
-
}
|
104
|
-
}
|
105
|
-
]
|
106
|
-
}
|
107
|
-
end
|
108
|
-
|
109
|
-
def resolve_review_threads(node_ids)
|
110
|
-
return unless node_ids.any?
|
111
|
-
|
112
|
-
owner, repo_name = (slug || "").split('/')
|
113
|
-
query = <<~GQL
|
114
|
-
mutation {
|
115
|
-
#{
|
116
|
-
node_ids.each_with_index.map {|id, index|
|
117
|
-
"q#{index}: resolveReviewThread(input: { threadId: \"#{id}\" }){ thread { id } } "
|
118
|
-
}.join("\n")
|
119
|
-
}
|
120
|
-
}
|
121
|
-
GQL
|
122
|
-
client.post :graphql, { query: query }.to_json
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
8
|
module Formatter
|
9
|
+
# monkey-patch stock formatter with altered behavior
|
127
10
|
module GithubResolving
|
11
|
+
# TODO: we can reuse some threads from graphql for existing messages detection (but there's no pagination)
|
128
12
|
def format(messages, repo, patches)
|
129
13
|
client = client_module.new(repo)
|
130
14
|
existing = existing_comments(messages, client, repo)
|
131
15
|
comments = new_comments(messages, patches)
|
132
16
|
additions = remove_duplicate_comments(existing, comments)
|
133
17
|
|
134
|
-
|
135
|
-
|
18
|
+
resolve_old_messages(client, comments)
|
19
|
+
submit_review(comments, messages, client, additions)
|
136
20
|
|
137
|
-
|
138
|
-
|
139
|
-
if bot_reviews.any?
|
140
|
-
bot_id = client.bot_user_id
|
141
|
-
current_bot_review_status = bot_reviews.inject(nil) do |prev_status, review|
|
142
|
-
next prev_status unless review.user.id == bot_id
|
21
|
+
"#{additions.count} Pronto messages posted to #{pretty_name}"
|
22
|
+
end
|
143
23
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
24
|
+
def submit_review(comments, messages, client, additions)
|
25
|
+
return post_approve_if_needed(client) if comments.none?
|
26
|
+
|
27
|
+
request_changes_at = %i[error fatal].freeze
|
28
|
+
request_changes = messages.any? { |message| request_changes_at.include?(message.level) } && "REQUEST_CHANGES"
|
29
|
+
submit_comments(client, additions, event: request_changes || nil)
|
30
|
+
end
|
31
|
+
|
32
|
+
def post_approve_if_needed(client)
|
33
|
+
bot_reviews = client.existing_pull_request_reviews.select { |review| review.user.type == "Bot" }
|
34
|
+
return if bot_reviews.none?
|
151
35
|
|
152
|
-
|
36
|
+
current_bot_review_status = bot_reviews.inject(nil) do |prev_status, review|
|
37
|
+
if review_by_this_bot?(review)
|
38
|
+
next review.state if review.state == "CHANGES_REQUESTED"
|
39
|
+
next nil if review.state == "APPROVED"
|
153
40
|
end
|
154
|
-
|
155
|
-
submit_comments(
|
156
|
-
client, additions,
|
157
|
-
event: messages.any? { |message| %i[error fatal].include?(message.level) } && 'REQUEST_CHANGES' || nil
|
158
|
-
)
|
41
|
+
prev_status
|
159
42
|
end
|
160
43
|
|
161
|
-
|
44
|
+
client.approve_pull_request if current_bot_review_status == "CHANGES_REQUESTED"
|
45
|
+
end
|
46
|
+
|
47
|
+
def review_by_this_bot?(review)
|
48
|
+
ENV["PRONTO_GITHUB_BOT_ID"] && review.user.id == ENV["PRONTO_GITHUB_BOT_ID"].to_i
|
162
49
|
end
|
163
50
|
|
51
|
+
# copied from upstream, added event param
|
164
52
|
def submit_comments(client, comments, event: nil)
|
165
53
|
client.publish_pull_request_comments(comments, event: event)
|
166
54
|
rescue Octokit::UnprocessableEntity, HTTParty::Error => e
|
167
|
-
$stderr.puts "Failed to post: #{e.message}"
|
55
|
+
$stderr.puts "Failed to post: #{e.message}" # rubocop:disable Style/StderrPuts like in upstream
|
168
56
|
end
|
169
57
|
|
170
|
-
def resolve_old_messages(client,
|
171
|
-
thread_ids_to_resolve =
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
comment[:author_id] == bot_node_id &&
|
176
|
-
(actual_comments[[repo.path.join(comment[:path]), comment[:position]]] || []).none? { |actual_comment|
|
58
|
+
def resolve_old_messages(client, actual_comments)
|
59
|
+
thread_ids_to_resolve = client.fetch_review_threads.select do |_thread_id, thread_comments|
|
60
|
+
thread_comments.all? do |comment|
|
61
|
+
comment[:authored] &&
|
62
|
+
(actual_comments[[comment[:path], comment[:position]]] || []).none? do |actual_comment|
|
177
63
|
comment[:body].include?(actual_comment.body)
|
178
|
-
|
64
|
+
end
|
179
65
|
end
|
180
|
-
|
181
|
-
end
|
66
|
+
end.keys
|
182
67
|
client.resolve_review_threads(thread_ids_to_resolve)
|
183
68
|
end
|
184
69
|
end
|
185
|
-
|
186
|
-
class GithubPullRequestResolvingReviewFormatter < PullRequestFormatter
|
187
|
-
prepend GithubResolving
|
188
|
-
end
|
189
|
-
|
190
|
-
Pronto::Formatter::GithubPullRequestReviewFormatter.prepend(GithubResolving)
|
191
70
|
end
|
192
71
|
end
|
193
72
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
end
|
73
|
+
# if pronto did not have formatters array frozen - instead of monkeypatch we might have
|
74
|
+
# class GithubPullRequestResolvingReviewFormatter < PullRequestFormatter
|
75
|
+
# prepend GithubResolving
|
76
|
+
# end
|
77
|
+
Pronto::Formatter::GithubPullRequestReviewFormatter.prepend(Pronto::Formatter::GithubResolving)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pronto-github_resolver
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vasily Fedoseyev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-12-
|
11
|
+
date: 2021-12-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pronto
|
@@ -41,6 +41,7 @@ files:
|
|
41
41
|
- bin/console
|
42
42
|
- bin/setup
|
43
43
|
- lib/pronto/github_resolver.rb
|
44
|
+
- lib/pronto/github_resolver/github_client_ext.rb
|
44
45
|
- lib/pronto/github_resolver/version.rb
|
45
46
|
- pronto-github_resolver.gemspec
|
46
47
|
homepage: https://github.com/Vasfed/pronto-github_resolver
|