gitlab-triage 1.57.0 → 1.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitlab/merge_request_templates/Default.md +4 -0
- data/CONTRIBUTING.md +22 -0
- data/Gemfile.lock +1 -1
- data/README.md +72 -16
- data/lib/gitlab/triage/action/comment.rb +13 -9
- data/lib/gitlab/triage/action/delete.rb +3 -2
- data/lib/gitlab/triage/action/work_item.rb +14 -4
- data/lib/gitlab/triage/action/work_item_comment.rb +39 -0
- data/lib/gitlab/triage/action.rb +10 -6
- data/lib/gitlab/triage/engine.rb +9 -16
- data/lib/gitlab/triage/milestone_wildcards.rb +22 -0
- data/lib/gitlab/triage/network_adapters/httparty_adapter.rb +1 -0
- data/lib/gitlab/triage/normalizers/work_item_normalizer.rb +12 -0
- data/lib/gitlab/triage/rest_api_network.rb +16 -0
- data/lib/gitlab/triage/sources/work_items_rest_source.rb +64 -20
- data/lib/gitlab/triage/validators/policy_validator.rb +145 -17
- data/lib/gitlab/triage/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8a6d5407a6cccbf8711ab5d9e6cfd060165831e32880681257a6d12ed2290df3
|
|
4
|
+
data.tar.gz: 30aca49b70d3c65f56fcb5cd34275d6aad83aea62f54b683d1196af54c8c312a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba6a0e711a7c06f6c99f1af0471d68cd7c335dbfd53d5456f571414e79ce6c9454f6e9df0379ff89fdb57f155f8947cde33ce164ee19766f75c75f89cc8df345
|
|
7
|
+
data.tar.gz: da23490510ce31cf2abb2ece62189c9d464421ef15be0b07a44245fce4619afb0df52216674759a9dd07c2d07a7284b00fde9ef66e252c5e1a9e43fb81635c81
|
|
@@ -8,4 +8,8 @@ that reviewers can understand your intent. Keeping the description updated is
|
|
|
8
8
|
especially important if they didn't participate in the discussion.
|
|
9
9
|
-->
|
|
10
10
|
|
|
11
|
+
## Checklist
|
|
12
|
+
|
|
13
|
+
- [ ] User-visible commits have a `Changelog:` trailer (see [CONTRIBUTING.md](https://gitlab.com/gitlab-org/ruby/gems/gitlab-triage/-/blob/master/CONTRIBUTING.md#changelog)).
|
|
14
|
+
|
|
11
15
|
<!-- template sourced from https://gitlab.com/gitlab-org/ruby/gems/gitlab-triage/-/blob/master/.gitlab/merge_request_templates/Default.md -->
|
data/CONTRIBUTING.md
CHANGED
|
@@ -29,3 +29,25 @@ git checkout my_branch
|
|
|
29
29
|
bundle install --path vendor/bundle
|
|
30
30
|
bundle exec rake spec
|
|
31
31
|
```
|
|
32
|
+
|
|
33
|
+
## Changelog
|
|
34
|
+
|
|
35
|
+
Release notes are generated from `Changelog:` commit trailers using the
|
|
36
|
+
[GitLab changelog API](https://docs.gitlab.com/ee/api/repositories.html#generate-changelog-data).
|
|
37
|
+
The available categories are listed in `.gitlab/changelog_config.yml`:
|
|
38
|
+
`added`, `fixed`, `changed`, `deprecated`, `removed`, `security`,
|
|
39
|
+
`performance`, and `other`.
|
|
40
|
+
|
|
41
|
+
- Add a trailer to every commit with a user-visible change
|
|
42
|
+
- Dependency updates, documentation-only changes, and internal refactors
|
|
43
|
+
don't need one
|
|
44
|
+
- Use `changed` when a change makes previously-loading policies fail
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Add remove_labels action to the work_items policy type
|
|
48
|
+
|
|
49
|
+
The work_items type only supported labels and assignees, so
|
|
50
|
+
remove_labels was rejected at load time.
|
|
51
|
+
|
|
52
|
+
Changelog: added
|
|
53
|
+
```
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -188,15 +188,61 @@ resource_rules:
|
|
|
188
188
|
|
|
189
189
|
#### Supported conditions
|
|
190
190
|
|
|
191
|
+
Sent to the Work Items REST API:
|
|
192
|
+
|
|
191
193
|
- `type` - match work item types by name, including custom types. Maps to the
|
|
192
|
-
`work_item_type_names` filter (case-insensitive, server-side).
|
|
193
|
-
|
|
194
|
+
`work_item_type_names` filter (case-insensitive, server-side). Any work item
|
|
195
|
+
type name is accepted.
|
|
196
|
+
- `state` - `opened` or `closed`, checked at load time.
|
|
194
197
|
- `status` - custom work item status name (for example `In progress`).
|
|
195
198
|
Enterprise Edition only.
|
|
196
199
|
- `labels` / `forbidden_labels` - label names to require or exclude.
|
|
197
200
|
- `assignees` - assignee usernames.
|
|
201
|
+
- `milestone` - one milestone title, or one of `none`, `any`, `upcoming`,
|
|
202
|
+
`started` (case-insensitive). Maps to the `milestone_title` or
|
|
203
|
+
`milestone_wildcard_id` filter. Same shape as the
|
|
204
|
+
[milestone condition](#milestone-condition). A list of more than one
|
|
205
|
+
milestone, or a blank one, is rejected at load time. Quote titles that look
|
|
206
|
+
like numbers (`milestone: '19.10'`): YAML reads an unquoted `19.10` as
|
|
207
|
+
`19.1`, so it is rejected too.
|
|
208
|
+
|
|
209
|
+
`upcoming` and `started` follow GitLab's current milestone logic, so they
|
|
210
|
+
can match different work items than the same values on `issues`. The
|
|
211
|
+
Issues REST API keeps the logic deprecated in GitLab 17.7 to avoid a
|
|
212
|
+
breaking change
|
|
213
|
+
([gitlab-org/gitlab#501294](https://gitlab.com/gitlab-org/gitlab/-/issues/501294)),
|
|
214
|
+
and the Work Items REST API does not:
|
|
215
|
+
|
|
216
|
+
- `upcoming`: on `issues`, the one milestone per project or group with the
|
|
217
|
+
nearest future due date, often the milestone in progress. On
|
|
218
|
+
`work_items`, every milestone that has not started yet.
|
|
219
|
+
- `started`: on `issues`, active milestones that have started, including
|
|
220
|
+
overdue ones. On `work_items`, only those whose due date has not passed.
|
|
221
|
+
|
|
222
|
+
Dry-run a rule that uses `upcoming` or `started` before moving it from
|
|
223
|
+
`issues` to `work_items`.
|
|
198
224
|
- `date` - `created_at` / `updated_at` staleness (same shape as the
|
|
199
|
-
[date condition](#date-condition)).
|
|
225
|
+
[date condition](#date-condition)). `attribute`, `condition`,
|
|
226
|
+
`interval_type`, and `interval` are all required and checked at load time,
|
|
227
|
+
with or without `filter_in_ruby`, and `interval` must be a positive number.
|
|
228
|
+
A missing or unsupported value would otherwise send no date filter, so the
|
|
229
|
+
rule would match every work item, and a misspelled `condition` would select
|
|
230
|
+
the opposite work items.
|
|
231
|
+
|
|
232
|
+
Run in Ruby on the fetched work items:
|
|
233
|
+
|
|
234
|
+
- `author_member` - see [author member condition](#author-member-condition).
|
|
235
|
+
- `assignee_member` - see [assignee member condition](#assignee-member-condition).
|
|
236
|
+
- `ruby` - see [ruby condition](#ruby-condition).
|
|
237
|
+
- `no_additional_labels` - see [no additional labels condition](#no-additional-labels-condition).
|
|
238
|
+
- `work_item_status` - see [work item status condition](#work-item-status-condition).
|
|
239
|
+
|
|
240
|
+
Any other condition key (for example `issue_type`, `weight`, or a misspelled
|
|
241
|
+
key) is rejected at load time, before any resource is fetched, with an error
|
|
242
|
+
listing the supported conditions. Ignoring it instead would make the rule match
|
|
243
|
+
more work items than intended. `votes`, `upvotes`, and `discussions` are not
|
|
244
|
+
supported yet (tracked in
|
|
245
|
+
[gitlab-org/gitlab#630293](https://gitlab.com/gitlab-org/gitlab/-/work_items/630293)).
|
|
200
246
|
|
|
201
247
|
#### Supported actions
|
|
202
248
|
|
|
@@ -208,20 +254,18 @@ resource_rules:
|
|
|
208
254
|
- `assignees` - set assignees by username. If any requested username does not
|
|
209
255
|
exist, the automation stops immediately rather than sending an empty
|
|
210
256
|
assignee set (which would unassign everyone).
|
|
211
|
-
- `comment` -
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
257
|
+
- `comment` - post a comment (note) on the work item through the Work Items REST
|
|
258
|
+
API. Supports the same body templating as the `issues` type, along with
|
|
259
|
+
`mention`, `comment_type: thread`, `comment_internal`, and
|
|
260
|
+
`redact_confidential_resources`. The `{{closed_by}}` placeholder needs
|
|
261
|
+
GitLab 19.6 or later. Quick actions in the body are run by the API, but the
|
|
262
|
+
available command set follows the work item type's enabled widgets, so some
|
|
263
|
+
issue-only commands are no-ops on work items.
|
|
218
264
|
|
|
219
265
|
Summaries are also supported (`summarize` and `comment_on_summary`). Any other
|
|
220
|
-
action key (for example `
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
work items do not support yet - rejecting them up front beats silently doing
|
|
224
|
-
nothing.
|
|
266
|
+
action key (for example `move` or `status`) is rejected at load time with an
|
|
267
|
+
error listing the supported actions, since those actions are not wired for the
|
|
268
|
+
`work_items` type yet - rejecting them up front beats silently doing nothing.
|
|
225
269
|
|
|
226
270
|
Actions require a token with sufficient permissions (Planner or Reporter role
|
|
227
271
|
and above). The update endpoint can return success without persisting a change
|
|
@@ -235,7 +279,15 @@ against; updates and label lookups target each work item's own namespace,
|
|
|
235
279
|
because a group run can return work items living in different sub-projects and
|
|
236
280
|
an iid is only unique within its namespace. Project-scoped resource helpers
|
|
237
281
|
(for example `project_path` in templates) likewise resolve the underlying
|
|
238
|
-
project from the work item's namespace.
|
|
282
|
+
project from the work item's namespace. `full_resource_reference` and the
|
|
283
|
+
`{{full_reference}}` placeholder render the work item's full reference (for
|
|
284
|
+
example `gitlab-org/gitlab#42`), the same as on `issues`. `resource[:health_status]`
|
|
285
|
+
is a string such as `on_track`, `needs_attention`, or `at_risk`, the same as on
|
|
286
|
+
`issues`. It is `nil` when no health status is set, and always `nil` on GitLab
|
|
287
|
+
Community Edition or a namespace without the license for health status. After a
|
|
288
|
+
license downgrade, work items can keep a stored health status that the API no
|
|
289
|
+
longer returns, so a rule such as `resource[:health_status] != 'at_risk'` can
|
|
290
|
+
match work items that are at risk.
|
|
239
291
|
|
|
240
292
|
The `api:` field is not applicable to the `work_items` type, since the type
|
|
241
293
|
already fixes the transport to the Work Items REST API.
|
|
@@ -1526,6 +1578,10 @@ resource_rules:
|
|
|
1526
1578
|
|
|
1527
1579
|
Delete the resource.
|
|
1528
1580
|
|
|
1581
|
+
When GitLab refuses a deletion, for example with 403 because the token may not
|
|
1582
|
+
delete the resource, a warning with the URL, the status and GitLab's message is
|
|
1583
|
+
printed, and the rule goes on with the next resource.
|
|
1584
|
+
|
|
1529
1585
|
Accept a boolean. Set to `true` to enable.
|
|
1530
1586
|
|
|
1531
1587
|
Example :
|
|
@@ -61,21 +61,25 @@ module Gitlab
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def perform(resource, comment)
|
|
64
|
-
|
|
65
|
-
if policy.actions.fetch(:redact_confidential_resources, true) && resource[:confidential]
|
|
66
|
-
puts Gitlab::Triage::UI.debug "Comment action resource: (confidential)"
|
|
67
|
-
else
|
|
68
|
-
puts Gitlab::Triage::UI.debug "Comment action resource: #{resource.inspect}"
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
puts Gitlab::Triage::UI.debug "Comment action content: #{comment}"
|
|
72
|
-
end
|
|
64
|
+
print_debug(resource, comment)
|
|
73
65
|
|
|
74
66
|
network.post_api(
|
|
75
67
|
build_post_url(resource),
|
|
76
68
|
post_body(comment))
|
|
77
69
|
end
|
|
78
70
|
|
|
71
|
+
def print_debug(resource, comment)
|
|
72
|
+
return unless network.options.debug
|
|
73
|
+
|
|
74
|
+
if policy.actions.fetch(:redact_confidential_resources, true) && resource[:confidential]
|
|
75
|
+
puts Gitlab::Triage::UI.debug "Comment action resource: (confidential)"
|
|
76
|
+
else
|
|
77
|
+
puts Gitlab::Triage::UI.debug "Comment action resource: #{resource.inspect}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
puts Gitlab::Triage::UI.debug "Comment action content: #{comment}"
|
|
81
|
+
end
|
|
82
|
+
|
|
79
83
|
def build_post_url(resource)
|
|
80
84
|
url_builder_opts = {
|
|
81
85
|
network_options: network.options,
|
|
@@ -15,7 +15,7 @@ module Gitlab
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
17
|
def perform(resource)
|
|
18
|
-
puts "DELETE
|
|
18
|
+
puts "DELETE #{build_delete_url(resource)}"
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -36,7 +36,8 @@ module Gitlab
|
|
|
36
36
|
def build_delete_url(resource)
|
|
37
37
|
delete_url = UrlBuilders::UrlBuilder.new(
|
|
38
38
|
source: policy.source,
|
|
39
|
-
|
|
39
|
+
# Branches carry no project_id, so they fall back to --source-id.
|
|
40
|
+
source_id: resource[policy.source_id_sym] || network.options.source_id,
|
|
40
41
|
resource_type: policy.type,
|
|
41
42
|
resource_id: resource_id(resource),
|
|
42
43
|
network_options: network.options
|
|
@@ -22,9 +22,8 @@ module Gitlab
|
|
|
22
22
|
class WorkItem < Base
|
|
23
23
|
UpdateNotAppliedError = Class.new(StandardError)
|
|
24
24
|
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# ids, mirroring how Comment::Dry avoids network calls.
|
|
25
|
+
# Prints the requested changes without writing them. verify! still
|
|
26
|
+
# looks the names up first, so a typo fails the dry run too.
|
|
28
27
|
class Dry < WorkItem
|
|
29
28
|
def act
|
|
30
29
|
puts "The following work items would be updated for the rule **#{policy.name}**:\n\n"
|
|
@@ -43,6 +42,17 @@ module Gitlab
|
|
|
43
42
|
end
|
|
44
43
|
end
|
|
45
44
|
|
|
45
|
+
# Resolves every item's labels and assignees up front, so a name missing
|
|
46
|
+
# in a later item's namespace fails the run before any item is labeled.
|
|
47
|
+
def verify!
|
|
48
|
+
policy.resources.each do |resource|
|
|
49
|
+
label_ids_to_add(resource) if requested_labels.any?
|
|
50
|
+
label_ids_to_remove(resource) if requested_removed_labels.any?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
assignee_ids if requested_assignees.any?
|
|
54
|
+
end
|
|
55
|
+
|
|
46
56
|
def act
|
|
47
57
|
return unless requested_labels.any? || requested_removed_labels.any? || requested_assignees.any?
|
|
48
58
|
|
|
@@ -126,7 +136,7 @@ module Gitlab
|
|
|
126
136
|
|
|
127
137
|
def labels_by_name(scope)
|
|
128
138
|
@labels_by_name_by_scope ||= {}
|
|
129
|
-
@labels_by_name_by_scope[scope] ||= network.
|
|
139
|
+
@labels_by_name_by_scope[scope] ||= network.query_api_cached(labels_url(scope)).index_by { |label| label[:name] }
|
|
130
140
|
end
|
|
131
141
|
|
|
132
142
|
# The Work Items REST update endpoint can return success without
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'comment'
|
|
4
|
+
require_relative '../sources/work_items_rest_source'
|
|
5
|
+
|
|
6
|
+
module Gitlab
|
|
7
|
+
module Triage
|
|
8
|
+
module Action
|
|
9
|
+
# Posts comments on work items through the Work Items REST API notes
|
|
10
|
+
# endpoint rather than the issues note/quick-action path. The comment body
|
|
11
|
+
# (templating, redaction, and mentions) is built exactly as on the issues
|
|
12
|
+
# type - only the write target differs, so this reuses Comment and
|
|
13
|
+
# overrides just where the note is sent.
|
|
14
|
+
class WorkItemComment < Comment
|
|
15
|
+
# WorkItem writes labels before this posts, so reject an unknown
|
|
16
|
+
# comment_type up front rather than after the labels are written.
|
|
17
|
+
def verify!
|
|
18
|
+
sub_resource_type
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# Raising would also lose every later item's note, and any labels are
|
|
24
|
+
# already written. Warn and move on, as issues drop a rejected note.
|
|
25
|
+
def perform(resource, comment)
|
|
26
|
+
print_debug(resource, comment)
|
|
27
|
+
source.create_note(resource, post_body(comment), sub_resource_type: sub_resource_type)
|
|
28
|
+
rescue Sources::WorkItemsRestSource::NoteNotCreatedError => e
|
|
29
|
+
puts Gitlab::Triage::UI.warn(e.message)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def source
|
|
33
|
+
@source ||= Sources::WorkItemsRestSource.new(
|
|
34
|
+
network: network, options: network.options)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/gitlab/triage/action.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative 'action/issue'
|
|
|
7
7
|
require_relative 'action/delete'
|
|
8
8
|
require_relative 'action/work_item_status'
|
|
9
9
|
require_relative 'action/work_item'
|
|
10
|
+
require_relative 'action/work_item_comment'
|
|
10
11
|
require_relative 'action/update'
|
|
11
12
|
|
|
12
13
|
module Gitlab
|
|
@@ -24,10 +25,9 @@ module Gitlab
|
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# scheduled run posts it again.
|
|
28
|
+
# Without this, a rule naming a label, user, milestone, or move
|
|
29
|
+
# destination that no longer exists raises only after earlier actions
|
|
30
|
+
# wrote, such as an issues note the next scheduled run posts again.
|
|
31
31
|
def self.verify!(actions, policy:, network:)
|
|
32
32
|
actions.each do |action|
|
|
33
33
|
action.new(policy: policy, network: network).verify!
|
|
@@ -36,13 +36,17 @@ module Gitlab
|
|
|
36
36
|
|
|
37
37
|
# Work items are fetched and updated through the Work Items REST API,
|
|
38
38
|
# which does not share the issues note/quick-action write path. Their
|
|
39
|
-
# actions
|
|
40
|
-
#
|
|
39
|
+
# actions are handled by dedicated actions: WorkItem (assign, label) and
|
|
40
|
+
# WorkItemComment, which posts through the REST notes endpoint rather than
|
|
41
|
+
# the issues Comment path. Unlike issues, labels are written before the
|
|
42
|
+
# note: a failed label write then loses a note once, instead of leaving
|
|
43
|
+
# unlabeled items that get the note again on every run.
|
|
41
44
|
def self.actions_for(policy)
|
|
42
45
|
if policy.type == 'work_items'
|
|
43
46
|
[
|
|
44
47
|
[Summarize, policy.summarize?],
|
|
45
48
|
[WorkItem, policy.work_item?],
|
|
49
|
+
[WorkItemComment, policy.comment?],
|
|
46
50
|
[CommentOnSummary, policy.comment_on_summary?]
|
|
47
51
|
]
|
|
48
52
|
else
|
data/lib/gitlab/triage/engine.rb
CHANGED
|
@@ -33,6 +33,7 @@ require_relative 'rest_api_network'
|
|
|
33
33
|
require_relative 'network_adapters/httparty_adapter'
|
|
34
34
|
require_relative 'network_adapters/graphql_adapter'
|
|
35
35
|
require_relative 'graphql_queries/query_builder'
|
|
36
|
+
require_relative 'milestone_wildcards'
|
|
36
37
|
require_relative 'sources/work_items_rest_source'
|
|
37
38
|
require_relative 'validators/params_validator'
|
|
38
39
|
require_relative 'validators/policy_validator'
|
|
@@ -47,6 +48,8 @@ module Gitlab
|
|
|
47
48
|
# This filter map is used to help make the filter_resource method
|
|
48
49
|
# smaller. We loop through each of the keys (conditions) and map
|
|
49
50
|
# that to the filters that will be used for it.
|
|
51
|
+
# A filter enabled for work_items must also be added to
|
|
52
|
+
# Validators::PolicyValidator::RUBY_WORK_ITEM_CONDITIONS.
|
|
50
53
|
FILTER_MAP = {
|
|
51
54
|
date: {
|
|
52
55
|
'branches' => Filters::BranchDateFilter,
|
|
@@ -70,9 +73,9 @@ module Gitlab
|
|
|
70
73
|
ALLOWED_STATE_VALUES = {
|
|
71
74
|
issues: %w[opened closed],
|
|
72
75
|
merge_requests: %w[opened closed merged],
|
|
73
|
-
work_items:
|
|
76
|
+
work_items: Validators::PolicyValidator::WORK_ITEM_STATES
|
|
74
77
|
}.with_indifferent_access.freeze
|
|
75
|
-
MILESTONE_TIMEBOX_VALUES =
|
|
78
|
+
MILESTONE_TIMEBOX_VALUES = MilestoneWildcards::VALUES
|
|
76
79
|
ITERATION_SELECTION_VALUES = %w[none any].freeze
|
|
77
80
|
EpicsTriagingForProjectImpossibleError = Class.new(StandardError)
|
|
78
81
|
MultiPolicyInInjectionModeError = Class.new(StandardError)
|
|
@@ -515,14 +518,8 @@ module Gitlab
|
|
|
515
518
|
conditions = expanded_conditions.dup
|
|
516
519
|
conditions[:iids] = options.resource_reference[1..] if options.resource_reference
|
|
517
520
|
|
|
518
|
-
# The work_items path bypasses build_get_url, so
|
|
519
|
-
# the
|
|
520
|
-
if conditions[:state]
|
|
521
|
-
ParamsValidator.new(
|
|
522
|
-
[{ name: 'state', type: String, values: ALLOWED_STATE_VALUES[:work_items] }],
|
|
523
|
-
{ 'state' => conditions[:state] }).validate!
|
|
524
|
-
end
|
|
525
|
-
|
|
521
|
+
# The work_items path bypasses build_get_url, so `state` is not checked by
|
|
522
|
+
# the URL builders. PolicyValidator checks it in #perform before any fetch.
|
|
526
523
|
work_items_source.list(conditions)
|
|
527
524
|
end
|
|
528
525
|
|
|
@@ -679,12 +676,8 @@ module Gitlab
|
|
|
679
676
|
return if milestone_value.empty?
|
|
680
677
|
|
|
681
678
|
# Issues API should use the `milestone_id` param for timebox values, and `milestone` for milestone title
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
['milestone_id', milestone_value.titleize] # The API only accepts titleized values.
|
|
685
|
-
else
|
|
686
|
-
['milestone', milestone_value]
|
|
687
|
-
end
|
|
679
|
+
wildcard = MilestoneWildcards.api_value(milestone_value) if resource_type.to_sym == :issues
|
|
680
|
+
args = wildcard ? ['milestone_id', wildcard] : ['milestone', milestone_value]
|
|
688
681
|
|
|
689
682
|
APIQueryBuilders::SingleQueryParamBuilder.new(*args)
|
|
690
683
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
|
4
|
+
|
|
5
|
+
module Gitlab
|
|
6
|
+
module Triage
|
|
7
|
+
# Milestone condition values that select a group of milestones rather than one
|
|
8
|
+
# title. Kept apart from Engine so the work items source and the policy validator
|
|
9
|
+
# can share it: Engine requires both of them.
|
|
10
|
+
module MilestoneWildcards
|
|
11
|
+
VALUES = %w[none any upcoming started].freeze
|
|
12
|
+
|
|
13
|
+
# The Issues API (milestone_id) and the Work Items API (milestone_wildcard_id)
|
|
14
|
+
# only accept the capitalised form, for example Started. Returns nil for a title.
|
|
15
|
+
def self.api_value(milestone)
|
|
16
|
+
value = milestone.to_s.downcase
|
|
17
|
+
|
|
18
|
+
value.titleize if VALUES.include?(value)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -27,10 +27,22 @@ module Gitlab
|
|
|
27
27
|
confidential: resource[:confidential],
|
|
28
28
|
created_at: resource[:created_at],
|
|
29
29
|
updated_at: resource[:updated_at],
|
|
30
|
+
closed_at: resource[:closed_at],
|
|
30
31
|
author: resource[:author],
|
|
32
|
+
closed_by: resource[:closed_by],
|
|
33
|
+
# Same shape as the Issues API, which full_resource_reference and
|
|
34
|
+
# the {{full_reference}} placeholder read.
|
|
35
|
+
references: ({ full: resource[:reference] } if resource[:reference]),
|
|
31
36
|
labels: label_titles(features),
|
|
32
37
|
assignees: assignees(features),
|
|
38
|
+
# The issues API's deprecated single assignee, read by {{assignee}}.
|
|
39
|
+
assignee: assignees(features).first,
|
|
33
40
|
milestone: features[:milestone],
|
|
41
|
+
upvotes: features.dig(:award_emoji, :upvotes),
|
|
42
|
+
downvotes: features.dig(:award_emoji, :downvotes),
|
|
43
|
+
# Flat string, like the Issues API: ruby conditions compare
|
|
44
|
+
# resource[:health_status] with values such as 'at_risk'.
|
|
45
|
+
health_status: features.dig(:health_status, :health_status),
|
|
34
46
|
work_item_type: resource.dig(:work_item_type, :name),
|
|
35
47
|
work_item_status: status_name(features),
|
|
36
48
|
start_date: features.dig(:start_and_due_date, :start_date),
|
|
@@ -147,6 +147,7 @@ module Gitlab
|
|
|
147
147
|
|
|
148
148
|
rate_limit_debug(response) if options.debug
|
|
149
149
|
rate_limit_wait(response)
|
|
150
|
+
warn_on_failed_delete(url, response)
|
|
150
151
|
end
|
|
151
152
|
|
|
152
153
|
private
|
|
@@ -218,6 +219,21 @@ module Gitlab
|
|
|
218
219
|
sleep(1) until Time.now >= reset_at
|
|
219
220
|
end
|
|
220
221
|
|
|
222
|
+
# Warns rather than raises, so one refused DELETE does not stop the
|
|
223
|
+
# other deletions of the rule.
|
|
224
|
+
def warn_on_failed_delete(url, response)
|
|
225
|
+
status = response[:status]
|
|
226
|
+
return if status.nil? || (200..299).cover?(status)
|
|
227
|
+
|
|
228
|
+
results = response[:results]
|
|
229
|
+
message = results['message'] || results['error'] if results.is_a?(Hash)
|
|
230
|
+
|
|
231
|
+
warning = "DELETE #{url} failed with status #{status}"
|
|
232
|
+
warning += ": #{message}" if message
|
|
233
|
+
|
|
234
|
+
puts Gitlab::Triage::UI.warn(warning)
|
|
235
|
+
end
|
|
236
|
+
|
|
221
237
|
def raise_unexpected_response(results)
|
|
222
238
|
raise Errors::Network::UnexpectedResponse, "Unexpected response: #{results.inspect}"
|
|
223
239
|
end
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require 'active_support/all'
|
|
4
4
|
|
|
5
|
+
require_relative '../milestone_wildcards'
|
|
5
6
|
require_relative '../url_builders/url_builder'
|
|
6
7
|
require_relative '../normalizers/work_item_normalizer'
|
|
7
8
|
require_relative '../errors'
|
|
8
9
|
require_relative '../ui'
|
|
10
|
+
require_relative '../validators/policy_validator'
|
|
9
11
|
|
|
10
12
|
module Gitlab
|
|
11
13
|
module Triage
|
|
@@ -19,12 +21,12 @@ module Gitlab
|
|
|
19
21
|
#
|
|
20
22
|
# The source is a self-contained peer to the engine's inline issues /
|
|
21
23
|
# merge_requests / branches fetch: it owns list URL building, filter
|
|
22
|
-
# push-down, normalization,
|
|
23
|
-
#
|
|
24
|
-
# action is rejected at load time by Validators::PolicyValidator.
|
|
24
|
+
# push-down, normalization, the PATCH update path, and the notes /
|
|
25
|
+
# discussions write path used by the `comment` action.
|
|
25
26
|
class WorkItemsRestSource
|
|
26
27
|
UnavailableError = Class.new(StandardError)
|
|
27
28
|
ScopeError = Class.new(StandardError)
|
|
29
|
+
NoteNotCreatedError = Class.new(StandardError)
|
|
28
30
|
|
|
29
31
|
# Base fields opted into on the list endpoint. The endpoint is sparse
|
|
30
32
|
# by default (id, iid, global_id, title only); every field a filter or
|
|
@@ -38,15 +40,22 @@ module Gitlab
|
|
|
38
40
|
author
|
|
39
41
|
namespace
|
|
40
42
|
work_item_type
|
|
43
|
+
reference
|
|
44
|
+
closed_at
|
|
45
|
+
closed_by
|
|
41
46
|
].freeze
|
|
42
47
|
|
|
43
|
-
# Widget payloads opted into via `features`.
|
|
48
|
+
# Widget payloads opted into via `features`. status and health_status are EE
|
|
49
|
+
# only: CE accepts them on read endpoints and leaves them out of the response,
|
|
50
|
+
# as an unlicensed EE namespace does, so their values are then nil.
|
|
44
51
|
LIST_FEATURES = %w[
|
|
45
52
|
labels
|
|
46
53
|
assignees
|
|
47
54
|
milestone
|
|
48
55
|
status
|
|
49
56
|
start_and_due_date
|
|
57
|
+
award_emoji
|
|
58
|
+
health_status
|
|
50
59
|
].freeze
|
|
51
60
|
|
|
52
61
|
def initialize(network:, options:, per_page: 100, normalizer: Normalizers::WorkItemNormalizer.new)
|
|
@@ -68,6 +77,16 @@ module Gitlab
|
|
|
68
77
|
@network.patch_api(work_item_url(resource), features: features)
|
|
69
78
|
end
|
|
70
79
|
|
|
80
|
+
# The adapter does not raise on a 4xx. Check for an error message, not a
|
|
81
|
+
# missing id, since a quick-action-only note returns 202 with no id.
|
|
82
|
+
def create_note(resource, body, sub_resource_type: 'notes')
|
|
83
|
+
response = @network.post_api(note_url(resource, sub_resource_type), body)
|
|
84
|
+
error = response[:id].blank? && (response[:message] || response[:error])
|
|
85
|
+
return response unless error
|
|
86
|
+
|
|
87
|
+
raise NoteNotCreatedError, "The note was not posted on #{resource[:web_url] || resource[:iid]} (#{error})."
|
|
88
|
+
end
|
|
89
|
+
|
|
71
90
|
# Returns [source, source_id] for the namespace the resource lives in.
|
|
72
91
|
# A group run can return work items from descendant projects, and an
|
|
73
92
|
# iid is only unique within its own namespace, so writes and label
|
|
@@ -168,12 +187,26 @@ module Gitlab
|
|
|
168
187
|
).build
|
|
169
188
|
end
|
|
170
189
|
|
|
190
|
+
def note_url(resource, sub_resource_type)
|
|
191
|
+
scope, scope_id = scope_for(resource)
|
|
192
|
+
|
|
193
|
+
UrlBuilders::UrlBuilder.new(
|
|
194
|
+
network_options: @options,
|
|
195
|
+
source: scope,
|
|
196
|
+
source_id: scope_id,
|
|
197
|
+
resource_type: '-/work_items',
|
|
198
|
+
resource_id: resource[:iid],
|
|
199
|
+
sub_resource_type: sub_resource_type
|
|
200
|
+
).build
|
|
201
|
+
end
|
|
202
|
+
|
|
171
203
|
def list_params(conditions)
|
|
172
204
|
params = {
|
|
173
205
|
per_page: @per_page,
|
|
174
206
|
fields: LIST_FIELDS.join(','),
|
|
175
207
|
features: LIST_FEATURES.join(',')
|
|
176
208
|
}
|
|
209
|
+
params[:include_descendants] = true if @options.source == 'groups'
|
|
177
210
|
|
|
178
211
|
apply_filters(params, conditions)
|
|
179
212
|
|
|
@@ -184,6 +217,8 @@ module Gitlab
|
|
|
184
217
|
# `type` uses `work_item_type_names`, which resolves names (including
|
|
185
218
|
# custom types) case-insensitively server-side - no id resolution
|
|
186
219
|
# needed.
|
|
220
|
+
# A new work_items condition must also be added to
|
|
221
|
+
# Validators::PolicyValidator::API_WORK_ITEM_CONDITIONS.
|
|
187
222
|
def apply_filters(params, conditions)
|
|
188
223
|
params[:iids] = Array(conditions[:iids]).join(',') if conditions[:iids].present?
|
|
189
224
|
params[:work_item_type_names] = Array(conditions[:type]).join(',') if conditions[:type]
|
|
@@ -193,30 +228,39 @@ module Gitlab
|
|
|
193
228
|
params[:assignee_usernames] = Array(conditions[:assignees]).join(',') if conditions[:assignees]
|
|
194
229
|
params['status[name]'] = conditions[:status] if conditions[:status]
|
|
195
230
|
|
|
231
|
+
apply_milestone_filter(params, conditions[:milestone]) if conditions[:milestone]
|
|
196
232
|
apply_date_filter(params, conditions[:date]) if conditions[:date]
|
|
197
233
|
end
|
|
198
234
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
235
|
+
# PolicyValidator rejects more than one value, so a list here has at most one.
|
|
236
|
+
# The Issues REST API keeps the legacy Started/Upcoming logic and this API does
|
|
237
|
+
# not, so the same wildcard can match different milestones (see README).
|
|
238
|
+
def apply_milestone_filter(params, milestone_condition)
|
|
239
|
+
milestone = Array(milestone_condition).first.to_s
|
|
240
|
+
return if milestone.empty?
|
|
241
|
+
|
|
242
|
+
wildcard = MilestoneWildcards.api_value(milestone)
|
|
203
243
|
|
|
204
|
-
|
|
205
|
-
|
|
244
|
+
if wildcard
|
|
245
|
+
params[:milestone_wildcard_id] = wildcard
|
|
246
|
+
else
|
|
247
|
+
params[:milestone_title] = milestone
|
|
248
|
+
end
|
|
249
|
+
end
|
|
206
250
|
|
|
207
|
-
|
|
208
|
-
|
|
251
|
+
# A bad date condition would otherwise send no date filter, or the opposite one,
|
|
252
|
+
# and widen the matched set, so raise even if load-time validation was skipped.
|
|
253
|
+
def apply_date_filter(params, date_condition)
|
|
254
|
+
Validators::PolicyValidator.validate_work_items_date!(date_condition)
|
|
255
|
+
|
|
256
|
+
attribute = date_condition[:attribute]
|
|
257
|
+
direction = date_condition[:condition] == 'older_than' ? 'before' : 'after'
|
|
258
|
+
params["#{attribute.delete_suffix('_at')}_#{direction}"] = date_boundary(date_condition).iso8601
|
|
209
259
|
end
|
|
210
260
|
|
|
211
261
|
def date_boundary(date_condition)
|
|
212
|
-
|
|
213
|
-
interval_type
|
|
214
|
-
return unless interval && interval_type
|
|
215
|
-
|
|
216
|
-
# A malformed interval_type raises here rather than silently dropping
|
|
217
|
-
# the date bound, which would widen the matched set - dangerous in an
|
|
218
|
-
# automation tool. interval_type is also validated by the date filter.
|
|
219
|
-
interval.public_send(interval_type).ago # rubocop:disable GitlabSecurity/PublicSend -- interval_type constrained to a known set
|
|
262
|
+
# validate_work_items_date! limits interval_type to the date filter's INTERVAL_TYPES.
|
|
263
|
+
date_condition[:interval].public_send(date_condition[:interval_type]).ago # rubocop:disable GitlabSecurity/PublicSend -- interval_type constrained to a known set
|
|
220
264
|
end
|
|
221
265
|
end
|
|
222
266
|
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../filters/work_item_date_conditions_filter'
|
|
4
|
+
require_relative '../milestone_wildcards'
|
|
5
|
+
|
|
3
6
|
module Gitlab
|
|
4
7
|
module Triage
|
|
5
8
|
module Validators
|
|
@@ -10,16 +13,77 @@ module Gitlab
|
|
|
10
13
|
# where nobody sees it).
|
|
11
14
|
class PolicyValidator
|
|
12
15
|
UnsupportedActionError = Class.new(StandardError)
|
|
16
|
+
UnsupportedConditionError = Class.new(StandardError)
|
|
17
|
+
InvalidConditionError = Class.new(StandardError)
|
|
13
18
|
ConflictingActionError = Class.new(StandardError)
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
# Action keys Action.actions_for dispatches for work_items policies
|
|
21
|
+
# (comment, mention, comment_type, comment_internal, and
|
|
22
|
+
# redact_confidential_resources are the comment action and its options).
|
|
23
|
+
# Anything outside this list would silently do nothing, so reject it up
|
|
24
|
+
# front.
|
|
25
|
+
SUPPORTED_WORK_ITEM_ACTIONS = %i[
|
|
26
|
+
labels remove_labels assignees
|
|
27
|
+
comment mention comment_type comment_internal redact_confidential_resources
|
|
28
|
+
summarize comment_on_summary
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
# A summary's own actions run over the whole summary, not per item, so
|
|
32
|
+
# the per-item actions would crash there.
|
|
33
|
+
SUPPORTED_SUMMARY_ACTIONS = %i[summarize comment_on_summary].freeze
|
|
34
|
+
|
|
35
|
+
# A rule nested in a summary only feeds it: the engine reads its
|
|
36
|
+
# summarize action and runs nothing else.
|
|
37
|
+
SUPPORTED_SUMMARY_RULE_ACTIONS = %i[summarize].freeze
|
|
38
|
+
|
|
39
|
+
# Sent to the Work Items REST API as query params by WorkItemsRestSource#apply_filters.
|
|
40
|
+
API_WORK_ITEM_CONDITIONS = %i[type state status labels forbidden_labels assignees milestone date].freeze
|
|
41
|
+
|
|
42
|
+
# Applied in Ruby to the fetched work items through Engine::FILTER_MAP.
|
|
43
|
+
RUBY_WORK_ITEM_CONDITIONS = %i[author_member assignee_member ruby no_additional_labels work_item_status].freeze
|
|
44
|
+
|
|
45
|
+
# Any other key would be silently ignored and widen the matched set, so reject it up front.
|
|
46
|
+
SUPPORTED_WORK_ITEM_CONDITIONS = (API_WORK_ITEM_CONDITIONS + RUBY_WORK_ITEM_CONDITIONS).freeze
|
|
47
|
+
|
|
48
|
+
# States the Work Items REST API filters on. Engine::ALLOWED_STATE_VALUES reads
|
|
49
|
+
# this too, since the engine requires this file and not the other way round.
|
|
50
|
+
WORK_ITEM_STATES = %w[opened closed].freeze
|
|
51
|
+
|
|
52
|
+
# WorkItemsRestSource#apply_date_filter would send no date filter for a bad attribute
|
|
53
|
+
# or a missing interval, so the rule would match every item, and reads any condition
|
|
54
|
+
# other than older_than as newer_than. Check the values against the same parameters
|
|
55
|
+
# the Ruby date filter uses, so filter_in_ruby: true accepts exactly the same ones.
|
|
56
|
+
# The source calls this too, so a condition that skipped load-time validation still
|
|
57
|
+
# cannot reach the API unchecked.
|
|
58
|
+
def self.validate_work_items_date!(date_condition)
|
|
59
|
+
# Despite the name, filter_parameters returns the definitions of the date
|
|
60
|
+
# condition's keys (name, type, allowed values), not API query params.
|
|
61
|
+
date_parameters = Filters::WorkItemDateConditionsFilter.filter_parameters
|
|
62
|
+
|
|
63
|
+
unless date_condition.is_a?(Hash)
|
|
64
|
+
raise InvalidConditionError,
|
|
65
|
+
"The date condition on work_items policies takes " \
|
|
66
|
+
"#{date_parameters.pluck(:name).to_sentence}, but got #{date_condition.inspect}."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
date_condition = date_condition.with_indifferent_access
|
|
70
|
+
ParamsValidator.new(date_parameters, date_condition).validate!
|
|
71
|
+
validate_date_interval!(date_condition)
|
|
72
|
+
rescue ParamsValidator::InvalidParameter => e
|
|
73
|
+
raise InvalidConditionError,
|
|
74
|
+
"Invalid date condition on work_items policies: #{e.message}, but got #{date_condition.inspect}."
|
|
75
|
+
end
|
|
19
76
|
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
|
|
77
|
+
# A zero or negative interval puts the boundary at or after now, so older_than
|
|
78
|
+
# matches every item; YAML's .inf and .nan load as Floats and raise only when
|
|
79
|
+
# the boundary is computed, partway through a run.
|
|
80
|
+
def self.validate_date_interval!(date_condition)
|
|
81
|
+
interval = date_condition[:interval]
|
|
82
|
+
return if interval.finite? && interval.positive?
|
|
83
|
+
|
|
84
|
+
raise ParamsValidator::InvalidParameter, 'interval must be a positive, finite number'
|
|
85
|
+
end
|
|
86
|
+
private_class_method :validate_date_interval!
|
|
23
87
|
|
|
24
88
|
def initialize(resource_rules)
|
|
25
89
|
@resource_rules = resource_rules || {}
|
|
@@ -29,16 +93,68 @@ module Gitlab
|
|
|
29
93
|
@resource_rules.each do |resource_type, policy_definition|
|
|
30
94
|
next unless resource_type.to_s == 'work_items'
|
|
31
95
|
|
|
96
|
+
validate_work_items_conditions!(policy_definition)
|
|
32
97
|
validate_work_items_actions!(policy_definition)
|
|
33
98
|
end
|
|
34
99
|
end
|
|
35
100
|
|
|
36
101
|
private
|
|
37
102
|
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
|
|
103
|
+
def validate_work_items_conditions!(policy_definition)
|
|
104
|
+
condition_sets_from(policy_definition).each do |conditions|
|
|
105
|
+
unsupported = conditions.keys.map(&:to_sym) - SUPPORTED_WORK_ITEM_CONDITIONS
|
|
106
|
+
unless unsupported.none?
|
|
107
|
+
raise UnsupportedConditionError,
|
|
108
|
+
"Unsupported condition(s) for work_items policies: #{unsupported.join(', ')}. " \
|
|
109
|
+
"Supported conditions: #{SUPPORTED_WORK_ITEM_CONDITIONS.join(', ')}."
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
validate_state!(conditions[:state]) if conditions.key?(:state)
|
|
113
|
+
validate_milestone!(conditions[:milestone]) if conditions.key?(:milestone)
|
|
114
|
+
self.class.validate_work_items_date!(conditions[:date]) if conditions.key?(:date)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Checked here rather than when the rule fetches, so a typo such as `open`
|
|
119
|
+
# fails before any earlier rule has acted. `merged` is valid for merge
|
|
120
|
+
# requests but not for work items.
|
|
121
|
+
def validate_state!(state)
|
|
122
|
+
return if WORK_ITEM_STATES.include?(state)
|
|
123
|
+
|
|
124
|
+
raise InvalidConditionError,
|
|
125
|
+
"The state condition on work_items policies takes one of #{WORK_ITEM_STATES.join(', ')}, " \
|
|
126
|
+
"but got #{state.inspect}."
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# The issues policy type silently uses the first of several milestones, and
|
|
130
|
+
# skips the filter for a blank one, so the rule matches every item. work_items
|
|
131
|
+
# has no old policies to stay compatible with, so it rejects both.
|
|
132
|
+
def validate_milestone!(milestone_condition)
|
|
133
|
+
milestones = Array(milestone_condition)
|
|
134
|
+
if milestones.size > 1
|
|
135
|
+
raise InvalidConditionError,
|
|
136
|
+
"The milestone condition takes one value on work_items policies, " \
|
|
137
|
+
"but got #{milestones.size}: #{milestones.join(', ')}."
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
milestone = milestones.first
|
|
141
|
+
return if milestone.is_a?(String) && milestone.strip.present?
|
|
142
|
+
|
|
143
|
+
raise InvalidConditionError, invalid_milestone_message(milestone)
|
|
144
|
+
end
|
|
41
145
|
|
|
146
|
+
# YAML reads an unquoted 19.10 as the number 19.1, so a version-like title
|
|
147
|
+
# must be quoted to reach the API intact.
|
|
148
|
+
def invalid_milestone_message(milestone)
|
|
149
|
+
message = "The milestone condition on work_items policies takes a milestone title " \
|
|
150
|
+
"or one of #{MilestoneWildcards::VALUES.join(', ')}, but got #{milestone.inspect}."
|
|
151
|
+
return message unless milestone.is_a?(Numeric)
|
|
152
|
+
|
|
153
|
+
"#{message} Quote titles that look like numbers, for example milestone: '19.10'."
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def validate_work_items_actions!(policy_definition)
|
|
157
|
+
action_sets_from(policy_definition).each do |actions, supported, scope|
|
|
42
158
|
# GitLab applies remove_label_ids after add_label_ids, so a label in
|
|
43
159
|
# both lists is always removed. Catch the typo here rather than let the
|
|
44
160
|
# run fail later with a misleading "token may lack permission" error.
|
|
@@ -49,25 +165,37 @@ module Gitlab
|
|
|
49
165
|
"A label cannot be added and removed in the same rule."
|
|
50
166
|
end
|
|
51
167
|
|
|
52
|
-
unsupported = actions.keys.map(&:to_sym) -
|
|
168
|
+
unsupported = actions.keys.map(&:to_sym) - supported
|
|
53
169
|
next if unsupported.none?
|
|
54
170
|
|
|
55
171
|
raise UnsupportedActionError,
|
|
56
|
-
"Unsupported action(s) for work_items
|
|
57
|
-
"Supported actions: #{
|
|
172
|
+
"Unsupported action(s) for work_items #{scope}: #{unsupported.join(', ')}. " \
|
|
173
|
+
"Supported actions: #{supported.join(', ')}."
|
|
58
174
|
end
|
|
59
175
|
end
|
|
60
176
|
|
|
61
|
-
# Every place a policy definition can carry actions
|
|
62
|
-
# rules nested in summaries, and the
|
|
177
|
+
# Every place a policy definition can carry actions, with the actions
|
|
178
|
+
# allowed there: top-level rules, rules nested in summaries, and the
|
|
179
|
+
# summaries' own actions.
|
|
63
180
|
def action_sets_from(policy_definition)
|
|
64
181
|
policy_definition ||= {}
|
|
65
182
|
|
|
66
183
|
summaries = Array(policy_definition[:summaries])
|
|
67
184
|
summary_rules = summaries.flat_map { |summary| Array(summary[:rules]) }
|
|
68
185
|
|
|
69
|
-
|
|
70
|
-
.map { |
|
|
186
|
+
Array(policy_definition[:rules]).map { |rule| [rule[:actions] || {}, SUPPORTED_WORK_ITEM_ACTIONS, 'policies'] } +
|
|
187
|
+
summary_rules.map { |rule| [rule[:actions] || {}, SUPPORTED_SUMMARY_RULE_ACTIONS, 'summary rules'] } +
|
|
188
|
+
summaries.map { |summary| [summary[:actions] || {}, SUPPORTED_SUMMARY_ACTIONS, 'summaries'] }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Conditions live on top-level rules and rules nested in summaries.
|
|
192
|
+
def condition_sets_from(policy_definition)
|
|
193
|
+
policy_definition ||= {}
|
|
194
|
+
|
|
195
|
+
summary_rules = Array(policy_definition[:summaries]).flat_map { |summary| Array(summary[:rules]) }
|
|
196
|
+
|
|
197
|
+
(Array(policy_definition[:rules]) + summary_rules)
|
|
198
|
+
.map { |definition| definition[:conditions] || {} }
|
|
71
199
|
end
|
|
72
200
|
end
|
|
73
201
|
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.58.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GitLab
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -252,6 +252,7 @@ files:
|
|
|
252
252
|
- lib/gitlab/triage/action/summarize.rb
|
|
253
253
|
- lib/gitlab/triage/action/update.rb
|
|
254
254
|
- lib/gitlab/triage/action/work_item.rb
|
|
255
|
+
- lib/gitlab/triage/action/work_item_comment.rb
|
|
255
256
|
- lib/gitlab/triage/action/work_item_status.rb
|
|
256
257
|
- lib/gitlab/triage/api_observability.rb
|
|
257
258
|
- lib/gitlab/triage/api_query_builders/base_query_param_builder.rb
|
|
@@ -294,6 +295,7 @@ files:
|
|
|
294
295
|
- lib/gitlab/triage/graphql_queries/query_param_builders/date_param_builder.rb
|
|
295
296
|
- lib/gitlab/triage/limiters/base_limiter.rb
|
|
296
297
|
- lib/gitlab/triage/limiters/date_field_limiter.rb
|
|
298
|
+
- lib/gitlab/triage/milestone_wildcards.rb
|
|
297
299
|
- lib/gitlab/triage/network.rb
|
|
298
300
|
- lib/gitlab/triage/network_adapters/base_adapter.rb
|
|
299
301
|
- lib/gitlab/triage/network_adapters/graphql_adapter.rb
|