gitlab-triage 1.13.0 → 1.15.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: a2927188b4b24b36453f4bac48a82b9af7d2ddc33bb7b5219f533282da9e8370
4
+ data.tar.gz: 8d35707ba7d219950ab3ba8dda852e89687f16b360cf1f368ae02038ff3ff7cb
5
5
  SHA512:
6
- metadata.gz: 37e60f6230d1467d18246aa4dc52f439c08939f5a7e2278ed543090c08d5c08b39bffc2840543cbd0a702480d8fbeeb65dc7932fe90a98c2d26a88c8b19f3805
7
- data.tar.gz: 8f82804d8431b08791f3e37974a0cc47aacec887af2969f3e612cba8ac8212e0ac6a25f4235424801b01f6eb0d5bc6db72535d0fed1f962658270164613034cd
6
+ metadata.gz: 12def4c673ceb025d1a286c9c10a53c410d037364865482b85500bff9215d6490e6d0fcc2737f859a8bbeeba5a302c5e2ff6cbddfc7bab1599e909c243240d67
7
+ data.tar.gz: b98bbed0aabc5cf72595f5dbb3b60ce8d5bb25ad4af19aff1de46c34642ac15c7dc367db39ad1092833a97a923ac73957768bf14b5ed06ccb8bfd518e5fdbdc2
@@ -1,2 +1,2 @@
1
1
  # The official maintainers
2
- * @rymai @godfat @markglenfletcher
2
+ * @rymai @godfat-gitlab @markglenfletcher
@@ -32,4 +32,4 @@ with the latest commit from https://gitlab.com/gitlab-org/gitlab-triage/commits/
32
32
  - Checklist after merging:
