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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96e8c0465386136dab740a7e36d4804a1a84fe6e2cb9bfab2ff2d567494c80e7
4
- data.tar.gz: 817f09eb49a66f716d384f301334bab472f5e939b03c71ba962780c02ce27002
3
+ metadata.gz: f182d2c36206cc761770cf1a135ac78f7a345eae10b756f45da8e0410c1d68db
4
+ data.tar.gz: cf595336d02afaa1bb68ea04495d10177f9b453a5b3139b96b1201f65b72a51a
5
5
  SHA512:
6
- metadata.gz: e06e67a83cf9c1cfec2d4152812c2948fd802f785f3565202dccfa8ca7ccf3af04f4a1ceee1fa2f8485f8f5ac2fb99cc183949c792088d7785381612e7fb8d59
7
- data.tar.gz: bb93f3ea985b455ad96897e05a2724dc0e63f4ecfd6b90c87e630a15b989aa2f4b06d0c56d2d4419ceb5ee255730ce6a265576ef0ea5fc37cfcd226be5709568
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.5)
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
- 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
18
-
19
- Or install it yourself as:
20
-
21
- $ gem install pronto-github_resolver
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. This enables posting PR 'APPROVE' review by bot after all messages are 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.
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Pronto
4
4
  module GithubResolver
5
- VERSION = "0.0.5"
5
+ VERSION = "0.0.6"
6
6
  end
7
7
  end
@@ -1,176 +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
- 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
- # TODO: we can reuse some threads from graphql for existing messages detection (but there's no pagination)
112
- resolve_old_messages(client, repo, comments)
18
+ resolve_old_messages(client, comments)
19
+ submit_review(comments, messages, client, additions)
113
20
 
114
- if comments.none?
115
- bot_reviews = client.existing_pull_request_reviews.select { |review| review.user.type == 'Bot' }
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
- case review.state
121
- when 'CHANGES_REQUESTED' then review.state
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
- client.approve_pull_request if current_bot_review_status == 'CHANGES_REQUESTED'
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
- else
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
- "#{additions.count} Pronto messages posted to #{pretty_name}"
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['PRONTO_GITHUB_BOT_ID'] && review.user.id == ENV['PRONTO_GITHUB_BOT_ID'].to_i
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, repo, actual_comments)
151
- thread_ids_to_resolve = []
152
- client.get_review_threads.each_pair do |thread_id, thread_comments|
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? { |actual_comment|
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
- thread_ids_to_resolve << thread_id
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
- module Pronto
174
- module GithubResolver
175
- end
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.5
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