gitlab-triage 1.54.0 → 1.55.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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +16 -4
- data/lib/gitlab/triage/action/issue.rb +5 -0
- data/lib/gitlab/triage/action/summarize.rb +5 -0
- data/lib/gitlab/triage/action/update.rb +26 -4
- data/lib/gitlab/triage/engine.rb +13 -4
- data/lib/gitlab/triage/entity_builders/issue_fields.rb +18 -5
- data/lib/gitlab/triage/entity_builders/summary_builder.rb +1 -1
- data/lib/gitlab/triage/graphql_network.rb +18 -11
- data/lib/gitlab/triage/graphql_queries/query_builder.rb +46 -3
- data/lib/gitlab/triage/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72ed33025703247cca10024af7e8be895ecc03094d5733830b6dcbc73742aae5
|
|
4
|
+
data.tar.gz: da8fc6fc4932bd8038bf69947533cdcfc5bd0efb75d07c3e25c262d23e822411
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0703eb40a6bd259c8931eb2ec71bdd3492e5bee59d0644737b1919b38c414776f547b06344bed54b9dc6d6825a5fca7bc06ea5e98de2cbcd8d0a1adccf936707
|
|
7
|
+
data.tar.gz: 59768f9c0f1be9f286af3a0e357a3a2be44db55e32b5df7e97381a273a4f397432574c2f65f09854a45eb7e7060ea9fcbad202ee12e076938278dea68df5eda6
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -928,7 +928,9 @@ Available action types:
|
|
|
928
928
|
|
|
929
929
|
Adds a number of labels to the resource.
|
|
930
930
|
|
|
931
|
-
Accepts an array of strings. Each element is the name of a label to add.
|
|
931
|
+
Accepts an array of strings. Each element is the name of a label to add. An
|
|
932
|
+
element may interpolate to a comma-separated list, which is expanded into one
|
|
933
|
+
label per value.
|
|
932
934
|
|
|
933
935
|
If any of the labels doesn't exist, the automation will stop immediately so
|
|
934
936
|
that if a label is renamed or deleted, you'll have to explicitly update or remove
|
|
@@ -957,7 +959,9 @@ actions:
|
|
|
957
959
|
|
|
958
960
|
Removes a number of labels from the resource.
|
|
959
961
|
|
|
960
|
-
Accepts an array of strings. Each element is the name of a label to remove.
|
|
962
|
+
Accepts an array of strings. Each element is the name of a label to remove. An
|
|
963
|
+
element may interpolate to a comma-separated list, which is expanded into one
|
|
964
|
+
label per value.
|
|
961
965
|
|
|
962
966
|
If any of the labels doesn't exist, the automation will stop immediately so
|
|
963
967
|
that if a label is renamed or deleted, you'll have to explicitly update or remove
|
|
@@ -1346,7 +1350,10 @@ Accepts a hash of fields.
|
|
|
1346
1350
|
| `destination` | integer or string | The project ID or path to create the generated issue in | no | no | no | source project |
|
|
1347
1351
|
| `item` | string | Template representing each triaged resource | no | yes | yes | |
|
|
1348
1352
|
| `summary` | string | The description of the generated issue | no | Only `{{title}}`, `{{items}}`, `{{type}}` | yes | |
|
|
1353
|
+
| `labels` | array of strings | Labels to add to the generated issue. An element may interpolate to a comma-separated list, which is expanded into one label per value | no | yes | yes | |
|
|
1354
|
+
| `assignees` | array of strings | Usernames to assign to the generated issue. An element may interpolate to a comma-separated list, which is expanded into one assignee per value | no | yes | yes | |
|
|
1349
1355
|
| `redact_confidential_resources` | boolean | Whether redact fields for confidential resources | no | no | no | true |
|
|
1356
|
+
| `confidential` | boolean or string | Create the issue confidential. A string is interpolated and counts as true when it evaluates to `true` | no | no | yes | false |
|
|
1350
1357
|
|
|
1351
1358
|
The following placeholders are supported for `summary`:
|
|
1352
1359
|
|
|
@@ -1360,14 +1367,16 @@ The following placeholders are supported for `summary`:
|
|
|
1360
1367
|
> - Placeholders work regularly for `item`, but for `summary` only `{{title}}`,
|
|
1361
1368
|
> `{{items}}`, `{{type}}` are supported because it's not tied to a particular
|
|
1362
1369
|
> resource like the comment action.
|
|
1370
|
+
> - `labels`, `assignees`, `due_date`, `milestone`, and `confidential` can read
|
|
1371
|
+
> `{{type}}`, `{{items}}`, and `resource[:items]`.
|
|
1363
1372
|
> - No issues will be created if:
|
|
1364
1373
|
> - the specific policy doesn't yield any resources; or
|
|
1365
1374
|
> - the source type is a group and `destination` is not set.
|
|
1366
1375
|
> - `redact_confidential_resources` defaults to `true`, so fields on
|
|
1367
1376
|
> confidential resources will be converted to `(confidential)` except for
|
|
1368
1377
|
> `{{web_url}}`. Setting it to `false` will reveal the confidential fields.
|
|
1369
|
-
> This
|
|
1370
|
-
>
|
|
1378
|
+
> This is useful when the summary is itself `confidential`, or if we're
|
|
1379
|
+
> posting to another private project (not implemented yet).
|
|
1371
1380
|
|
|
1372
1381
|
Example:
|
|
1373
1382
|
|
|
@@ -1480,7 +1489,10 @@ Accepts a hash of fields.
|
|
|
1480
1489
|
| `title` | string | The title of the generated issue | yes | yes | yes | |
|
|
1481
1490
|
| `destination` | integer or string | The project ID or path to create the generated issue in | no | no | no | source project |
|
|
1482
1491
|
| `description` | string | The description of the generated issue | no | yes | yes | |
|
|
1492
|
+
| `labels` | array of strings | Labels to add to the generated issue. An element may interpolate to a comma-separated list, which is expanded into one label per value | no | yes | yes | |
|
|
1493
|
+
| `assignees` | array of strings | Usernames to assign to the generated issue. An element may interpolate to a comma-separated list, which is expanded into one assignee per value | no | yes | yes | |
|
|
1483
1494
|
| `redact_confidential_resources` | boolean | Whether redact fields for confidential resources | no | no | no | true |
|
|
1495
|
+
| `confidential` | boolean or string | Create the issue confidential. A string is interpolated and counts as true when it evaluates to `true` | no | no | yes | false |
|
|
1484
1496
|
|
|
1485
1497
|
The placeholders available in `title` and `destination` are the properties of the resource being used to generate the issue.
|
|
1486
1498
|
|
|
@@ -23,6 +23,7 @@ module Gitlab
|
|
|
23
23
|
puts "* Assignees: #{issue.assignee_usernames.join(', ')}" if issue.assignee_usernames&.any?
|
|
24
24
|
puts "* Due date: #{issue.due_date}" if issue.due_date
|
|
25
25
|
puts "* Milestone: #{issue.milestone_title}" if issue.milestone_title
|
|
26
|
+
puts "* Confidential: true" if issue.confidential?
|
|
26
27
|
puts "* Description: #{issue.description}"
|
|
27
28
|
puts ">>>"
|
|
28
29
|
end
|
|
@@ -40,6 +41,9 @@ module Gitlab
|
|
|
40
41
|
ensure_labels_exist!(issue.labels, destination_id)
|
|
41
42
|
resolve_assignee_ids(issue.assignee_usernames) if issue.assignee_usernames.any?
|
|
42
43
|
resolve_milestone_id(destination_id, issue.milestone_title) if issue.milestone_title
|
|
44
|
+
# Evaluated here too, so a bad confidential: template raises before
|
|
45
|
+
# any action writes rather than mid-way through post_issue_body.
|
|
46
|
+
issue.confidential?
|
|
43
47
|
end
|
|
44
48
|
end
|
|
45
49
|
|
|
@@ -83,6 +87,7 @@ module Gitlab
|
|
|
83
87
|
body[:assignee_ids] = resolve_assignee_ids(issue.assignee_usernames) if issue.assignee_usernames.any?
|
|
84
88
|
body[:due_date] = issue.due_date if issue.due_date
|
|
85
89
|
body[:milestone_id] = resolve_milestone_id(destination_id, issue.milestone_title) if issue.milestone_title
|
|
90
|
+
body[:confidential] = true if issue.confidential?
|
|
86
91
|
|
|
87
92
|
body
|
|
88
93
|
end
|
|
@@ -31,6 +31,7 @@ module Gitlab
|
|
|
31
31
|
puts "* Assignees: #{issue.assignee_usernames.join(', ')}" if issue.assignee_usernames&.any?
|
|
32
32
|
puts "* Due date: #{issue.due_date}" if issue.due_date
|
|
33
33
|
puts "* Milestone: #{issue.milestone_title}" if issue.milestone_title
|
|
34
|
+
puts "* Confidential: true" if issue.confidential?
|
|
34
35
|
puts "* Description: #{issue.description}"
|
|
35
36
|
puts ">>>"
|
|
36
37
|
end
|
|
@@ -43,6 +44,9 @@ module Gitlab
|
|
|
43
44
|
ensure_labels_exist!(issue.labels, destination)
|
|
44
45
|
resolve_assignee_ids(issue.assignee_usernames) if issue.assignee_usernames.any?
|
|
45
46
|
resolve_milestone_id(destination, issue.milestone_title) if issue.milestone_title
|
|
47
|
+
# Evaluated here too, so a bad confidential: template raises before
|
|
48
|
+
# any action writes rather than mid-way through post_issue_body.
|
|
49
|
+
issue.confidential?
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
def act
|
|
@@ -97,6 +101,7 @@ module Gitlab
|
|
|
97
101
|
body[:assignee_ids] = resolve_assignee_ids(issue.assignee_usernames) if issue.assignee_usernames.any?
|
|
98
102
|
body[:due_date] = issue.due_date if issue.due_date
|
|
99
103
|
body[:milestone_id] = resolve_milestone_id(destination, issue.milestone_title) if issue.milestone_title
|
|
104
|
+
body[:confidential] = true if issue.confidential?
|
|
100
105
|
|
|
101
106
|
body
|
|
102
107
|
end
|
|
@@ -314,10 +314,13 @@ module Gitlab
|
|
|
314
314
|
"Supported: #{VALID_STATUS_VALUES.join(', ')}."
|
|
315
315
|
end
|
|
316
316
|
|
|
317
|
-
# An entry may interpolate to
|
|
318
|
-
# label conditional on the resource.
|
|
317
|
+
# An entry may interpolate to a comma-separated list, or to nothing so a
|
|
318
|
+
# policy can make a label conditional on the resource.
|
|
319
319
|
def eval_labels(labels, resource)
|
|
320
|
-
Array(labels)
|
|
320
|
+
Array(labels)
|
|
321
|
+
.flat_map { |label| eval_value(label, resource).split(',') }
|
|
322
|
+
.map(&:strip)
|
|
323
|
+
.reject(&:empty?)
|
|
321
324
|
end
|
|
322
325
|
|
|
323
326
|
def join_labels(labels)
|
|
@@ -393,6 +396,7 @@ module Gitlab
|
|
|
393
396
|
verify_state!(resource, response) if actions[:status]
|
|
394
397
|
verify_labels_applied!(resource, response) if actions[:labels]
|
|
395
398
|
verify_assignees_applied!(resource, response) if actions[:assignees]
|
|
399
|
+
verify_reviewers_applied!(resource, response) if actions[:reviewers]
|
|
396
400
|
end
|
|
397
401
|
|
|
398
402
|
def verify_labels_applied!(resource, response)
|
|
@@ -409,7 +413,7 @@ module Gitlab
|
|
|
409
413
|
def verify_assignees_applied!(resource, response)
|
|
410
414
|
return if response[:assignees].nil?
|
|
411
415
|
|
|
412
|
-
missing =
|
|
416
|
+
missing = missing_usernames(actions[:assignees], resource, response[:assignees])
|
|
413
417
|
return if missing.empty?
|
|
414
418
|
|
|
415
419
|
raise UpdateNotAppliedError,
|
|
@@ -417,6 +421,24 @@ module Gitlab
|
|
|
417
421
|
"the token may lack permission."
|
|
418
422
|
end
|
|
419
423
|
|
|
424
|
+
def verify_reviewers_applied!(resource, response)
|
|
425
|
+
return if response[:reviewers].nil?
|
|
426
|
+
|
|
427
|
+
missing = missing_usernames(actions[:reviewers], resource, response[:reviewers])
|
|
428
|
+
return if missing.empty?
|
|
429
|
+
|
|
430
|
+
raise UpdateNotAppliedError,
|
|
431
|
+
"Reviewers #{missing.join(', ')} were not requested on #{resource[:web_url]}; " \
|
|
432
|
+
"the token may lack permission, or the users may not have access to it."
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# The users lookup ignores case, so the response can spell a username
|
|
436
|
+
# differently from the rule.
|
|
437
|
+
def missing_usernames(requested, resource, returned_users)
|
|
438
|
+
returned = returned_users.map { |user| user.fetch(:username).downcase }
|
|
439
|
+
eval_usernames(requested, resource).reject { |username| returned.include?(username.downcase) }
|
|
440
|
+
end
|
|
441
|
+
|
|
420
442
|
def verify_state!(resource, response)
|
|
421
443
|
expected = actions[:status] == 'close' ? 'closed' : 'opened'
|
|
422
444
|
actual = response[:state]
|
data/lib/gitlab/triage/engine.rb
CHANGED
|
@@ -240,6 +240,10 @@ module Gitlab
|
|
|
240
240
|
rule.fetch(:conditions) { {} }
|
|
241
241
|
end
|
|
242
242
|
|
|
243
|
+
def rule_actions(rule)
|
|
244
|
+
rule.fetch(:actions) { {} }
|
|
245
|
+
end
|
|
246
|
+
|
|
243
247
|
def rule_limits(rule)
|
|
244
248
|
rule.fetch(:limits) { {} }
|
|
245
249
|
end
|
|
@@ -456,7 +460,7 @@ module Gitlab
|
|
|
456
460
|
graphql_query_options[:iids] = expanded_conditions[:iids]
|
|
457
461
|
end
|
|
458
462
|
|
|
459
|
-
graphql_query = build_graphql_query(resource_type, expanded_conditions, true)
|
|
463
|
+
graphql_query = build_graphql_query(resource_type, expanded_conditions, rule_definition, true)
|
|
460
464
|
|
|
461
465
|
resources = graphql_network.query(graphql_query, **graphql_query_options)
|
|
462
466
|
else
|
|
@@ -482,7 +486,7 @@ module Gitlab
|
|
|
482
486
|
iids = resources.pluck('iid').map(&:to_s)
|
|
483
487
|
expanded_conditions[:iids] = iids
|
|
484
488
|
|
|
485
|
-
graphql_query = build_graphql_query(resource_type, expanded_conditions)
|
|
489
|
+
graphql_query = build_graphql_query(resource_type, expanded_conditions, rule_definition)
|
|
486
490
|
graphql_resources = graphql_network.query(graphql_query, source: source_full_path, iids: iids) if graphql_query.any?
|
|
487
491
|
|
|
488
492
|
decorate_resources_with_graphql_data(resources, graphql_resources)
|
|
@@ -879,9 +883,14 @@ module Gitlab
|
|
|
879
883
|
status_by_iid
|
|
880
884
|
end
|
|
881
885
|
|
|
882
|
-
def build_graphql_query(resource_type, conditions, graphql_only = false)
|
|
886
|
+
def build_graphql_query(resource_type, conditions, rule_definition, graphql_only = false)
|
|
883
887
|
Gitlab::Triage::GraphqlQueries::QueryBuilder
|
|
884
|
-
.new(
|
|
888
|
+
.new(
|
|
889
|
+
options.source,
|
|
890
|
+
resource_type,
|
|
891
|
+
conditions,
|
|
892
|
+
graphql_only: graphql_only,
|
|
893
|
+
actions: rule_actions(rule_definition))
|
|
885
894
|
end
|
|
886
895
|
|
|
887
896
|
def source_full_path
|
|
@@ -20,14 +20,26 @@ module Gitlab
|
|
|
20
20
|
evaluate_present(@milestone_title)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
def confidential?
|
|
24
|
+
return @confidential if @confidential == true || @confidential == false
|
|
25
|
+
return false unless @confidential
|
|
26
|
+
|
|
27
|
+
evaluate(@confidential.to_s).strip == 'true'
|
|
28
|
+
end
|
|
29
|
+
|
|
23
30
|
private
|
|
24
31
|
|
|
25
|
-
# Returns
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
32
|
+
# Returns one value per label or username. An entry may interpolate to
|
|
33
|
+
# a comma-separated list so a policy can compute a list whose length
|
|
34
|
+
# varies, and an entry that comes out empty is dropped. The caller
|
|
35
|
+
# treats an empty result as "the policy named nothing here", so a
|
|
36
|
+
# conditional entry leaves the field off the request rather than
|
|
37
|
+
# clearing it.
|
|
29
38
|
def evaluate_entries(templates)
|
|
30
|
-
Array(templates)
|
|
39
|
+
Array(templates)
|
|
40
|
+
.flat_map { |template| evaluate(template).split(',') }
|
|
41
|
+
.map(&:strip)
|
|
42
|
+
.reject(&:empty?)
|
|
31
43
|
end
|
|
32
44
|
|
|
33
45
|
def init_issue_fields(action)
|
|
@@ -35,6 +47,7 @@ module Gitlab
|
|
|
35
47
|
@assignee_usernames = action[:assignees]
|
|
36
48
|
@due_date = action[:due_date]
|
|
37
49
|
@milestone_title = action[:milestone]
|
|
50
|
+
@confidential = action[:confidential]
|
|
38
51
|
end
|
|
39
52
|
|
|
40
53
|
# An empty string is truthy, so a template that interpolates to nothing
|
|
@@ -71,24 +71,31 @@ module Gitlab
|
|
|
71
71
|
|
|
72
72
|
private
|
|
73
73
|
|
|
74
|
+
# Builds a resource from one GraphQL node, holding only the fields the response
|
|
75
|
+
# actually carried. The query asks for a field only when the rule's conditions
|
|
76
|
+
# or actions need it, and the caller merges this over the REST resource, so a
|
|
77
|
+
# key written for a field nobody asked for would wipe what REST returned.
|
|
74
78
|
def normalize(resource)
|
|
75
79
|
resource
|
|
76
80
|
.slice(:iid, :title, :state, :author, :merged_at, :user_notes_count, :user_discussions_count, :upvotes, :downvotes, :project_id, :web_url)
|
|
77
|
-
.merge(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)
|
|
81
|
-
|
|
81
|
+
.merge(id: extract_id_from_global_id(resource[:id]))
|
|
82
|
+
.merge(normalize_labels(resource))
|
|
83
|
+
.merge(normalize_users(resource, :assignees))
|
|
84
|
+
.merge(normalize_users(resource, :reviewers))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def normalize_labels(resource)
|
|
88
|
+
nodes = resource.dig(:labels, :nodes)
|
|
89
|
+
return {} unless nodes
|
|
90
|
+
|
|
91
|
+
{ labels: nodes.pluck(:title) }
|
|
82
92
|
end
|
|
83
93
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# would wipe the assignees REST already returned.
|
|
87
|
-
def normalize_assignees(resource)
|
|
88
|
-
nodes = resource.dig(:assignees, :nodes)
|
|
94
|
+
def normalize_users(resource, key)
|
|
95
|
+
nodes = resource.dig(key, :nodes)
|
|
89
96
|
return {} unless nodes
|
|
90
97
|
|
|
91
|
-
{
|
|
98
|
+
{ key => nodes.map { |node| node.merge(id: extract_id_from_global_id(node[:id])) } }
|
|
92
99
|
end
|
|
93
100
|
|
|
94
101
|
def extract_id_from_global_id(global_id)
|
|
@@ -25,10 +25,17 @@ module Gitlab
|
|
|
25
25
|
}
|
|
26
26
|
GRAPHQL
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
# Neither user field exists on every type in GitLab's GraphQL schema: Epic
|
|
29
|
+
# carries no assignees, and reviewers belong to MergeRequest alone.
|
|
30
|
+
# Selecting a field the type lacks makes the whole query fail to parse.
|
|
31
|
+
ASSIGNEES_RESOURCE_TYPES = %w[issues merge_requests].freeze
|
|
32
|
+
REVIEWERS_RESOURCE_TYPES = %w[merge_requests].freeze
|
|
33
|
+
|
|
34
|
+
def initialize(source_type, resource_type, conditions, graphql_only: false, actions: {})
|
|
29
35
|
@source_type = source_type.to_s.singularize
|
|
30
36
|
@resource_type = resource_type
|
|
31
37
|
@conditions = conditions
|
|
38
|
+
@actions = actions || {}
|
|
32
39
|
@graphql_only = graphql_only
|
|
33
40
|
@resource_declarations = [
|
|
34
41
|
'$source: ID!',
|
|
@@ -60,7 +67,7 @@ module Gitlab
|
|
|
60
67
|
|
|
61
68
|
private
|
|
62
69
|
|
|
63
|
-
attr_reader :source_type, :resource_type, :conditions, :graphql_only, :resource_declarations
|
|
70
|
+
attr_reader :source_type, :resource_type, :conditions, :actions, :graphql_only, :resource_declarations
|
|
64
71
|
|
|
65
72
|
def vote_attribute
|
|
66
73
|
@vote_attribute ||= (conditions.dig(:votes, :attribute) || conditions.dig(:upvotes, :attribute)).to_s
|
|
@@ -75,7 +82,8 @@ module Gitlab
|
|
|
75
82
|
if graphql_only
|
|
76
83
|
fields << 'labels { nodes { title } }'
|
|
77
84
|
fields << 'author { id name username }'
|
|
78
|
-
fields << 'assignees { nodes { id name username } }' if
|
|
85
|
+
fields << 'assignees { nodes { id name username } }' if query_assignees?
|
|
86
|
+
fields << 'reviewers { nodes { id name username } }' if query_reviewers?
|
|
79
87
|
fields << 'upvotes' if vote_attribute == 'upvotes'
|
|
80
88
|
fields << 'downvotes' if vote_attribute == 'downvotes'
|
|
81
89
|
fields.push('draft', 'mergedAt') if resource_type == 'merge_requests'
|
|
@@ -84,6 +92,41 @@ module Gitlab
|
|
|
84
92
|
fields
|
|
85
93
|
end
|
|
86
94
|
|
|
95
|
+
def query_assignees?
|
|
96
|
+
return false unless ASSIGNEES_RESOURCE_TYPES.include?(resource_type)
|
|
97
|
+
|
|
98
|
+
conditions.key?(:assignee_member) || actions_need?(:assignees)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def query_reviewers?
|
|
102
|
+
return false unless REVIEWERS_RESOURCE_TYPES.include?(resource_type)
|
|
103
|
+
|
|
104
|
+
actions_need?(:reviewers)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# An action needs a user field when it removes from that field, or when its
|
|
108
|
+
# text carries the field's placeholder. Either way the users have to be on
|
|
109
|
+
# the resource before the action runs, so the rule's actions decide these
|
|
110
|
+
# fields as much as its conditions do. The placeholder test matches the one
|
|
111
|
+
# in TextContentBuilder#replace_placeholders, so a policy that reaches for
|
|
112
|
+
# the users through Ruby interpolation instead is not covered.
|
|
113
|
+
def actions_need?(field)
|
|
114
|
+
actions[:"remove_#{field}"].present? || action_text.include?("{{#{field}}}")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def action_text
|
|
118
|
+
@action_text ||= collect_strings(actions).join("\n")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def collect_strings(value)
|
|
122
|
+
case value
|
|
123
|
+
when String then [value]
|
|
124
|
+
when Hash then value.values.flat_map { |nested| collect_strings(nested) }
|
|
125
|
+
when Array then value.flat_map { |nested| collect_strings(nested) }
|
|
126
|
+
else []
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
87
130
|
def resource_query
|
|
88
131
|
condition_queries = []
|
|
89
132
|
|
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.55.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GitLab
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|