gitlab-triage 1.14.1 → 1.17.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab/merge_request_templates/Release.md +1 -1
  3. data/README.md +75 -4
  4. data/bin/gitlab-triage +5 -2
  5. data/lib/gitlab/triage/action.rb +8 -4
  6. data/lib/gitlab/triage/action/comment.rb +7 -4
  7. data/lib/gitlab/triage/action/comment_on_summary.rb +83 -0
  8. data/lib/gitlab/triage/action/summarize.rb +7 -1
  9. data/lib/gitlab/triage/api_query_builders/base_query_param_builder.rb +11 -2
  10. data/lib/gitlab/triage/api_query_builders/date_query_param_builder.rb +13 -50
  11. data/lib/gitlab/triage/api_query_builders/multi_query_param_builder.rb +10 -2
  12. data/lib/gitlab/triage/engine.rb +52 -11
  13. data/lib/gitlab/triage/errors/network.rb +2 -0
  14. data/lib/gitlab/triage/filters/discussions_conditions_filter.rb +1 -3
  15. data/lib/gitlab/triage/graphql_network.rb +28 -1
  16. data/lib/gitlab/triage/graphql_queries/query_builder.rb +72 -16
  17. data/lib/gitlab/triage/graphql_queries/query_param_builders/base_param_builder.rb +25 -0
  18. data/lib/gitlab/triage/graphql_queries/query_param_builders/date_param_builder.rb +35 -0
  19. data/lib/gitlab/triage/graphql_queries/query_param_builders/labels_param_builder.rb +18 -0
  20. data/lib/gitlab/triage/network.rb +9 -3
  21. data/lib/gitlab/triage/network_adapters/graphql_adapter.rb +33 -10
  22. data/lib/gitlab/triage/network_adapters/httparty_adapter.rb +10 -0
  23. data/lib/gitlab/triage/option_parser.rb +2 -0
  24. data/lib/gitlab/triage/param_builders/date_param_builder.rb +58 -0
  25. data/lib/gitlab/triage/policies/base_policy.rb +30 -1
  26. data/lib/gitlab/triage/resource/context.rb +1 -0
  27. data/lib/gitlab/triage/resource/epic.rb +24 -0
  28. data/lib/gitlab/triage/retryable.rb +7 -5
  29. data/lib/gitlab/triage/utils.rb +13 -0
  30. data/lib/gitlab/triage/validators/params_validator.rb +1 -1
  31. data/lib/gitlab/triage/version.rb +1 -1
  32. metadata +10 -5
  33. data/lib/gitlab/triage/graphql_queries/threads_query.rb +0 -31
  34. data/lib/gitlab/triage/graphql_queries/user_notes_query.rb +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbce3d75b063c517b80bbbfa5a0cf8ef2cf0f6ffe9eabcebe743f7ea5e6fbc23
4
- data.tar.gz: '09687c2d578d80f351a54a614113bcf36b70a15144f848409b8143be53c95f9b'
3
+ metadata.gz: 68a42526c02ea2ea87e7f0ca3d24ac0fb5acbdd145fb30fc2bbd79e533ce9b65
4
+ data.tar.gz: 274ba6fb0d4040abc095207eb3349c3719e57e6e37d6087af1bedaed78e8b6b5
5
5
  SHA512:
