gitlab-triage 1.51.1 → 1.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0852608feca48b98d92771b30203ae3f2a83220fdcce24f08d435b9af12c7f20'
4
- data.tar.gz: 224da2c7b14a0513d2d06c43ae1c13bb3ac5459114df3daa32da6785354b2247
3
+ metadata.gz: e4b12c328cc4c5a865ceca6ccadd49a8dedc1066564fa62a28718c3b164f3a15
4
+ data.tar.gz: 1298d537c79ef8d74f3816e18d482a8bbee02fde1b3a1717869709795f40f547
5
5
  SHA512:
6
- metadata.gz: 435c98010e80f7bf81e31a0976c36d948e56e2b94da2e08ad9eefef5f6680967ab95c4e91f9667233111d564de27c5380890f546d047e6b1cf55a7c6e6915881
7
- data.tar.gz: 9e87b898dab83aee3b7f0585e805a6200478e27fc7cefa441b99422666d7ee51b85819b5e04812542b3e0d6a136384409a0261f7971d97571606d3a369fabc26
6
+ metadata.gz: c8d68d1f8162e2324685ee874a862429569f76d4a060d138f922f1b4866b57eabfc9420f72a6da68f28f0285669542169f6b7294f785e6152c3075c36280bf40
7
+ data.tar.gz: aaf7e8821f7cbfc9112c2f25776566c7775276c5eeebaccd43dedcee686824cce2db97ee813648504274befd5db4696f4e816afb6bab83d72039e2cfa7e13437
data/.rubocop_todo.yml CHANGED
@@ -6,13 +6,15 @@
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 78
9
+ # Offense count: 81
10
10
  CodeReuse/ActiveRecord:
11
11
  Exclude:
12
+ - 'lib/gitlab/triage/action/work_item.rb'
12
13
  - 'lib/gitlab/triage/engine.rb'
13
14
  - 'lib/gitlab/triage/filters/base_conditions_filter.rb'
14
15
  - 'lib/gitlab/triage/filters/member_conditions_filter.rb'
15
16
  - 'lib/gitlab/triage/graphql_network.rb'
17
+ - 'lib/gitlab/triage/normalizers/work_item_normalizer.rb'
16
18
  - 'lib/gitlab/triage/validators/limiter_validator.rb'
17
19
  - 'spec/gitlab/triage/action_spec.rb'
18
20
  - 'spec/gitlab/triage/engine_spec.rb'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab-triage (1.51.1)
4
+ gitlab-triage (1.53.0)
5
5
  activesupport (>= 5.1)
6
6
  globalid (~> 1.0, >= 1.0.1)
7
7
  graphql (< 2.1.0)
data/README.md CHANGED
@@ -50,6 +50,7 @@ Select which resource to add the policy to:
50
50
  - `issues`
51
51
  - `merge_requests`
52
52
  - `branches`
