gitlab-triage 1.13.0 → 1.15.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitlab/CODEOWNERS +1 -1
- data/.gitlab/merge_request_templates/Release.md +1 -1
- data/README.md +72 -0
- data/gitlab-triage.gemspec +2 -0
- data/lib/gitlab/triage/action.rb +8 -4
- data/lib/gitlab/triage/action/comment_on_summary.rb +83 -0
- data/lib/gitlab/triage/action/summarize.rb +7 -1
- data/lib/gitlab/triage/api_query_builders/base_query_param_builder.rb +11 -2
- data/lib/gitlab/triage/api_query_builders/multi_query_param_builder.rb +10 -2
- data/lib/gitlab/triage/engine.rb +64 -2
- data/lib/gitlab/triage/errors/network.rb +1 -0
- data/lib/gitlab/triage/filters/discussions_conditions_filter.rb +58 -0
- data/lib/gitlab/triage/graphql_network.rb +71 -0
- data/lib/gitlab/triage/graphql_queries/query_builder.rb +47 -0
- data/lib/gitlab/triage/graphql_queries/threads_query.rb +23 -0
- data/lib/gitlab/triage/graphql_queries/user_notes_query.rb +23 -0
- data/lib/gitlab/triage/network.rb +3 -1
- data/lib/gitlab/triage/network_adapters/graphql_adapter.rb +92 -0
- data/lib/gitlab/triage/policies/base_policy.rb +12 -1
- data/lib/gitlab/triage/validators/params_validator.rb +1 -1
- data/lib/gitlab/triage/version.rb +1 -1
- metadata +38 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a2927188b4b24b36453f4bac48a82b9af7d2ddc33bb7b5219f533282da9e8370
|
4
|
+
data.tar.gz: 8d35707ba7d219950ab3ba8dda852e89687f16b360cf1f368ae02038ff3ff7cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 12def4c673ceb025d1a286c9c10a53c410d037364865482b85500bff9215d6490e6d0fcc2737f859a8bbeeba5a302c5e2ff6cbddfc7bab1599e909c243240d67
|
7
|
+
data.tar.gz: b98bbed0aabc5cf72595f5dbb3b60ce8d5bb25ad4af19aff1de46c34642ac15c7dc367db39ad1092833a97a923ac73957768bf14b5ed06ccb8bfd518e5fdbdc2
|
data/.gitlab/CODEOWNERS
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
# The official maintainers
|
2
|
-
* @rymai @godfat @markglenfletcher
|
2
|
+
* @rymai @godfat-gitlab @markglenfletcher
|
@@ -32,4 +32,4 @@ with the latest commit from https://gitlab.com/gitlab-org/gitlab-triage/commits/
|
|
32
32
|
- Checklist after merging:
|
33
33
|
- [ ] [Update the release notes for the newly created tag](docs/release_process.md#how-to).
|
34
34
|
|
35
|
-
/label ~"Engineering Productivity" ~"ep::triage" ~"tooling::workflow"
|
35
|
+
/label ~"Engineering Productivity" ~"ep::triage" ~tooling ~"tooling::workflow"
|
data/README.md
CHANGED
@@ -142,6 +142,7 @@ Available condition types:
|
|
142
142
|
- [`source_branch` condition](#source-branch-condition)
|
143
143
|
- [`target_branch` condition](#target-branch-condition)
|
144
144
|
- [`weight` condition](#weight-condition)
|
145
|
+
- [`discussions` condition](#discussions-condition)
|
145
146
|
- [`ruby` condition](#ruby-condition)
|
146
147
|
|
147
148
|
##### Date condition
|
@@ -486,6 +487,26 @@ conditions:
|
|
486
487
|
weight: Any
|
487
488
|
```
|
488
489
|
|
490
|
+
##### Discussions condition
|
491
|
+
|
492
|
+
Accepts a hash of fields.
|
493
|
+
|
494
|
+
| Field | Type | Values | Required |
|
495
|
+
| --------- | ---- | ---- | -------- |
|
496
|
+
| `attribute` | string | `threads`, `notes` | yes |
|
497
|
+
| `condition` | string | `less_than`, `greater_than` | yes |
|
498
|
+
| `threshold` | integer | integer | yes |
|
499
|
+
|
500
|
+
Example:
|
501
|
+
|
502
|
+
```yml
|
503
|
+
conditions:
|
504
|
+
discussions:
|
505
|
+
attribute: threads
|
506
|
+
condition: greater_than
|
507
|
+
threshold: 15
|
508
|
+
```
|
509
|
+
|
489
510
|
##### Ruby condition
|
490
511
|
|
491
512
|
This condition allows users to write a Ruby expression to be evaluated for
|
@@ -564,6 +585,7 @@ Available action types:
|
|
564
585
|
- [`comment` action](#comment-action)
|
565
586
|
- [`comment_type` action option](#comment-type-action-option)
|
566
587
|
- [`summarize` action](#summarize-action)
|
588
|
+
- [`comment_on_summary` action](#comment-on-summary-action)
|
567
589
|
|
568
590
|
##### Labels action
|
569
591
|
|
@@ -846,6 +868,56 @@ Which could generate an issue like:
|
|
846
868
|
/label ~"needs attention"
|
847
869
|
```
|
848
870
|
|
871
|
+
##### Comment on summary action
|
872
|
+
|
873
|
+
Generates one comment for each resource, attaching these comments to the summary
|
874
|
+
created by the [`summarize` action](#summarize-action).
|
875
|
+
|
876
|
+
The use case for this is wanting to create a summary with an overview, and then
|
877
|
+
a threaded discussion for each resource, with a header comment starting each
|
878
|
+
discussion.
|
879
|
+
|
880
|
+
Accepts a single string value: the template used to generate the comments. For
|
881
|
+
details of the syntax of this template, see the [comment action](#comment-action).
|
882
|
+
|
883
|
+
Since this action depends on the summary, it is invalid to supply a
|
884
|
+
`comment_on_summary` action without an accompanying `summarize` sibling action.
|
885
|
+
The `summarize` action will always be completed first.
|
886
|
+
|
887
|
+
Just like for [comment action](#comment-action), setting `comment_type` in the
|
888
|
+
`actions` set controls whether the comment must be resolved for merge requests.
|
889
|
+
See: [`comment_type` action option](#comment-type-action-option).
|
890
|
+
|
891
|
+
Example:
|
892
|
+
|
893
|
+
```yml
|
894
|
+
resource_rules:
|
895
|
+
issues:
|
896
|
+
rules:
|
897
|
+
- name: List of issues to discuss
|
898
|
+
limits:
|
899
|
+
most_recent: 15
|
900
|
+
actions:
|
901
|
+
comment_type: thread
|
902
|
+
comment_on_summary: |
|
903
|
+
# {{title}}
|
904
|
+
|
905
|
+
author: {{author}}
|
906
|
+
summarize:
|
907
|
+
title: |
|
908
|
+
#{resource[:type].capitalize} require labels
|
909
|
+
item: |
|
910
|
+
- [ ] [{{title}}]({{web_url}}) {{labels}}
|
911
|
+
summary: |
|
912
|
+
The following {{type}} require labels:
|
913
|
+
|
914
|
+
{{items}}
|
915
|
+
|
916
|
+
Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
|
917
|
+
|
918
|
+
/label ~"needs attention"
|
919
|
+
```
|
920
|
+
|
849
921
|
### Summary policies
|
850
922
|
|
851
923
|
Summary policies are special policies that join multiple rule policies together
|
data/gitlab-triage.gemspec
CHANGED
@@ -20,6 +20,8 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.require_paths = ['lib']
|
21
21
|
|
22
22
|
spec.add_dependency 'activesupport', '~> 5.1'
|
23
|
+
spec.add_dependency 'globalid', '~> 0.4'
|
24
|
+
spec.add_dependency 'graphql-client', '~> 0.16'
|
23
25
|
spec.add_dependency 'httparty', '~> 0.17'
|
24
26
|
|
25
27
|
spec.add_development_dependency 'bundler'
|
data/lib/gitlab/triage/action.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
11
|
-
|
9
|
+
policy.validate!
|
10
|
+
|
11
|
+
[
|
12
|
+
[Summarize, policy.summarize?],
|
13
|
+
[Comment, policy.comment?],
|
14
|
+
[CommentOnSummary, policy.comment_on_summary?]
|
15
|
+
].each do |action, active|
|
12
16
|
act(action: action, policy: policy, **args) if active
|
13
17
|
end
|
14
18
|
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require_relative '../command_builders/text_content_builder'
|
5
|
+
require_relative '../command_builders/comment_command_builder'
|
6
|
+
require_relative '../command_builders/label_command_builder'
|
7
|
+
require_relative '../command_builders/remove_label_command_builder'
|
8
|
+
require_relative '../command_builders/cc_command_builder'
|
9
|
+
require_relative '../command_builders/status_command_builder'
|
10
|
+
require_relative '../command_builders/move_command_builder'
|
11
|
+
|
12
|
+
module Gitlab
|
13
|
+
module Triage
|
14
|
+
module Action
|
15
|
+
class CommentOnSummary < Base
|
16
|
+
class Dry < CommentOnSummary
|
17
|
+
def act
|
18
|
+
puts "The following comments would be posted for the rule **#{policy.name}**:\n\n"
|
19
|
+
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def perform(comment)
|
26
|
+
puts "# #{summary[:web_url]}\n```\n#{comment}\n```\n"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :summary
|
31
|
+
|
32
|
+
def initialize(policy:, network:)
|
33
|
+
super(policy: policy, network: network)
|
34
|
+
@summary = policy.summary
|
35
|
+
end
|
36
|
+
|
37
|
+
def act
|
38
|
+
policy.resources.each do |resource|
|
39
|
+
comment = build_comment(resource).strip
|
40
|
+
|
41
|
+
perform(comment) unless comment.empty?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def build_comment(resource)
|
48
|
+
CommandBuilders::TextContentBuilder.new(policy.actions[:comment_on_summary], resource: resource, network: network).build_command
|
49
|
+
end
|
50
|
+
|
51
|
+
def perform(comment)
|
52
|
+
network.post_api(build_post_url, body: comment)
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_post_url
|
56
|
+
# POST /projects/:id/issues/:issue_iid/notes
|
57
|
+
post_url = UrlBuilders::UrlBuilder.new(
|
58
|
+
network_options: network.options,
|
59
|
+
source_id: summary['project_id'],
|
60
|
+
resource_type: policy.type,
|
61
|
+
resource_id: summary['iid'],
|
62
|
+
sub_resource_type: sub_resource_type
|
63
|
+
).build
|
64
|
+
|
65
|
+
puts Gitlab::Triage::UI.debug "post_url: #{post_url}" if network.options.debug
|
66
|
+
|
67
|
+
post_url
|
68
|
+
end
|
69
|
+
|
70
|
+
def sub_resource_type
|
71
|
+
case type = policy.actions[:comment_type]
|
72
|
+
when 'comment', nil # nil is default
|
73
|
+
'notes'
|
74
|
+
when 'thread'
|
75
|
+
'discussions'
|
76
|
+
else
|
77
|
+
raise ArgumentError, "Unknown comment type: #{type}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -10,6 +10,12 @@ module Gitlab
|
|
10
10
|
private
|
11
11
|
|
12
12
|
def perform
|
13
|
+
policy.summary = {
|
14
|
+
web_url: '[the-created-issue-url]',
|
15
|
+
project_id: 'some-id',
|
16
|
+
iid: 'some-iid'
|
17
|
+
}.with_indifferent_access
|
18
|
+
|
13
19
|
if group_summary_without_destination?
|
14
20
|
puts Gitlab::Triage::UI.warn("No issue will be created: No summary destination specified when source is 'groups'.")
|
15
21
|
return
|
@@ -35,7 +41,7 @@ module Gitlab
|
|
35
41
|
return
|
36
42
|
end
|
37
43
|
|
38
|
-
network.post_api(post_issue_url, post_issue_body)
|
44
|
+
policy.summary = network.post_api(post_issue_url, post_issue_body)
|
39
45
|
end
|
40
46
|
|
41
47
|
def issue
|
@@ -2,16 +2,25 @@ module Gitlab
|
|
2
2
|
module Triage
|
3
3
|
module APIQueryBuilders
|
4
4
|
class BaseQueryParamBuilder
|
5
|
-
attr_reader :param_name, :param_contents
|
5
|
+
attr_reader :param_name, :param_contents, :allowed_values
|
6
6
|
|
7
|
-
def initialize(param_name, param_contents)
|
7
|
+
def initialize(param_name, param_contents, allowed_values: nil)
|
8
8
|
@param_name = param_name
|
9
9
|
@param_contents = param_contents
|
10
|
+
@allowed_values = allowed_values
|
11
|
+
|
12
|
+
validate_allowed_values! if allowed_values
|
10
13
|
end
|
11
14
|
|
12
15
|
def build_param
|
13
16
|
"&#{param_name}=#{param_content.strip}"
|
14
17
|
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def validate_allowed_values!
|
22
|
+
ParamsValidator.new([{ name: param_name, type: String, values: allowed_values }], { param_name => param_contents }).validate!
|
23
|
+
end
|
15
24
|
end
|
16
25
|
end
|
17
26
|
end
|
@@ -6,14 +6,22 @@ module Gitlab
|
|
6
6
|
class MultiQueryParamBuilder < BaseQueryParamBuilder
|
7
7
|
attr_reader :separator
|
8
8
|
|
9
|
-
def initialize(param_name, param_contents, separator)
|
9
|
+
def initialize(param_name, param_contents, separator, allowed_values: nil)
|
10
10
|
@separator = separator
|
11
|
-
super(param_name, param_contents)
|
11
|
+
super(param_name, Array(param_contents), allowed_values: allowed_values)
|
12
12
|
end
|
13
13
|
|
14
14
|
def param_content
|
15
15
|
param_contents.map(&:strip).join(separator)
|
16
16
|
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def validate_allowed_values!
|
21
|
+
param_contents.each do |param|
|
22
|
+
ParamsValidator.new([{ name: param_name, type: String, values: allowed_values }], { param_name => param }).validate!
|
23
|
+
end
|
24
|
+
end
|
17
25
|
end
|
18
26
|
end
|
19
27
|
end
|
data/lib/gitlab/triage/engine.rb
CHANGED
@@ -7,6 +7,7 @@ require_relative 'filters/votes_conditions_filter'
|
|
7
7
|
require_relative 'filters/no_additional_labels_conditions_filter'
|
8
8
|
require_relative 'filters/author_member_conditions_filter'
|
9
9
|
require_relative 'filters/assignee_member_conditions_filter'
|
10
|
+
require_relative 'filters/discussions_conditions_filter'
|
10
11
|
require_relative 'filters/ruby_conditions_filter'
|
11
12
|
require_relative 'limiters/date_field_limiter'
|
12
13
|
require_relative 'action'
|
@@ -19,7 +20,10 @@ require_relative 'api_query_builders/single_query_param_builder'
|
|
19
20
|
require_relative 'api_query_builders/multi_query_param_builder'
|
20
21
|
require_relative 'url_builders/url_builder'
|
21
22
|
require_relative 'network'
|
23
|
+
require_relative 'graphql_network'
|
22
24
|
require_relative 'network_adapters/httparty_adapter'
|
25
|
+
require_relative 'network_adapters/graphql_adapter'
|
26
|
+
require_relative 'graphql_queries/query_builder'
|
23
27
|
require_relative 'ui'
|
24
28
|
|
25
29
|
module Gitlab
|
@@ -27,7 +31,14 @@ module Gitlab
|
|
27
31
|
class Engine
|
28
32
|
attr_reader :per_page, :policies, :options
|
29
33
|
|
30
|
-
|
34
|
+
DEFAULT_NETWORK_ADAPTER = Gitlab::Triage::NetworkAdapters::HttpartyAdapter
|
35
|
+
DEFAULT_GRAPHQL_ADAPTER = Gitlab::Triage::NetworkAdapters::GraphqlAdapter
|
36
|
+
ALLOWED_STATE_VALUES = {
|
37
|
+
issues: %w[opened closed],
|
38
|
+
merge_requests: %w[opened closed merged]
|
39
|
+
}.with_indifferent_access.freeze
|
40
|
+
|
41
|
+
def initialize(policies:, options:, network_adapter_class: DEFAULT_NETWORK_ADAPTER, graphql_network_adapter_class: DEFAULT_GRAPHQL_ADAPTER)
|
31
42
|
options.host_url = policies.delete(:host_url) { options.host_url }
|
32
43
|
options.api_version = policies.delete(:api_version) { 'v4' }
|
33
44
|
options.dry_run = ENV['TEST'] == 'true' if options.dry_run.nil?
|
@@ -36,6 +47,7 @@ module Gitlab
|
|
36
47
|
@policies = policies
|
37
48
|
@options = options
|
38
49
|
@network_adapter_class = network_adapter_class
|
50
|
+
@graphql_network_adapter_class = graphql_network_adapter_class
|
39
51
|
|
40
52
|
assert_all!
|
41
53
|
assert_project_id!
|
@@ -62,6 +74,10 @@ module Gitlab
|
|
62
74
|
@network ||= Network.new(network_adapter)
|
63
75
|
end
|
64
76
|
|
77
|
+
def graphql_network
|
78
|
+
@graphql_network ||= GraphqlNetwork.new(graphql_network_adapter)
|
79
|
+
end
|
80
|
+
|
65
81
|
private
|
66
82
|
|
67
83
|
def assert_project_id!
|
@@ -94,6 +110,10 @@ module Gitlab
|
|
94
110
|
@network_adapter ||= @network_adapter_class.new(options)
|
95
111
|
end
|
96
112
|
|
113
|
+
def graphql_network_adapter
|
114
|
+
@graphql_network_adapter ||= @graphql_network_adapter_class.new(options)
|
115
|
+
end
|
116
|
+
|
97
117
|
def rule_conditions(rule)
|
98
118
|
rule.fetch(:conditions) { {} }
|
99
119
|
end
|
@@ -158,8 +178,13 @@ module Gitlab
|
|
158
178
|
# retrieving the resources for every rule is inefficient
|
159
179
|
# however, previous rules may affect those upcoming
|
160
180
|
resources = network.query_api(build_get_url(resource_type, conditions))
|
181
|
+
iids = resources.pluck('iid').map(&:to_s)
|
182
|
+
|
183
|
+
graphql_query = build_graphql_query(resource_type, conditions)
|
184
|
+
graphql_resources = graphql_network.query(graphql_query, source: source_full_path, iids: iids) if graphql_query.present?
|
161
185
|
# In some filters/actions we want to know which resource type it is
|
162
186
|
attach_resource_type(resources, resource_type)
|
187
|
+
decorate_resources_with_graphql_data(resources, graphql_resources)
|
163
188
|
|
164
189
|
puts "\n\n* Found #{resources.count} resources..."
|
165
190
|
print "* Filtering resources..."
|
@@ -180,6 +205,13 @@ module Gitlab
|
|
180
205
|
resources.each { |resource| resource[:type] ||= resource_type }
|
181
206
|
end
|
182
207
|
|
208
|
+
def decorate_resources_with_graphql_data(resources, graphql_resources)
|
209
|
+
return if graphql_resources.nil?
|
210
|
+
|
211
|
+
graphql_resources_by_id = graphql_resources.to_h { |resource| [resource[:id], resource] }
|
212
|
+
resources.each { |resource| resource.merge!(graphql_resources_by_id[resource[:id]].to_h) }
|
213
|
+
end
|
214
|
+
|
183
215
|
def process_action(policy)
|
184
216
|
Action.process(
|
185
217
|
policy: policy,
|
@@ -213,6 +245,10 @@ module Gitlab
|
|
213
245
|
results << Filters::AssigneeMemberConditionsFilter.new(resource, conditions[:assignee_member], network).calculate
|
214
246
|
end
|
215
247
|
|
248
|
+
if conditions[:discussions]
|
249
|
+
results << Filters::DiscussionsConditionsFilter.new(resource, conditions[:discussions]).calculate
|
250
|
+
end
|
251
|
+
|
216
252
|
if conditions[:ruby]
|
217
253
|
results << Filters::RubyConditionsFilter.new(resource, conditions, network).calculate
|
218
254
|
end
|
@@ -244,7 +280,13 @@ module Gitlab
|
|
244
280
|
condition_builders << APIQueryBuilders::MultiQueryParamBuilder.new('not[labels]', conditions[:forbidden_labels], ',')
|
245
281
|
end
|
246
282
|
|
247
|
-
|
283
|
+
if conditions[:state]
|
284
|
+
condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new(
|
285
|
+
'state',
|
286
|
+
conditions[:state],
|
287
|
+
allowed_values: ALLOWED_STATE_VALUES[resource_type])
|
288
|
+
end
|
289
|
+
|
248
290
|
condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('milestone', Array(conditions[:milestone])[0]) if conditions[:milestone]
|
249
291
|
condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('source_branch', conditions[:source_branch]) if conditions[:source_branch]
|
250
292
|
condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('target_branch', conditions[:target_branch]) if conditions[:target_branch]
|
@@ -270,6 +312,26 @@ module Gitlab
|
|
270
312
|
params: params
|
271
313
|
).build
|
272
314
|
end
|
315
|
+
|
316
|
+
def build_graphql_query(resource_type, conditions)
|
317
|
+
Gitlab::Triage::GraphqlQueries::QueryBuilder
|
318
|
+
.new(options.source, resource_type, conditions)
|
319
|
+
end
|
320
|
+
|
321
|
+
def source_full_path
|
322
|
+
@source_full_path ||= fetch_source_full_path
|
323
|
+
end
|
324
|
+
|
325
|
+
def fetch_source_full_path
|
326
|
+
return options.source_id unless /\A\d+\z/.match?(options.source_id)
|
327
|
+
|
328
|
+
source_details = network.query_api(build_get_url(nil, {})).first
|
329
|
+
full_path = source_details['full_path'] || source_details['path_with_namespace']
|
330
|
+
|
331
|
+
raise ArgumentError, 'A source with given source_id was not found!' if full_path.blank?
|
332
|
+
|
333
|
+
full_path
|
334
|
+
end
|
273
335
|
end
|
274
336
|
end
|
275
337
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require_relative 'base_conditions_filter'
|
2
|
+
|
3
|
+
module Gitlab
|
4
|
+
module Triage
|
5
|
+
module Filters
|
6
|
+
class DiscussionsConditionsFilter < BaseConditionsFilter
|
7
|
+
ATTRIBUTES = %w[notes threads].freeze
|
8
|
+
CONDITIONS = %w[greater_than less_than].freeze
|
9
|
+
|
10
|
+
def self.filter_parameters
|
11
|
+
[
|
12
|
+
{
|
13
|
+
name: :attribute,
|
14
|
+
type: String,
|
15
|
+
values: ATTRIBUTES
|
16
|
+
},
|
17
|
+
{
|
18
|
+
name: :condition,
|
19
|
+
type: String,
|
20
|
+
values: CONDITIONS
|
21
|
+
},
|
22
|
+
{
|
23
|
+
name: :threshold,
|
24
|
+
type: Numeric
|
25
|
+
}
|
26
|
+
]
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize_variables(condition)
|
30
|
+
@attribute = condition[:attribute].to_sym
|
31
|
+
@condition = condition[:condition].to_sym
|
32
|
+
@threshold = condition[:threshold]
|
33
|
+
end
|
34
|
+
|
35
|
+
def resource_value
|
36
|
+
if @attribute == :notes
|
37
|
+
@resource[:user_notes_count]
|
38
|
+
else
|
39
|
+
@resource[:user_discussions_count]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def condition_value
|
44
|
+
@threshold
|
45
|
+
end
|
46
|
+
|
47
|
+
def calculate
|
48
|
+
case @condition
|
49
|
+
when :greater_than
|
50
|
+
resource_value.to_i > condition_value
|
51
|
+
when :less_than
|
52
|
+
resource_value.to_i < condition_value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require 'net/protocol'
|
3
|
+
require 'globalid'
|
4
|
+
|
5
|
+
require_relative 'retryable'
|
6
|
+
require_relative 'ui'
|
7
|
+
require_relative 'errors'
|
8
|
+
|
9
|
+
module Gitlab
|
10
|
+
module Triage
|
11
|
+
class GraphqlNetwork
|
12
|
+
attr_reader :options, :adapter
|
13
|
+
|
14
|
+
MINIMUM_RATE_LIMIT = 25
|
15
|
+
|
16
|
+
def initialize(adapter)
|
17
|
+
@adapter = adapter
|
18
|
+
@options = adapter.options
|
19
|
+
end
|
20
|
+
|
21
|
+
def query(graphql_query, variables = {})
|
22
|
+
return if graphql_query.blank?
|
23
|
+
|
24
|
+
response = {}
|
25
|
+
resources = []
|
26
|
+
|
27
|
+
parsed_graphql_query = adapter.parse(graphql_query.query)
|
28
|
+
|
29
|
+
begin
|
30
|
+
print '.'
|
31
|
+
|
32
|
+
response = adapter.query(
|
33
|
+
parsed_graphql_query,
|
34
|
+
resource_path: graphql_query.resource_path,
|
35
|
+
variables: variables.merge(after: response.delete(:end_cursor))
|
36
|
+
)
|
37
|
+
|
38
|
+
rate_limit_debug(response) if options.debug
|
39
|
+
rate_limit_wait(response)
|
40
|
+
|
41
|
+
resources.concat(Array.wrap(response.delete(:results)))
|
42
|
+
end while response.delete(:more_pages)
|
43
|
+
|
44
|
+
resources
|
45
|
+
.map { |resource| resource.deep_transform_keys(&:underscore) }
|
46
|
+
.map(&:with_indifferent_access)
|
47
|
+
.map { |resource| resource.merge(id: extract_id_from_global_id(resource[:id])) }
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def extract_id_from_global_id(global_id)
|
53
|
+
return if global_id.blank?
|
54
|
+
|
55
|
+
GlobalID.parse(global_id).model_id.to_i
|
56
|
+
end
|
57
|
+
|
58
|
+
def rate_limit_debug(response)
|
59
|
+
rate_limit_infos = "Rate limit remaining: #{response[:ratelimit_remaining]} (reset at #{response[:ratelimit_reset_at]})"
|
60
|
+
puts Gitlab::Triage::UI.debug "rate_limit_infos: #{rate_limit_infos}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def rate_limit_wait(response)
|
64
|
+
return unless response.delete(:ratelimit_remaining) < MINIMUM_RATE_LIMIT
|
65
|
+
|
66
|
+
puts Gitlab::Triage::UI.debug "Rate limit almost exceeded, sleeping for #{response[:ratelimit_reset_at] - Time.now} seconds" if options.debug
|
67
|
+
sleep(1) until Time.now >= response[:ratelimit_reset_at]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative 'user_notes_query'
|
2
|
+
require_relative 'threads_query'
|
3
|
+
|
4
|
+
module Gitlab
|
5
|
+
module Triage
|
6
|
+
module GraphqlQueries
|
7
|
+
class QueryBuilder
|
8
|
+
def initialize(source_type, resource_type, conditions)
|
9
|
+
@source_type = source_type.to_s.singularize
|
10
|
+
@resource_type = resource_type
|
11
|
+
@conditions = conditions
|
12
|
+
end
|
13
|
+
|
14
|
+
def resource_path
|
15
|
+
[source_type, resource_type]
|
16
|
+
end
|
17
|
+
|
18
|
+
def query
|
19
|
+
return if query_template.nil?
|
20
|
+
|
21
|
+
format(query_template, source_type: source_type, resource_type: resource_type.to_s.camelize(:lower), group_query: group_query)
|
22
|
+
end
|
23
|
+
|
24
|
+
delegate :present?, to: :query_template
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :source_type, :resource_type, :conditions
|
29
|
+
|
30
|
+
def query_template
|
31
|
+
case conditions.dig(:discussions, :attribute).to_s
|
32
|
+
when 'notes'
|
33
|
+
UserNotesQuery
|
34
|
+
when 'threads'
|
35
|
+
ThreadsQuery
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def group_query
|
40
|
+
return if source_type != 'group'
|
41
|
+
|
42
|
+
', includeSubgroups: true'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Gitlab
|
2
|
+
module Triage
|
3
|
+
module GraphqlQueries
|
4
|
+
ThreadsQuery = <<-GRAPHQL.freeze # rubocop:disable Naming/ConstantName
|
5
|
+
query($source: ID!, $after: String, $iids: [String!]) {
|
6
|
+
%{source_type}(fullPath: $source) {
|
7
|
+
id
|
8
|
+
%{resource_type}(after: $after, iids: $iids%{group_query}) {
|
9
|
+
pageInfo {
|
10
|
+
hasNextPage
|
11
|
+
endCursor
|
12
|
+
}
|
13
|
+
nodes {
|
14
|
+
id
|
15
|
+
userDiscussionsCount
|
16
|
+
}
|
17
|
+
}
|
18
|
+
}
|
19
|
+
}
|
20
|
+
GRAPHQL
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Gitlab
|
2
|
+
module Triage
|
3
|
+
module GraphqlQueries
|
4
|
+
UserNotesQuery = <<-GRAPHQL.freeze # rubocop:disable Naming/ConstantName
|
5
|
+
query($source: ID!, $after: String, $iids: [String!]) {
|
6
|
+
%{source_type}(fullPath: $source) {
|
7
|
+
id
|
8
|
+
%{resource_type}(after: $after, iids: $iids%{group_query}) {
|
9
|
+
pageInfo {
|
10
|
+
hasNextPage
|
11
|
+
endCursor
|
12
|
+
}
|
13
|
+
nodes {
|
14
|
+
id
|
15
|
+
userNotesCount
|
16
|
+
}
|
17
|
+
}
|
18
|
+
}
|
19
|
+
}
|
20
|
+
GRAPHQL
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -44,8 +44,10 @@ module Gitlab
|
|
44
44
|
case results
|
45
45
|
when Array
|
46
46
|
resources.concat(results)
|
47
|
-
|
47
|
+
when Hash
|
48
48
|
resources << results
|
49
|
+
else
|
50
|
+
raise Errors::Network::UnexpectedResponse, "Unexpected response: #{results.inspect}"
|
49
51
|
end
|
50
52
|
|
51
53
|
rate_limit_debug(response) if options.debug
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'graphql/client'
|
3
|
+
require 'graphql/client/http'
|
4
|
+
|
5
|
+
require_relative 'base_adapter'
|
6
|
+
require_relative '../ui'
|
7
|
+
require_relative '../errors'
|
8
|
+
|
9
|
+
module Gitlab
|
10
|
+
module Triage
|
11
|
+
module NetworkAdapters
|
12
|
+
class GraphqlAdapter < BaseAdapter
|
13
|
+
Client = GraphQL::Client
|
14
|
+
|
15
|
+
def query(graphql_query, resource_path: [], variables: {})
|
16
|
+
response = client.query(graphql_query, variables: variables, context: { token: options.token })
|
17
|
+
|
18
|
+
raise_on_error!(response)
|
19
|
+
|
20
|
+
parsed_response = parse_response(response, resource_path)
|
21
|
+
headers = response.extensions.fetch('headers', {})
|
22
|
+
|
23
|
+
graphql_response = {
|
24
|
+
ratelimit_remaining: headers['ratelimit-remaining'].to_i,
|
25
|
+
ratelimit_reset_at: Time.at(headers['ratelimit-reset'].to_i)
|
26
|
+
}
|
27
|
+
|
28
|
+
return graphql_response.merge(results: {}) if parsed_response.nil?
|
29
|
+
return graphql_response.merge(results: parsed_response.map(&:to_h)) if parsed_response.is_a?(Client::List)
|
30
|
+
return graphql_response.merge(results: parsed_response.to_h) unless parsed_response.nodes?
|
31
|
+
|
32
|
+
graphql_response.merge(
|
33
|
+
more_pages: parsed_response.page_info.has_next_page,
|
34
|
+
end_cursor: parsed_response.page_info.end_cursor,
|
35
|
+
results: parsed_response.nodes.map(&:to_h)
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
delegate :parse, to: :client
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def parse_response(response, resource_path)
|
44
|
+
resource_path.reduce(response.data) { |data, resource| data&.send(resource) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def raise_on_error!(response)
|
48
|
+
return if response.errors.blank?
|
49
|
+
|
50
|
+
puts Gitlab::Triage::UI.debug response.inspect if options.debug
|
51
|
+
|
52
|
+
raise "There was an error: #{response.errors.messages.to_json}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def http_client
|
56
|
+
Client::HTTP.new("#{options.host_url}/api/graphql") do
|
57
|
+
def execute(document:, operation_name: nil, variables: {}, context: {}) # rubocop:disable Lint/NestedMethodDefinition
|
58
|
+
body = {}
|
59
|
+
body['query'] = document.to_query_string
|
60
|
+
body['variables'] = variables if variables.any?
|
61
|
+
body['operationName'] = operation_name if operation_name
|
62
|
+
|
63
|
+
response = HTTParty.post(
|
64
|
+
uri,
|
65
|
+
body: body.to_json,
|
66
|
+
headers: {
|
67
|
+
'Content-type' => 'application/json',
|
68
|
+
'PRIVATE-TOKEN' => context[:token]
|
69
|
+
}
|
70
|
+
)
|
71
|
+
|
72
|
+
case response.code
|
73
|
+
when 200, 400
|
74
|
+
JSON.parse(response.body).merge('extensions' => { 'headers' => response.headers })
|
75
|
+
else
|
76
|
+
{ 'errors' => [{ 'message' => "#{response.code} #{response.message}" }] }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def schema
|
83
|
+
@schema ||= Client.load_schema(http_client)
|
84
|
+
end
|
85
|
+
|
86
|
+
def client
|
87
|
+
@client ||= Client.new(schema: schema, execute: http_client).tap { |client| client.allow_dynamic_queries = true }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -4,7 +4,10 @@ module Gitlab
|
|
4
4
|
module Triage
|
5
5
|
module Policies
|
6
6
|
class BasePolicy
|
7
|
+
InvalidPolicyError = Class.new(StandardError)
|
8
|
+
|
7
9
|
attr_reader :type, :policy_spec, :resources, :network
|
10
|
+
attr_accessor :summary
|
8
11
|
|
9
12
|
def initialize(type, policy_spec, resources, network)
|
10
13
|
@type = type
|
@@ -13,6 +16,10 @@ module Gitlab
|
|
13
16
|
@network = network
|
14
17
|
end
|
15
18
|
|
19
|
+
def validate!
|
20
|
+
raise InvalidPolicyError, 'Policies that comment_on_summary must include summarize action' if comment_on_summary? && !summarize?
|
21
|
+
end
|
22
|
+
|
16
23
|
def name
|
17
24
|
@name ||= (policy_spec[:name] || "#{type}-#{object_id}")
|
18
25
|
end
|
@@ -25,9 +32,13 @@ module Gitlab
|
|
25
32
|
actions.key?(:summarize)
|
26
33
|
end
|
27
34
|
|
35
|
+
def comment_on_summary?
|
36
|
+
actions.key?(:comment_on_summary)
|
37
|
+
end
|
38
|
+
|
28
39
|
def comment?
|
29
40
|
# The actual keys are strings
|
30
|
-
(actions.keys.map(&:to_sym) - [:summarize]).any?
|
41
|
+
(actions.keys.map(&:to_sym) - [:summarize, :comment_on_summary]).any?
|
31
42
|
end
|
32
43
|
|
33
44
|
def build_issue
|
@@ -34,7 +34,7 @@ module Gitlab
|
|
34
34
|
def validate_parameter_content(value)
|
35
35
|
@parameter_definitions.each do |param|
|
36
36
|
if param[:values]
|
37
|
-
raise InvalidParameter, "#{param[:name]} must be
|
37
|
+
raise InvalidParameter, "#{param[:name]} must be one of #{param[:values].join(',')}" unless param[:values].include?(value[param[:name]])
|
38
38
|
end
|
39
39
|
end
|
40
40
|
end
|
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.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:
|
11
|
+
date: 2021-01-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -24,6 +24,34 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '5.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: globalid
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.4'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.4'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: graphql-client
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.16'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.16'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: httparty
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -134,6 +162,7 @@ files:
|
|
134
162
|
- lib/gitlab/triage/action.rb
|
135
163
|
- lib/gitlab/triage/action/base.rb
|
136
164
|
- lib/gitlab/triage/action/comment.rb
|
165
|
+
- lib/gitlab/triage/action/comment_on_summary.rb
|
137
166
|
- lib/gitlab/triage/action/summarize.rb
|
138
167
|
- lib/gitlab/triage/api_query_builders/base_query_param_builder.rb
|
139
168
|
- lib/gitlab/triage/api_query_builders/date_query_param_builder.rb
|
@@ -158,16 +187,22 @@ files:
|
|
158
187
|
- lib/gitlab/triage/filters/assignee_member_conditions_filter.rb
|
159
188
|
- lib/gitlab/triage/filters/author_member_conditions_filter.rb
|
160
189
|
- lib/gitlab/triage/filters/base_conditions_filter.rb
|
190
|
+
- lib/gitlab/triage/filters/discussions_conditions_filter.rb
|
161
191
|
- lib/gitlab/triage/filters/member_conditions_filter.rb
|
162
192
|
- lib/gitlab/triage/filters/merge_request_date_conditions_filter.rb
|
163
193
|
- lib/gitlab/triage/filters/name_conditions_filter.rb
|
164
194
|
- lib/gitlab/triage/filters/no_additional_labels_conditions_filter.rb
|
165
195
|
- lib/gitlab/triage/filters/ruby_conditions_filter.rb
|
166
196
|
- lib/gitlab/triage/filters/votes_conditions_filter.rb
|
197
|
+
- lib/gitlab/triage/graphql_network.rb
|
198
|
+
- lib/gitlab/triage/graphql_queries/query_builder.rb
|
199
|
+
- lib/gitlab/triage/graphql_queries/threads_query.rb
|
200
|
+
- lib/gitlab/triage/graphql_queries/user_notes_query.rb
|
167
201
|
- lib/gitlab/triage/limiters/base_limiter.rb
|
168
202
|
- lib/gitlab/triage/limiters/date_field_limiter.rb
|
169
203
|
- lib/gitlab/triage/network.rb
|
170
204
|
- lib/gitlab/triage/network_adapters/base_adapter.rb
|
205
|
+
- lib/gitlab/triage/network_adapters/graphql_adapter.rb
|
171
206
|
- lib/gitlab/triage/network_adapters/httparty_adapter.rb
|
172
207
|
- lib/gitlab/triage/network_adapters/test_adapter.rb
|
173
208
|
- lib/gitlab/triage/option_parser.rb
|
@@ -213,7 +248,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
213
248
|
- !ruby/object:Gem::Version
|
214
249
|
version: '0'
|
215
250
|
requirements: []
|
216
|
-
rubygems_version: 3.1.
|
251
|
+
rubygems_version: 3.1.4
|
217
252
|
signing_key:
|
218
253
|
specification_version: 4
|
219
254
|
summary: GitLab triage automation project.
|