gitlab-triage 1.16.0 → 1.20.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: 843acca8deb39647c9cea0af3b5fbeb5bfd620c7cc00f007c1ad3c891db198f4
4
- data.tar.gz: b0445de658103d20efa09b883f98360f1569ce7c0a9e03a8928431bb05b4e61f
3
+ metadata.gz: 84173833ef78959cc6fdb59fa9dbe7e69ad7061265d22a1941c57a36b2f8e42b
4
+ data.tar.gz: 1293b7b39aca995152acfbed956af54c35d4897eee90f9db586bb318c20bd18c
5
5
  SHA512:
6
- metadata.gz: fa98bec626946cfb312e48b6b0a5d7a06b5b47eda6d8caca6a2775a4d55c5cb1d1ffe97334fbae673da58a1ad324c3440a68be7d8665590e2bb536f11afb05df
7
- data.tar.gz: '098cd6f58e0f2799911fed6650e0d729e9efd85721722340a5bd6215400dca2a3bef92f3d34c1e70ec306d49c1d1be15a27cb6d5d3d4ae055f406c3afb71cd35'
6
+ metadata.gz: 73b18f51c5386dc91946fe55531057d04908b74f42b16830458c8af11efdbac16a13791191310eddb5732764e2ffa49a89500d5fbbd8ca10a63dbae9f89acef8
7
+ data.tar.gz: ab7cefaa8a5574fb8f0e9b1c73875441975976275e28eb0f2e57194f6b26cf69323f60c3094b9d10cad88120df4abdbe112516c8c5e6a3c9f1e5e1efaad46a6f
data/.rubocop.yml CHANGED
@@ -3,7 +3,7 @@ inherit_gem:
3
3
  - rubocop-default.yml
4
4
 
5
5
  AllCops:
6
- TargetRubyVersion: 2.6
6
+ TargetRubyVersion: 2.7
7
7
  Exclude:
8
8
  - 'vendor/**/*'
9
9
  - 'tmp/**/*'
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:
@@ -797,8 +817,8 @@ Accepts a hash of fields.
797
817
 
798
818
  | Field | Type | Description | Required | Placeholders | Ruby expression | Default |
799
819
  | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
