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