gitlab-triage 1.13.0 → 1.14.0

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: 68fbfe16d2db2547d8b262ff52cb7065860d13822a92b38d6d505234aafa0855
4
- data.tar.gz: 99dc42b1125665a331b8dd6afe576e1c0b8b0abbd0cf8239d5be44184e900c52
3
+ metadata.gz: 817c2b22f9f62ba6ec09e4441aea29dd4f7ae639bd8b49888236ff11a27e33a9
4
+ data.tar.gz: fbd6f4fff1adc6e99e271d438fe7ced46cc40db7a83c59ae985a156d9f57a414
5
5
  SHA512:
6
- metadata.gz: 37e60f6230d1467d18246aa4dc52f439c08939f5a7e2278ed543090c08d5c08b39bffc2840543cbd0a702480d8fbeeb65dc7932fe90a98c2d26a88c8b19f3805
7
- data.tar.gz: 8f82804d8431b08791f3e37974a0cc47aacec887af2969f3e612cba8ac8212e0ac6a25f4235424801b01f6eb0d5bc6db72535d0fed1f962658270164613034cd
6
+ metadata.gz: bab557c9169e861a96fce7b471080194943da6753541860ec3bb01d8790b43bc1c604a59d8fa3e73a0e4dd8dac735dec98746ff2e8a6c97d78d3ee7d87681fa1
7
+ data.tar.gz: 76a6ce14b74488879f14ca43bb0dcbd1b8d2f22e55d1ce7ee7b4cca5efd42715f462eac219c9f5f40de2a81060aa0c4e58e87db8dd3f1e90788a74c1c61b8f2c
data/README.md CHANGED
@@ -142,6 +142,7 @@ Available condition types:
142
142
  - [`source_branch` condition](#source-branch-condition)
143
143
  - [`target_branch` condition](#target-branch-condition)
144
144
  - [`weight` condition](#weight-condition)
145
+ - [`discussions` condition](#discussions-condition)
145
146
  - [`ruby` condition](#ruby-condition)
146
147
 
147
148
  ##### Date condition
@@ -486,6 +487,26 @@ conditions:
486
487
  weight: Any
487
488
  ```
488
489
 
490
+ ##### Discussions condition
491
+
492
+ Accepts a hash of fields.
493
+
494
+ | Field | Type | Values | Required |
495
+ | --------- | ---- | ---- | -------- |
496
+ | `attribute` | string | `threads`, `notes` | yes |
497
+ | `condition` | string | `less_than`, `greater_than` | yes |
498
+ | `threshold` | integer | integer | yes |
499
+
500
+ Example:
501
+
502
+ ```yml
503
+ conditions:
504
+ discussions:
505
+ attribute: threads
506
+ condition: greater_than
507
+ threshold: 15
508
+ ```
509
+
489
510
  ##### Ruby condition
490
511
 
491
512
  This condition allows users to write a Ruby expression to be evaluated for
@@ -20,6 +20,8 @@ Gem::Specification.new do |spec|
20
20
  spec.require_paths = ['lib']
21
21
 
22
22
  spec.add_dependency 'activesupport', '~> 5.1'
23
+ spec.add_dependency 'globalid', '~> 0.4'
24
+ spec.add_dependency 'graphql-client', '~> 0.16'
23
25
  spec.add_dependency 'httparty', '~> 0.17'
24
26
 
25
27
  spec.add_development_dependency 'bundler'
@@ -7,6 +7,7 @@ require_relative 'filters/votes_conditions_filter'
7
7
  require_relative 'filters/no_additional_labels_conditions_filter'
8
8
  require_relative 'filters/author_member_conditions_filter'
9
9
  require_relative 'filters/assignee_member_conditions_filter'
10
+ require_relative 'filters/discussions_conditions_filter'
10
11
  require_relative 'filters/ruby_conditions_filter'
11
12
  require_relative 'limiters/date_field_limiter'
12
13
  require_relative 'action'
@@ -19,7 +20,10 @@ require_relative 'api_query_builders/single_query_param_builder'
19
20
  require_relative 'api_query_builders/multi_query_param_builder'
20
21
  require_relative 'url_builders/url_builder'
21
22
  require_relative 'network'
23
+ require_relative 'graphql_network'
22
24
  require_relative 'network_adapters/httparty_adapter'
25
+ require_relative 'network_adapters/graphql_adapter'
26
+ require_relative 'graphql_queries/query_builder'
23
27
  require_relative 'ui'
24
28
 
25
29
  module Gitlab
@@ -27,7 +31,10 @@ module Gitlab
27
31
  class Engine
28
32
  attr_reader :per_page, :policies, :options
29
33
 
30
- def initialize(policies:, options:, network_adapter_class: Gitlab::Triage::NetworkAdapters::HttpartyAdapter)
34
+ DEFAULT_NETWORK_ADAPTER = Gitlab::Triage::NetworkAdapters::HttpartyAdapter
35
+ DEFAULT_GRAPHQL_ADAPTER = Gitlab::Triage::NetworkAdapters::GraphqlAdapter
36
+
37
+ def initialize(policies:, options:, network_adapter_class: DEFAULT_NETWORK_ADAPTER, graphql_network_adapter_class: DEFAULT_GRAPHQL_ADAPTER)
31
38
  options.host_url = policies.delete(:host_url) { options.host_url }
32
39
  options.api_version = policies.delete(:api_version) { 'v4' }
33
40
  options.dry_run = ENV['TEST'] == 'true' if options.dry_run.nil?
@@ -36,6 +43,7 @@ module Gitlab
36
43
  @policies = policies
37
44
  @options = options
38
45
  @network_adapter_class = network_adapter_class
46
+ @graphql_network_adapter_class = graphql_network_adapter_class
39
47
 
40
48
  assert_all!
41
49
  assert_project_id!
@@ -62,6 +70,10 @@ module Gitlab
62
70
  @network ||= Network.new(network_adapter)
63
71
  end
64
72
 
73
+ def graphql_network
74
+ @graphql_network ||= GraphqlNetwork.new(graphql_network_adapter)
75
+ end
76
+
65
77
  private
66
78
 
67
79
  def assert_project_id!
@@ -94,6 +106,10 @@ module Gitlab
94
106
  @network_adapter ||= @network_adapter_class.new(options)
95
107
  end
96
108
 
109
+ def graphql_network_adapter
110
+ @graphql_network_adapter ||= @graphql_network_adapter_class.new(options)
111
+ end
112
+
97
113
  def rule_conditions(rule)
98
114
  rule.fetch(:conditions) { {} }
99
115
  end
@@ -158,8 +174,11 @@ module Gitlab
158
174
  # retrieving the resources for every rule is inefficient
159
175
  # however, previous rules may affect those upcoming
160
176
  resources = network.query_api(build_get_url(resource_type, conditions))
177
+ graphql_query = build_graphql_query(resource_type, conditions)
178
+ graphql_resources = graphql_network.query(graphql_query, source: options.source_id) if graphql_query.present?
161
179
  # In some filters/actions we want to know which resource type it is
162
180
  attach_resource_type(resources, resource_type)
181
+ decorate_resources_with_graphql_data(resources, graphql_resources)
163
182
 
164
183
  puts "\n\n* Found #{resources.count} resources..."
165
184
  print "* Filtering resources..."
@@ -180,6 +199,13 @@ module Gitlab
180
199
  resources.each { |resource| resource[:type] ||= resource_type }
181
200
  end
182
201
 
202
+ def decorate_resources_with_graphql_data(resources, graphql_resources)
203
+ return if graphql_resources.nil?
204
+
205
+ graphql_resources_by_id = graphql_resources.to_h { |resource| [resource[:id], resource] }
206
+ resources.each { |resource| resource.merge!(graphql_resources_by_id[resource[:id]].to_h) }
207
+ end
208
+
183
209
  def process_action(policy)
184
210
  Action.process(
185
211
  policy: policy,
@@ -213,6 +239,10 @@ module Gitlab
213
239
  results << Filters::AssigneeMemberConditionsFilter.new(resource, conditions[:assignee_member], network).calculate
214
240
  end
215
241
 
242
+ if conditions[:discussions]
243
+ results << Filters::DiscussionsConditionsFilter.new(resource, conditions[:discussions]).calculate
244
+ end
245
+
216
246
  if conditions[:ruby]
217
247
  results << Filters::RubyConditionsFilter.new(resource, conditions, network).calculate
218
248
  end
@@ -270,6 +300,11 @@ module Gitlab
270
300
  params: params
271
301
  ).build
272
302
  end
303
+
304
+ def build_graphql_query(resource_type, conditions)
305
+ Gitlab::Triage::GraphqlQueries::QueryBuilder
306
+ .new(options.source, resource_type, conditions)
307
+ end
273
308
  end
274
309
  end
275
310
  end
@@ -0,0 +1,60 @@
1
+ require_relative 'base_conditions_filter'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module Filters
6
+ class DiscussionsConditionsFilter < BaseConditionsFilter
7
+ ATTRIBUTES = %w[notes threads].freeze
8
+ CONDITIONS = %w[greater_than less_than].freeze
9
+
10
+ def self.filter_parameters
11
+ [
12
+ {
13
+ name: :attribute,
14
+ type: String,
15
+ values: ATTRIBUTES
16
+ },
17
+ {
18
+ name: :condition,
19
+ type: String,
20
+ values: CONDITIONS
21
+ },
22
+ {
23
+ name: :threshold,
24
+ type: Numeric
25
+ }
26
+ ]
27
+ end
28
+
29
+ def initialize_variables(condition)
30
+ @attribute = condition[:attribute].to_sym
31
+ @condition = condition[:condition].to_sym
32
+ @threshold = condition[:threshold]
33
+ end
34
+
35
+ def resource_value
36
+ if @attribute == :notes
37
+ @resource[:user_notes_count]
38
+ else
39
+ @resource.dig(:discussions, :nodes)&.count do |node|
40
+ !node&.dig(:notes, :nodes, 0, :system)
41
+ end
42
+ end
43
+ end
44
+
45
+ def condition_value
46
+ @threshold
47
+ end
48
+
49
+ def calculate
50
+ case @condition
51
+ when :greater_than
52
+ resource_value.to_i > condition_value
53
+ when :less_than
54
+ resource_value.to_i < condition_value
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,54 @@
1
+ require 'active_support/all'
2
+ require 'net/protocol'
3
+ require 'globalid'
4
+
5
+ require_relative 'retryable'
6
+ require_relative 'ui'
7
+ require_relative 'errors'
8
+
9
+ module Gitlab
10
+ module Triage
11
+ class GraphqlNetwork
12
+ attr_reader :options, :adapter
13
+
14
+ def initialize(adapter)
15
+ @adapter = adapter
16
+ @options = adapter.options
17
+ end
18
+
19
+ def query(graphql_query, variables = {})
20
+ return if graphql_query.blank?
21
+
22
+ response = {}
23
+ resources = []
24
+
25
+ parsed_graphql_query = adapter.parse(graphql_query.query)
26
+
27
+ begin
28
+ print '.'
29
+
30
+ response = adapter.query(
31
+ parsed_graphql_query,
32
+ resource_path: graphql_query.resource_path,
33
+ variables: variables.merge(after: response.delete(:end_cursor))
34
+ )
35
+
36
+ resources.concat(Array.wrap(response.delete(:results)))
37
+ end while response.delete(:more_pages)
38
+
39
+ resources
40
+ .map { |resource| resource.deep_transform_keys(&:underscore) }
41
+ .map(&:with_indifferent_access)
42
+ .map { |resource| resource.merge(id: extract_id_from_global_id(resource[:id])) }
43
+ end
44
+
45
+ private
46
+
47
+ def extract_id_from_global_id(global_id)
48
+ return if global_id.blank?
49
+
50
+ GlobalID.parse(global_id).model_id.to_i
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,41 @@
1
+ require_relative 'user_notes_query'
2
+ require_relative 'threads_query'
3
+
4
+ module Gitlab
5
+ module Triage
6
+ module GraphqlQueries
7
+ class QueryBuilder
8
+ def initialize(source_type, resource_type, conditions)
9
+ @source_type = source_type.to_s.singularize
10
+ @resource_type = resource_type
11
+ @conditions = conditions
12
+ end
13
+
14
+ def resource_path
15
+ [source_type, resource_type]
16
+ end
17
+
18
+ def query
19
+ return if query_template.nil?
20
+
21
+ format(query_template, source_type: source_type, resource_type: resource_type.to_s.camelize(:lower))
22
+ end
23
+
24
+ delegate :present?, to: :query_template
25
+
26
+ private
27
+
28
+ attr_reader :source_type, :resource_type, :conditions
29
+
30
+ def query_template
31
+ case conditions.dig(:discussions, :attribute).to_s
32
+ when 'notes'
33
+ UserNotesQuery
34
+ when 'threads'
35
+ ThreadsQuery
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,31 @@
1
+ module Gitlab
2
+ module Triage
3
+ module GraphqlQueries
4
+ ThreadsQuery = <<-GRAPHQL.freeze # rubocop:disable Naming/ConstantName
5
+ query($source: ID!, $after: String) {
6
+ %{source_type}(fullPath: $source) {
7
+ id
8
+ %{resource_type}(after: $after) {
9
+ pageInfo {
10
+ hasNextPage
11
+ endCursor
12
+ }
13
+ nodes {
14
+ id
15
+ discussions {
16
+ nodes {
17
+ notes {
18
+ nodes {
19
+ system
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ GRAPHQL
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ module Gitlab
2
+ module Triage
3
+ module GraphqlQueries
4
+ UserNotesQuery = <<-GRAPHQL.freeze # rubocop:disable Naming/ConstantName
5
+ query($source: ID!, $after: String) {
6
+ %{source_type}(fullPath: $source) {
7
+ id
8
+ %{resource_type}(after: $after) {
9
+ pageInfo {
10
+ hasNextPage
11
+ endCursor
12
+ }
13
+ nodes {
14
+ id
15
+ userNotesCount
16
+ }
17
+ }
18
+ }
19
+ }
20
+ GRAPHQL
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,69 @@
1
+ require 'graphql/client'
2
+ require 'graphql/client/http'
3
+
4
+ require_relative 'base_adapter'
5
+ require_relative '../ui'
6
+ require_relative '../errors'
7
+
8
+ module Gitlab
9
+ module Triage
10
+ module NetworkAdapters
11
+ class GraphqlAdapter < BaseAdapter
12
+ Client = GraphQL::Client
13
+
14
+ def query(graphql_query, resource_path: [], variables: {})
15
+ response = client.query(graphql_query, variables: variables, context: { token: options.token })
16
+
17
+ raise_on_error!(response)
18
+
19
+ parsed_response = parse_response(response, resource_path)
20
+
21
+ return { results: {} } if parsed_response.nil?
22
+ return { results: parsed_response.map(&:to_h) } if parsed_response.is_a?(Client::List)
23
+ return { results: parsed_response.to_h } unless parsed_response.nodes?
24
+
25
+ {
26
+ more_pages: parsed_response.page_info.has_next_page,
27
+ end_cursor: parsed_response.page_info.end_cursor,
28
+ results: parsed_response.nodes.map(&:to_h)
29
+ }
30
+ end
31
+
32
+ delegate :parse, to: :client
33
+
34
+ private
35
+
36
+ def parse_response(response, resource_path)
37
+ resource_path.reduce(response.data) { |data, resource| data&.send(resource) }
38
+ end
39
+
40
+ def raise_on_error!(response)
41
+ return if response.errors.blank?
42
+
43
+ puts Gitlab::Triage::UI.debug response.inspect if options.debug
44
+
45
+ raise "There was an error: #{response.errors.messages.to_json}"
46
+ end
47
+
48
+ def http_client
49
+ Client::HTTP.new("#{options.host_url}/api/graphql") do
50
+ def headers(context) # rubocop:disable Lint/NestedMethodDefinition
51
+ {
52
+ 'Content-type' => 'application/json',
53
+ 'PRIVATE-TOKEN' => context[:token]
54
+ }
55
+ end
56
+ end
57
+ end
58
+
59
+ def schema
60
+ @schema ||= Client.load_schema(http_client)
61
+ end
62
+
63
+ def client
64
+ @client ||= Client.new(schema: schema, execute: http_client).tap { |client| client.allow_dynamic_queries = true }
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
- VERSION = '1.13.0'
5
+ VERSION = '1.14.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-triage
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.0
4
+ version: 1.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-26 00:00:00.000000000 Z
11
+ date: 2020-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '5.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: globalid
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: graphql-client
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.16'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.16'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: httparty
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -158,16 +186,22 @@ files:
158
186
  - lib/gitlab/triage/filters/assignee_member_conditions_filter.rb
159
187
  - lib/gitlab/triage/filters/author_member_conditions_filter.rb
160
188
  - lib/gitlab/triage/filters/base_conditions_filter.rb
189
+ - lib/gitlab/triage/filters/discussions_conditions_filter.rb
161
190
  - lib/gitlab/triage/filters/member_conditions_filter.rb
162
191
  - lib/gitlab/triage/filters/merge_request_date_conditions_filter.rb
163
192
  - lib/gitlab/triage/filters/name_conditions_filter.rb
164
193
  - lib/gitlab/triage/filters/no_additional_labels_conditions_filter.rb
165
194
  - lib/gitlab/triage/filters/ruby_conditions_filter.rb
166
195
  - lib/gitlab/triage/filters/votes_conditions_filter.rb
196
+ - lib/gitlab/triage/graphql_network.rb
197
+ - lib/gitlab/triage/graphql_queries/query_builder.rb
198
+ - lib/gitlab/triage/graphql_queries/threads_query.rb
199
+ - lib/gitlab/triage/graphql_queries/user_notes_query.rb
167
200
  - lib/gitlab/triage/limiters/base_limiter.rb
168
201
  - lib/gitlab/triage/limiters/date_field_limiter.rb
169
202
  - lib/gitlab/triage/network.rb
170
203
  - lib/gitlab/triage/network_adapters/base_adapter.rb
204
+ - lib/gitlab/triage/network_adapters/graphql_adapter.rb
171
205
  - lib/gitlab/triage/network_adapters/httparty_adapter.rb
172
206
  - lib/gitlab/triage/network_adapters/test_adapter.rb
173
207
  - lib/gitlab/triage/option_parser.rb
@@ -213,7 +247,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
213
247
  - !ruby/object:Gem::Version
214
248
  version: '0'
215
249
  requirements: []
216
- rubygems_version: 3.1.2
250
+ rubygems_version: 3.1.4
217
251
  signing_key:
218
252
  specification_version: 4
219
253
  summary: GitLab triage automation project.