pronto-github_resolver 0.0.5 → 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 +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
|
+
[![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
|
-
|
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
|