6
- metadata.gz: f88d9507fd36e15941fa9b1345887e138893e8e7e5ae1b872408cfea449c2565c48c4267c1df278fa0f1ddf3f1c857050e4868eef88268b4acf9c7078dda0a53
7
- data.tar.gz: 77dfd58b8e61d3115e9057b688d7f2b91b8473dd4e7a177a32cad0585f1f42c115d371f9d10f10a0a27fd3ee1b4230ee757982dcc85207395ebef82274165e70
6
+ metadata.gz: be15fd2ef8f9e4d12116f83906905bd43a45f0ba81978f03504aa751e97ae367f92110072abb2b2c4ae2419fd01c9599d89db3469f96c871341291dfdcddc43a
7
+ data.tar.gz: e2a9669fd8373304b66d8470fe24fae0c55349c229accf9a523462ead4e63ea4f8ef3f3bc3c1d9a6765073a5d7226f8787f2feeb8a1c6c4491d6dcedbaa3af1f
@@ -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
@@ -38,6 +38,7 @@ The format of the file is [YAML](https://en.wikipedia.org/wiki/YAML).
38
38
  project.
39
39
 
40
40
  Select which resource to add the policy to:
41
+ - `epics`
41
42
  - `issues`
42
43
  - `merge_requests`
43
44
 
@@ -47,9 +48,28 @@ For example:
47
48
 
48
49
  ```yml
49
50
  resource_rules:
51
+ epics:
52
+ rules:
53
+ - name: My epic policy
54
+ conditions:
55
+ date:
56
+ attribute: updated_at
57
+ condition: older_than
58
+ interval_type: days
59
+ interval: 5
60
+ state: opened
61
+ labels:
62
+ - None
63
+ actions:
64
+ labels:
65
+ - needs attention
66
+ mention:
67
+ - markglenfletcher
68
+ comment: |
69
+ {{author}} This epic is unlabelled after 5 days. It needs attention. Please take care of this before the end of #{2.days.from_now.strftime('%Y-%m-%d')}
50
70
  issues:
51
71
  rules:
52
- - name: My policy
72
+ - name: My issue policy
53
73
  conditions:
54
74
  date:
55
75
  attribute: updated_at
@@ -86,7 +106,7 @@ resource_rules:
86
106
  /label ~"needs attention"
87
107
  merge_requests:
88
108
  rules:
89
- - name: My policy
109
+ - name: My merge request policy
90
110
  conditions:
91
111
  state: opened
92
112
  labels:
@@ -585,6 +605,7 @@ Available action types:
585
605
  - [`comment` action](#comment-action)
586
606
  - [`comment_type` action option](#comment-type-action-option)
587
607
  - [`summarize` action](#summarize-action)
608
+ - [`comment_on_summary` action](#comment-on-summary-action)
588
609
 
589
610
  ##### Labels action
590
611
 
@@ -796,8 +817,8 @@ Accepts a hash of fields.
796
817
 
797
818
  | Field | Type | Description | Required | Placeholders | Ruby expression | Default |
798
819
  | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
799
- | `title` | string | The title of the generated issue | yes | yes | no | |
800
- | `destination` | integer or string | The project ID or path to create the generated issue in | no | no | no | source project |
820
+ | `title` | string | The title of the generated issue | yes | yes | yes | |
821
+ | `destination` | integer or string | The project ID or path to create the generated issue in | no | no | no | source project |
801
822
  | `item` | string | Template representing each triaged resource | no | yes | yes | |
802
823
  | `summary` | string | The description of the generated issue | no | Only `{{title}}`, `{{items}}`, `{{type}}` | yes | |
803
824
  | `redact_confidential_resources` | boolean | Whether redact fields for confidential resources | no | no | no | true |
@@ -867,6 +888,56 @@ Which could generate an issue like:
867
888
  /label ~"needs attention"
868
889
  ```
869
890
 
891
+ ##### Comment on summary action
892
+
893
+ Generates one comment for each resource, attaching these comments to the summary
894
+ created by the [`summarize` action](#summarize-action).
895
+
896
+ The use case for this is wanting to create a summary with an overview, and then
897
+ a threaded discussion for each resource, with a header comment starting each
898
+ discussion.
899
+
900
+ Accepts a single string value: the template used to generate the comments. For
901
+ details of the syntax of this template, see the [comment action](#comment-action).
902
+
903
+ Since this action depends on the summary, it is invalid to supply a
904
+ `comment_on_summary` action without an accompanying `summarize` sibling action.
905
+ The `summarize` action will always be completed first.
906
+
907
+ Just like for [comment action](#comment-action), setting `comment_type` in the
908
+ `actions` set controls whether the comment must be resolved for merge requests.
909
+ See: [`comment_type` action option](#comment-type-action-option).
910
+
911
+ Example:
912
+
913
+ ```yml
914
+ resource_rules:
915
+ issues:
916
+ rules:
917
+ - name: List of issues to discuss
918
+ limits:
919
+ most_recent: 15
920
+ actions:
921
+ comment_type: thread
922
+ comment_on_summary: |
923
+ # {{title}}
924
+
925
+ author: {{author}}
926
+ summarize:
927
+ title: |
928
+ #{resource[:type].capitalize} require labels
929
+ item: |
930
+ - [ ] [{{title}}]({{web_url}}) {{labels}}
931
+ summary: |
932
+ The following {{type}} require labels:
933
+
934
+ {{items}}
935
+
936
+ Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
937
+
938
+ /label ~"needs attention"
939
+ ```
940
+
870
941
  ### Summary policies
871
942
 
872
943
  Summary policies are special policies that join multiple rule policies together
data/bin/gitlab-triage CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'yaml'
4
4
  require_relative '../lib/gitlab/triage/option_parser'
5
5
  require_relative '../lib/gitlab/triage/engine'
6
+ require_relative '../lib/gitlab/triage/ui'
6
7
 
7
8
  options = Gitlab::Triage::OptionParser.parse(ARGV)
8
9
  options.policies_files << '.triage-policies.yml' if options.policies_files.empty?
@@ -10,7 +11,9 @@ options.policies_files << '.triage-policies.yml' if options.policies_files.empty
10
11
  options.policies_files.each do |policies_file|
11
12
  policies = HashWithIndifferentAccess.new(YAML.load_file(policies_file))
12
13
 
13
- Gitlab::Triage::Engine
14
+ policy_engine = Gitlab::Triage::Engine
14
15
  .new(policies: policies, options: options)
15
- .perform
16
+
17
+ puts Gitlab::Triage::UI.header("Executing policies from #{policies_file}.", char: '*')
18
+ policy_engine.perform
16
19
  end
@@ -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
@@ -57,14 +57,17 @@ module Gitlab
57
57
  end
58
58
 
59
59
  def build_post_url(resource)
60
- # POST /projects/:id/issues/:issue_iid/notes
61
- post_url = UrlBuilders::UrlBuilder.new(
60
+ url_builder_opts = {
62
61
  network_options: network.options,
63
- source_id: resource[:project_id],
62
+ source: policy.source,
63
+ source_id: resource[policy.source_id_sym],
64
64
  resource_type: policy.type,
65
65
  resource_id: resource['iid'],
66
66
  sub_resource_type: sub_resource_type
67
- ).build
67
+ }
68
+
69
+ # POST /(groups|projects)/:id/(epics|issues|merge_requests)/:iid/notes
70
+ post_url = UrlBuilders::UrlBuilder.new(url_builder_opts).build
68
71
 
69
72
  puts Gitlab::Triage::UI.debug "post_url: #{post_url}" if network.options.debug
70
73
 
@@ -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
@@ -1,57 +1,32 @@
1
- require_relative '../validators/params_validator'
1
+ require_relative '../param_builders/date_param_builder'
2
+ require_relative 'base_query_param_builder'
2
3
 
3
4
  module Gitlab
4
5
  module Triage
5
6
  module APIQueryBuilders
6
- class DateQueryParamBuilder
7
+ class DateQueryParamBuilder < BaseQueryParamBuilder
7
8
  ATTRIBUTES = %w[updated_at created_at].freeze
8
- CONDITIONS = %w[older_than newer_than].freeze
9
- INTERVAL_TYPES = %w[days weeks months years].freeze
10
-
11
- def self.filter_parameters
12
- [
13
- {
14
- name: :attribute,
15
- type: String,
16
- values: ATTRIBUTES
17
- },
18
- {
19
- name: :condition,
20
- type: String,
21
- values: CONDITIONS
22
- },
23
- {
24
- name: :interval_type,
25
- type: String,
26
- values: INTERVAL_TYPES
27
- },
28
- {
29
- name: :interval,
30
- type: Numeric
31
- }
32
- ]
33
- end
34
9
 
35
10
  def self.applicable?(condition)
36
11
  ATTRIBUTES.include?(condition[:attribute].to_s)
37
12
  end
38
13
 
39
14
  def initialize(condition_hash)
40
- @attribute = condition_hash[:attribute].to_s
41
- @interval_condition = condition_hash[:condition].to_sym
42
- @interval_type = condition_hash[:interval_type]
43
- @interval = condition_hash[:interval]
44
- validate_condition(condition_hash)
15
+ date_param_builder = ParamBuilders::DateParamBuilder.new(ATTRIBUTES, condition_hash)
16
+
17
+ super(build_param_name(condition_hash), date_param_builder.param_content)
45
18
  end
46
19
 
47
- def validate_condition(condition)
48
- ParamsValidator.new(self.class.filter_parameters, condition).validate!
20
+ def param_content
21
+ param_contents
49
22
  end
50
23
 
51
- def param_name
52
- prefix = attribute.sub(/_at\z/, '')
24
+ private
25
+
26
+ def build_param_name(condition_hash)
27
+ prefix = condition_hash[:attribute].to_s.sub(/_at\z/, '')
53
28
  suffix =
54
- case interval_condition
29
+ case condition_hash[:condition].to_sym
55
30
  when :older_than
56
31
  'before'
57
32
  when :newer_than
@@ -60,18 +35,6 @@ module Gitlab
60
35
 
61
36
  "#{prefix}_#{suffix}"
62
37
  end
63
-
64
- def param_content
65
- interval.public_send(interval_type).ago.to_date # rubocop:disable GitlabSecurity/PublicSend
66
- end
67
-
68
- def build_param
69
- "&#{param_name}=#{param_content.strip}"
70
- end
71
-
72
- private
73
-
74
- attr_reader :condition_hash, :attribute, :interval_condition, :interval_type, :interval
75
38
  end
76
39
  end
77
40
  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