pronto-github_resolver 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +20 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +11 -1
- data/README.md +33 -10
- 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 +39 -138
- 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,30 +1,53 @@
|
|
1
1
|
# Pronto::GithubResolver
|
2
|
+
[](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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
+
```
|
22
28
|
|
23
29
|
## Usage
|
24
30
|
|
31
|
+
Pronto will pick up this from gemfile automatically.
|
32
|
+
|
25
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.
|
26
34
|
- On each run comment threads where message is no longer generated will be marked as resolved.
|
27
|
-
- Set ENV['PRONTO_GITHUB_BOT_ID'] to github id of your bot user
|
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.
|
49
|
+
|
50
|
+
Personal Access Token is generated in [developer settings in your GitHub profile](https://github.com/settings/tokens).
|
28
51
|
|
29
52
|
## Development
|
30
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,176 +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
|
-
def publish_pull_request_comments(comments, event: nil)
|
10
|
-
comments_left = comments.clone
|
11
|
-
while comments_left.any?
|
12
|
-
comments_to_publish = comments_left.slice!(0, warnings_per_review)
|
13
|
-
create_pull_request_review(comments_to_publish, event: event)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def create_pull_request_review(comments, event: nil)
|
18
|
-
options = {
|
19
|
-
event: event || @config.github_review_type,
|
20
|
-
accept: 'application/vnd.github.v3.diff+json', # https://developer.github.com/v3/pulls/reviews/#create-a-pull-request-review
|
21
|
-
comments: comments.map do |comment|
|
22
|
-
{
|
23
|
-
path: comment.path,
|
24
|
-
position: comment.position,
|
25
|
-
body: comment.body
|
26
|
-
}
|
27
|
-
end
|
28
|
-
}
|
29
|
-
client.create_pull_request_review(slug, pull_id, options)
|
30
|
-
end
|
31
|
-
|
32
|
-
def approve_pull_request(message="")
|
33
|
-
client.create_pull_request_review(slug, pull_id, {
|
34
|
-
event: 'APPROVE', body: message, accept: 'application/vnd.github.v3.diff+json'
|
35
|
-
})
|
36
|
-
end
|
37
|
-
|
38
|
-
def existing_pull_request_reviews
|
39
|
-
client.pull_request_reviews(slug, pull_id)
|
40
|
-
end
|
41
|
-
|
42
|
-
def get_review_threads
|
43
|
-
owner, repo_name = (slug || "").split('/')
|
44
|
-
res = client.post :graphql, { query: <<~GQL }.to_json
|
45
|
-
query getUserId {
|
46
|
-
repository(owner: "#{owner}", name: "#{repo_name}") {
|
47
|
-
pullRequest: issueOrPullRequest(number: #{pull_id}) {
|
48
|
-
... on PullRequest {
|
49
|
-
reviewThreads(last:100) {
|
50
|
-
totalCount
|
51
|
-
nodes {
|
52
|
-
id
|
53
|
-
comments(last: 10) {
|
54
|
-
nodes {
|
55
|
-
viewerDidAuthor
|
56
|
-
path position body
|
57
|
-
}
|
58
|
-
}
|
59
|
-
}
|
60
|
-
}
|
61
|
-
}
|
62
|
-
}
|
63
|
-
}
|
64
|
-
}
|
65
|
-
GQL
|
66
|
-
|
67
|
-
if res.errors || !res.data
|
68
|
-
# ex: [{:message=>"Parse error on \"11\" (INT) at [1, 22]", :locations=>[{:line=>1, :column=>22}]}]
|
69
|
-
# TODO: handle errors
|
70
|
-
return []
|
71
|
-
end
|
72
|
-
|
73
|
-
res.data.repository.pullRequest.reviewThreads.nodes.to_h { |node|
|
74
|
-
[
|
75
|
-
node.id,
|
76
|
-
node.comments.nodes.map{ |comment|
|
77
|
-
{
|
78
|
-
authored: comment.viewerDidAuthor,
|
79
|
-
path: comment.path, position: comment.position, body: comment.body
|
80
|
-
}
|
81
|
-
}
|
82
|
-
]
|
83
|
-
}
|
84
|
-
end
|
85
|
-
|
86
|
-
def resolve_review_threads(node_ids)
|
87
|
-
return unless node_ids.any?
|
88
|
-
|
89
|
-
owner, repo_name = (slug || "").split('/')
|
90
|
-
query = <<~GQL
|
91
|
-
mutation {
|
92
|
-
#{
|
93
|
-
node_ids.each_with_index.map {|id, index|
|
94
|
-
"q#{index}: resolveReviewThread(input: { threadId: \"#{id}\" }){ thread { id } } "
|
95
|
-
}.join("\n")
|
96
|
-
}
|
97
|
-
}
|
98
|
-
GQL
|
99
|
-
client.post :graphql, { query: query }.to_json
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
8
|
module Formatter
|
9
|
+
# monkey-patch stock formatter with altered behavior
|
104
10
|
module GithubResolving
|
11
|
+
# TODO: we can reuse some threads from graphql for existing messages detection (but there's no pagination)
|
105
12
|
def format(messages, repo, patches)
|
106
13
|
client = client_module.new(repo)
|
107
14
|
existing = existing_comments(messages, client, repo)
|
108
15
|
comments = new_comments(messages, patches)
|
109
16
|
additions = remove_duplicate_comments(existing, comments)
|
110
17
|
|
111
|
-
|
112
|
-
|
18
|
+
resolve_old_messages(client, comments)
|
19
|
+
submit_review(comments, messages, client, additions)
|
113
20
|
|
114
|
-
|
115
|
-
|
116
|
-
if bot_reviews.any?
|
117
|
-
current_bot_review_status = bot_reviews.inject(nil) do |prev_status, review|
|
118
|
-
next prev_status unless review_by_this_bot?(review)
|
21
|
+
"#{additions.count} Pronto messages posted to #{pretty_name}"
|
22
|
+
end
|
119
23
|
|
120
|
-
|
121
|
-
|
122
|
-
when 'APPROVED' then nil
|
123
|
-
else
|
124
|
-
prev_status
|
125
|
-
end
|
126
|
-
end
|
24
|
+
def submit_review(comments, messages, client, additions)
|
25
|
+
return post_approve_if_needed(client) if comments.none?
|
127
26
|
|
128
|
-
|
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?
|
35
|
+
|
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"
|
129
40
|
end
|
130
|
-
|
131
|
-
submit_comments(
|
132
|
-
client, additions,
|
133
|
-
event: messages.any? { |message| %i[error fatal].include?(message.level) } && 'REQUEST_CHANGES' || nil
|
134
|
-
)
|
41
|
+
prev_status
|
135
42
|
end
|
136
43
|
|
137
|
-
|
44
|
+
client.approve_pull_request if current_bot_review_status == "CHANGES_REQUESTED"
|
138
45
|
end
|
139
46
|
|
140
47
|
def review_by_this_bot?(review)
|
141
|
-
ENV[
|
48
|
+
ENV["PRONTO_GITHUB_BOT_ID"] && review.user.id == ENV["PRONTO_GITHUB_BOT_ID"].to_i
|
142
49
|
end
|
143
50
|
|
51
|
+
# copied from upstream, added event param
|
144
52
|
def submit_comments(client, comments, event: nil)
|
145
53
|
client.publish_pull_request_comments(comments, event: event)
|
146
54
|
rescue Octokit::UnprocessableEntity, HTTParty::Error => e
|
147
|
-
$stderr.puts "Failed to post: #{e.message}"
|
55
|
+
$stderr.puts "Failed to post: #{e.message}" # rubocop:disable Style/StderrPuts like in upstream
|
148
56
|
end
|
149
57
|
|
150
|
-
def resolve_old_messages(client,
|
151
|
-
thread_ids_to_resolve =
|
152
|
-
|
153
|
-
next unless thread_comments.all? do |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|
|
154
61
|
comment[:authored] &&
|
155
|
-
(actual_comments[[comment[:path], comment[:position]]] || []).none?
|
62
|
+
(actual_comments[[comment[:path], comment[:position]]] || []).none? do |actual_comment|
|
156
63
|
comment[:body].include?(actual_comment.body)
|
157
|
-
|
64
|
+
end
|
158
65
|
end
|
159
|
-
|
160
|
-
end
|
66
|
+
end.keys
|
161
67
|
client.resolve_review_threads(thread_ids_to_resolve)
|
162
68
|
end
|
163
69
|
end
|
164
|
-
|
165
|
-
class GithubPullRequestResolvingReviewFormatter < PullRequestFormatter
|
166
|
-
prepend GithubResolving
|
167
|
-
end
|
168
|
-
|
169
|
-
Pronto::Formatter::GithubPullRequestReviewFormatter.prepend(GithubResolving)
|
170
70
|
end
|
171
71
|
end
|
172
72
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
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
|