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 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