53
+ - `work_items` (see [Work items policy type](#work-items-policy-type))
53
54
 
54
55
  And create an array of `rules` to define your policies:
55
56
 
@@ -148,6 +149,107 @@ We're enforcing multiple polices with pipeline schedules at [triage-ops](
148
149
  https://gitlab.com/gitlab-org/quality/triage-ops), where we're also
149
150
  extensively utilizing the [plugins system](#can-i-customize).
150
151
 
152
+ ### Work items policy type
153
+
154
+ The `work_items` policy type fetches and updates resources through the
155
+ [Work Items REST API](https://docs.gitlab.com/api/work_items/) instead of the
156
+ legacy issues REST API. The legacy issues API cannot see custom work item
157
+ types; the `work_items` type can, which lets you triage work items of any type,
158
+ including namespace-defined custom types.
159
+
160
+ > **Warning:** The Work Items REST API is currently behind the
161
+ `:work_item_rest_api` feature flag and is pre-GA. The `work_items` policy type
162
+ only works on instances where that API is available and enabled, and its
163
+ surface may change before GA. It is not yet a stable contract for external
164
+ users. Treat this policy type as experimental until the API reaches GA.
165
+
166
+ > **Note:** A `404` or `403` from the list endpoint (feature flag off, or
167
+ insufficient access) raises a clear error rather than silently matching zero
168
+ resources.
169
+
170
+ Example:
171
+
172
+ ```yml
173
+ resource_rules:
174
+ work_items:
175
+ rules:
176
+ - name: Triage new bugs
177
+ conditions:
178
+ type: Bug
179
+ state: opened
180
+ labels:
181
+ - needs-triage
182
+ actions:
183
+ assignees:
184
+ - my-username
185
+ labels:
186
+ - triaged
187
+ ```
188
+
189
+ #### Supported conditions
190
+
191
+ - `type` - match work item types by name, including custom types. Maps to the
192
+ `work_item_type_names` filter (case-insensitive, server-side).
193
+ - `state` - `opened` or `closed`.
194
+ - `status` - custom work item status name (for example `In progress`).
195
+ Enterprise Edition only.
196
+ - `labels` / `forbidden_labels` - label names to require or exclude.
197
+ - `assignees` - assignee usernames.
198
+ - `date` - `created_at` / `updated_at` staleness (same shape as the
199
+ [date condition](#date-condition)).
200
+
201
+ #### Supported actions
202
+
203
+ - `labels` - add labels by name. If any requested label does not exist, the
204
+ automation stops immediately (matching the `labels` action on the `issues`
205
+ type) rather than applying nothing.
206
+ - `assignees` - set assignees by username. If any requested username does not
207
+ exist, the automation stops immediately rather than sending an empty
208
+ assignee set (which would unassign everyone).
209
+ - `comment` - **not yet supported.** The Work Items REST API does not yet
210
+ provide a note-create endpoint (tracked in
211
+ [gitlab-org/gitlab#604028](https://gitlab.com/gitlab-org/gitlab/-/work_items/604028)).
212
+ A `comment` action on a `work_items` policy is rejected at load time, before
213
+ any resource is fetched or acted on, with a clear error - so an unsupported
214
+ policy never runs partway through. Use the `issues` policy type if you need
215
+ to comment.
216
+
217
+ Summaries are also supported (`summarize` and `comment_on_summary`). Any other
218
+ action key (for example `remove_labels`, `mention`, `move`, or `status`) is
219
+ rejected at load time with an error listing the supported actions. On the
220
+ older policy types these keys are applied through comment quick actions, which
221
+ work items do not support yet - rejecting them up front beats silently doing
222
+ nothing.
223
+
224
+ Actions require a token with sufficient permissions (Planner or Reporter role
225
+ and above). The update endpoint can return success without persisting a change
226
+ when the token lacks permission, so after each update the applied labels and
227
+ assignees are read back from the response and the run raises if the change was
228
+ not reflected.
229
+
230
+ The `work_items` type works against both project and group sources
231
+ (`--source projects` or `--source groups`). Fetches use the source you run
232
+ against; updates and label lookups target each work item's own namespace,
233
+ because a group run can return work items living in different sub-projects and
234
+ an iid is only unique within its namespace. Project-scoped resource helpers
235
+ (for example `project_path` in templates) likewise resolve the underlying
236
+ project from the work item's namespace.
237
+
238
+ The `api:` field is not applicable to the `work_items` type, since the type
239
+ already fixes the transport to the Work Items REST API.
240
+
241
+ #### Field vocabulary
242
+
243
+ The `work_items` condition names (`type`, `status`, `state`) are deliberately
244
+ cleaner than the `issues` equivalents (`issue_type`, `work_item_status`,
245
+ `state`). This is intentional: they are meant to become the shared vocabulary
246
+ that `issues` converges onto over time (long term, `issues` would alias to
247
+ `work_items` and eventually deprecate in a major version), not a throwaway
248
+ parallel dialect. `state` (`opened` / `closed`) already means the same on both
249
+ types; `status` is new on `work_items` and has no clean one-to-one equivalent
250
+ on `issues` (where it is the prefixed `work_item_status`). Whether the old
251
+ names are carried as aliases or dropped at a major bump is a future decision.
252
+
151
253
  ### Fields
152
254
 
153
255
  A policy consists of the following fields:
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative '../sources/work_items_rest_source'
5
+ require_relative '../url_builders/url_builder'
6
+ require_relative '../resource/label'
7
+
8
+ module Gitlab
9
+ module Triage
10
+ module Action
11
+ # Applies work_items actions through the Work Items REST API update
12
+ # endpoint (PATCH) rather than the issues note/quick-action path.
13
+ #
14
+ # - labels -> features.labels.add_label_ids (resolved from names)
15
+ # - assignees -> features.assignees.assignee_ids (resolved from usernames)
16
+ #
17
+ # Missing labels or usernames stop the automation immediately (matching
18
+ # the labels action behaviour on the issues type) rather than silently
19
+ # applying nothing - which is especially important because clearing the
20
+ # assignees array would unassign everyone.
21
+ class WorkItem < Base
22
+ AssigneeDoesntExistError = Class.new(StandardError)
23
+ UpdateNotAppliedError = Class.new(StandardError)
24
+
25
+ # The dry run must not touch the network. It prints the requested
26
+ # label names and assignee usernames rather than resolving them to
27
+ # ids, mirroring how Comment::Dry avoids network calls.
28
+ class Dry < WorkItem
29
+ def act
30
+ puts "The following work items would be updated for the rule **#{policy.name}**:\n\n"
31
+
32
+ super
33
+ end
34
+
35
+ private
36
+
37
+ def perform(resource)
38
+ puts "# #{resource[:web_url]}"
39
+ puts "Would add labels: #{requested_labels.join(', ')}" if requested_labels.any?
40
+ puts "Would assign: #{requested_assignees.join(', ')}" if requested_assignees.any?
41
+ puts "\n"
42
+ end
43
+ end
44
+
45
+ def act
46
+ return unless requested_labels.any? || requested_assignees.any?
47
+
48
+ policy.resources.each do |resource|
49
+ perform(resource)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def perform(resource)
56
+ features = build_features(resource)
57
+ return if features.empty?
58
+
59
+ response = source.update(resource, features)
60
+
61
+ verify_applied!(resource, response)
62
+ end
63
+
64
+ def build_features(resource)
65
+ features = {}
66
+
67
+ features[:labels] = { add_label_ids: label_ids_to_add(resource) } if requested_labels.any?
68
+ features[:assignees] = { assignee_ids: assignee_ids } if requested_assignees.any?
69
+
70
+ features
71
+ end
72
+
73
+ def requested_labels
74
+ @requested_labels ||= Array(policy.actions[:labels])
75
+ end
76
+
77
+ def requested_assignees
78
+ @requested_assignees ||= Array(policy.actions[:assignees])
79
+ end
80
+
81
+ def source
82
+ @source ||= Sources::WorkItemsRestSource.new(
83
+ network: network, options: network.options)
84
+ end
85
+
86
+ # Label ids are namespace-specific, so resolve them against the
87
+ # resource's own namespace and cache per scope - a group-spanning run
88
+ # then makes one label lookup per distinct project/group.
89
+ def label_ids_to_add(resource)
90
+ scope = source.scope_for(resource)
91
+
92
+ @label_ids_by_scope ||= {}
93
+ @label_ids_by_scope[scope] ||= resolve_label_ids(requested_labels, scope)
94
+ end
95
+
96
+ def assignee_ids
97
+ @assignee_ids ||= resolve_assignee_ids(requested_assignees)
98
+ end
99
+
100
+ # Resolve every requested label name to an id. Any name that does not
101
+ # resolve raises, so the run stops instead of PATCHing a partial (or
102
+ # empty) label set.
103
+ def resolve_label_ids(names, scope)
104
+ return [] if names.empty?
105
+
106
+ by_name = network.query_api(labels_url(scope)).index_by { |label| label[:name] }
107
+
108
+ names.map do |name|
109
+ label = by_name[name]
110
+ raise Resource::Label::LabelDoesntExistError, "Label `#{name}` doesn't exist!" unless label
111
+
112
+ label[:id]
113
+ end
114
+ end
115
+
116
+ # Resolve every requested username to an id. A missing username raises
117
+ # rather than being dropped, because an empty assignee_ids array would
118
+ # clear all assignees on the work item.
119
+ def resolve_assignee_ids(usernames)
120
+ return [] if usernames.empty?
121
+
122
+ usernames.map do |username|
123
+ id = resolve_user_id(username)
124
+ raise AssigneeDoesntExistError, "User `#{username}` doesn't exist!" unless id
125
+
126
+ id
127
+ end
128
+ end
129
+
130
+ def resolve_user_id(username)
131
+ users = network.query_api(users_url(username))
132
+
133
+ users.first&.dig(:id)
134
+ end
135
+
136
+ # The Work Items REST update endpoint can return success without
137
+ # persisting a change when the token lacks permission. The response
138
+ # echoes the resulting widgets, so confirm the requested ids landed and
139
+ # raise otherwise rather than reporting a silent no-op as success.
140
+ def verify_applied!(resource, response)
141
+ verify_labels_applied!(resource, response) if requested_labels.any?
142
+ verify_assignees_applied!(resource, response) if requested_assignees.any?
143
+ end
144
+
145
+ def verify_labels_applied!(resource, response)
146
+ applied_ids = [*response.dig(:features, :labels, :labels)].pluck(:id)
147
+ missing = label_ids_to_add(resource) - applied_ids
148
+ return if missing.empty?
149
+
150
+ raise UpdateNotAppliedError,
151
+ "Labels were not applied to #{resource[:web_url]}; the token may lack permission."
152
+ end
153
+
154
+ def verify_assignees_applied!(resource, response)
155
+ applied_ids = [*response.dig(:features, :assignees)].pluck(:id)
156
+ missing = assignee_ids - applied_ids
157
+ return if missing.empty?
158
+
159
+ raise UpdateNotAppliedError,
160
+ "Assignees were not applied to #{resource[:web_url]}; the token may lack permission."
161
+ end
162
+
163
+ def labels_url(scope)
164
+ scope_source, scope_id = scope
165
+
166
+ UrlBuilders::UrlBuilder.new(
167
+ network_options: network.options,
168
+ source: scope_source,
169
+ source_id: scope_id,
170
+ resource_type: 'labels',
171
+ params: { per_page: 100 }
172
+ ).build
173
+ end
174
+
175
+ def users_url(username)
176
+ options = network.options
177
+
178
+ "#{options.host_url}/api/#{options.api_version}/users?username=#{CGI.escape(username)}"
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -6,6 +6,7 @@ require_relative 'action/comment_on_summary'
6
6
  require_relative 'action/issue'
7
7
  require_relative 'action/delete'
8
8
  require_relative 'action/work_item_status'
9
+ require_relative 'action/work_item'
9
10
 
10
11
  module Gitlab
11
12
  module Triage
@@ -13,18 +14,34 @@ module Gitlab
13
14
  def self.process(policy:, **args)
14
15
  policy.validate!
15
16
 
16
- [
17
- [Summarize, policy.summarize?],
18
- [Comment, policy.comment?],
19
- [CommentOnSummary, policy.comment_on_summary?],
20
- [Issue, policy.issue?],
21
- [Delete, policy.delete?],
22
- [WorkItemStatus, policy.work_item_status?]
23
- ].each do |action, active|
17
+ actions_for(policy).each do |action, active|
24
18
  act(action: action, policy: policy, **args) if active
25
19
  end
26
20
  end
27
21
 
22
+ # Work items are fetched and updated through the Work Items REST API,
23
+ # which does not share the issues note/quick-action write path. Their
24
+ # actions (assign, label, comment) are handled by a dedicated action
25
+ # instead of the note-based Comment action.
26
+ def self.actions_for(policy)
27
+ if policy.type == 'work_items'
28
+ [
29
+ [Summarize, policy.summarize?],
30
+ [WorkItem, policy.work_item?],
31
+ [CommentOnSummary, policy.comment_on_summary?]
32
+ ]
33
+ else
34
+ [
35
+ [Summarize, policy.summarize?],
36
+ [Comment, policy.comment?],
37
+ [CommentOnSummary, policy.comment_on_summary?],
38
+ [Issue, policy.issue?],
39
+ [Delete, policy.delete?],
40
+ [WorkItemStatus, policy.work_item_status?]
41
+ ]
42
+ end
43
+ end
44
+
28
45
  def self.act(action:, dry:, **args)
29
46
  klass =
30
47
  if dry
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Gitlab
6
+ module Triage
7
+ module APIObservability
8
+ class Tracker
9
+ PAGINATION_PARAMS = %w[page per_page].freeze
10
+
11
+ attr_reader :calls, :resources_fetched
12
+
13
+ def initialize
14
+ @calls = Hash.new(0)
15
+ @resources_fetched = 0
16
+ @rate_limit_waits = 0
17
+ @start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
18
+ end
19
+
20
+ def record(method, url, resource_count: 0)
21
+ @calls["#{method} #{normalize_endpoint(url)}"] += 1
22
+ @resources_fetched += resource_count
23
+ end
24
+
25
+ def record_rate_limit_wait
26
+ @rate_limit_waits += 1
27
+ end
28
+
29
+ def total
30
+ @calls.values.sum
31
+ end
32
+
33
+ def print_summary
34
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_time
35
+
36
+ puts "\n"
37
+ puts '=' * 70
38
+ puts ' API Call Summary'
39
+ puts '=' * 70
40
+ puts " Total HTTP requests: #{total}"
41
+ puts " Resources fetched: #{@resources_fetched}"
42
+ puts " Rate limit waits: #{@rate_limit_waits}"
43
+ puts " Elapsed time: #{elapsed.round(1)}s"
44
+ puts '-' * 70
45
+
46
+ @calls.sort_by { |_, count| -count }.each do |endpoint, count|
47
+ puts " #{count.to_s.rjust(5)}x #{endpoint}"
48
+ end
49
+
50
+ puts '=' * 70
51
+ end
52
+
53
+ private
54
+
55
+ def normalize_endpoint(url)
56
+ uri = URI.parse(url)
57
+ path = uri.path
58
+ .sub(%r{/api/v\d+}, '')
59
+ .sub(%r{/projects/[^/]+}, '/projects/:id')
60
+ .sub(%r{/groups/[^/]+}, '/groups/:id')
61
+ .gsub(%r{/\d+(/|\z)}, '/:id\1')
62
+
63
+ query_keys = (URI.decode_www_form(uri.query || '').map(&:first) - PAGINATION_PARAMS).sort
64
+
65
+ path += "?#{query_keys.join('&')}" if query_keys.any?
66
+ path
67
+ rescue URI::InvalidURIError
68
+ url
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -15,6 +15,7 @@ require_relative 'filters/assignee_member_conditions_filter'
15
15
  require_relative 'filters/discussions_conditions_filter'
16
16
  require_relative 'filters/ruby_conditions_filter'
17
17
  require_relative 'filters/work_item_status_conditions_filter'
18
+ require_relative 'filters/work_item_date_conditions_filter'
18
19
  require_relative 'limiters/date_field_limiter'
19
20
  require_relative 'action'
20
21
  require_relative 'policies/rule_policy'
@@ -31,7 +32,11 @@ require_relative 'rest_api_network'
31
32
  require_relative 'network_adapters/httparty_adapter'
32
33
  require_relative 'network_adapters/graphql_adapter'
33
34
  require_relative 'graphql_queries/query_builder'
35
+ require_relative 'sources/work_items_rest_source'
36
+ require_relative 'validators/params_validator'
37
+ require_relative 'validators/policy_validator'
34
38
  require_relative 'ui'
39
+ require_relative 'api_observability'
35
40
 
36
41
  module Gitlab
37
42
  module Triage
@@ -45,7 +50,8 @@ module Gitlab
45
50
  date: {
46
51
  'branches' => Filters::BranchDateFilter,
47
52
  'issues' => Filters::IssueDateConditionsFilter,
48
- 'merge_requests' => Filters::MergeRequestDateConditionsFilter
53
+ 'merge_requests' => Filters::MergeRequestDateConditionsFilter,
54
+ 'work_items' => Filters::WorkItemDateConditionsFilter
49
55
  },
50
56
  protected: Filters::BranchProtectedFilter,
51
57
  assignee_member: Filters::AssigneeMemberConditionsFilter,
@@ -62,7 +68,8 @@ module Gitlab
62
68
  DEFAULT_GRAPHQL_ADAPTER = Gitlab::Triage::NetworkAdapters::GraphqlAdapter
63
69
  ALLOWED_STATE_VALUES = {
64
70
  issues: %w[opened closed],
65
- merge_requests: %w[opened closed merged]
71
+ merge_requests: %w[opened closed merged],
72
+ work_items: %w[opened closed]
66
73
  }.with_indifferent_access.freeze
67
74
  MILESTONE_TIMEBOX_VALUES = %w[none any upcoming started].freeze
68
75
  ITERATION_SELECTION_VALUES = %w[none any].freeze
@@ -88,22 +95,30 @@ module Gitlab
88
95
  end
89
96
 
90
97
  def perform
98
+ Validators::PolicyValidator.new(resource_rules).validate!
99
+
91
100
  puts "Performing a dry run.\n\n" if options.dry_run
92
101
 
93
- puts Gitlab::Triage::UI.header("Triaging the `#{options.source_id}` #{options.source.singularize}", char: '=')
94
- puts
102
+ unless options.quiet
103
+ puts Gitlab::Triage::UI.header("Triaging the `#{options.source_id}` #{options.source.singularize}", char: '=')
104
+ puts
105
+ end
95
106
 
96
107
  resource_rules.each do |resource_type, policy_definition|
97
108
  next unless right_resource_type_for_resource_option?(resource_type)
98
109
 
99
110
  assert_epic_rule!(resource_type)
100
111
 
101
- puts Gitlab::Triage::UI.header("Processing summaries & rules for #{resource_type}", char: '-')
102
- puts
112
+ unless options.quiet
113
+ puts Gitlab::Triage::UI.header("Processing summaries & rules for #{resource_type}", char: '-')
114
+ puts
115
+ end
103
116
 
104
117
  process_summaries(resource_type, policy_definition[:summaries])
105
118
  process_rules(resource_type, policy_definition[:rules])
106
119
  end
120
+
121
+ tracker.print_summary if options.summary
107
122
  end
108
123
 
109
124
  def network
@@ -118,6 +133,11 @@ module Gitlab
118
133
  @graphql_network ||= GraphqlNetwork.new(graphql_network_adapter)
119
134
  end
120
135
 
136
+ def work_items_source
137
+ @work_items_source ||= Sources::WorkItemsRestSource.new(
138
+ network: network, options: options, per_page: per_page)
139
+ end
140
+
121
141
  private
122
142
 
123
143
  def assert_options!
@@ -185,7 +205,7 @@ module Gitlab
185
205
  resource_reference = options.resource_reference
186
206
 
187
207
  case resource_type
188
- when 'issues'
208
+ when 'issues', 'work_items'
189
209
  resource_reference.start_with?('#')
190
210
  when 'merge_requests'
191
211
  resource_reference.start_with?('!')
@@ -204,12 +224,16 @@ module Gitlab
204
224
  @resource_rules ||= policies.delete(:resource_rules) { {} }
205
225
  end
206
226
 
227
+ def tracker
228
+ @tracker ||= options.summary ? APIObservability::Tracker.new : nil
229
+ end
230
+
207
231
  def network_adapter
208
- @network_adapter ||= @network_adapter_class.new(options)
232
+ @network_adapter ||= @network_adapter_class.new(options, tracker: tracker)
209
233
  end
210
234
 
211
235
  def graphql_network_adapter
212
- @graphql_network_adapter ||= @graphql_network_adapter_class.new(options)
236
+ @graphql_network_adapter ||= @graphql_network_adapter_class.new(options, tracker: tracker)
213
237
  end
214
238
 
215
239
  def rule_conditions(rule)
@@ -310,8 +334,10 @@ module Gitlab
310
334
  #
311
335
  # @return [nil]
312
336
  def process_summary(resource_type, summary_definition)
313
- puts Gitlab::Triage::UI.header("Processing summary: **#{summary_definition[:name]}**", char: '~')
314
- puts
337
+ unless options.quiet
338
+ puts Gitlab::Triage::UI.header("Processing summary: **#{summary_definition[:name]}**", char: '~')
339
+ puts
340
+ end
315
341
 
316
342
  summary_parts_for_rules(resource_type, summary_definition[:rules]) do |summary_resources|
317
343
  policy = Policies::SummaryPolicy.new(
@@ -365,7 +391,7 @@ module Gitlab
365
391
  #
366
392
  # @return [nil]
367
393
  def resources_for_rule(resource_type, rule_definition, in_summary: false)
368
- puts Gitlab::Triage::UI.header("Gathering resources for rule: **#{rule_definition[:name]}**", char: '-')
394
+ puts Gitlab::Triage::UI.header("Gathering resources for rule: **#{rule_definition[:name]}**", char: '-') unless options.quiet
369
395
 
370
396
  # Skip resource fetching for custom type rules
371
397
  if rule_definition[:type] == 'custom'
@@ -373,7 +399,7 @@ module Gitlab
373
399
  raise ArgumentError, "type: custom can only be used in summary rules, not in top-level rules (rule: #{rule_definition[:name]})"
374
400
  end
375
401
 
376
- puts "\n* Skipping resource fetch for custom type rule"
402
+ puts "\n* Skipping resource fetch for custom type rule" unless options.quiet
377
403
  yield(PoliciesResources::RuleResources.new([]), {})
378
404
  return
379
405
  end
@@ -391,14 +417,23 @@ module Gitlab
391
417
  # In some filters/actions we want to know which resource type it is
392
418
  attach_resource_type(resources, resource_type)
393
419
 
394
- puts "\n\n* Found #{resources.count} resources..."
395
- print "* Filtering resources..."
420
+ unless options.quiet
421
+ puts "\n\n* Found #{resources.count} resources..."
422
+ print "* Filtering resources..."
423
+ end
424
+
396
425
  resources = filter_resources(resources, expanded_conditions)
397
- puts "\n* Total after filtering: #{resources.count} resources"
398
- print "* Limiting resources..."
426
+
427
+ unless options.quiet
428
+ puts "\n* Total after filtering: #{resources.count} resources"
429
+ print "* Limiting resources..."
430
+ end
431
+
399
432
  resources = limit_resources(resources, rule_limits(rule_definition))
400
- puts "\n* Total after limiting: #{resources.count} resources"
401
- puts
433
+ unless options.quiet
434
+ puts "\n* Total after limiting: #{resources.count} resources"
435
+ puts
436
+ end
402
437
 
403
438
  resources = sanitize_resources(resources)
404
439
 
@@ -407,6 +442,8 @@ module Gitlab
407
442
  end
408
443
 
409
444
  def fetch_resources(resource_type, expanded_conditions, rule_definition)
445
+ return fetch_work_items(expanded_conditions) if resource_type == 'work_items'
446
+
410
447
  resources = []
411
448
 
412
449
  if rule_definition[:api] == 'graphql'
@@ -454,6 +491,21 @@ module Gitlab
454
491
  resources
455
492
  end
456
493
 
494
+ def fetch_work_items(expanded_conditions)
495
+ conditions = expanded_conditions.dup
496
+ conditions[:iids] = options.resource_reference[1..] if options.resource_reference
497
+
498
+ # The work_items path bypasses build_get_url, so validate `state` here
499
+ # the same way the URL builders do for the other resource types.
500
+ if conditions[:state]
501
+ ParamsValidator.new(
502
+ [{ name: 'state', type: String, values: ALLOWED_STATE_VALUES[:work_items] }],
503
+ { 'state' => conditions[:state] }).validate!
504
+ end
505
+
506
+ work_items_source.list(conditions)
507
+ end
508
+
457
509
  def attach_resource_type(resources, resource_type)
458
510
  resources.each { |resource| resource[:type] = resource_type }
459
511
  end
@@ -470,7 +522,7 @@ module Gitlab
470
522
  policy: policy,
471
523
  network: network,
472
524
  dry: options.dry_run)
473
- puts
525
+ puts unless options.quiet
474
526
  end
475
527
 
476
528
  def filter_resources(resources, conditions)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'issue_date_conditions_filter'
4
+
5
+ module Gitlab
6
+ module Triage
7
+ module Filters
8
+ class WorkItemDateConditionsFilter < IssueDateConditionsFilter
9
+ end
10
+ end
11
+ end
12
+ end