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 +4 -4
- data/.rubocop.yml +1 -1
- data/README.md +25 -5
- data/bin/gitlab-triage +5 -2
- data/lib/gitlab/triage/action/comment.rb +17 -5
- data/lib/gitlab/triage/api_query_builders/date_query_param_builder.rb +13 -50
- data/lib/gitlab/triage/command_builders/label_command_builder.rb +5 -1
- data/lib/gitlab/triage/engine.rb +31 -10
- data/lib/gitlab/triage/graphql_network.rb +11 -1
- data/lib/gitlab/triage/graphql_queries/query_builder.rb +88 -16
- data/lib/gitlab/triage/graphql_queries/query_param_builders/base_param_builder.rb +30 -0
- data/lib/gitlab/triage/graphql_queries/query_param_builders/date_param_builder.rb +35 -0
- data/lib/gitlab/triage/graphql_queries/query_param_builders/labels_param_builder.rb +18 -0
- data/lib/gitlab/triage/network.rb +13 -2
- data/lib/gitlab/triage/network_adapters/base_adapter.rb +3 -1
- data/lib/gitlab/triage/network_adapters/graphql_adapter.rb +1 -1
- data/lib/gitlab/triage/network_adapters/httparty_adapter.rb +2 -0
- data/lib/gitlab/triage/param_builders/date_param_builder.rb +58 -0
- data/lib/gitlab/triage/policies/base_policy.rb +18 -0
- data/lib/gitlab/triage/resource/shared/issuable.rb +2 -0
- data/lib/gitlab/triage/utils.rb +13 -0
- data/lib/gitlab/triage/version.rb +1 -1
- metadata +8 -5
- data/lib/gitlab/triage/graphql_queries/threads_query.rb +0 -23
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 84173833ef78959cc6fdb59fa9dbe7e69ad7061265d22a1941c57a36b2f8e42b
|
|
4
|
+
data.tar.gz: 1293b7b39aca995152acfbed956af54c35d4897eee90f9db586bb318c20bd18c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73b18f51c5386dc91946fe55531057d04908b74f42b16830458c8af11efdbac16a13791191310eddb5732764e2ffa49a89500d5fbbd8ca10a63dbae9f89acef8
|
|
7
|
+
data.tar.gz: ab7cefaa8a5574fb8f0e9b1c73875441975976275e28eb0f2e57194f6b26cf69323f60c3094b9d10cad88120df4abdbe112516c8c5e6a3c9f1e5e1efaad46a6f
|
data/.rubocop.yml
CHANGED
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 |
|
|
801
|
-
| `destination` | integer or string | The project ID or path to create the generated issue in | no
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
post_url = UrlBuilders::UrlBuilder.new(
|
|
60
|
+
url_builder_opts = {
|
|
62
61
|
network_options: network.options,
|
|
63
|
-
|
|
62
|
+
source: policy.source,
|
|
63
|
+
source_id: resource[policy.source_id_sym],
|
|
64
64
|
resource_type: policy.type,
|
|
65
|
-
resource_id: resource
|
|
65
|
+
resource_id: resource_id(resource),
|
|
66
66
|
sub_resource_type: sub_resource_type
|
|
67
|
-
|
|
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 '../
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
48
|
-
|
|
20
|
+
def param_content
|
|
21
|
+
param_contents
|
|
49
22
|
end
|
|
50
23
|
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
data/lib/gitlab/triage/engine.rb
CHANGED
|
@@ -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 =
|
|
181
|
-
|
|
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]
|
|
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|
|
|
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 '
|
|
2
|
-
require_relative '
|
|
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
|
|
21
|
+
return if resource_fields.empty?
|
|
20
22
|
|
|
21
|
-
format(
|
|
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 :
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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,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
|
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.
|
|
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-
|
|
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/
|
|
200
|
-
- lib/gitlab/triage/graphql_queries/
|
|
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.
|
|
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
|