payping-gitlab-triage 0.1.1
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 +7 -0
- data/.codeclimate.yml +19 -0
- data/.gitignore +15 -0
- data/.gitlab/CODEOWNERS +2 -0
- data/.gitlab/changelog_config.yml +13 -0
- data/.gitlab/issue_templates/Default.md +13 -0
- data/.gitlab/merge_request_templates/Default.md +11 -0
- data/.gitlab/merge_request_templates/Release.md +13 -0
- data/.gitlab-ci.yml +146 -0
- data/.rubocop.yml +21 -0
- data/.rubocop_todo.yml +145 -0
- data/.ruby-version +1 -0
- data/.tool-versions +1 -0
- data/.yardopts +4 -0
- data/CONTRIBUTING.md +31 -0
- data/Dangerfile +5 -0
- data/Gemfile +15 -0
- data/Guardfile +70 -0
- data/LICENSE.md +25 -0
- data/README.md +1480 -0
- data/Rakefile +6 -0
- data/bin/gitlab-triage +19 -0
- data/gitlab-triage.gemspec +41 -0
- data/lib/gitlab/triage/action/base.rb +14 -0
- data/lib/gitlab/triage/action/comment.rb +104 -0
- data/lib/gitlab/triage/action/comment_on_summary.rb +83 -0
- data/lib/gitlab/triage/action/delete.rb +56 -0
- data/lib/gitlab/triage/action/issue.rb +64 -0
- data/lib/gitlab/triage/action/summarize.rb +82 -0
- data/lib/gitlab/triage/action.rb +36 -0
- data/lib/gitlab/triage/api_query_builders/base_query_param_builder.rb +27 -0
- data/lib/gitlab/triage/api_query_builders/date_query_param_builder.rb +42 -0
- data/lib/gitlab/triage/api_query_builders/multi_query_param_builder.rb +28 -0
- data/lib/gitlab/triage/api_query_builders/single_query_param_builder.rb +13 -0
- data/lib/gitlab/triage/command_builders/base_command_builder.rb +40 -0
- data/lib/gitlab/triage/command_builders/cc_command_builder.rb +19 -0
- data/lib/gitlab/triage/command_builders/comment_command_builder.rb +19 -0
- data/lib/gitlab/triage/command_builders/label_command_builder.rb +40 -0
- data/lib/gitlab/triage/command_builders/move_command_builder.rb +19 -0
- data/lib/gitlab/triage/command_builders/remove_label_command_builder.rb +15 -0
- data/lib/gitlab/triage/command_builders/status_command_builder.rb +23 -0
- data/lib/gitlab/triage/command_builders/text_content_builder.rb +138 -0
- data/lib/gitlab/triage/engine.rb +635 -0
- data/lib/gitlab/triage/entity_builders/issue_builder.rb +54 -0
- data/lib/gitlab/triage/entity_builders/summary_builder.rb +82 -0
- data/lib/gitlab/triage/errors/network.rb +11 -0
- data/lib/gitlab/triage/errors.rb +1 -0
- data/lib/gitlab/triage/expand_condition/expansion.rb +203 -0
- data/lib/gitlab/triage/expand_condition/list.rb +25 -0
- data/lib/gitlab/triage/expand_condition/sequence.rb +25 -0
- data/lib/gitlab/triage/expand_condition.rb +23 -0
- data/lib/gitlab/triage/filters/assignee_member_conditions_filter.rb +13 -0
- data/lib/gitlab/triage/filters/author_member_conditions_filter.rb +13 -0
- data/lib/gitlab/triage/filters/base_conditions_filter.rb +58 -0
- data/lib/gitlab/triage/filters/branch_date_filter.rb +73 -0
- data/lib/gitlab/triage/filters/branch_protected_filter.rb +26 -0
- data/lib/gitlab/triage/filters/discussions_conditions_filter.rb +58 -0
- data/lib/gitlab/triage/filters/issue_date_conditions_filter.rb +78 -0
- data/lib/gitlab/triage/filters/member_conditions_filter.rb +84 -0
- data/lib/gitlab/triage/filters/merge_request_date_conditions_filter.rb +13 -0
- data/lib/gitlab/triage/filters/name_conditions_filter.rb +26 -0
- data/lib/gitlab/triage/filters/no_additional_labels_conditions_filter.rb +30 -0
- data/lib/gitlab/triage/filters/ruby_conditions_filter.rb +33 -0
- data/lib/gitlab/triage/filters/votes_conditions_filter.rb +54 -0
- data/lib/gitlab/triage/graphql_network.rb +81 -0
- data/lib/gitlab/triage/graphql_queries/query_builder.rb +158 -0
- 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/limiters/base_limiter.rb +35 -0
- data/lib/gitlab/triage/limiters/date_field_limiter.rb +45 -0
- data/lib/gitlab/triage/network.rb +39 -0
- data/lib/gitlab/triage/network_adapters/base_adapter.rb +17 -0
- data/lib/gitlab/triage/network_adapters/graphql_adapter.rb +92 -0
- data/lib/gitlab/triage/network_adapters/httparty_adapter.rb +116 -0
- data/lib/gitlab/triage/network_adapters/test_adapter.rb +39 -0
- data/lib/gitlab/triage/option_parser.rb +105 -0
- data/lib/gitlab/triage/options.rb +30 -0
- data/lib/gitlab/triage/param_builders/date_param_builder.rb +64 -0
- data/lib/gitlab/triage/policies/base_policy.rb +80 -0
- data/lib/gitlab/triage/policies/rule_policy.rb +36 -0
- data/lib/gitlab/triage/policies/summary_policy.rb +29 -0
- data/lib/gitlab/triage/policies_resources/rule_resources.rb +11 -0
- data/lib/gitlab/triage/policies_resources/summary_resources.rb +11 -0
- data/lib/gitlab/triage/resource/base.rb +102 -0
- data/lib/gitlab/triage/resource/branch.rb +13 -0
- data/lib/gitlab/triage/resource/context.rb +47 -0
- data/lib/gitlab/triage/resource/epic.rb +20 -0
- data/lib/gitlab/triage/resource/instance_version.rb +35 -0
- data/lib/gitlab/triage/resource/issue.rb +52 -0
- data/lib/gitlab/triage/resource/label.rb +56 -0
- data/lib/gitlab/triage/resource/label_event.rb +48 -0
- data/lib/gitlab/triage/resource/linked_issue.rb +15 -0
- data/lib/gitlab/triage/resource/merge_request.rb +23 -0
- data/lib/gitlab/triage/resource/milestone.rb +98 -0
- data/lib/gitlab/triage/resource/shared/issuable.rb +119 -0
- data/lib/gitlab/triage/rest_api_network.rb +125 -0
- data/lib/gitlab/triage/retryable.rb +33 -0
- data/lib/gitlab/triage/ui.rb +23 -0
- data/lib/gitlab/triage/url_builders/url_builder.rb +54 -0
- data/lib/gitlab/triage/utils.rb +13 -0
- data/lib/gitlab/triage/validators/limiter_validator.rb +21 -0
- data/lib/gitlab/triage/validators/params_validator.rb +43 -0
- data/lib/gitlab/triage/version.rb +7 -0
- data/lib/gitlab/triage.rb +6 -0
- data/support/.gitlab-ci.example.yml +22 -0
- data/support/.triage-policies.example.yml +51 -0
- metadata +280 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
require 'active_support/all'
|
|
2
|
+
require 'active_support/inflector'
|
|
3
|
+
|
|
4
|
+
require_relative 'expand_condition'
|
|
5
|
+
require_relative 'filters/issue_date_conditions_filter'
|
|
6
|
+
require_relative 'filters/merge_request_date_conditions_filter'
|
|
7
|
+
require_relative 'filters/branch_date_filter'
|
|
8
|
+
require_relative 'filters/branch_protected_filter'
|
|
9
|
+
require_relative 'filters/votes_conditions_filter'
|
|
10
|
+
require_relative 'filters/no_additional_labels_conditions_filter'
|
|
11
|
+
require_relative 'filters/author_member_conditions_filter'
|
|
12
|
+
require_relative 'filters/assignee_member_conditions_filter'
|
|
13
|
+
require_relative 'filters/discussions_conditions_filter'
|
|
14
|
+
require_relative 'filters/ruby_conditions_filter'
|
|
15
|
+
require_relative 'limiters/date_field_limiter'
|
|
16
|
+
require_relative 'action'
|
|
17
|
+
require_relative 'policies/rule_policy'
|
|
18
|
+
require_relative 'policies/summary_policy'
|
|
19
|
+
require_relative 'policies_resources/rule_resources'
|
|
20
|
+
require_relative 'policies_resources/summary_resources'
|
|
21
|
+
require_relative 'api_query_builders/date_query_param_builder'
|
|
22
|
+
require_relative 'api_query_builders/single_query_param_builder'
|
|
23
|
+
require_relative 'api_query_builders/multi_query_param_builder'
|
|
24
|
+
require_relative 'url_builders/url_builder'
|
|
25
|
+
require_relative 'network'
|
|
26
|
+
require_relative 'graphql_network'
|
|
27
|
+
require_relative 'rest_api_network'
|
|
28
|
+
require_relative 'network_adapters/httparty_adapter'
|
|
29
|
+
require_relative 'network_adapters/graphql_adapter'
|
|
30
|
+
require_relative 'graphql_queries/query_builder'
|
|
31
|
+
require_relative 'ui'
|
|
32
|
+
|
|
33
|
+
module Gitlab
|
|
34
|
+
module Triage
|
|
35
|
+
class Engine
|
|
36
|
+
attr_reader :per_page, :policies, :options
|
|
37
|
+
|
|
38
|
+
DEFAULT_NETWORK_ADAPTER = Gitlab::Triage::NetworkAdapters::HttpartyAdapter
|
|
39
|
+
DEFAULT_GRAPHQL_ADAPTER = Gitlab::Triage::NetworkAdapters::GraphqlAdapter
|
|
40
|
+
ALLOWED_STATE_VALUES = {
|
|
41
|
+
issues: %w[opened closed],
|
|
42
|
+
merge_requests: %w[opened closed merged]
|
|
43
|
+
}.with_indifferent_access.freeze
|
|
44
|
+
MILESTONE_TIMEBOX_VALUES = %w[none any upcoming started].freeze
|
|
45
|
+
ITERATION_SELECTION_VALUES = %w[none any].freeze
|
|
46
|
+
EpicsTriagingForProjectImpossibleError = Class.new(StandardError)
|
|
47
|
+
MultiPolicyInInjectionModeError = Class.new(StandardError)
|
|
48
|
+
|
|
49
|
+
def initialize(policies:, options:, network_adapter_class: DEFAULT_NETWORK_ADAPTER, graphql_network_adapter_class: DEFAULT_GRAPHQL_ADAPTER)
|
|
50
|
+
options.host_url = policies.delete(:host_url) { options.host_url }
|
|
51
|
+
options.api_version = policies.delete(:api_version) { 'v4' }
|
|
52
|
+
options.dry_run = ENV['TEST'] == 'true' if options.dry_run.nil?
|
|
53
|
+
|
|
54
|
+
@per_page = policies.delete(:per_page) { 100 }
|
|
55
|
+
@policies = policies
|
|
56
|
+
@options = options
|
|
57
|
+
@network_adapter_class = network_adapter_class
|
|
58
|
+
@graphql_network_adapter_class = graphql_network_adapter_class
|
|
59
|
+
|
|
60
|
+
assert_options!
|
|
61
|
+
|
|
62
|
+
@options.source = @options.source.to_s
|
|
63
|
+
|
|
64
|
+
require_ruby_files
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def perform
|
|
68
|
+
puts "Performing a dry run.\n\n" if options.dry_run
|
|
69
|
+
|
|
70
|
+
puts Gitlab::Triage::UI.header("Triaging the `#{options.source_id}` #{options.source.singularize}", char: '=')
|
|
71
|
+
puts
|
|
72
|
+
|
|
73
|
+
resource_rules.each do |resource_type, policy_definition|
|
|
74
|
+
next unless right_resource_type_for_resource_option?(resource_type)
|
|
75
|
+
|
|
76
|
+
assert_epic_rule!(resource_type)
|
|
77
|
+
|
|
78
|
+
puts Gitlab::Triage::UI.header("Processing summaries & rules for #{resource_type}", char: '-')
|
|
79
|
+
puts
|
|
80
|
+
|
|
81
|
+
process_summaries(resource_type, policy_definition[:summaries])
|
|
82
|
+
process_rules(resource_type, policy_definition[:rules])
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def network
|
|
87
|
+
@network ||= Network.new(restapi: restapi_network, graphql: graphql_network)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def restapi_network
|
|
91
|
+
@restapi_network ||= RestAPINetwork.new(network_adapter)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def graphql_network
|
|
95
|
+
@graphql_network ||= GraphqlNetwork.new(graphql_network_adapter)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def assert_options!
|
|
101
|
+
assert_all!
|
|
102
|
+
assert_source!
|
|
103
|
+
assert_source_id!
|
|
104
|
+
assert_resource_reference!
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# rubocop:disable Style/IfUnlessModifier
|
|
108
|
+
def assert_all!
|
|
109
|
+
return unless options.all
|
|
110
|
+
|
|
111
|
+
if options.source
|
|
112
|
+
raise ArgumentError, '--all-projects option cannot be used in conjunction with --source option!'
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if options.source_id
|
|
116
|
+
raise ArgumentError, '--all-projects option cannot be used in conjunction with --source-id option!'
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
if options.resource_reference # rubocop:disable Style/GuardClause
|
|
120
|
+
raise ArgumentError, '--all-projects option cannot be used in conjunction with --resource-reference option!'
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
# rubocop:enable Style/IfUnlessModifier
|
|
124
|
+
|
|
125
|
+
def assert_source!
|
|
126
|
+
return if options.source
|
|
127
|
+
return if options.all
|
|
128
|
+
|
|
129
|
+
raise ArgumentError, 'A source is needed (pass it with the `--source` option)!'
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def assert_source_id!
|
|
133
|
+
return if options.source_id
|
|
134
|
+
return if options.all
|
|
135
|
+
|
|
136
|
+
raise ArgumentError, 'A project or group ID is needed (pass it with the `--source-id` option)!'
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def assert_resource_reference!
|
|
140
|
+
return unless options.resource_reference
|
|
141
|
+
|
|
142
|
+
if options.source == 'groups' && !options.resource_reference.start_with?('&')
|
|
143
|
+
raise ArgumentError, "--resource-reference can only start with '&' when --source=groups is passed ('#{options.resource_reference}' passed)!"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
if options.source == 'projects' && !options.resource_reference.start_with?('#', '!') # rubocop:disable Style/GuardClause
|
|
147
|
+
raise(
|
|
148
|
+
ArgumentError,
|
|
149
|
+
"--resource-reference can only start with '#' or '!' when --source=projects is passed " \
|
|
150
|
+
"('#{options.resource_reference}' passed)!"
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def require_ruby_files
|
|
156
|
+
options.require_files.each(&method(:require))
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def right_resource_type_for_resource_option?(resource_type)
|
|
160
|
+
return true unless options.resource_reference
|
|
161
|
+
|
|
162
|
+
resource_reference = options.resource_reference
|
|
163
|
+
|
|
164
|
+
case resource_type
|
|
165
|
+
when 'issues'
|
|
166
|
+
resource_reference.start_with?('#')
|
|
167
|
+
when 'merge_requests'
|
|
168
|
+
resource_reference.start_with?('!')
|
|
169
|
+
when 'epics'
|
|
170
|
+
resource_reference.start_with?('&')
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def assert_epic_rule!(resource_type)
|
|
175
|
+
return if resource_type != 'epics' || options.source == 'groups'
|
|
176
|
+
|
|
177
|
+
raise EpicsTriagingForProjectImpossibleError, "Epics can only be triaged at the group level. Please set the `--source groups` option."
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def resource_rules
|
|
181
|
+
@resource_rules ||= policies.delete(:resource_rules) { {} }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def network_adapter
|
|
185
|
+
@network_adapter ||= @network_adapter_class.new(options)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def graphql_network_adapter
|
|
189
|
+
@graphql_network_adapter ||= @graphql_network_adapter_class.new(options)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def rule_conditions(rule)
|
|
193
|
+
rule.fetch(:conditions) { {} }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def rule_limits(rule)
|
|
197
|
+
rule.fetch(:limits) { {} }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Process an array of +summary_definitions+.
|
|
201
|
+
#
|
|
202
|
+
# @example Example of an array of summary definitions (shown as YAML for readability).
|
|
203
|
+
#
|
|
204
|
+
# - name: Newest and oldest issues summary
|
|
205
|
+
# rules:
|
|
206
|
+
# - name: New issues
|
|
207
|
+
# conditions:
|
|
208
|
+
# state: opened
|
|
209
|
+
# limits:
|
|
210
|
+
# most_recent: 2
|
|
211
|
+
# actions:
|
|
212
|
+
# summarize:
|
|
213
|
+
# item: "- [ ] [{{title}}]({{web_url}}) {{labels}}"
|
|
214
|
+
# summary: |
|
|
215
|
+
# Please triage the following new {{type}}:
|
|
216
|
+
# {{items}}
|
|
217
|
+
# actions:
|
|
218
|
+
# summarize:
|
|
219
|
+
# title: "Newest and oldest {{type}} summary"
|
|
220
|
+
# summary: |
|
|
221
|
+
# Please triage the following {{type}}:
|
|
222
|
+
# {{items}}
|
|
223
|
+
# Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
|
|
224
|
+
# /label ~"needs attention"
|
|
225
|
+
#
|
|
226
|
+
# @param summary_definitions [Array<Hash>] An array usually given as YAML in a triage policy file.
|
|
227
|
+
#
|
|
228
|
+
# @return [nil]
|
|
229
|
+
def process_summaries(resource_type, summary_definitions)
|
|
230
|
+
return if summary_definitions.blank?
|
|
231
|
+
|
|
232
|
+
summary_definitions.each do |summary_definition|
|
|
233
|
+
process_summary(resource_type, summary_definition)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Process an array of +rule_definitions+.
|
|
238
|
+
#
|
|
239
|
+
# @example Example of an array of rule definitions.
|
|
240
|
+
#
|
|
241
|
+
# [{ name: "New issues", conditions: { state: opened }, limits: { most_recent: 2 }, actions: { labels: ["needs attention"] } }]
|
|
242
|
+
#
|
|
243
|
+
# @param rule_definitions [Array<Hash>] An array usually given as YAML in a triage policy file.
|
|
244
|
+
#
|
|
245
|
+
# @return [nil]
|
|
246
|
+
def process_rules(resource_type, rule_definitions)
|
|
247
|
+
return if rule_definitions.blank?
|
|
248
|
+
|
|
249
|
+
rule_definitions.each do |rule_definition|
|
|
250
|
+
resources_for_rule(resource_type, rule_definition) do |resources|
|
|
251
|
+
policy = Policies::RulePolicy.new(
|
|
252
|
+
resource_type, rule_definition, resources, network)
|
|
253
|
+
|
|
254
|
+
process_action(policy)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Process a +summary_definition+.
|
|
260
|
+
#
|
|
261
|
+
# @example Example of a summary definition hash (shown as YAML for readability).
|
|
262
|
+
#
|
|
263
|
+
# name: Newest and oldest issues summary
|
|
264
|
+
# rules:
|
|
265
|
+
# - name: New issues
|
|
266
|
+
# conditions:
|
|
267
|
+
# state: opened
|
|
268
|
+
# limits:
|
|
269
|
+
# most_recent: 2
|
|
270
|
+
# actions:
|
|
271
|
+
# summarize:
|
|
272
|
+
# item: "- [ ] [{{title}}]({{web_url}}) {{labels}}"
|
|
273
|
+
# summary: |
|
|
274
|
+
# Please triage the following new {{type}}:
|
|
275
|
+
# {{items}}
|
|
276
|
+
# actions:
|
|
277
|
+
# summarize:
|
|
278
|
+
# title: "Newest and oldest {{type}} summary"
|
|
279
|
+
# summary: |
|
|
280
|
+
# Please triage the following {{type}}:
|
|
281
|
+
# {{items}}
|
|
282
|
+
# Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
|
|
283
|
+
# /label ~"needs attention"
|
|
284
|
+
#
|
|
285
|
+
# @param resource_type [String] The resource type, e.g. +issues+ or +merge_requests+.
|
|
286
|
+
# @param summary_definition [Hash] A hash usually given as YAML in a triage policy file:
|
|
287
|
+
#
|
|
288
|
+
# @return [nil]
|
|
289
|
+
def process_summary(resource_type, summary_definition)
|
|
290
|
+
puts Gitlab::Triage::UI.header("Processing summary: **#{summary_definition[:name]}**", char: '~')
|
|
291
|
+
puts
|
|
292
|
+
|
|
293
|
+
summary_parts_for_rules(resource_type, summary_definition[:rules]) do |summary_resources|
|
|
294
|
+
policy = Policies::SummaryPolicy.new(
|
|
295
|
+
resource_type, summary_definition, summary_resources, network)
|
|
296
|
+
|
|
297
|
+
process_action(policy)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Transform an array of +rule_definitions+ into a +PoliciesResources::SummaryResources.new(rule => rule_resources)+ object.
|
|
302
|
+
#
|
|
303
|
+
# @example Example of an array of rule definitions.
|
|
304
|
+
#
|
|
305
|
+
# [{ name: "New issues", conditions: { state: opened }, limits: { most_recent: 2 }, actions: { labels: ["needs attention"] } }]
|
|
306
|
+
#
|
|
307
|
+
# @param resource_type [String] The resource type, e.g. +issues+ or +merge_requests+.
|
|
308
|
+
# @param rule_definitions [Array<Hash>] An array of rule definitions, e.g.
|
|
309
|
+
# +[{ name: 'Foo', conditions: { milestone: 'v1' } }, { name: 'Foo', conditions: { state: 'opened' } }]+.
|
|
310
|
+
#
|
|
311
|
+
# @yieldparam summary_resources [PoliciesResources::SummaryResources] An object which contains a +{ rule_definition => resources }+ hash.
|
|
312
|
+
# @yieldreturn [nil]
|
|
313
|
+
#
|
|
314
|
+
# @return [nil]
|
|
315
|
+
def summary_parts_for_rules(resource_type, rule_definitions)
|
|
316
|
+
# { summary_rule => resources }
|
|
317
|
+
parts = rule_definitions.each_with_object({}) do |rule_definition, result|
|
|
318
|
+
to_enum(:resources_for_rule, resource_type, rule_definition).each do |rule_resources, expanded_conditions|
|
|
319
|
+
# We replace the non-expanded rule conditions with the expanded ones
|
|
320
|
+
result.merge!(rule_definition.merge(conditions: expanded_conditions) => rule_resources)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
result
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
yield(PoliciesResources::SummaryResources.new(parts))
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Transform a non-expanded +rule_definition+ into a +PoliciesResources::RuleResources.new(resources)+ object.
|
|
330
|
+
#
|
|
331
|
+
# @example Example of a rule definition hash.
|
|
332
|
+
#
|
|
333
|
+
# { name: "New issues", conditions: { state: opened }, limits: { most_recent: 2 }, actions: { labels: ["needs attention"] } }
|
|
334
|
+
#
|
|
335
|
+
# @param resource_type [String] The resource type, e.g. +issues+ or +merge_requests+.
|
|
336
|
+
# @param rule_definition [Hash] A rule definition, e.g. +{ name: 'Foo', conditions: { milestone: 'v1' } }+.
|
|
337
|
+
#
|
|
338
|
+
# @yieldparam rule_resources [PoliciesResources::RuleResources] An object which contains an array of resources.
|
|
339
|
+
# @yieldparam expanded_conditions [Hash] A hash of expanded conditions.
|
|
340
|
+
# @yieldreturn [nil]
|
|
341
|
+
#
|
|
342
|
+
# @return [nil]
|
|
343
|
+
def resources_for_rule(resource_type, rule_definition)
|
|
344
|
+
puts Gitlab::Triage::UI.header("Gathering resources for rule: **#{rule_definition[:name]}**", char: '-')
|
|
345
|
+
|
|
346
|
+
ExpandCondition.perform(rule_conditions(rule_definition)) do |expanded_conditions|
|
|
347
|
+
# retrieving the resources for every rule is inefficient
|
|
348
|
+
# however, previous rules may affect those upcoming
|
|
349
|
+
resources = options.resources ||
|
|
350
|
+
fetch_resources(resource_type, expanded_conditions, rule_definition)
|
|
351
|
+
|
|
352
|
+
# In some filters/actions we want to know which resource type it is
|
|
353
|
+
attach_resource_type(resources, resource_type)
|
|
354
|
+
|
|
355
|
+
puts "\n\n* Found #{resources.count} resources..."
|
|
356
|
+
print "* Filtering resources..."
|
|
357
|
+
resources = filter_resources(resources, expanded_conditions)
|
|
358
|
+
puts "\n* Total after filtering: #{resources.count} resources"
|
|
359
|
+
print "* Limiting resources..."
|
|
360
|
+
resources = limit_resources(resources, rule_limits(rule_definition))
|
|
361
|
+
puts "\n* Total after limiting: #{resources.count} resources"
|
|
362
|
+
puts
|
|
363
|
+
|
|
364
|
+
yield(PoliciesResources::RuleResources.new(resources), expanded_conditions)
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def fetch_resources(resource_type, expanded_conditions, rule_definition)
|
|
369
|
+
resources = []
|
|
370
|
+
|
|
371
|
+
if rule_definition[:api] == 'graphql'
|
|
372
|
+
graphql_query_options = { source: source_full_path }
|
|
373
|
+
|
|
374
|
+
if options.resource_reference
|
|
375
|
+
expanded_conditions[:iids] = options.resource_reference[1..]
|
|
376
|
+
graphql_query_options[:iids] = [expanded_conditions[:iids]]
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
graphql_query = build_graphql_query(resource_type, expanded_conditions, true)
|
|
380
|
+
|
|
381
|
+
resources = graphql_network.query(graphql_query, **graphql_query_options)
|
|
382
|
+
else
|
|
383
|
+
# FIXME: Epics listing endpoint doesn't support filtering by `iids`, so instead we
|
|
384
|
+
# get a single epic when `--resource-reference` is given for epics.
|
|
385
|
+
# Because of that, the query could return a single epic, so we make sure we get an array.
|
|
386
|
+
resources = Array(network.query_api(build_get_url(resource_type, expanded_conditions)))
|
|
387
|
+
|
|
388
|
+
iids = resources.pluck('iid').map(&:to_s)
|
|
389
|
+
expanded_conditions[:iids] = iids
|
|
390
|
+
|
|
391
|
+
graphql_query = build_graphql_query(resource_type, expanded_conditions)
|
|
392
|
+
graphql_resources = graphql_network.query(graphql_query, source: source_full_path, iids: iids) if graphql_query.any?
|
|
393
|
+
|
|
394
|
+
decorate_resources_with_graphql_data(resources, graphql_resources)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
resources
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def attach_resource_type(resources, resource_type)
|
|
401
|
+
resources.each { |resource| resource[:type] = resource_type }
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def decorate_resources_with_graphql_data(resources, graphql_resources)
|
|
405
|
+
return if graphql_resources.nil?
|
|
406
|
+
|
|
407
|
+
graphql_resources_by_id = graphql_resources.to_h { |resource| [resource[:id], resource] }
|
|
408
|
+
resources.each { |resource| resource.merge!(graphql_resources_by_id[resource[:id]].to_h) }
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def process_action(policy)
|
|
412
|
+
Action.process(
|
|
413
|
+
policy: policy,
|
|
414
|
+
network: network,
|
|
415
|
+
dry: options.dry_run)
|
|
416
|
+
puts
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def filter_resources(resources, conditions) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
420
|
+
resources.select do |resource|
|
|
421
|
+
results = []
|
|
422
|
+
|
|
423
|
+
# rubocop:disable Style/IfUnlessModifier
|
|
424
|
+
if conditions[:date]
|
|
425
|
+
case resource[:type]
|
|
426
|
+
when 'branches'
|
|
427
|
+
results << Filters::BranchDateFilter.new(resource, conditions[:date]).calculate
|
|
428
|
+
when 'issues'
|
|
429
|
+
if conditions.dig(:date, :filter_in_ruby)
|
|
430
|
+
results << Filters::IssueDateConditionsFilter.new(resource, conditions[:date]).calculate
|
|
431
|
+
end
|
|
432
|
+
when 'merge_requests'
|
|
433
|
+
if conditions.dig(:date, :filter_in_ruby) ||
|
|
434
|
+
# REST API does not support filtering with merged_at,
|
|
435
|
+
# so we have to filter it in Ruby
|
|
436
|
+
conditions.dig(:date, :attribute) == 'merged_at'
|
|
437
|
+
results << Filters::MergeRequestDateConditionsFilter.new(resource, conditions[:date]).calculate
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
if resource[:type] == 'branches'
|
|
443
|
+
results << Filters::BranchProtectedFilter.new(resource, conditions[:protected]).calculate
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
votes_condition = conditions[:votes] || conditions[:upvotes]
|
|
447
|
+
if votes_condition
|
|
448
|
+
results << Filters::VotesConditionsFilter.new(resource, votes_condition).calculate
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
if conditions[:no_additional_labels]
|
|
452
|
+
results << Filters::NoAdditionalLabelsConditionsFilter.new(resource, conditions.fetch(:labels) { [] }).calculate
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
if conditions[:author_member]
|
|
456
|
+
results << Filters::AuthorMemberConditionsFilter.new(resource, conditions[:author_member], network).calculate
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
if conditions[:assignee_member]
|
|
460
|
+
results << Filters::AssigneeMemberConditionsFilter.new(resource, conditions[:assignee_member], network).calculate
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
if conditions[:discussions]
|
|
464
|
+
results << Filters::DiscussionsConditionsFilter.new(resource, conditions[:discussions]).calculate
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
if conditions[:ruby]
|
|
468
|
+
results << Filters::RubyConditionsFilter.new(resource, conditions, network).calculate
|
|
469
|
+
end
|
|
470
|
+
# rubocop:enable Style/IfUnlessModifier
|
|
471
|
+
|
|
472
|
+
results.all?
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def limit_resources(resources, limits)
|
|
477
|
+
if limits.empty?
|
|
478
|
+
resources
|
|
479
|
+
else
|
|
480
|
+
Limiters::DateFieldLimiter.new(resources, limits).limit
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# rubocop:disable Metrics/AbcSize
|
|
485
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
486
|
+
def build_get_url(resource_type, conditions)
|
|
487
|
+
# Example issues query with state and labels
|
|
488
|
+
# https://gitlab.com/api/v4/projects/test-triage%2Fissue-project/issues?state=open&labels=project%20label%20with%20spaces,group_label_no_spaces
|
|
489
|
+
params = {
|
|
490
|
+
per_page: per_page
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
condition_builders = []
|
|
494
|
+
condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('iids', options.resource_reference[1..]) if options.resource_reference
|
|
495
|
+
|
|
496
|
+
condition_builders << APIQueryBuilders::MultiQueryParamBuilder.new('labels', conditions[:labels], ',') if conditions[:labels]
|
|
497
|
+
|
|
498
|
+
if conditions[:forbidden_labels]
|
|
499
|
+
condition_builders << APIQueryBuilders::MultiQueryParamBuilder.new('not[labels]', conditions[:forbidden_labels], ',')
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
if conditions[:state]
|
|
503
|
+
condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new(
|
|
504
|
+
'state',
|
|
505
|
+
conditions[:state],
|
|
506
|
+
allowed_values: ALLOWED_STATE_VALUES[resource_type])
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
condition_builders << milestone_condition_builder(resource_type, conditions[:milestone]) if conditions[:milestone]
|
|
510
|
+
|
|
511
|
+
if conditions[:date] && APIQueryBuilders::DateQueryParamBuilder.applicable?(conditions[:date]) && resource_type&.to_sym != :branches
|
|
512
|
+
condition_builders << APIQueryBuilders::DateQueryParamBuilder.new(conditions.delete(:date))
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
case resource_type&.to_sym
|
|
516
|
+
when :issues
|
|
517
|
+
condition_builders.concat(issues_resource_query(conditions))
|
|
518
|
+
when :merge_requests
|
|
519
|
+
condition_builders.concat(merge_requests_resource_query(conditions))
|
|
520
|
+
when :branches
|
|
521
|
+
condition_builders.concat(branches_resource_query(conditions))
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
condition_builders.compact.each do |condition_builder|
|
|
525
|
+
params[condition_builder.param_name] = condition_builder.param_content
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
url_builder_options = {
|
|
529
|
+
network_options: options,
|
|
530
|
+
all: options.all,
|
|
531
|
+
source: options.source,
|
|
532
|
+
source_id: options.source_id,
|
|
533
|
+
resource_type: resource_type,
|
|
534
|
+
params: params
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
# FIXME: Epics listing endpoint doesn't support filtering by `iids`, so instead we
|
|
538
|
+
# get a single epic when `--resource-reference` is given for epics.
|
|
539
|
+
url_builder_options[:resource_id] = options.resource_reference[1..] if options.resource_reference && resource_type == 'epics'
|
|
540
|
+
|
|
541
|
+
UrlBuilders::UrlBuilder.new(url_builder_options).build
|
|
542
|
+
end
|
|
543
|
+
# rubocop:enable Metrics/AbcSize
|
|
544
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
545
|
+
|
|
546
|
+
def milestone_condition_builder(resource_type, milestone_condition)
|
|
547
|
+
milestone_value = Array(milestone_condition)[0].to_s # back-compatibility
|
|
548
|
+
return if milestone_value.empty?
|
|
549
|
+
|
|
550
|
+
# Issues API should use the `milestone_id` param for timebox values, and `milestone` for milestone title
|
|
551
|
+
args =
|
|
552
|
+
if resource_type.to_sym == :issues && MILESTONE_TIMEBOX_VALUES.include?(milestone_value.downcase)
|
|
553
|
+
['milestone_id', milestone_value.titleize] # The API only accepts titleized values.
|
|
554
|
+
else
|
|
555
|
+
['milestone', milestone_value]
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
APIQueryBuilders::SingleQueryParamBuilder.new(*args)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def iteration_condition_builder(iteration_value)
|
|
562
|
+
# Issues API should use the `iteration_id` param for timebox values, and `iteration_title` for iteration title
|
|
563
|
+
args =
|
|
564
|
+
if ITERATION_SELECTION_VALUES.include?(iteration_value.downcase)
|
|
565
|
+
['iteration_id', iteration_value.titleize] # The API only accepts titleized values.
|
|
566
|
+
else
|
|
567
|
+
['iteration_title', iteration_value]
|
|
568
|
+
end
|
|
569
|
+
APIQueryBuilders::SingleQueryParamBuilder.new(*args)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def issues_resource_query(conditions)
|
|
573
|
+
[].tap do |condition_builders|
|
|
574
|
+
condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('weight', conditions[:weight]) if conditions[:weight]
|
|
575
|
+
condition_builders << iteration_condition_builder(conditions[:iteration]) if conditions[:iteration]
|
|
576
|
+
condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('health_status', conditions[:health_status]) if conditions[:health_status]
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def merge_requests_resource_query(conditions)
|
|
581
|
+
[].tap do |condition_builders|
|
|
582
|
+
[
|
|
583
|
+
:source_branch,
|
|
584
|
+
:target_branch,
|
|
585
|
+
:reviewer_id
|
|
586
|
+
].each do |key|
|
|
587
|
+
condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new(key.to_s, conditions[key]) if conditions[key]
|
|
588
|
+
end
|
|
589
|
+
condition_builders << draft_condition_builder(conditions[:draft]) if conditions.key?(:draft)
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def branches_resource_query(conditions)
|
|
594
|
+
[].tap do |condition_builders|
|
|
595
|
+
condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('search', conditions[:name]) if conditions[:name]
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def draft_condition_builder(draft_condittion)
|
|
600
|
+
# Issues API only accepts 'yes' and 'no' as strings: https://docs.gitlab.com/ee/api/merge_requests.html
|
|
601
|
+
wip =
|
|
602
|
+
case draft_condittion
|
|
603
|
+
when true
|
|
604
|
+
'yes'
|
|
605
|
+
when false
|
|
606
|
+
'no'
|
|
607
|
+
else
|
|
608
|
+
raise ArgumentError, 'The "draft" condition only accepts true or false.'
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
APIQueryBuilders::SingleQueryParamBuilder.new('wip', wip)
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def build_graphql_query(resource_type, conditions, graphql_only = false)
|
|
615
|
+
Gitlab::Triage::GraphqlQueries::QueryBuilder
|
|
616
|
+
.new(options.source, resource_type, conditions, graphql_only: graphql_only)
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def source_full_path
|
|
620
|
+
@source_full_path ||= fetch_source_full_path
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def fetch_source_full_path
|
|
624
|
+
return options.source_id unless /\A\d+\z/.match?(options.source_id)
|
|
625
|
+
|
|
626
|
+
source_details = network.query_api(build_get_url(nil, {})).first
|
|
627
|
+
full_path = source_details['full_path'] || source_details['path_with_namespace']
|
|
628
|
+
|
|
629
|
+
raise ArgumentError, 'A source with given source_id was not found!' if full_path.blank?
|
|
630
|
+
|
|
631
|
+
full_path
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../command_builders/text_content_builder'
|
|
4
|
+
|
|
5
|
+
module Gitlab
|
|
6
|
+
module Triage
|
|
7
|
+
module EntityBuilders
|
|
8
|
+
class IssueBuilder
|
|
9
|
+
attr_reader :destination
|
|
10
|
+
|
|
11
|
+
def initialize(
|
|
12
|
+
type:, action:, resource:, network:,
|
|
13
|
+
policy_spec: {}, separator: "\n")
|
|
14
|
+
@type = type
|
|
15
|
+
@policy_spec = policy_spec
|
|
16
|
+
@item_template = action[:item]
|
|
17
|
+
@title_template = action[:title]
|
|
18
|
+
@description_template = action[:description]
|
|
19
|
+
@destination = action[:destination]
|
|
20
|
+
@redact_confidentials =
|
|
21
|
+
action[:redact_confidential_resources] != false
|
|
22
|
+
@resource = resource
|
|
23
|
+
@network = network
|
|
24
|
+
@separator = separator
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def title
|
|
28
|
+
@title ||= build_text(@title_template)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def description
|
|
32
|
+
@description ||= build_text(@description_template)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def valid?
|
|
36
|
+
title =~ /\S+/
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def build_text(template)
|
|
42
|
+
return '' unless template
|
|
43
|
+
|
|
44
|
+
CommandBuilders::TextContentBuilder.new(
|
|
45
|
+
template,
|
|
46
|
+
resource: @resource,
|
|
47
|
+
network: @network,
|
|
48
|
+
redact_confidentials: @redact_confidentials)
|
|
49
|
+
.build_command.chomp
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|