gitlab-triage 1.14.3 → 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: 274f8c4fdb85670a259e32f6c378512e93a52619184714baea85372f77e54c9b
4
- data.tar.gz: 469563e881bb1678de111984c70cdd53c64abe241ab27b69ac206c262a10d078
3
+ metadata.gz: a2927188b4b24b36453f4bac48a82b9af7d2ddc33bb7b5219f533282da9e8370
4
+ data.tar.gz: 8d35707ba7d219950ab3ba8dda852e89687f16b360cf1f368ae02038ff3ff7cb
5
5
  SHA512:
6
- metadata.gz: 8d850e51292669d2763dfbf7f313fe49b24e99c1bce44a12322901a8dc9f164f1a09c96ff56c6b9fcd798fbc82f43aeed06ed6cf80ede7fb2ec517edbe49d631
7
- data.tar.gz: ef78109d3c19698308979c36ef43d3f8e163c5d053f45e093f56dd8da05f6a1c34b5463339a6631944718c561b01ded46845b35924a3d1b1454ed0379faa3310
6
+ metadata.gz: 12def4c673ceb025d1a286c9c10a53c410d037364865482b85500bff9215d6490e6d0fcc2737f859a8bbeeba5a302c5e2ff6cbddfc7bab1599e909c243240d67
7
+ data.tar.gz: b98bbed0aabc5cf72595f5dbb3b60ce8d5bb25ad4af19aff1de46c34642ac15c7dc367db39ad1092833a97a923ac73957768bf14b5ed06ccb8bfd518e5fdbdc2
data/README.md CHANGED
@@ -585,6 +585,7 @@ Available action types:
585
585
  - [`comment` action](#comment-action)
586
586
  - [`comment_type` action option](#comment-type-action-option)
587
587
  - [`summarize` action](#summarize-action)
588
+ - [`comment_on_summary` action](#comment-on-summary-action)
588
589
 
589
590
  ##### Labels action
590
591
 
@@ -867,6 +868,56 @@ Which could generate an issue like:
867
868
  /label ~"needs attention"
868
869
  ```
869
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
+
870
921
  ### Summary policies
871
922
 
872
923
  Summary policies are special policies that join multiple rule policies together
@@ -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
@@ -181,7 +181,7 @@ module Gitlab
181
181
  iids = resources.pluck('iid').map(&:to_s)
182
182
 
183
183
  graphql_query = build_graphql_query(resource_type, conditions)
184
- graphql_resources = graphql_network.query(graphql_query, source: options.source_id, iids: iids) if graphql_query.present?
184
+ graphql_resources = graphql_network.query(graphql_query, source: source_full_path, iids: iids) if graphql_query.present?
185
185
  # In some filters/actions we want to know which resource type it is
186
186
  attach_resource_type(resources, resource_type)
187
187
  decorate_resources_with_graphql_data(resources, graphql_resources)
@@ -317,6 +317,21 @@ module Gitlab
317
317
  Gitlab::Triage::GraphqlQueries::QueryBuilder
318
318
  .new(options.source, resource_type, conditions)
319
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
320
335
  end
321
336
  end
322
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
@@ -11,6 +11,8 @@ module Gitlab
11
11
  class GraphqlNetwork
12
12
  attr_reader :options, :adapter
13
13
 
14
+ MINIMUM_RATE_LIMIT = 25
15
+
14
16
  def initialize(adapter)
15
17
  @adapter = adapter
16
18
  @options = adapter.options
@@ -33,6 +35,9 @@ module Gitlab
33
35
  variables: variables.merge(after: response.delete(:end_cursor))
34
36
  )
35
37
 
38
+ rate_limit_debug(response) if options.debug
39
+ rate_limit_wait(response)
40
+
36
41
  resources.concat(Array.wrap(response.delete(:results)))
37
42
  end while response.delete(:more_pages)
38
43
 
@@ -49,6 +54,18 @@ module Gitlab
49
54
 
50
55
  GlobalID.parse(global_id).model_id.to_i
51
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
52
69
  end
53
70
  end
54
71
  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
@@ -1,3 +1,4 @@
1
+ require 'httparty'
1
2
  require 'graphql/client'
2
3
  require 'graphql/client/http'
3
4
 
@@ -17,16 +18,22 @@ module Gitlab
17
18
  raise_on_error!(response)
18
19
 
19
20
  parsed_response = parse_response(response, resource_path)
21
+ headers = response.extensions.fetch('headers', {})
20
22
 
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?
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?
24
31
 
25
- {
32
+ graphql_response.merge(
26
33
  more_pages: parsed_response.page_info.has_next_page,
27
34
  end_cursor: parsed_response.page_info.end_cursor,
28
35
  results: parsed_response.nodes.map(&:to_h)
29
- }
36
+ )
30
37
  end
31
38
 
32
39
  delegate :parse, to: :client
@@ -47,11 +54,27 @@ module Gitlab
47
54
 
48
55
  def http_client
49
56
  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
- }
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
55
78
  end
56
79
  end
57
80
  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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
- VERSION = '1.14.3'
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.14.3
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-11-06 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
@@ -162,6 +162,7 @@ files:
162
162
  - lib/gitlab/triage/action.rb
163
163
  - lib/gitlab/triage/action/base.rb
164
164
  - lib/gitlab/triage/action/comment.rb
165
+ - lib/gitlab/triage/action/comment_on_summary.rb
165
166
  - lib/gitlab/triage/action/summarize.rb
166
167
  - lib/gitlab/triage/api_query_builders/base_query_param_builder.rb
167
168
  - lib/gitlab/triage/api_query_builders/date_query_param_builder.rb