gitlab-triage 1.13.0 → 1.14.0

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