33
33
  - [ ] [Update the release notes for the newly created tag](docs/release_process.md#how-to).
34
34
 
35
- /label ~"Engineering Productivity" ~"ep::triage" ~"tooling::workflow"
35
+ /label ~"Engineering Productivity" ~"ep::triage" ~tooling ~"tooling::workflow"
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
@@ -564,6 +585,7 @@ Available action types:
564
585
  - [`comment` action](#comment-action)
565
586
  - [`comment_type` action option](#comment-type-action-option)
566
587
  - [`summarize` action](#summarize-action)
588
+ - [`comment_on_summary` action](#comment-on-summary-action)
567
589
 
568
590
  ##### Labels action
569
591
 
@@ -846,6 +868,56 @@ Which could generate an issue like:
846
868
  /label ~"needs attention"
847
869
  ```
848
870
 
871
+ ##### Comment on summary action
872
+
873
+ Generates one comment for each resource, attaching these comments to the summary
874
+ created by the [`summarize` action](#summarize-action).
875
+
876
+ The use case for this is wanting to create a summary with an overview, and then
877
+ a threaded discussion for each resource, with a header comment starting each
878
+ discussion.
879
+
880
+ Accepts a single string value: the template used to generate the comments. For
881
+ details of the syntax of this template, see the [comment action](#comment-action).
882
+
883
+ Since this action depends on the summary, it is invalid to supply a
884
+ `comment_on_summary` action without an accompanying `summarize` sibling action.
885
+ The `summarize` action will always be completed first.
886
+
887
+ Just like for [comment action](#comment-action), setting `comment_type` in the
888
+ `actions` set controls whether the comment must be resolved for merge requests.
889
+ See: [`comment_type` action option](#comment-type-action-option).
890
+
891
+ Example:
892
+
893
+ ```yml
894
+ resource_rules:
895
+ issues:
896
+ rules:
897
+ - name: List of issues to discuss
898
+ limits:
899
+ most_recent: 15
900
+ actions:
901
+ comment_type: thread
902
+ comment_on_summary: |
903
+ # {{title}}
904
+
905
+ author: {{author}}
906
+ summarize:
907
+ title: |
908
+ #{resource[:type].capitalize} require labels
909
+ item: |
910
+ - [ ] [{{title}}]({{web_url}}) {{labels}}
911
+ summary: |
912
+ The following {{type}} require labels:
913
+
914
+ {{items}}
915
+
916
+ Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
917
+
918
+ /label ~"needs attention"
919
+ ```
920
+
849
921
  ### Summary policies
850
922
 
851
923
  Summary policies are special policies that join multiple rule policies together
@@ -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'
@@ -1,14 +1,18 @@
1
1
  require_relative 'action/summarize'
2
2
  require_relative 'action/comment'
3
+ require_relative 'action/comment_on_summary'
3
4
 
4
5
  module Gitlab
5
6
  module Triage
6
7
  module Action
7
8
  def self.process(policy:, **args)
8
- {
9
- Summarize => policy.summarize?,
10
- Comment => policy.comment?
11
- }.compact.each do |action, active|
9
+ policy.validate!
10
+
11
+ [
12
+ [Summarize, policy.summarize?],
13
+ [Comment, policy.comment?],
14
+ [CommentOnSummary, policy.comment_on_summary?]
15
+ ].each do |action, active|
12
16
  act(action: action, policy: policy, **args) if active
13
17
  end
14
18
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative '../command_builders/text_content_builder'
5
+ require_relative '../command_builders/comment_command_builder'
6
+ require_relative '../command_builders/label_command_builder'
7
+ require_relative '../command_builders/remove_label_command_builder'
8
+ require_relative '../command_builders/cc_command_builder'
9
+ require_relative '../command_builders/status_command_builder'
10
+ require_relative '../command_builders/move_command_builder'
11
+
12
+ module Gitlab
13
+ module Triage
14
+ module Action
15
+ class CommentOnSummary < Base
16
+ class Dry < CommentOnSummary
17
+ def act
18
+ puts "The following comments would be posted for the rule **#{policy.name}**:\n\n"
19
+
20
+ super
21
+ end
22
+
23
+ private
24
+
25
+ def perform(comment)
26
+ puts "# #{summary[:web_url]}\n```\n#{comment}\n```\n"
27
+ end
28
+ end
29
+
30
+ attr_reader :summary
31
+
32
+ def initialize(policy:, network:)
33
+ super(policy: policy, network: network)
34
+ @summary = policy.summary
35
+ end
36
+
37
+ def act
38
+ policy.resources.each do |resource|
39
+ comment = build_comment(resource).strip
40
+
41
+ perform(comment) unless comment.empty?
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def build_comment(resource)
48
+ CommandBuilders::TextContentBuilder.new(policy.actions[:comment_on_summary], resource: resource, network: network).build_command
49
+ end
50
+
51
+ def perform(comment)
52
+ network.post_api(build_post_url, body: comment)
53
+ end
54
+
55
+ def build_post_url
56
+ # POST /projects/:id/issues/:issue_iid/notes
57
+ post_url = UrlBuilders::UrlBuilder.new(
58
+ network_options: network.options,
59
+ source_id: summary['project_id'],
60
+ resource_type: policy.type,
61
+ resource_id: summary['iid'],
62
+ sub_resource_type: sub_resource_type
63
+ ).build
64
+
65
+ puts Gitlab::Triage::UI.debug "post_url: #{post_url}" if network.options.debug
66
+
67
+ post_url
68
+ end
69
+
70
+ def sub_resource_type
71
+ case type = policy.actions[:comment_type]
72
+ when 'comment', nil # nil is default
73
+ 'notes'
74
+ when 'thread'
75
+ 'discussions'
76
+ else
77
+ raise ArgumentError, "Unknown comment type: #{type}"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -10,6 +10,12 @@ module Gitlab
10
10
  private
11
11
 
12
12
  def perform
13
+ policy.summary = {
14
+ web_url: '[the-created-issue-url]',
15
+ project_id: 'some-id',
16
+ iid: 'some-iid'
17
+ }.with_indifferent_access
18
+
13
19
  if group_summary_without_destination?
14
20
  puts Gitlab::Triage::UI.warn("No issue will be created: No summary destination specified when source is 'groups'.")
15
21
  return
@@ -35,7 +41,7 @@ module Gitlab
35
41
  return
36
42
  end
37
43
 
38
- network.post_api(post_issue_url, post_issue_body)
44
+ policy.summary = network.post_api(post_issue_url, post_issue_body)
39
45
  end
40
46
 
41
47
  def issue
@@ -2,16 +2,25 @@ module Gitlab
2
2
  module Triage
3
3
  module APIQueryBuilders
4
4
  class BaseQueryParamBuilder
5
- attr_reader :param_name, :param_contents
5
+ attr_reader :param_name, :param_contents, :allowed_values
6
6
 
7
- def initialize(param_name, param_contents)
7
+ def initialize(param_name, param_contents, allowed_values: nil)
8
8
  @param_name = param_name
9
9
  @param_contents = param_contents
10
+ @allowed_values = allowed_values
11
+
12
+ validate_allowed_values! if allowed_values
10
13
  end
11
14
 
12
15
  def build_param
13
16
  "&#{param_name}=#{param_content.strip}"
14
17
  end
18
+
19
+ private
20
+
21
+ def validate_allowed_values!
22
+ ParamsValidator.new([{ name: param_name, type: String, values: allowed_values }], { param_name => param_contents }).validate!
23
+ end
15
24
  end
16
25
  end
17
26
  end
@@ -6,14 +6,22 @@ module Gitlab
6
6
  class MultiQueryParamBuilder < BaseQueryParamBuilder
7
7
  attr_reader :separator
8
8
 
9
- def initialize(param_name, param_contents, separator)
9
+ def initialize(param_name, param_contents, separator, allowed_values: nil)
10
10
  @separator = separator
11
- super(param_name, param_contents)
11
+ super(param_name, Array(param_contents), allowed_values: allowed_values)
12
12
  end
13
13
 
14
14
  def param_content
15
15
  param_contents.map(&:strip).join(separator)
16
16
  end
17
+
18
+ private
19
+
20
+ def validate_allowed_values!
21
+ param_contents.each do |param|
22
+ ParamsValidator.new([{ name: param_name, type: String, values: allowed_values }], { param_name => param }).validate!
23
+ end
24
+ end
17
25
  end
18
26
  end
19
27
  end
@@ -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,14 @@ 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
+ ALLOWED_STATE_VALUES = {
37
+ issues: %w[opened closed],
38
+ merge_requests: %w[opened closed merged]
39
+ }.with_indifferent_access.freeze
40
+
41
+ def initialize(policies:, options:, network_adapter_class: DEFAULT_NETWORK_ADAPTER, graphql_network_adapter_class: DEFAULT_GRAPHQL_ADAPTER)
31
42
  options.host_url = policies.delete(:host_url) { options.host_url }
32
43
  options.api_version = policies.delete(:api_version) { 'v4' }
33
44
  options.dry_run = ENV['TEST'] == 'true' if options.dry_run.nil?
@@ -36,6 +47,7 @@ module Gitlab
36
47
  @policies = policies
37
48
  @options = options
38
49
  @network_adapter_class = network_adapter_class
50
+ @graphql_network_adapter_class = graphql_network_adapter_class
39
51
 
40
52
  assert_all!
41
53
  assert_project_id!
@@ -62,6 +74,10 @@ module Gitlab
62
74
  @network ||= Network.new(network_adapter)
63
75
  end
64
76
 
77
+ def graphql_network
78
+ @graphql_network ||= GraphqlNetwork.new(graphql_network_adapter)
79
+ end
80
+
65
81
  private
66
82
 
67
83
  def assert_project_id!
@@ -94,6 +110,10 @@ module Gitlab
94
110
  @network_adapter ||= @network_adapter_class.new(options)
95
111
  end
96
112
 
113
+ def graphql_network_adapter
114
+ @graphql_network_adapter ||= @graphql_network_adapter_class.new(options)
115
+ end
116
+
97
117
  def rule_conditions(rule)
98
118
  rule.fetch(:conditions) { {} }
99
119
  end
@@ -158,8 +178,13 @@ module Gitlab
158
178
  # retrieving the resources for every rule is inefficient
159
179
  # however, previous rules may affect those upcoming
160
180
  resources = network.query_api(build_get_url(resource_type, conditions))
181
+ iids = resources.pluck('iid').map(&:to_s)
182
+
183
+ graphql_query = build_graphql_query(resource_type, conditions)
184
+ graphql_resources = graphql_network.query(graphql_query, source: source_full_path, iids: iids) if graphql_query.present?
161
185
  # In some filters/actions we want to know which resource type it is
162
186
  attach_resource_type(resources, resource_type)
187
+ decorate_resources_with_graphql_data(resources, graphql_resources)
163
188
 
164
189
  puts "\n\n* Found #{resources.count} resources..."
165
190
  print "* Filtering resources..."
@@ -180,6 +205,13 @@ module Gitlab
180
205
  resources.each { |resource| resource[:type] ||= resource_type }
181
206
  end
182
207
 
208
+ def decorate_resources_with_graphql_data(resources, graphql_resources)
209
+ return if graphql_resources.nil?
210
+
211
+ graphql_resources_by_id = graphql_resources.to_h { |resource| [resource[:id], resource] }
212
+ resources.each { |resource| resource.merge!(graphql_resources_by_id[resource[:id]].to_h) }
213
+ end
214
+
183
215
  def process_action(policy)
184
216
  Action.process(
185
217
  policy: policy,
@@ -213,6 +245,10 @@ module Gitlab
213
245
  results << Filters::AssigneeMemberConditionsFilter.new(resource, conditions[:assignee_member], network).calculate
214
246
  end
215
247
 
248
+ if conditions[:discussions]
249
+ results << Filters::DiscussionsConditionsFilter.new(resource, conditions[:discussions]).calculate
250
+ end
251
+
216
252
  if conditions[:ruby]
217
253
  results << Filters::RubyConditionsFilter.new(resource, conditions, network).calculate
218
254
  end
@@ -244,7 +280,13 @@ module Gitlab
244
280
  condition_builders << APIQueryBuilders::MultiQueryParamBuilder.new('not[labels]', conditions[:forbidden_labels], ',')
245
281
  end
246
282
 
247
- condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('state', conditions[:state]) if conditions[:state]
283
+ if conditions[:state]
284
+ condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new(
285
+ 'state',
286
+ conditions[:state],
287
+ allowed_values: ALLOWED_STATE_VALUES[resource_type])
288
+ end
289
+
248
290
  condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('milestone', Array(conditions[:milestone])[0]) if conditions[:milestone]
249
291
  condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('source_branch', conditions[:source_branch]) if conditions[:source_branch]
250
292
  condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('target_branch', conditions[:target_branch]) if conditions[:target_branch]
@@ -270,6 +312,26 @@ module Gitlab
270
312
  params: params
271
313
  ).build
272
314
  end
315
+
316
+ def build_graphql_query(resource_type, conditions)
317
+ Gitlab::Triage::GraphqlQueries::QueryBuilder
318
+ .new(options.source, resource_type, conditions)
319
+ end
320
+
321
+ def source_full_path
322
+ @source_full_path ||= fetch_source_full_path
323
+ end
324
+
325
+ def fetch_source_full_path
326
+ return options.source_id unless /\A\d+\z/.match?(options.source_id)
327
+
328
+ source_details = network.query_api(build_get_url(nil, {})).first
329
+ full_path = source_details['full_path'] || source_details['path_with_namespace']
330
+
331
+ raise ArgumentError, 'A source with given source_id was not found!' if full_path.blank?
332
+
333
+ full_path
334
+ end
273
335
  end
274
336
  end
275
337
  end
@@ -3,6 +3,7 @@ module Gitlab
3
3
  module Errors
4
4
  module Network
5
5
  InternalServerError = Class.new(StandardError)
6
+ UnexpectedResponse = Class.new(StandardError)
6
7
  end
7
8
  end
8
9
  end
@@ -0,0 +1,58 @@
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[:user_discussions_count]
40
+ end
41
+ end
42
+
43
+ def condition_value
44
+ @threshold
45
+ end
46
+
47
+ def calculate
48
+ case @condition
49
+ when :greater_than
50
+ resource_value.to_i > condition_value
51
+ when :less_than
52
+ resource_value.to_i < condition_value
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,71 @@
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
+ MINIMUM_RATE_LIMIT = 25
15
+
16
+ def initialize(adapter)
17
+ @adapter = adapter
18
+ @options = adapter.options
19
+ end
20
+
21
+ def query(graphql_query, variables = {})
22
+ return if graphql_query.blank?
23
+
24
+ response = {}
25
+ resources = []
26
+
27
+ parsed_graphql_query = adapter.parse(graphql_query.query)
28
+
29
+ begin
30
+ print '.'
31
+
32
+ response = adapter.query(
33
+ parsed_graphql_query,
34
+ resource_path: graphql_query.resource_path,
35
+ variables: variables.merge(after: response.delete(:end_cursor))
36
+ )
37
+
38
+ rate_limit_debug(response) if options.debug
39
+ rate_limit_wait(response)
40
+
41
+ resources.concat(Array.wrap(response.delete(:results)))
42
+ end while response.delete(:more_pages)
43
+
44
+ resources
45
+ .map { |resource| resource.deep_transform_keys(&:underscore) }
46
+ .map(&:with_indifferent_access)
47
+ .map { |resource| resource.merge(id: extract_id_from_global_id(resource[:id])) }
48
+ end
49
+
50
+ private
51
+
52
+ def extract_id_from_global_id(global_id)
53
+ return if global_id.blank?
54
+
55
+ GlobalID.parse(global_id).model_id.to_i
56
+ end
57
+
58
+ def rate_limit_debug(response)
59
+ rate_limit_infos = "Rate limit remaining: #{response[:ratelimit_remaining]} (reset at #{response[:ratelimit_reset_at]})"
60
+ puts Gitlab::Triage::UI.debug "rate_limit_infos: #{rate_limit_infos}"
61
+ end
62
+
63
+ def rate_limit_wait(response)
64
+ return unless response.delete(:ratelimit_remaining) < MINIMUM_RATE_LIMIT
65
+
66
+ puts Gitlab::Triage::UI.debug "Rate limit almost exceeded, sleeping for #{response[:ratelimit_reset_at] - Time.now} seconds" if options.debug
67
+ sleep(1) until Time.now >= response[:ratelimit_reset_at]
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,47 @@
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), group_query: group_query)
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
+
39
+ def group_query
40
+ return if source_type != 'group'
41
+
42
+ ', includeSubgroups: true'
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,23 @@
1
+ module Gitlab
2
+ module Triage
3
+ module GraphqlQueries
4
+ ThreadsQuery = <<-GRAPHQL.freeze # rubocop:disable Naming/ConstantName
5
+ query($source: ID!, $after: String, $iids: [String!]) {
6
+ %{source_type}(fullPath: $source) {
7
+ id
8
+ %{resource_type}(after: $after, iids: $iids%{group_query}) {
9
+ pageInfo {
10
+ hasNextPage
11
+ endCursor
12
+ }
13
+ nodes {
14
+ id
15
+ userDiscussionsCount
16
+ }
17
+ }
18
+ }
19
+ }
20
+ GRAPHQL
21
+ end
22
+ end
23
+ 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, $iids: [String!]) {
6
+ %{source_type}(fullPath: $source) {
7
+ id
8
+ %{resource_type}(after: $after, iids: $iids%{group_query}) {
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
@@ -44,8 +44,10 @@ module Gitlab
44
44
  case results
45
45
  when Array
46
46
  resources.concat(results)
47
- else
47
+ when Hash
48
48
  resources << results
49
+ else
50
+ raise Errors::Network::UnexpectedResponse, "Unexpected response: #{results.inspect}"
49
51
  end
50
52
 
51
53
  rate_limit_debug(response) if options.debug
@@ -0,0 +1,92 @@
1
+ require 'httparty'
2
+ require 'graphql/client'
3
+ require 'graphql/client/http'
4
+
5
+ require_relative 'base_adapter'
6
+ require_relative '../ui'
7
+ require_relative '../errors'
8
+
9
+ module Gitlab
10
+ module Triage
11
+ module NetworkAdapters
12
+ class GraphqlAdapter < BaseAdapter
13
+ Client = GraphQL::Client
14
+
15
+ def query(graphql_query, resource_path: [], variables: {})
16
+ response = client.query(graphql_query, variables: variables, context: { token: options.token })
17
+
18
+ raise_on_error!(response)
19
+
20
+ parsed_response = parse_response(response, resource_path)
21
+ headers = response.extensions.fetch('headers', {})
22
+
23
+ graphql_response = {
24
+ ratelimit_remaining: headers['ratelimit-remaining'].to_i,
25
+ ratelimit_reset_at: Time.at(headers['ratelimit-reset'].to_i)
26
+ }
27
+
28
+ return graphql_response.merge(results: {}) if parsed_response.nil?
29
+ return graphql_response.merge(results: parsed_response.map(&:to_h)) if parsed_response.is_a?(Client::List)
30
+ return graphql_response.merge(results: parsed_response.to_h) unless parsed_response.nodes?
31
+
32
+ graphql_response.merge(
33
+ more_pages: parsed_response.page_info.has_next_page,
34
+ end_cursor: parsed_response.page_info.end_cursor,
35
+ results: parsed_response.nodes.map(&:to_h)
36
+ )
37
+ end
38
+
39
+ delegate :parse, to: :client
40
+
41
+ private
42
+
43
+ def parse_response(response, resource_path)
44
+ resource_path.reduce(response.data) { |data, resource| data&.send(resource) }
45
+ end
46
+
47
+ def raise_on_error!(response)
48
+ return if response.errors.blank?
49
+
50
+ puts Gitlab::Triage::UI.debug response.inspect if options.debug
51
+
52
+ raise "There was an error: #{response.errors.messages.to_json}"
53
+ end
54
+
55
+ def http_client
56
+ Client::HTTP.new("#{options.host_url}/api/graphql") do
57
+ def execute(document:, operation_name: nil, variables: {}, context: {}) # rubocop:disable Lint/NestedMethodDefinition
58
+ body = {}
59
+ body['query'] = document.to_query_string
60
+ body['variables'] = variables if variables.any?
61
+ body['operationName'] = operation_name if operation_name
62
+
63
+ response = HTTParty.post(
64
+ uri,
65
+ body: body.to_json,
66
+ headers: {
67
+ 'Content-type' => 'application/json',
68
+ 'PRIVATE-TOKEN' => context[:token]
69
+ }
70
+ )
71
+
72
+ case response.code
73
+ when 200, 400
74
+ JSON.parse(response.body).merge('extensions' => { 'headers' => response.headers })
75
+ else
76
+ { 'errors' => [{ 'message' => "#{response.code} #{response.message}" }] }
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ def schema
83
+ @schema ||= Client.load_schema(http_client)
84
+ end
85
+
86
+ def client
87
+ @client ||= Client.new(schema: schema, execute: http_client).tap { |client| client.allow_dynamic_queries = true }
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -4,7 +4,10 @@ module Gitlab
4
4
  module Triage
5
5
  module Policies
6
6
  class BasePolicy
7
+ InvalidPolicyError = Class.new(StandardError)
8
+
7
9
  attr_reader :type, :policy_spec, :resources, :network
10
+ attr_accessor :summary
8
11
 
9
12
  def initialize(type, policy_spec, resources, network)
10
13
  @type = type
@@ -13,6 +16,10 @@ module Gitlab
13
16
  @network = network
14
17
  end
15
18
 
19
+ def validate!
20
+ raise InvalidPolicyError, 'Policies that comment_on_summary must include summarize action' if comment_on_summary? && !summarize?
21
+ end
22
+
16
23
  def name
17
24
  @name ||= (policy_spec[:name] || "#{type}-#{object_id}")
18
25
  end
@@ -25,9 +32,13 @@ module Gitlab
25
32
  actions.key?(:summarize)
26
33
  end
27
34
 
35
+ def comment_on_summary?
36
+ actions.key?(:comment_on_summary)
37
+ end
38
+
28
39
  def comment?
29
40
  # The actual keys are strings
30
- (actions.keys.map(&:to_sym) - [:summarize]).any?
41
+ (actions.keys.map(&:to_sym) - [:summarize, :comment_on_summary]).any?
31
42
  end
32
43
 
33
44
  def build_issue
@@ -34,7 +34,7 @@ module Gitlab
34
34
  def validate_parameter_content(value)
35
35
  @parameter_definitions.each do |param|
36
36
  if param[:values]
37
- raise InvalidParameter, "#{param[:name]} must be of one of #{param[:values].join(',')}" unless param[:values].include?(value[param[:name]])
37
+ raise InvalidParameter, "#{param[:name]} must be one of #{param[:values].join(',')}" unless param[:values].include?(value[param[:name]])
38
38
  end
39
39
  end
40
40
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
- VERSION = '1.13.0'
5
+ VERSION = '1.15.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.15.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: 2021-01-20 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
@@ -134,6 +162,7 @@ files:
134
162
  - lib/gitlab/triage/action.rb
135
163
  - lib/gitlab/triage/action/base.rb
136
164
  - lib/gitlab/triage/action/comment.rb
165
+ - lib/gitlab/triage/action/comment_on_summary.rb
137
166
  - lib/gitlab/triage/action/summarize.rb
138
167
  - lib/gitlab/triage/api_query_builders/base_query_param_builder.rb
139
168
  - lib/gitlab/triage/api_query_builders/date_query_param_builder.rb
@@ -158,16 +187,22 @@ files:
158
187
  - lib/gitlab/triage/filters/assignee_member_conditions_filter.rb
159
188
  - lib/gitlab/triage/filters/author_member_conditions_filter.rb
160
189
  - lib/gitlab/triage/filters/base_conditions_filter.rb
190
+ - lib/gitlab/triage/filters/discussions_conditions_filter.rb
161
191
  - lib/gitlab/triage/filters/member_conditions_filter.rb
162
192
  - lib/gitlab/triage/filters/merge_request_date_conditions_filter.rb
163
193
  - lib/gitlab/triage/filters/name_conditions_filter.rb
164
194
  - lib/gitlab/triage/filters/no_additional_labels_conditions_filter.rb
165
195
  - lib/gitlab/triage/filters/ruby_conditions_filter.rb
166
196
  - lib/gitlab/triage/filters/votes_conditions_filter.rb
197
+ - lib/gitlab/triage/graphql_network.rb
198
+ - lib/gitlab/triage/graphql_queries/query_builder.rb
199
+ - lib/gitlab/triage/graphql_queries/threads_query.rb
200
+ - lib/gitlab/triage/graphql_queries/user_notes_query.rb
167
201
  - lib/gitlab/triage/limiters/base_limiter.rb
168
202
  - lib/gitlab/triage/limiters/date_field_limiter.rb
169
203
  - lib/gitlab/triage/network.rb
170
204
  - lib/gitlab/triage/network_adapters/base_adapter.rb
205
+ - lib/gitlab/triage/network_adapters/graphql_adapter.rb
171
206
  - lib/gitlab/triage/network_adapters/httparty_adapter.rb
172
207
  - lib/gitlab/triage/network_adapters/test_adapter.rb
173
208
  - lib/gitlab/triage/option_parser.rb
@@ -213,7 +248,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
213
248
  - !ruby/object:Gem::Version
214
249
  version: '0'
215
250
  requirements: []
216
- rubygems_version: 3.1.2
251
+ rubygems_version: 3.1.4
217
252
  signing_key:
218
253
  specification_version: 4
219
254
  summary: GitLab triage automation project.