gitlab-triage 1.13.0 → 1.15.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: 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.