pronto-github_resolver 0.0.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 152e4319e389105c14b8c208f8b668c17a25eafbab0f1270b00c071994f9320c
4
- data.tar.gz: b7a1b40e35543a93e7924e82a7b3596e35ac9e416df34855939112e5222db953
3
+ metadata.gz: f182d2c36206cc761770cf1a135ac78f7a345eae10b756f45da8e0410c1d68db
4
+ data.tar.gz: cf595336d02afaa1bb68ea04495d10177f9b453a5b3139b96b1201f65b72a51a
5
5
  SHA512:
6
- metadata.gz: 46e7f768e794754c2e2d1159779332f980408d77f5fe4c88f825ed63c27c85dad49318c66da9236ca05d133532de5c6d20a3d39df9c8bd68eef7141bf1763368
7
- data.tar.gz: '0758a279f57ba8c0a153528f0b2b06ab3baee909e396e92c86845b86672f18d312ec41f9efbc574f45cf5aa1ec093f988ae728fd987a72e3d571b31f074d5201'
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
@@ -10,3 +10,6 @@ gem "rake", "~> 13.0"
10
10
  gem "rspec", "~> 3.0"
11
11
 
12
12
  gem "rubocop", "~> 1.21"
13
+ gem "rubocop-performance"
14
+ gem "rubocop-rake"
15
+ gem "rubocop-rspec"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pronto-github_resolver (0.0.2)
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
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/pronto/github_resolver`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- $ bundle install
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
- Or install it yourself as:
29
+ ## Usage
20
30
 
21
- $ gem install pronto-github_resolver
31
+ Pronto will pick up this from gemfile automatically.
22
32
 
23
- ## Usage
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
- TODO: Write usage instructions here
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Pronto
4
4
  module GithubResolver
5
- VERSION = "0.0.2"
5
+ VERSION = "0.0.6"
6
6
  end
7
7
  end
@@ -1,197 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pronto"
3
4
  require_relative "github_resolver/version"
4
- require 'pronto'
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
- # TODO: we can reuse some threads from graphql for existing messages detection (but there's no pagination)
135
- resolve_old_messages(client, repo, comments)
18
+ resolve_old_messages(client, comments)
19
+ submit_review(comments, messages, client, additions)
136
20
 
137
- if comments.none?
138
- bot_reviews = client.existing_pull_request_reviews.select { |review| review.user.type == 'Bot' }
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
- case review.state
145
- when 'CHANGES_REQUESTED' then review.state
146
- when 'APPROVED' then nil
147
- else
148
- prev_status
149
- end
150
- end
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
- client.approve_pull_request if current_bot_review_status == 'CHANGES_REQUESTED'
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
- else
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
- "#{additions.count} Pronto messages posted to #{pretty_name}"
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, repo, actual_comments)
171
- thread_ids_to_resolve = []
172
- bot_node_id = client.bot_user.node_id
173
- client.get_review_threads.each_pair do |thread_id, thread_comments|
174
- next unless thread_comments.all? do |comment|
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
- thread_ids_to_resolve << thread_id
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
- module Pronto
195
- module GithubResolver
196
- end
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.2
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-14 00:00:00.000000000 Z
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