800
- | `title` | string | The title of the generated issue | yes | yes | no | |
801
- | `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 |
802
822
  | `item` | string | Template representing each triaged resource | no | yes | yes | |
803
823
  | `summary` | string | The description of the generated issue | no | Only `{{title}}`, `{{items}}`, `{{type}}` | yes | |
804
824
  | `redact_confidential_resources` | boolean | Whether redact fields for confidential resources | no | no | no | true |
@@ -901,7 +921,7 @@ resource_rules:
901
921
  comment_type: thread
902
922
  comment_on_summary: |
903
923
  # {{title}}
904
-
924
+
905
925
  author: {{author}}
906
926
  summarize:
907
927
  title: |
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
@@ -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
- resource_id: resource['iid'],
65
+ resource_id: resource_id(resource),
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
 
@@ -81,6 +84,15 @@ module Gitlab
81
84
  raise ArgumentError, "Unknown comment type: #{type}"
82
85
  end
83
86
  end
87
+
88
+ def resource_id(resource)
89
+ case policy.type
90
+ when 'epics'
91
+ resource['id']
92
+ else
93
+ resource['iid']
94
+ end
95
+ end
84
96
  end
85
97
  end
86
98
  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
@@ -14,7 +14,11 @@ module Gitlab
14
14
 
15
15
  def ensure_labels_exist!
16
16
  items.each do |label|
17
- label_opts = { project_id: resource[:project_id], name: label }
17
+ source_id_key = resource.key?(:group_id) ? :group_id : :project_id
18
+ label_opts = {
19
+ source_id_key => resource[source_id_key],
20
+ name: label
21
+ }
18
22
 
19
23
  unless Resource::Label.new(label_opts, network: network).exist?
20
24
  raise Resource::Label::LabelDoesntExistError,
@@ -37,6 +37,7 @@ module Gitlab
37
37
  issues: %w[opened closed],
38
38
  merge_requests: %w[opened closed merged]
39
39
  }.with_indifferent_access.freeze
40
+ EpicsTriagingForProjectImpossibleError = Class.new(StandardError)
40
41
 
41
42
  def initialize(policies:, options:, network_adapter_class: DEFAULT_NETWORK_ADAPTER, graphql_network_adapter_class: DEFAULT_GRAPHQL_ADAPTER)
42
43
  options.host_url = policies.delete(:host_url) { options.host_url }
@@ -62,6 +63,10 @@ module Gitlab
62
63
  puts
63
64
 
64
65
  resource_rules.each do |resource_type, resource|
66
+ if resource_type == 'epics' && options.source != :groups
67
+ raise(EpicsTriagingForProjectImpossibleError, "Epics can only be triaged at the group level. Please set the `--source groups` option.")
68
+ end
69
+
65
70
  puts Gitlab::Triage::UI.header("Processing rules for #{resource_type}", char: '-')
66
71
  puts
67
72
 
@@ -177,14 +182,23 @@ module Gitlab
177
182
  ExpandCondition.perform(rule_conditions(rule)) do |conditions|
178
183
  # retrieving the resources for every rule is inefficient
179
184
  # however, previous rules may affect those upcoming
180
- resources = network.query_api(build_get_url(resource_type, conditions))
181
- iids = resources.pluck('iid').map(&:to_s)
185
+ resources = []
186
+
187
+ if rule[:api] == 'graphql'
188
+ graphql_query = build_graphql_query(resource_type, conditions, true)
189
+ resources = graphql_network.query(graphql_query, source: source_full_path)
190
+ else
191
+ resources = network.query_api(build_get_url(resource_type, conditions))
192
+ iids = resources.pluck('iid').map(&:to_s)
193
+
194
+ graphql_query = build_graphql_query(resource_type, conditions)
195
+ graphql_resources = graphql_network.query(graphql_query, source: source_full_path, iids: iids) if graphql_query.any?
196
+
197
+ decorate_resources_with_graphql_data(resources, graphql_resources)
198
+ end
182
199
 
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?
185
200
  # In some filters/actions we want to know which resource type it is
186
201
  attach_resource_type(resources, resource_type)
187
- decorate_resources_with_graphql_data(resources, graphql_resources)
188
202
 
189
203
  puts "\n\n* Found #{resources.count} resources..."
190
204
  print "* Filtering resources..."
@@ -199,10 +213,17 @@ module Gitlab
199
213
  end
200
214
  end
201
215
 
202
- # We don't have to do this once the response will contain the type
203
- # of the resource. For now let's just attach it.
204
216
  def attach_resource_type(resources, resource_type)
205
- resources.each { |resource| resource[:type] ||= resource_type }
217
+ resources.each { |resource| resource[:type] = resource_type }
218
+ # TODO: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
219
+ # We should not overwrite the attribute here, but we need to
220
+ # fix it first. We should instead use something like
221
+ # gitlab_triage_resource_type so it won't conflict with the
222
+ # existing fields.
223
+ # And we need to retain the backward compatibility that using
224
+ # {{type}} will give us this value, rather than from the REST API,
225
+ # which will give us ISSUE from:
226
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59648
206
227
  end
207
228
 
208
229
  def decorate_resources_with_graphql_data(resources, graphql_resources)
@@ -313,9 +334,9 @@ module Gitlab
313
334
  ).build
314
335
  end
315
336
 
316
- def build_graphql_query(resource_type, conditions)
337
+ def build_graphql_query(resource_type, conditions, graphql_only = false)
317
338
  Gitlab::Triage::GraphqlQueries::QueryBuilder
318
- .new(options.source, resource_type, conditions)
339
+ .new(options.source, resource_type, conditions, graphql_only: graphql_only)
319
340
  end
320
341
 
321
342
  def source_full_path
@@ -44,11 +44,21 @@ module Gitlab
44
44
  resources
45
45
  .map { |resource| resource.deep_transform_keys(&:underscore) }
46
46
  .map(&:with_indifferent_access)
47
- .map { |resource| resource.merge(id: extract_id_from_global_id(resource[:id])) }
47
+ .map { |resource| normalize(resource) }
48
48
  end
49
49
 
50
50
  private
51
51
 
52
+ def normalize(resource)
53
+ resource
54
+ .slice(:iid, :title, :state, :author, :merged_at, :user_notes_count, :user_discussions_count, :upvotes, :downvotes, :project_id, :web_url)
55
+ .merge(
56
+ id: extract_id_from_global_id(resource[:id]),
57
+ labels: [*resource.dig(:labels, :nodes)].pluck(:title),
58
+ assignees: [*resource.dig(:assignees, :nodes)]
59
+ )
60
+ end
61
+
52
62
  def extract_id_from_global_id(global_id)
53
63
  return if global_id.blank?
54
64
 
@@ -1,14 +1,16 @@
1
- require_relative 'user_notes_query'
2
- require_relative 'threads_query'
1
+ require_relative 'query_param_builders/base_param_builder'
2
+ require_relative 'query_param_builders/date_param_builder'
3
+ require_relative 'query_param_builders/labels_param_builder'
3
4
 
4
5
  module Gitlab
5
6
  module Triage
6
7
  module GraphqlQueries
7
8
  class QueryBuilder
8
- def initialize(source_type, resource_type, conditions)
9
+ def initialize(source_type, resource_type, conditions, graphql_only: false)
9
10
  @source_type = source_type.to_s.singularize
10
11
  @resource_type = resource_type
11
12
  @conditions = conditions
13
+ @graphql_only = graphql_only
12
14
  end
13
15
 
14
16
  def resource_path
@@ -16,30 +18,100 @@ module Gitlab
16
18
  end
17
19
 
18
20
  def query
19
- return if query_template.nil?
21
+ return if resource_fields.empty?
20
22
 
21
- format(query_template, source_type: source_type, resource_type: resource_type.to_s.camelize(:lower), group_query: group_query)
23
+ format(
24
+ BASE_QUERY,
25
+ source_type: source_type,
26
+ resource_type: resource_type.to_s.camelize(:lower),
27
+ resource_fields: resource_fields.join(' '),
28
+ resource_query: resource_query,
29
+ iids_declaration: graphql_only ? nil : ', $iids: [String!]',
30
+ iids_query: graphql_only ? nil : ', iids: $iids'
31
+ )
22
32
  end
23
33
 
24
- delegate :present?, to: :query_template
34
+ delegate :any?, to: :resource_fields
25
35
 
26
36
  private
27
37
 
28
- attr_reader :source_type, :resource_type, :conditions
38
+ attr_reader :source_type, :resource_type, :conditions, :graphql_only
29
39
 
30
- def query_template
31
- case conditions.dig(:discussions, :attribute).to_s
32
- when 'notes'
33
- UserNotesQuery
34
- when 'threads'
35
- ThreadsQuery
40
+ BASE_QUERY = <<~GRAPHQL.freeze
41
+ query($source: ID!, $after: String%{iids_declaration}) {
42
+ %{source_type}(fullPath: $source) {
43
+ id
44
+ %{resource_type}(after: $after%{iids_query}%{resource_query}) {
45
+ pageInfo {
46
+ hasNextPage
47
+ endCursor
48
+ }
49
+ nodes {
50
+ id iid title updatedAt createdAt webUrl projectId %{resource_fields}
51
+ }
52
+ }
53
+ }
54
+ }
55
+ GRAPHQL
56
+
57
+ def resource_fields
58
+ fields = []
59
+
60
+ fields << 'userNotesCount' if conditions.dig(:discussions, :attribute).to_s == 'notes'
61
+ fields << 'userDiscussionsCount' if conditions.dig(:discussions, :attribute).to_s == 'threads'
62
+
63
+ if graphql_only
64
+ fields << 'labels { nodes { title } }'
65
+ fields << 'author { id name username }'
66
+ fields << 'assignees { nodes { id name username } }' if conditions.key?(:assignee_member)
67
+ fields << 'upvotes' if conditions.dig(:upvotes, :attribute).to_s == 'upvotes'
68
+ fields << 'downvotes' if conditions.dig(:upvotes, :attribute).to_s == 'downvotes'
69
+ fields << 'mergedAt' if resource_type == 'merge_requests'
70
+ end
71
+
72
+ fields
73
+ end
74
+
75
+ def resource_query
76
+ condition_queries = []
77
+
78
+ condition_queries << QueryParamBuilders::BaseParamBuilder.new('includeSubgroups', true, with_quotes: false) if source_type == 'group'
79
+
80
+ conditions.each do |condition, condition_params|
81
+ condition_queries << QueryParamBuilders::DateParamBuilder.new(condition_params) if condition.to_s == 'date'
82
+ condition_queries << QueryParamBuilders::BaseParamBuilder.new('milestoneTitle', condition_params) if condition.to_s == 'milestone'
83
+ condition_queries << QueryParamBuilders::BaseParamBuilder.new('state', condition_params, with_quotes: false) if condition.to_s == 'state'
84
+
85
+ condition_queries << merge_requests_resource_query(condition, condition_params) if resource_type == 'merge_requests'
86
+ condition_queries << issues_resource_query(condition, condition_params) if resource_type == 'issues'
36
87
  end
88
+
89
+ condition_queries
90
+ .compact
91
+ .map(&:build_param)
92
+ .join
37
93
  end
38
94
 
39
- def group_query
40
- return if source_type != 'group'
95
+ def merge_requests_resource_query(condition, condition_params)
96
+ case condition.to_s
97
+ when 'forbidden_labels'
98
+ QueryParamBuilders::LabelsParamBuilder.new('labels', condition_params, negated: true)
99
+ when 'labels'
100
+ QueryParamBuilders::LabelsParamBuilder.new('labels', condition_params)
101
+ when 'source_branch'
102
+ QueryParamBuilders::BaseParamBuilder.new('sourceBranch', condition_params)
103
+ when 'target_branch'
104
+ QueryParamBuilders::BaseParamBuilder.new('targetBranch', condition_params)
105
+ end
106
+ end
41
107
 
42
- ', includeSubgroups: true'
108
+ def issues_resource_query(condition, condition_params)
109
+ case condition.to_s
110
+ when 'forbidden_labels'
111
+ QueryParamBuilders::LabelsParamBuilder.new('labelName', condition_params, negated: true)
112
+ when 'labels'
113
+ QueryParamBuilders::LabelsParamBuilder.new('labelName', condition_params)
114
+ end
43
115
  end
44
116
  end
45
117
  end
@@ -0,0 +1,30 @@
1
+ require_relative '../../utils'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module GraphqlQueries
6
+ module QueryParamBuilders
7
+ class BaseParamBuilder
8
+ attr_reader :param_name, :param_contents, :with_quotes, :negated
9
+
10
+ def initialize(param_name, param_contents, with_quotes: true, negated: false)
11
+ @param_name = param_name
12
+ @param_contents = param_contents.to_s.strip
13
+ @with_quotes = with_quotes
14
+ @negated = negated
15
+ end
16
+
17
+ def build_param
18
+ contents = with_quotes ? Utils.graphql_quote(param_contents) : param_contents
19
+
20
+ if negated
21
+ ", not: { #{param_name}: #{contents} }"
22
+ else
23
+ ", #{param_name}: #{contents}"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ require_relative '../../param_builders/date_param_builder'
2
+ require_relative 'base_param_builder'
3
+
4
+ module Gitlab
5
+ module Triage
6
+ module GraphqlQueries
7
+ module QueryParamBuilders
8
+ class DateParamBuilder < BaseParamBuilder
9
+ ATTRIBUTES = %w[updated_at created_at merged_at].freeze
10
+
11
+ def initialize(condition_hash)
12
+ date_param_builder = ParamBuilders::DateParamBuilder.new(ATTRIBUTES, condition_hash)
13
+
14
+ super(build_param_name(condition_hash), date_param_builder.param_content)
15
+ end
16
+
17
+ private
18
+
19
+ def build_param_name(condition_hash)
20
+ prefix = condition_hash[:attribute].to_s.sub(/_at\z/, '')
21
+ suffix =
22
+ case condition_hash[:condition].to_sym
23
+ when :older_than
24
+ 'Before'
25
+ when :newer_than
26
+ 'After'
27
+ end
28
+
29
+ "#{prefix}#{suffix}"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,18 @@
1
+ require_relative '../../utils'
2
+ require_relative 'base_param_builder'
3
+
4
+ module Gitlab
5
+ module Triage
6
+ module GraphqlQueries
7
+ module QueryParamBuilders
8
+ class LabelsParamBuilder < BaseParamBuilder
9
+ def initialize(param_name, labels, negated: false)
10
+ label_param_content = labels.map { |label| Utils.graphql_quote(label) }.join(', ').then { |content| "[#{content}]" }
11
+
12
+ super(param_name, label_param_content, with_quotes: false, negated: negated)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -49,7 +49,7 @@ module Gitlab
49
49
  when Hash
50
50
  resources << results
51
51
  else
52
- raise Errors::Network::UnexpectedResponse, "Unexpected response: #{results.inspect}"
52
+ raise_unexpected_response(results)
53
53
  end
54
54
 
55
55
  rate_limit_debug(response) if options.debug
@@ -73,7 +73,14 @@ module Gitlab
73
73
  rate_limit_debug(response) if options.debug
74
74
  rate_limit_wait(response)
75
75
 
76
- response.delete(:results).with_indifferent_access
76
+ results = response.delete(:results)
77
+
78
+ case results
79
+ when Hash
80
+ results.with_indifferent_access
81
+ else
82
+ raise_unexpected_response(results)
83
+ end
77
84
  rescue Net::ReadTimeout
78
85
  {}
79
86
  end
@@ -95,6 +102,10 @@ module Gitlab
95
102
  puts Gitlab::Triage::UI.debug "Rate limit almost exceeded, sleeping for #{response[:ratelimit_reset_at] - Time.now} seconds" if options.debug
96
103
  sleep(1) until Time.now >= response[:ratelimit_reset_at]
97
104
  end
105
+
106
+ def raise_unexpected_response(results)
107
+ raise Errors::Network::UnexpectedResponse, "Unexpected response: #{results.inspect}"
108
+ end
98
109
  end
99
110
  end
100
111
  end
@@ -1,9 +1,11 @@
1
- require 'httparty'
1
+ require_relative '../version'
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
5
  module NetworkAdapters
6
6
  class BaseAdapter
7
+ USER_AGENT = "GitLab Triage #{Gitlab::Triage::VERSION}".freeze
8
+
7
9
  attr_reader :options
8
10
 
9
11
  def initialize(options)
@@ -1,4 +1,3 @@
1
- require 'httparty'
2
1
  require 'graphql/client'
3
2
  require 'graphql/client/http'
4
3
 
@@ -64,6 +63,7 @@ module Gitlab
64
63
  uri,
65
64
  body: body.to_json,
66
65
  headers: {
66
+ 'User-Agent' => USER_AGENT,
67
67
  'Content-type' => 'application/json',
68
68
  'PRIVATE-TOKEN' => context[:token]
69
69
  }
@@ -12,6 +12,7 @@ module Gitlab
12
12
  response = HTTParty.get(
13
13
  url,
14
14
  headers: {
15
+ 'User-Agent' => USER_AGENT,
15
16
  'Content-type' => 'application/json',
16
17
  'PRIVATE-TOKEN' => token
17
18
  }
@@ -35,6 +36,7 @@ module Gitlab
35
36
  url,
36
37
  body: body.to_json,
37
38
  headers: {
39
+ 'User-Agent' => "GitLab Triage #{Gitlab::Triage::VERSION}",
38
40
  'Content-type' => 'application/json',
39
41
  'PRIVATE-TOKEN' => token
40
42
  }
@@ -0,0 +1,58 @@
1
+ require_relative '../validators/params_validator'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module ParamBuilders
6
+ class DateParamBuilder
7
+ CONDITIONS = %w[older_than newer_than].freeze
8
+ INTERVAL_TYPES = %w[days weeks months years].freeze
9
+
10
+ def initialize(allowed_attributes, condition_hash)
11
+ @allowed_attributes = allowed_attributes
12
+ @attribute = condition_hash[:attribute].to_s
13
+ @interval_condition = condition_hash[:condition].to_sym
14
+ @interval_type = condition_hash[:interval_type]
15
+ @interval = condition_hash[:interval]
16
+
17
+ validate_condition(condition_hash)
18
+ end
19
+
20
+ def param_content
21
+ interval.public_send(interval_type).ago.to_date # rubocop:disable GitlabSecurity/PublicSend
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :allowed_attributes, :attribute, :interval_condition, :interval_type, :interval
27
+
28
+ def validate_condition(condition)
29
+ ParamsValidator.new(filter_parameters, condition).validate!
30
+ end
31
+
32
+ def filter_parameters
33
+ [
34
+ {
35
+ name: :attribute,
36
+ type: String,
37
+ values: allowed_attributes
38
+ },
39
+ {
40
+ name: :condition,
41
+ type: String,
42
+ values: CONDITIONS
43
+ },
44
+ {
45
+ name: :interval_type,
46
+ type: String,
47
+ values: INTERVAL_TYPES
48
+ },
49
+ {
50
+ name: :interval,
51
+ type: Numeric
52
+ }
53
+ ]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -24,6 +24,24 @@ module Gitlab
24
24
  @name ||= (policy_spec[:name] || "#{type}-#{object_id}")
25
25
  end
26
26
 
27
+ def source
28
+ case type
29
+ when 'epics'
30
+ 'groups'
31
+ else
32
+ 'projects'
33
+ end
34
+ end
35
+
36
+ def source_id_sym
37
+ case type
38
+ when 'epics'
39
+ :group_id
40
+ else
41
+ :project_id
42
+ end
43
+ end
44
+
27
45
  def actions
28
46
  @actions ||= policy_spec.fetch(:actions) { {} }
29
47
  end
@@ -32,6 +32,8 @@ module Gitlab
32
32
  @labels_with_details ||= label_events
33
33
  .select { |event| event.action == 'add' && event.label }
34
34
  .map(&:label)
35
+ .sort_by(&:added_at)
36
+ .reverse
35
37
  .uniq(&:name)
36
38
  .select { |label| resource[:labels].include?(label.name) }
37
39
  end
@@ -0,0 +1,13 @@
1
+ module Gitlab
2
+ module Triage
3
+ module Utils
4
+ module_function
5
+
6
+ def graphql_quote(string)
7
+ contents = string.to_s.gsub('"', '\\"')
8
+
9
+ %("#{contents}")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
- VERSION = '1.16.0'
5
+ VERSION = '1.20.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.16.0
4
+ version: 1.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-10 00:00:00.000000000 Z
11
+ date: 2021-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -196,8 +196,9 @@ files:
196
196
  - lib/gitlab/triage/filters/votes_conditions_filter.rb
197
197
  - lib/gitlab/triage/graphql_network.rb
198
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
199
+ - lib/gitlab/triage/graphql_queries/query_param_builders/base_param_builder.rb
200
+ - lib/gitlab/triage/graphql_queries/query_param_builders/date_param_builder.rb
201
+ - lib/gitlab/triage/graphql_queries/query_param_builders/labels_param_builder.rb
201
202
  - lib/gitlab/triage/limiters/base_limiter.rb
202
203
  - lib/gitlab/triage/limiters/date_field_limiter.rb
203
204
  - lib/gitlab/triage/network.rb
@@ -207,6 +208,7 @@ files:
207
208
  - lib/gitlab/triage/network_adapters/test_adapter.rb
208
209
  - lib/gitlab/triage/option_parser.rb
209
210
  - lib/gitlab/triage/options.rb
211
+ - lib/gitlab/triage/param_builders/date_param_builder.rb
210
212
  - lib/gitlab/triage/policies/base_policy.rb
211
213
  - lib/gitlab/triage/policies/rule_policy.rb
212
214
  - lib/gitlab/triage/policies/summary_policy.rb
@@ -225,6 +227,7 @@ files:
225
227
  - lib/gitlab/triage/retryable.rb
226
228
  - lib/gitlab/triage/ui.rb
227
229
  - lib/gitlab/triage/url_builders/url_builder.rb
230
+ - lib/gitlab/triage/utils.rb
228
231
  - lib/gitlab/triage/validators/limiter_validator.rb
229
232
  - lib/gitlab/triage/validators/params_validator.rb
230
233
  - lib/gitlab/triage/version.rb
@@ -249,7 +252,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
249
252
  - !ruby/object:Gem::Version
250
253
  version: '0'
251
254
  requirements: []
252
- rubygems_version: 3.1.4
255
+ rubygems_version: 3.1.6
253
256
  signing_key:
254
257
  specification_version: 4
255
258
  summary: GitLab triage automation project.
@@ -1,23 +0,0 @@
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
@@ -1,23 +0,0 @@
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