gitlab-triage 1.56.1 → 1.57.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/CODEOWNERS +1 -2
- data/.gitlab-ci.yml +0 -13
- data/.rubocop.yml +5 -0
- data/.rubocop_todo.yml +0 -21
- data/Gemfile.lock +39 -26
- data/README.md +15 -11
- data/gitlab-triage.gemspec +4 -7
- data/lib/gitlab/triage/action/issue.rb +1 -1
- data/lib/gitlab/triage/action/summarize.rb +1 -1
- data/lib/gitlab/triage/action/work_item.rb +42 -13
- data/lib/gitlab/triage/engine.rb +82 -112
- data/lib/gitlab/triage/network_adapters/base_adapter.rb +17 -0
- data/lib/gitlab/triage/network_adapters/graphql_adapter.rb +1 -19
- data/lib/gitlab/triage/network_adapters/httparty_adapter.rb +10 -10
- data/lib/gitlab/triage/option_parser.rb +4 -4
- data/lib/gitlab/triage/policies/base_policy.rb +1 -1
- data/lib/gitlab/triage/rest_api_network.rb +5 -2
- data/lib/gitlab/triage/validators/policy_validator.rb +12 -1
- data/lib/gitlab/triage/version.rb +1 -1
- metadata +12 -7
- data/.codeclimate.yml +0 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 624c4d1977031933b3f310f7ae5c82778ac62bcda0f86a0c2c4a375504b6e715
|
|
4
|
+
data.tar.gz: 9545979a57eea0ff733995ac553271ad310c2c2710ac4fe78645daff962449b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1721670e0285a0d0fffd8cb076becb239e859267d1da55f4819526f39128dec62e884251f6ae28e9d1550eeca2eb29f8c5b08c1c8d6e16b480f22254bf20016c
|
|
7
|
+
data.tar.gz: 7b233404c63d41de48dfadf6e7fdacadb6412c47fe00562d417626534ef21b5dd8c4c14340b1d46f27bad1c5c2e58378d55e80b6aac5c8e0f25f12d2075c3515
|
data/.gitlab/CODEOWNERS
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* @gl-quality/eng-prod
|
|
1
|
+
* @gl-dx
|
data/.gitlab-ci.yml
CHANGED
|
@@ -107,7 +107,6 @@ dry-run:custom:
|
|
|
107
107
|
allow_failure: true
|
|
108
108
|
|
|
109
109
|
include:
|
|
110
|
-
- component: gitlab.com/components/code-quality/code-quality@~latest
|
|
111
110
|
- component: gitlab.com/components/sast/sast@~latest
|
|
112
111
|
- component: gitlab.com/components/secret-detection/secret-detection@~latest
|
|
113
112
|
- component: gitlab.com/gitlab-org/components/danger-review/danger-review@~latest
|
|
@@ -119,18 +118,6 @@ include:
|
|
|
119
118
|
- if: '$CI_MERGE_REQUEST_IID'
|
|
120
119
|
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
121
120
|
|
|
122
|
-
code_quality:
|
|
123
|
-
# We explicitly set dependencies here as the component sets them to []
|
|
124
|
-
# This allows us to get the cache from the setup job that includes the Gemfile.lock
|
|
125
|
-
dependencies: ["setup-test-env"]
|
|
126
|
-
tags:
|
|
127
|
-
- gitlab-org-docker
|
|
128
|
-
# We explicitly override allow_failure here as the component sets it to true
|
|
129
|
-
# Can be moved to a component input after: https://gitlab.com/components/code-quality/-/merge_requests/8
|
|
130
|
-
allow_failure: false
|
|
131
|
-
<<: *ruby-cache
|
|
132
|
-
<<: *component-general-rules
|
|
133
|
-
|
|
134
121
|
semgrep-sast:
|
|
135
122
|
<<: *component-general-rules
|
|
136
123
|
|
data/.rubocop.yml
CHANGED
|
@@ -51,3 +51,8 @@ RSpec/IndexedLet:
|
|
|
51
51
|
# rationale: Maybe we should? For now, let's keep it disabled and revisit at a later point.
|
|
52
52
|
Rails/Output:
|
|
53
53
|
Enabled: false
|
|
54
|
+
|
|
55
|
+
# rationale: gitlab-triage has no database / ActiveRecord models
|
|
56
|
+
# context: https://gitlab.com/gitlab-org/ruby/gems/gitlab-triage/-/work_items/399
|
|
57
|
+
CodeReuse/ActiveRecord:
|
|
58
|
+
Enabled: false
|
data/.rubocop_todo.yml
CHANGED
|
@@ -6,27 +6,6 @@
|
|
|
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: 81
|
|
10
|
-
CodeReuse/ActiveRecord:
|
|
11
|
-
Exclude:
|
|
12
|
-
- 'lib/gitlab/triage/action/work_item.rb'
|
|
13
|
-
- 'lib/gitlab/triage/engine.rb'
|
|
14
|
-
- 'lib/gitlab/triage/filters/base_conditions_filter.rb'
|
|
15
|
-
- 'lib/gitlab/triage/filters/member_conditions_filter.rb'
|
|
16
|
-
- 'lib/gitlab/triage/graphql_network.rb'
|
|
17
|
-
- 'lib/gitlab/triage/normalizers/work_item_normalizer.rb'
|
|
18
|
-
- 'lib/gitlab/triage/validators/limiter_validator.rb'
|
|
19
|
-
- 'spec/gitlab/triage/action_spec.rb'
|
|
20
|
-
- 'spec/gitlab/triage/engine_spec.rb'
|
|
21
|
-
- 'spec/gitlab/triage/graphql_network_spec.rb'
|
|
22
|
-
- 'spec/gitlab/triage/network_adapters/graphql_adapter_spec.rb'
|
|
23
|
-
- 'spec/gitlab/triage/policies/rule_policy_spec.rb'
|
|
24
|
-
- 'spec/gitlab/triage/resource/label_spec.rb'
|
|
25
|
-
- 'spec/gitlab/triage/rest_api_network_spec.rb'
|
|
26
|
-
- 'spec/support/shared_examples/issuable_shared_examples.rb'
|
|
27
|
-
- 'spec/support/shared_examples/label_command_shared_examples.rb'
|
|
28
|
-
- 'spec/support/stub_api.rb'
|
|
29
|
-
|
|
30
9
|
# Offense count: 1
|
|
31
10
|
Lint/ToEnumArguments:
|
|
32
11
|
Exclude:
|
data/Gemfile.lock
CHANGED
|
@@ -1,31 +1,35 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
gitlab-triage (1.
|
|
4
|
+
gitlab-triage (1.57.0)
|
|
5
5
|
activesupport (>= 5.1)
|
|
6
6
|
globalid (~> 1.0, >= 1.0.1)
|
|
7
|
-
graphql (< 2.
|
|
8
|
-
graphql-client (~> 0.
|
|
7
|
+
graphql (>= 2.4.13, < 2.7.0)
|
|
8
|
+
graphql-client (~> 0.23)
|
|
9
9
|
httparty (>= 0.21.0, < 1.0)
|
|
10
10
|
|
|
11
11
|
GEM
|
|
12
12
|
remote: https://rubygems.org/
|
|
13
13
|
specs:
|
|
14
|
-
activesupport (7.1.
|
|
14
|
+
activesupport (7.1.6)
|
|
15
15
|
base64
|
|
16
|
+
benchmark (>= 0.3)
|
|
16
17
|
bigdecimal
|
|
17
18
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
18
19
|
connection_pool (>= 2.2.5)
|
|
19
20
|
drb
|
|
20
21
|
i18n (>= 1.6, < 2)
|
|
22
|
+
logger (>= 1.4.2)
|
|
21
23
|
minitest (>= 5.1)
|
|
22
24
|
mutex_m
|
|
25
|
+
securerandom (>= 0.3)
|
|
23
26
|
tzinfo (~> 2.0)
|
|
24
|
-
addressable (2.
|
|
25
|
-
public_suffix (>= 2.0.2, <
|
|
27
|
+
addressable (2.9.0)
|
|
28
|
+
public_suffix (>= 2.0.2, < 8.0)
|
|
26
29
|
ast (2.4.2)
|
|
27
|
-
base64 (0.
|
|
28
|
-
|
|
30
|
+
base64 (0.3.0)
|
|
31
|
+
benchmark (0.5.0)
|
|
32
|
+
bigdecimal (4.1.3)
|
|
29
33
|
claide (1.1.0)
|
|
30
34
|
claide-plugins (0.9.2)
|
|
31
35
|
cork
|
|
@@ -33,8 +37,8 @@ GEM
|
|
|
33
37
|
open4 (~> 1.3)
|
|
34
38
|
coderay (1.1.3)
|
|
35
39
|
colored2 (3.1.2)
|
|
36
|
-
concurrent-ruby (1.3.
|
|
37
|
-
connection_pool (2.
|
|
40
|
+
concurrent-ruby (1.3.8)
|
|
41
|
+
connection_pool (2.5.5)
|
|
38
42
|
cork (0.3.0)
|
|
39
43
|
colored2 (~> 3.1)
|
|
40
44
|
crack (1.0.0)
|
|
@@ -59,13 +63,16 @@ GEM
|
|
|
59
63
|
gitlab (~> 4.2, >= 4.2.0)
|
|
60
64
|
diff-lcs (1.5.1)
|
|
61
65
|
drb (2.2.3)
|
|
62
|
-
faraday (2.
|
|
63
|
-
faraday-net_http (>= 2.0, < 3.
|
|
66
|
+
faraday (2.14.4)
|
|
67
|
+
faraday-net_http (>= 2.0, < 3.5)
|
|
68
|
+
json
|
|
69
|
+
logger
|
|
64
70
|
faraday-http-cache (2.5.1)
|
|
65
71
|
faraday (>= 0.8)
|
|
66
|
-
faraday-net_http (3.
|
|
67
|
-
net-http
|
|
72
|
+
faraday-net_http (3.4.4)
|
|
73
|
+
net-http (~> 0.5)
|
|
68
74
|
ffi (1.17.0)
|
|
75
|
+
fiber-storage (1.0.0)
|
|
69
76
|
formatador (1.1.0)
|
|
70
77
|
git (1.19.1)
|
|
71
78
|
addressable (~> 2.8)
|
|
@@ -85,9 +92,11 @@ GEM
|
|
|
85
92
|
rubocop-rspec (~> 2.27.1)
|
|
86
93
|
globalid (1.2.1)
|
|
87
94
|
activesupport (>= 6.1)
|
|
88
|
-
graphql (2.
|
|
95
|
+
graphql (2.6.11)
|
|
89
96
|
base64
|
|
90
|
-
|
|
97
|
+
fiber-storage
|
|
98
|
+
logger
|
|
99
|
+
graphql-client (0.26.0)
|
|
91
100
|
activesupport (>= 3.0)
|
|
92
101
|
graphql (>= 1.13.0)
|
|
93
102
|
guard (2.18.1)
|
|
@@ -109,7 +118,7 @@ GEM
|
|
|
109
118
|
csv
|
|
110
119
|
mini_mime (>= 1.0.0)
|
|
111
120
|
multi_xml (>= 0.5.2)
|
|
112
|
-
i18n (1.14.
|
|
121
|
+
i18n (1.14.8)
|
|
113
122
|
concurrent-ruby (~> 1.0)
|
|
114
123
|
json (2.7.2)
|
|
115
124
|
kramdown (2.4.0)
|
|
@@ -121,16 +130,20 @@ GEM
|
|
|
121
130
|
listen (3.9.0)
|
|
122
131
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
|
123
132
|
rb-inotify (~> 0.9, >= 0.9.10)
|
|
133
|
+
logger (1.7.0)
|
|
124
134
|
lumberjack (1.2.10)
|
|
125
135
|
method_source (1.1.0)
|
|
126
136
|
mini_mime (1.1.5)
|
|
127
137
|
minitest (5.26.1)
|
|
128
138
|
multi_xml (0.6.0)
|
|
129
|
-
mutex_m (0.
|
|
139
|
+
mutex_m (0.3.0)
|
|
130
140
|
nap (1.1.0)
|
|
131
141
|
nenv (0.3.0)
|
|
132
|
-
net-http (0.
|
|
133
|
-
|
|
142
|
+
net-http (0.10.0)
|
|
143
|
+
net-protocol (>= 0.3.0)
|
|
144
|
+
uri (>= 0.11.1)
|
|
145
|
+
net-protocol (0.4.0)
|
|
146
|
+
timeout
|
|
134
147
|
no_proxy_fix (0.1.2)
|
|
135
148
|
notiffany (0.1.3)
|
|
136
149
|
nenv (~> 0.1)
|
|
@@ -146,9 +159,9 @@ GEM
|
|
|
146
159
|
pry (0.14.2)
|
|
147
160
|
coderay (~> 1.1)
|
|
148
161
|
method_source (~> 1.0)
|
|
149
|
-
public_suffix (
|
|
162
|
+
public_suffix (6.0.2)
|
|
150
163
|
racc (1.8.0)
|
|
151
|
-
rack (3.
|
|
164
|
+
rack (3.2.7)
|
|
152
165
|
rainbow (3.1.1)
|
|
153
166
|
rake (13.2.1)
|
|
154
167
|
rb-fsevent (0.11.2)
|
|
@@ -156,8 +169,7 @@ GEM
|
|
|
156
169
|
ffi (~> 1.0)
|
|
157
170
|
rchardet (1.8.0)
|
|
158
171
|
regexp_parser (2.9.2)
|
|
159
|
-
rexml (3.
|
|
160
|
-
strscan
|
|
172
|
+
rexml (3.4.4)
|
|
161
173
|
rspec (3.13.0)
|
|
162
174
|
rspec-core (~> 3.13.0)
|
|
163
175
|
rspec-expectations (~> 3.13.0)
|
|
@@ -206,15 +218,16 @@ GEM
|
|
|
206
218
|
sawyer (0.9.2)
|
|
207
219
|
addressable (>= 2.3.5)
|
|
208
220
|
faraday (>= 0.17.3, < 3)
|
|
221
|
+
securerandom (0.3.2)
|
|
209
222
|
shellany (0.0.1)
|
|
210
|
-
strscan (3.1.0)
|
|
211
223
|
terminal-table (3.0.2)
|
|
212
224
|
unicode-display_width (>= 1.1.1, < 3)
|
|
213
225
|
thor (1.3.1)
|
|
226
|
+
timeout (0.6.1)
|
|
214
227
|
tzinfo (2.0.6)
|
|
215
228
|
concurrent-ruby (~> 1.0)
|
|
216
229
|
unicode-display_width (2.5.0)
|
|
217
|
-
uri (
|
|
230
|
+
uri (1.1.2)
|
|
218
231
|
webmock (3.23.1)
|
|
219
232
|
addressable (>= 2.8.0)
|
|
220
233
|
crack (>= 0.3.2)
|
data/README.md
CHANGED
|
@@ -203,6 +203,8 @@ resource_rules:
|
|
|
203
203
|
- `labels` - add labels by name. If any requested label does not exist, the
|
|
204
204
|
automation stops immediately (matching the `labels` action on the `issues`
|
|
205
205
|
type) rather than applying nothing.
|
|
206
|
+
- `remove_labels` - remove labels by name. Like `labels`, a requested label that
|
|
207
|
+
does not exist stops the automation rather than applying a partial change.
|
|
206
208
|
- `assignees` - set assignees by username. If any requested username does not
|
|
207
209
|
exist, the automation stops immediately rather than sending an empty
|
|
208
210
|
assignee set (which would unassign everyone).
|
|
@@ -215,7 +217,7 @@ resource_rules:
|
|
|
215
217
|
to comment.
|
|
216
218
|
|
|
217
219
|
Summaries are also supported (`summarize` and `comment_on_summary`). Any other
|
|
218
|
-
action key (for example `
|
|
220
|
+
action key (for example `mention`, `move`, or `status`) is
|
|
219
221
|
rejected at load time with an error listing the supported actions. On the
|
|
220
222
|
older policy types these keys are applied through comment quick actions, which
|
|
221
223
|
work items do not support yet - rejecting them up front beats silently doing
|
|
@@ -345,7 +347,7 @@ Accepts the name of a milestone to filter upon. Also accepts the following timeb
|
|
|
345
347
|
- `upcoming`
|
|
346
348
|
- `started`
|
|
347
349
|
|
|
348
|
-
See the [`milestone_id` API field documentation](https://docs.gitlab.com/
|
|
350
|
+
See the [`milestone_id` API field documentation](https://docs.gitlab.com/api/issues/) for their meaning.
|
|
349
351
|
|
|
350
352
|
Example:
|
|
351
353
|
|
|
@@ -362,7 +364,7 @@ timebox values:
|
|
|
362
364
|
- `none`
|
|
363
365
|
- `any`
|
|
364
366
|
|
|
365
|
-
See the [`iteration_id` API field documentation](https://docs.gitlab.com/
|
|
367
|
+
See the [`iteration_id` API field documentation](https://docs.gitlab.com/api/issues/) for their meaning.
|
|
366
368
|
|
|
367
369
|
Example:
|
|
368
370
|
|
|
@@ -427,7 +429,7 @@ conditions:
|
|
|
427
429
|
|
|
428
430
|
###### Predefined special label names
|
|
429
431
|
|
|
430
|
-
Basing on the [issues API](https://docs.gitlab.com/
|
|
432
|
+
Basing on the [issues API](https://docs.gitlab.com/api/issues/), there
|
|
431
433
|
are two special predefined label names we can use here:
|
|
432
434
|
|
|
433
435
|
* `None`: This indicates that no labels were present
|
|
@@ -694,7 +696,7 @@ conditions:
|
|
|
694
696
|
|
|
695
697
|
**This condition is only applicable for issues.**
|
|
696
698
|
|
|
697
|
-
Accepts a string per the [API documentation](https://docs.gitlab.com/
|
|
699
|
+
Accepts a string per the [API documentation](https://docs.gitlab.com/api/issues/#list-all-issues).
|
|
698
700
|
|
|
699
701
|
| State | Type | Value |
|
|
700
702
|
| --------- | ---- | ------ |
|
|
@@ -715,7 +717,7 @@ conditions:
|
|
|
715
717
|
|
|
716
718
|
**This condition is only applicable for issues.**
|
|
717
719
|
|
|
718
|
-
Accepts a string per the [API documentation](https://docs.gitlab.com/
|
|
720
|
+
Accepts a string per the [API documentation](https://docs.gitlab.com/api/issues/#list-all-issues).
|
|
719
721
|
|
|
720
722
|
| State | Type | Value |
|
|
721
723
|
| --------- | ---- | ------ |
|
|
@@ -734,7 +736,7 @@ conditions:
|
|
|
734
736
|
|
|
735
737
|
**This condition is only applicable for issues.**
|
|
736
738
|
|
|
737
|
-
Accepts a string per the [API documentation](https://docs.gitlab.com/
|
|
739
|
+
Accepts a string per the [API documentation](https://docs.gitlab.com/api/issues/#list-all-issues). This condition can only filter by one issue type.
|
|
738
740
|
|
|
739
741
|
| Issue type | Type | Value |
|
|
740
742
|
| --------- | ---- | ------ |
|
|
@@ -1260,7 +1262,7 @@ The following comment types are supported:
|
|
|
1260
1262
|
- `comment` (default): creates a regular comment on the resource
|
|
1261
1263
|
- `thread`: starts a resolvable thread (discussion) on the resource
|
|
1262
1264
|
|
|
1263
|
-
For merge requests, if `comment_type` is set to `thread`, we can also configure that [all threads should be resolved before merging](https://docs.gitlab.com/
|
|
1265
|
+
For merge requests, if `comment_type` is set to `thread`, we can also configure that [all threads should be resolved before merging](https://docs.gitlab.com/user/project/merge_requests/#prevent-merge-unless-all-threads-are-resolved), therefore this comment can prevent it from merging.
|
|
1264
1266
|
|
|
1265
1267
|
Example:
|
|
1266
1268
|
|
|
@@ -1289,14 +1291,14 @@ actions:
|
|
|
1289
1291
|
|
|
1290
1292
|
###### Harnessing Quick Actions
|
|
1291
1293
|
|
|
1292
|
-
[GitLab's quick actions feature](https://docs.gitlab.com/
|
|
1294
|
+
[GitLab's quick actions feature](https://docs.gitlab.com/user/project/quick_actions/) is available in Core.
|
|
1293
1295
|
All of the operations supported by executing a quick action can be carried out via the comment action.
|
|
1294
1296
|
|
|
1295
1297
|
If GitLab triage does not support an operation natively, it may be possible via a quick action in a comment.
|
|
1296
1298
|
|
|
1297
1299
|
For example:
|
|
1298
|
-
- Flagging an issue as [confidential](https://docs.gitlab.com/
|
|
1299
|
-
- [Locking issue discussion](https://docs.gitlab.com/
|
|
1300
|
+
- Flagging an issue as [confidential](https://docs.gitlab.com/user/project/issues/confidential_issues/)
|
|
1301
|
+
- [Locking issue discussion](https://docs.gitlab.com/user/discussions/#prevent-comments-by-locking-the-discussion)
|
|
1300
1302
|
|
|
1301
1303
|
```yml
|
|
1302
1304
|
resource_rules:
|
|
@@ -1801,6 +1803,8 @@ Usage: gitlab-triage [options]
|
|
|
1801
1803
|
-H, --host-url [string] A valid host url
|
|
1802
1804
|
-r, --require [string] Require a file before performing
|
|
1803
1805
|
-d, --debug Print debug information
|
|
1806
|
+
--summary Print API call summary at end of execution
|
|
1807
|
+
--quiet Suppress pagination dots and section headers
|
|
1804
1808
|
--cache-dir [string] Directory containing pre-fetched API responses.
|
|
1805
1809
|
Files must follow the naming convention: <source>_<id>_<resource>.json
|
|
1806
1810
|
(e.g. groups_9970_members.json).
|
data/gitlab-triage.gemspec
CHANGED
|
@@ -33,13 +33,10 @@ Gem::Specification.new do |spec|
|
|
|
33
33
|
|
|
34
34
|
spec.add_dependency 'activesupport', '>= 5.1'
|
|
35
35
|
spec.add_dependency 'globalid', '~> 1.0', '>= 1.0.1'
|
|
36
|
-
spec.add_dependency 'graphql-client', '~> 0.
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
# see: https://github.com/github/graphql-client/pull/314
|
|
41
|
-
# We can remove this check once PR 314 is merged and released
|
|
42
|
-
spec.add_dependency 'graphql', '< 2.1.0'
|
|
36
|
+
spec.add_dependency 'graphql-client', '~> 0.23'
|
|
37
|
+
# graphql >= 2.3.5 needs graphql-client >= 0.23 (possible_types keys changed).
|
|
38
|
+
# 2.4.13 is the patched version on the highest affected minor line.
|
|
39
|
+
spec.add_dependency 'graphql', '>= 2.4.13', '< 2.7.0'
|
|
43
40
|
spec.add_dependency 'httparty', '>= 0.21.0', '< 1.0'
|
|
44
41
|
|
|
45
42
|
spec.add_development_dependency 'gitlab-dangerfiles', '~> 2.11.0'
|
|
@@ -63,7 +63,7 @@ module Gitlab
|
|
|
63
63
|
|
|
64
64
|
def build_post_url(resource, issue)
|
|
65
65
|
# POST /projects/:id/issues
|
|
66
|
-
# https://docs.gitlab.com/
|
|
66
|
+
# https://docs.gitlab.com/api/issues/#create-an-issue
|
|
67
67
|
post_url = UrlBuilders::UrlBuilder.new(
|
|
68
68
|
network_options: network.options,
|
|
69
69
|
source_id: issue.destination || resource[policy.source_id_sym],
|
|
@@ -79,7 +79,7 @@ module Gitlab
|
|
|
79
79
|
|
|
80
80
|
def post_issue_url
|
|
81
81
|
# POST /projects/:id/issues
|
|
82
|
-
# https://docs.gitlab.com/
|
|
82
|
+
# https://docs.gitlab.com/api/issues/#create-an-issue
|
|
83
83
|
post_url = UrlBuilders::UrlBuilder.new(
|
|
84
84
|
network_options: network.options,
|
|
85
85
|
source_id: destination,
|
|
@@ -11,8 +11,9 @@ module Gitlab
|
|
|
11
11
|
# Applies work_items actions through the Work Items REST API update
|
|
12
12
|
# endpoint (PATCH) rather than the issues note/quick-action path.
|
|
13
13
|
#
|
|
14
|
-
# - labels
|
|
15
|
-
# -
|
|
14
|
+
# - labels -> features.labels.add_label_ids (resolved from names)
|
|
15
|
+
# - remove_labels -> features.labels.remove_label_ids (resolved from names)
|
|
16
|
+
# - assignees -> features.assignees.assignee_ids (resolved from usernames)
|
|
16
17
|
#
|
|
17
18
|
# Missing labels or usernames stop the automation immediately (matching
|
|
18
19
|
# the labels action behaviour on the issues type) rather than silently
|
|
@@ -36,13 +37,14 @@ module Gitlab
|
|
|
36
37
|
def perform(resource)
|
|
37
38
|
puts "# #{resource[:web_url]}"
|
|
38
39
|
puts "Would add labels: #{requested_labels.join(', ')}" if requested_labels.any?
|
|
40
|
+
puts "Would remove labels: #{requested_removed_labels.join(', ')}" if requested_removed_labels.any?
|
|
39
41
|
puts "Would assign: #{requested_assignees.join(', ')}" if requested_assignees.any?
|
|
40
42
|
puts "\n"
|
|
41
43
|
end
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
def act
|
|
45
|
-
return unless requested_labels.any? || requested_assignees.any?
|
|
47
|
+
return unless requested_labels.any? || requested_removed_labels.any? || requested_assignees.any?
|
|
46
48
|
|
|
47
49
|
policy.resources.each do |resource|
|
|
48
50
|
perform(resource)
|
|
@@ -63,7 +65,11 @@ module Gitlab
|
|
|
63
65
|
def build_features(resource)
|
|
64
66
|
features = {}
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
label_changes = {}
|
|
69
|
+
label_changes[:add_label_ids] = label_ids_to_add(resource) if requested_labels.any?
|
|
70
|
+
label_changes[:remove_label_ids] = label_ids_to_remove(resource) if requested_removed_labels.any?
|
|
71
|
+
features[:labels] = label_changes if label_changes.any?
|
|
72
|
+
|
|
67
73
|
features[:assignees] = { assignee_ids: assignee_ids } if requested_assignees.any?
|
|
68
74
|
|
|
69
75
|
features
|
|
@@ -73,6 +79,10 @@ module Gitlab
|
|
|
73
79
|
@requested_labels ||= Array(policy.actions[:labels])
|
|
74
80
|
end
|
|
75
81
|
|
|
82
|
+
def requested_removed_labels
|
|
83
|
+
@requested_removed_labels ||= Array(policy.actions[:remove_labels])
|
|
84
|
+
end
|
|
85
|
+
|
|
76
86
|
def requested_assignees
|
|
77
87
|
@requested_assignees ||= Array(policy.actions[:assignees])
|
|
78
88
|
end
|
|
@@ -82,14 +92,16 @@ module Gitlab
|
|
|
82
92
|
network: network, options: network.options)
|
|
83
93
|
end
|
|
84
94
|
|
|
85
|
-
# Label ids are namespace-specific, so resolve
|
|
86
|
-
#
|
|
87
|
-
#
|
|
95
|
+
# Label ids are namespace-specific, so resolve against the resource's
|
|
96
|
+
# own namespace. The fetched label list is cached per scope, so a rule
|
|
97
|
+
# that both adds and removes labels - or a group-spanning run - makes
|
|
98
|
+
# one lookup per distinct project/group.
|
|
88
99
|
def label_ids_to_add(resource)
|
|
89
|
-
|
|
100
|
+
resolve_label_ids(requested_labels, source.scope_for(resource))
|
|
101
|
+
end
|
|
90
102
|
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
def label_ids_to_remove(resource)
|
|
104
|
+
resolve_label_ids(requested_removed_labels, source.scope_for(resource))
|
|
93
105
|
end
|
|
94
106
|
|
|
95
107
|
def assignee_ids
|
|
@@ -102,7 +114,7 @@ module Gitlab
|
|
|
102
114
|
def resolve_label_ids(names, scope)
|
|
103
115
|
return [] if names.empty?
|
|
104
116
|
|
|
105
|
-
by_name =
|
|
117
|
+
by_name = labels_by_name(scope)
|
|
106
118
|
|
|
107
119
|
names.map do |name|
|
|
108
120
|
label = by_name[name]
|
|
@@ -112,24 +124,41 @@ module Gitlab
|
|
|
112
124
|
end
|
|
113
125
|
end
|
|
114
126
|
|
|
127
|
+
def labels_by_name(scope)
|
|
128
|
+
@labels_by_name_by_scope ||= {}
|
|
129
|
+
@labels_by_name_by_scope[scope] ||= network.query_api(labels_url(scope)).index_by { |label| label[:name] }
|
|
130
|
+
end
|
|
131
|
+
|
|
115
132
|
# The Work Items REST update endpoint can return success without
|
|
116
133
|
# persisting a change when the token lacks permission. The response
|
|
117
134
|
# echoes the resulting widgets, so confirm the requested ids landed and
|
|
118
135
|
# raise otherwise rather than reporting a silent no-op as success.
|
|
119
136
|
def verify_applied!(resource, response)
|
|
120
137
|
verify_labels_applied!(resource, response) if requested_labels.any?
|
|
138
|
+
verify_labels_removed!(resource, response) if requested_removed_labels.any?
|
|
121
139
|
verify_assignees_applied!(resource, response) if requested_assignees.any?
|
|
122
140
|
end
|
|
123
141
|
|
|
124
142
|
def verify_labels_applied!(resource, response)
|
|
125
|
-
|
|
126
|
-
missing = label_ids_to_add(resource) - applied_ids
|
|
143
|
+
missing = label_ids_to_add(resource) - applied_label_ids(response)
|
|
127
144
|
return if missing.empty?
|
|
128
145
|
|
|
129
146
|
raise UpdateNotAppliedError,
|
|
130
147
|
"Labels were not applied to #{resource[:web_url]}; the token may lack permission."
|
|
131
148
|
end
|
|
132
149
|
|
|
150
|
+
def verify_labels_removed!(resource, response)
|
|
151
|
+
still_present = label_ids_to_remove(resource) & applied_label_ids(response)
|
|
152
|
+
return if still_present.empty?
|
|
153
|
+
|
|
154
|
+
raise UpdateNotAppliedError,
|
|
155
|
+
"Labels were not removed from #{resource[:web_url]}; the token may lack permission."
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def applied_label_ids(response)
|
|
159
|
+
[*response.dig(:features, :labels, :labels)].pluck(:id)
|
|
160
|
+
end
|
|
161
|
+
|
|
133
162
|
def verify_assignees_applied!(resource, response)
|
|
134
163
|
applied_ids = [*response.dig(:features, :assignees)].pluck(:id)
|
|
135
164
|
missing = assignee_ids - applied_ids
|
data/lib/gitlab/triage/engine.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'active_support/all'
|
|
4
4
|
require 'active_support/inflector'
|
|
5
|
+
require 'globalid'
|
|
5
6
|
|
|
6
7
|
require_relative 'expand_condition'
|
|
7
8
|
require_relative 'filters/issue_date_conditions_filter'
|
|
@@ -409,14 +410,14 @@ module Gitlab
|
|
|
409
410
|
end
|
|
410
411
|
|
|
411
412
|
ExpandCondition.perform(rule_conditions(rule_definition)) do |expanded_conditions|
|
|
412
|
-
|
|
413
|
+
status_by_id = pre_filter_by_work_item_status!(resource_type, expanded_conditions)
|
|
413
414
|
|
|
414
415
|
# retrieving the resources for every rule is inefficient
|
|
415
416
|
# however, previous rules may affect those upcoming
|
|
416
417
|
resources = options.resources ||
|
|
417
418
|
fetch_resources(resource_type, expanded_conditions, rule_definition)
|
|
418
419
|
|
|
419
|
-
apply_work_item_status!(resource_type, expanded_conditions, rule_definition, resources,
|
|
420
|
+
apply_work_item_status!(resource_type, expanded_conditions, rule_definition, resources, status_by_id)
|
|
420
421
|
|
|
421
422
|
# In some filters/actions we want to know which resource type it is
|
|
422
423
|
attach_resource_type(resources, resource_type)
|
|
@@ -452,6 +453,7 @@ module Gitlab
|
|
|
452
453
|
|
|
453
454
|
if rule_definition[:api] == 'graphql'
|
|
454
455
|
graphql_query_options = { source: source_full_path }
|
|
456
|
+
pre_filter_status_ids = expanded_conditions.delete(:work_item_status_ids)
|
|
455
457
|
|
|
456
458
|
if options.resource_reference
|
|
457
459
|
expanded_conditions[:iids] = options.resource_reference[1..]
|
|
@@ -463,25 +465,16 @@ module Gitlab
|
|
|
463
465
|
graphql_query = build_graphql_query(resource_type, expanded_conditions, rule_definition, true)
|
|
464
466
|
|
|
465
467
|
resources = graphql_network.query(graphql_query, **graphql_query_options)
|
|
468
|
+
resources = narrow_to_pre_filtered(resources, nil, pre_filter_status_ids)
|
|
466
469
|
else
|
|
467
470
|
# FIXME: Epics listing endpoint doesn't support filtering by `iids`, so instead we
|
|
468
471
|
# get a single epic when `--resource-reference` is given for epics.
|
|
469
472
|
# Because of that, the query could return a single epic, so we make sure we get an array.
|
|
470
473
|
pre_filter_iids = expanded_conditions.delete(:iids)
|
|
474
|
+
pre_filter_status_ids = expanded_conditions.delete(:work_item_status_ids)
|
|
471
475
|
|
|
472
476
|
resources = Array(network.query_api(build_get_url(resource_type, expanded_conditions)))
|
|
473
|
-
|
|
474
|
-
# Narrow the REST results to the pre-filtered IID set, when a pre-filter
|
|
475
|
-
# ran. The contract is carried by the value itself: `nil` means no
|
|
476
|
-
# pre-filter ran (keep all resources), while any array - including `[]` -
|
|
477
|
-
# means "narrow to exactly these IIDs". An empty array therefore yields
|
|
478
|
-
# zero resources, which is correct when a pre-filter (e.g. work_item_status)
|
|
479
|
-
# ran and matched nothing. `present?` cannot express this because it is
|
|
480
|
-
# false for `[]`.
|
|
481
|
-
unless pre_filter_iids.nil?
|
|
482
|
-
filtered_iids = pre_filter_iids.map(&:to_s)
|
|
483
|
-
resources = resources.select { |r| filtered_iids.include?(r['iid'].to_s) }
|
|
484
|
-
end
|
|
477
|
+
resources = narrow_to_pre_filtered(resources, pre_filter_iids, pre_filter_status_ids)
|
|
485
478
|
|
|
486
479
|
iids = resources.pluck('iid').map(&:to_s)
|
|
487
480
|
expanded_conditions[:iids] = iids
|
|
@@ -495,6 +488,29 @@ module Gitlab
|
|
|
495
488
|
resources
|
|
496
489
|
end
|
|
497
490
|
|
|
491
|
+
# Narrow the fetched results to a pre-filtered set, when a pre-filter ran.
|
|
492
|
+
# The contract is carried by each value: `nil` means that pre-filter did not
|
|
493
|
+
# run (keep all resources), while any array - including `[]` - means "narrow
|
|
494
|
+
# to exactly these". An empty array therefore yields zero resources, which is
|
|
495
|
+
# correct when a pre-filter (e.g. work_item_status) ran and matched nothing;
|
|
496
|
+
# `present?` cannot express this because it is false for `[]`.
|
|
497
|
+
#
|
|
498
|
+
# The work_item_status pre-filter narrows by the globally-unique id, so it
|
|
499
|
+
# cannot conflate same-iid issues from different projects of a group.
|
|
500
|
+
def narrow_to_pre_filtered(resources, pre_filter_iids, pre_filter_status_ids)
|
|
501
|
+
unless pre_filter_status_ids.nil?
|
|
502
|
+
filtered_ids = pre_filter_status_ids.map(&:to_s)
|
|
503
|
+
resources = resources.select { |r| filtered_ids.include?(r[:id].to_s) }
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
unless pre_filter_iids.nil?
|
|
507
|
+
filtered_iids = pre_filter_iids.map(&:to_s)
|
|
508
|
+
resources = resources.select { |r| filtered_iids.include?(r[:iid].to_s) }
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
resources
|
|
512
|
+
end
|
|
513
|
+
|
|
498
514
|
def fetch_work_items(expanded_conditions)
|
|
499
515
|
conditions = expanded_conditions.dup
|
|
500
516
|
conditions[:iids] = options.resource_reference[1..] if options.resource_reference
|
|
@@ -714,7 +730,7 @@ module Gitlab
|
|
|
714
730
|
end
|
|
715
731
|
|
|
716
732
|
def draft_condition_builder(draft_condittion)
|
|
717
|
-
# Issues API only accepts 'yes' and 'no' as strings: https://docs.gitlab.com/
|
|
733
|
+
# Issues API only accepts 'yes' and 'no' as strings: https://docs.gitlab.com/api/merge_requests/
|
|
718
734
|
wip =
|
|
719
735
|
case draft_condittion
|
|
720
736
|
when true
|
|
@@ -729,32 +745,33 @@ module Gitlab
|
|
|
729
745
|
end
|
|
730
746
|
|
|
731
747
|
# When filtering by work_item_status, query workItems with server-side
|
|
732
|
-
# status filtering first to narrow the
|
|
748
|
+
# status filtering first to narrow the resource set before the main fetch.
|
|
733
749
|
# This avoids fetching all resources just to discard most of them.
|
|
750
|
+
#
|
|
751
|
+
# We narrow by the globally-unique work item id, not the iid: a group
|
|
752
|
+
# source flattens work items from every descendant project into one list,
|
|
753
|
+
# and iids are unique only within a project, so two projects can share an
|
|
754
|
+
# iid. The id side-steps that collision.
|
|
734
755
|
def pre_filter_by_work_item_status!(resource_type, expanded_conditions)
|
|
735
|
-
return
|
|
736
|
-
|
|
737
|
-
pre_filter_iids = fetch_work_item_iids_by_status(expanded_conditions[:work_item_status])
|
|
756
|
+
return unless resource_type == 'issues' && expanded_conditions.key?(:work_item_status)
|
|
738
757
|
|
|
739
|
-
expanded_conditions[:
|
|
740
|
-
|
|
741
|
-
Array(expanded_conditions[:iids]).map(&:to_s) & pre_filter_iids
|
|
742
|
-
else
|
|
743
|
-
pre_filter_iids
|
|
744
|
-
end
|
|
758
|
+
status_by_id = fetch_work_item_status_by_id(expanded_conditions[:work_item_status])
|
|
759
|
+
expanded_conditions[:work_item_status_ids] = status_by_id.keys
|
|
745
760
|
|
|
746
|
-
|
|
761
|
+
status_by_id
|
|
747
762
|
end
|
|
748
763
|
|
|
749
|
-
|
|
764
|
+
# The pre-filter (pre_filter_by_work_item_status!) always runs before this
|
|
765
|
+
# for an issues rule that filters by work_item_status, and returns a map of
|
|
766
|
+
# id to matched status name. We stamp each resource from that map instead of
|
|
767
|
+
# a second, iid-keyed status query: the pre-filter already knows the answer,
|
|
768
|
+
# and keying by the globally-unique id avoids the group iid collision that a
|
|
769
|
+
# flattened `iids:` query would reintroduce.
|
|
770
|
+
def apply_work_item_status!(resource_type, expanded_conditions, rule_definition, resources, status_by_id)
|
|
750
771
|
return unless resource_type == 'issues' && needs_work_item_status?(expanded_conditions, rule_definition)
|
|
751
772
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
if status_pre_filtered && status_values.size == 1
|
|
755
|
-
resources.each { |r| r[:work_item_status] = status_values.first }
|
|
756
|
-
else
|
|
757
|
-
decorate_resources_with_work_item_status(resources)
|
|
773
|
+
resources.each do |resource|
|
|
774
|
+
resource[:work_item_status] = status_by_id[resource[:id].to_s]
|
|
758
775
|
end
|
|
759
776
|
end
|
|
760
777
|
|
|
@@ -762,24 +779,40 @@ module Gitlab
|
|
|
762
779
|
conditions.key?(:work_item_status)
|
|
763
780
|
end
|
|
764
781
|
|
|
782
|
+
# A group's `workItems` connection returns only the group's own work items
|
|
783
|
+
# (epics), whose IIDs do not match the project-scoped issue IIDs the issues
|
|
784
|
+
# fetch returns. `includeDescendants: true` pulls in the descendant project
|
|
785
|
+
# issues so the status IIDs line up. The argument does not exist on
|
|
786
|
+
# `project.workItems`, so it is only emitted for a group source.
|
|
787
|
+
def work_item_descendants_argument
|
|
788
|
+
options.source == 'groups' ? 'includeDescendants: true, ' : ''
|
|
789
|
+
end
|
|
790
|
+
|
|
765
791
|
# Pre-filter: query workItems with server-side status filtering to get only
|
|
766
|
-
# the
|
|
767
|
-
# items won't pass the status filter.
|
|
768
|
-
|
|
792
|
+
# the items that match, avoiding fetching the full resource set when most
|
|
793
|
+
# items won't pass the status filter. Because it queries one status at a
|
|
794
|
+
# time, it also learns which status each id matched, and returns that as a
|
|
795
|
+
# map of globally-unique id (from the work item GID) to status name. The id
|
|
796
|
+
# keys stay correct across a group's descendant projects where iids can
|
|
797
|
+
# collide, and the map lets the caller stamp statuses without a second
|
|
798
|
+
# iid-keyed query. When several statuses match the same id, the last one
|
|
799
|
+
# queried wins; a work item has a single status, so this does not happen in
|
|
800
|
+
# practice.
|
|
801
|
+
def fetch_work_item_status_by_id(status_condition)
|
|
769
802
|
status_names = Array(status_condition)
|
|
770
|
-
|
|
803
|
+
status_by_id = {}
|
|
771
804
|
|
|
772
805
|
status_names.each do |status_name|
|
|
773
806
|
query = <<~GRAPHQL
|
|
774
807
|
query($source: ID!, $after: String, $statusName: String!) {
|
|
775
808
|
#{options.source.singularize}(fullPath: $source) {
|
|
776
|
-
workItems(status: { name: $statusName }, after: $after, first: 100) {
|
|
809
|
+
workItems(status: { name: $statusName }, #{work_item_descendants_argument}after: $after, first: 100) {
|
|
777
810
|
pageInfo {
|
|
778
811
|
hasNextPage
|
|
779
812
|
endCursor
|
|
780
813
|
}
|
|
781
814
|
nodes {
|
|
782
|
-
|
|
815
|
+
id
|
|
783
816
|
}
|
|
784
817
|
}
|
|
785
818
|
}
|
|
@@ -797,8 +830,9 @@ module Gitlab
|
|
|
797
830
|
|
|
798
831
|
work_items = Array.wrap(response[:results])
|
|
799
832
|
work_items.each do |wi|
|
|
800
|
-
|
|
801
|
-
|
|
833
|
+
gid = wi.is_a?(Hash) ? (wi['id'] || wi[:id]) : wi.try(:id)
|
|
834
|
+
id = numeric_id_from_gid(gid)
|
|
835
|
+
status_by_id[id.to_s] = status_name if id
|
|
802
836
|
end
|
|
803
837
|
|
|
804
838
|
break unless response[:more_pages]
|
|
@@ -807,80 +841,16 @@ module Gitlab
|
|
|
807
841
|
end
|
|
808
842
|
end
|
|
809
843
|
|
|
810
|
-
|
|
811
|
-
end
|
|
812
|
-
|
|
813
|
-
# Status is a work item capability not available on the IssueType GraphQL type
|
|
814
|
-
# or the REST API. We fetch it via a separate workItems query and merge it back
|
|
815
|
-
# by IID, similar to how decorate_resources_with_graphql_data works for other fields.
|
|
816
|
-
# We use widgets (not features) for backward compatibility with GitLab versions
|
|
817
|
-
# before the features field was introduced in 18.9.
|
|
818
|
-
def decorate_resources_with_work_item_status(resources)
|
|
819
|
-
return if resources.empty?
|
|
820
|
-
|
|
821
|
-
iids = resources.filter_map { |r| (r['iid'] || r[:iid])&.to_s }
|
|
822
|
-
return if iids.empty?
|
|
823
|
-
|
|
824
|
-
status_by_iid = fetch_work_item_statuses_by_iid(iids)
|
|
825
|
-
|
|
826
|
-
resources.each do |resource|
|
|
827
|
-
iid = (resource['iid'] || resource[:iid]).to_s
|
|
828
|
-
resource[:work_item_status] = status_by_iid[iid]
|
|
829
|
-
end
|
|
844
|
+
status_by_id
|
|
830
845
|
end
|
|
831
846
|
|
|
832
|
-
#
|
|
833
|
-
#
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
#{options.source.singularize}(fullPath: $source) {
|
|
838
|
-
workItems(iids: $iids, after: $after, first: 100) {
|
|
839
|
-
pageInfo {
|
|
840
|
-
hasNextPage
|
|
841
|
-
endCursor
|
|
842
|
-
}
|
|
843
|
-
nodes {
|
|
844
|
-
iid
|
|
845
|
-
widgets(onlyTypes: [STATUS]) {
|
|
846
|
-
... on WorkItemWidgetStatus {
|
|
847
|
-
status {
|
|
848
|
-
id
|
|
849
|
-
name
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
GRAPHQL
|
|
858
|
-
|
|
859
|
-
status_by_iid = {}
|
|
860
|
-
|
|
861
|
-
iids.each_slice(100) do |iids_batch|
|
|
862
|
-
after_cursor = nil
|
|
863
|
-
|
|
864
|
-
loop do
|
|
865
|
-
response = graphql_network.adapter.query_raw(
|
|
866
|
-
query,
|
|
867
|
-
resource_path: [options.source.singularize, 'workItems'],
|
|
868
|
-
variables: { source: source_full_path, iids: iids_batch, after: after_cursor }
|
|
869
|
-
)
|
|
870
|
-
|
|
871
|
-
Array.wrap(response[:results]).each do |wi|
|
|
872
|
-
wi = wi.deep_transform_keys(&:underscore).with_indifferent_access
|
|
873
|
-
status_widget = Array.wrap(wi[:widgets]).find { |w| w[:status].present? }
|
|
874
|
-
status_by_iid[wi[:iid].to_s] = status_widget.dig(:status, :name) if status_widget
|
|
875
|
-
end
|
|
876
|
-
|
|
877
|
-
break unless response[:more_pages]
|
|
878
|
-
|
|
879
|
-
after_cursor = response[:end_cursor]
|
|
880
|
-
end
|
|
881
|
-
end
|
|
847
|
+
# A work item GID (`gid://gitlab/WorkItem/1234`) carries the same model id
|
|
848
|
+
# the REST issues endpoint returns as `id`. Parsed with GlobalID, matching
|
|
849
|
+
# GraphqlNetwork#extract_id_from_global_id.
|
|
850
|
+
def numeric_id_from_gid(gid)
|
|
851
|
+
return if gid.blank?
|
|
882
852
|
|
|
883
|
-
|
|
853
|
+
GlobalID.parse(gid).model_id
|
|
884
854
|
end
|
|
885
855
|
|
|
886
856
|
def build_graphql_query(resource_type, conditions, rule_definition, graphql_only = false)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support/core_ext/object/blank'
|
|
4
|
+
|
|
3
5
|
require_relative '../version'
|
|
4
6
|
|
|
5
7
|
module Gitlab
|
|
@@ -14,6 +16,21 @@ module Gitlab
|
|
|
14
16
|
@options = options
|
|
15
17
|
@tracker = tracker
|
|
16
18
|
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# When the rate-limit headers are absent (e.g. a server that doesn't send
|
|
23
|
+
# them), treat the limit as not-a-concern rather than coercing a missing
|
|
24
|
+
# header to 0/1970, which would otherwise trip rate_limit_wait.
|
|
25
|
+
def rate_limit_remaining(headers)
|
|
26
|
+
value = headers['ratelimit-remaining']
|
|
27
|
+
value.present? ? value.to_i : Float::INFINITY
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def rate_limit_reset_at(headers)
|
|
31
|
+
value = headers['ratelimit-reset']
|
|
32
|
+
Time.at(value.to_i) if value.present?
|
|
33
|
+
end
|
|
17
34
|
end
|
|
18
35
|
end
|
|
19
36
|
end
|
|
@@ -58,14 +58,9 @@ module Gitlab
|
|
|
58
58
|
|
|
59
59
|
raise_on_error!(response)
|
|
60
60
|
|
|
61
|
-
parsed_response = response.data
|
|
62
61
|
headers = response.extensions.fetch('headers', {})
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
ratelimit_remaining: headers['ratelimit-remaining'].to_i,
|
|
66
|
-
ratelimit_reset_at: Time.at(headers['ratelimit-reset'].to_i),
|
|
67
|
-
results: parsed_response.to_h
|
|
68
|
-
}
|
|
63
|
+
build_graphql_response(response.data.to_h, headers)
|
|
69
64
|
end
|
|
70
65
|
|
|
71
66
|
delegate :parse, to: :client
|
|
@@ -96,19 +91,6 @@ module Gitlab
|
|
|
96
91
|
end
|
|
97
92
|
end
|
|
98
93
|
|
|
99
|
-
# When the rate-limit headers are absent (e.g. a server that doesn't send
|
|
100
|
-
# them), treat the limit as not-a-concern rather than coercing a missing
|
|
101
|
-
# header to 0/1970, which would otherwise trip rate_limit_wait.
|
|
102
|
-
def rate_limit_remaining(headers)
|
|
103
|
-
value = headers['ratelimit-remaining']
|
|
104
|
-
value.present? ? value.to_i : Float::INFINITY
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def rate_limit_reset_at(headers)
|
|
108
|
-
value = headers['ratelimit-reset']
|
|
109
|
-
Time.at(value.to_i) if value.present?
|
|
110
|
-
end
|
|
111
|
-
|
|
112
94
|
# Converts a parsed GraphQL::Client response node into plain Ruby so the
|
|
113
95
|
# shared builder only ever deals with Hash/Array, never client objects.
|
|
114
96
|
def plain_node(parsed_response)
|
|
@@ -28,8 +28,8 @@ module Gitlab
|
|
|
28
28
|
more_pages: (response.headers["x-next-page"].to_s != ""),
|
|
29
29
|
next_page_url: next_page_url(url, response),
|
|
30
30
|
results: response.parsed_response,
|
|
31
|
-
ratelimit_remaining: response.headers
|
|
32
|
-
ratelimit_reset_at:
|
|
31
|
+
ratelimit_remaining: rate_limit_remaining(response.headers),
|
|
32
|
+
ratelimit_reset_at: rate_limit_reset_at(response.headers)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
resource_count = result[:results].is_a?(Array) ? result[:results].size : 0
|
|
@@ -57,8 +57,8 @@ module Gitlab
|
|
|
57
57
|
|
|
58
58
|
{
|
|
59
59
|
results: response.parsed_response,
|
|
60
|
-
ratelimit_remaining: response.headers
|
|
61
|
-
ratelimit_reset_at:
|
|
60
|
+
ratelimit_remaining: rate_limit_remaining(response.headers),
|
|
61
|
+
ratelimit_reset_at: rate_limit_reset_at(response.headers)
|
|
62
62
|
}
|
|
63
63
|
end
|
|
64
64
|
|
|
@@ -81,8 +81,8 @@ module Gitlab
|
|
|
81
81
|
|
|
82
82
|
{
|
|
83
83
|
results: response.parsed_response,
|
|
84
|
-
ratelimit_remaining: response.headers
|
|
85
|
-
ratelimit_reset_at:
|
|
84
|
+
ratelimit_remaining: rate_limit_remaining(response.headers),
|
|
85
|
+
ratelimit_reset_at: rate_limit_reset_at(response.headers)
|
|
86
86
|
}
|
|
87
87
|
end
|
|
88
88
|
|
|
@@ -105,8 +105,8 @@ module Gitlab
|
|
|
105
105
|
|
|
106
106
|
{
|
|
107
107
|
results: response.parsed_response,
|
|
108
|
-
ratelimit_remaining: response.headers
|
|
109
|
-
ratelimit_reset_at:
|
|
108
|
+
ratelimit_remaining: rate_limit_remaining(response.headers),
|
|
109
|
+
ratelimit_reset_at: rate_limit_reset_at(response.headers)
|
|
110
110
|
}
|
|
111
111
|
end
|
|
112
112
|
|
|
@@ -127,8 +127,8 @@ module Gitlab
|
|
|
127
127
|
|
|
128
128
|
{
|
|
129
129
|
results: response.parsed_response,
|
|
130
|
-
ratelimit_remaining: response.headers
|
|
131
|
-
ratelimit_reset_at:
|
|
130
|
+
ratelimit_remaining: rate_limit_remaining(response.headers),
|
|
131
|
+
ratelimit_reset_at: rate_limit_reset_at(response.headers)
|
|
132
132
|
}
|
|
133
133
|
end
|
|
134
134
|
|
|
@@ -74,10 +74,10 @@ module Gitlab
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
opts.on('--cache-dir [string]', String,
|
|
77
|
-
'Directory containing pre-fetched API responses.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
'Directory containing pre-fetched API responses.',
|
|
78
|
+
'Files must follow the naming convention: <source>_<id>_<resource>.json',
|
|
79
|
+
'(e.g. groups_9970_members.json).',
|
|
80
|
+
'Can also be set via the GITLAB_TRIAGE_CACHE_DIR environment variable.') do |value|
|
|
81
81
|
options.cache_dir = value
|
|
82
82
|
end
|
|
83
83
|
|
|
@@ -210,9 +210,12 @@ module Gitlab
|
|
|
210
210
|
def rate_limit_wait(response)
|
|
211
211
|
return unless response.delete(:ratelimit_remaining) < MINIMUM_RATE_LIMIT
|
|
212
212
|
|
|
213
|
-
|
|
213
|
+
reset_at = response[:ratelimit_reset_at]
|
|
214
|
+
return unless reset_at
|
|
215
|
+
|
|
216
|
+
puts Gitlab::Triage::UI.debug "Rate limit almost exceeded, sleeping for #{reset_at - Time.now} seconds" if options.debug
|
|
214
217
|
@adapter.tracker&.record_rate_limit_wait
|
|
215
|
-
sleep(1) until Time.now >=
|
|
218
|
+
sleep(1) until Time.now >= reset_at
|
|
216
219
|
end
|
|
217
220
|
|
|
218
221
|
def raise_unexpected_response(results)
|
|
@@ -10,6 +10,7 @@ module Gitlab
|
|
|
10
10
|
# where nobody sees it).
|
|
11
11
|
class PolicyValidator
|
|
12
12
|
UnsupportedActionError = Class.new(StandardError)
|
|
13
|
+
ConflictingActionError = Class.new(StandardError)
|
|
13
14
|
|
|
14
15
|
COMMENT_NOT_SUPPORTED_MESSAGE =
|
|
15
16
|
'Commenting on work_items is not yet supported by the Work Items REST API ' \
|
|
@@ -18,7 +19,7 @@ module Gitlab
|
|
|
18
19
|
|
|
19
20
|
# Action keys Action.actions_for dispatches for work_items policies.
|
|
20
21
|
# Anything outside this list would silently do nothing, so reject it up front.
|
|
21
|
-
SUPPORTED_WORK_ITEM_ACTIONS = %i[labels assignees summarize comment_on_summary].freeze
|
|
22
|
+
SUPPORTED_WORK_ITEM_ACTIONS = %i[labels remove_labels assignees summarize comment_on_summary].freeze
|
|
22
23
|
|
|
23
24
|
def initialize(resource_rules)
|
|
24
25
|
@resource_rules = resource_rules || {}
|
|
@@ -38,6 +39,16 @@ module Gitlab
|
|
|
38
39
|
action_sets_from(policy_definition).each do |actions|
|
|
39
40
|
raise UnsupportedActionError, COMMENT_NOT_SUPPORTED_MESSAGE if actions.key?(:comment)
|
|
40
41
|
|
|
42
|
+
# GitLab applies remove_label_ids after add_label_ids, so a label in
|
|
43
|
+
# both lists is always removed. Catch the typo here rather than let the
|
|
44
|
+
# run fail later with a misleading "token may lack permission" error.
|
|
45
|
+
conflicting = Array(actions[:labels]).map(&:to_s) & Array(actions[:remove_labels]).map(&:to_s)
|
|
46
|
+
unless conflicting.none?
|
|
47
|
+
raise ConflictingActionError,
|
|
48
|
+
"Label(s) in both labels and remove_labels for a work_items rule: #{conflicting.join(', ')}. " \
|
|
49
|
+
"A label cannot be added and removed in the same rule."
|
|
50
|
+
end
|
|
51
|
+
|
|
41
52
|
unsupported = actions.keys.map(&:to_sym) - SUPPORTED_WORK_ITEM_ACTIONS
|
|
42
53
|
next if unsupported.none?
|
|
43
54
|
|
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.57.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-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -50,28 +50,34 @@ dependencies:
|
|
|
50
50
|
requirements:
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0.
|
|
53
|
+
version: '0.23'
|
|
54
54
|
type: :runtime
|
|
55
55
|
prerelease: false
|
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
58
|
- - "~>"
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '0.
|
|
60
|
+
version: '0.23'
|
|
61
61
|
- !ruby/object:Gem::Dependency
|
|
62
62
|
name: graphql
|
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 2.4.13
|
|
65
68
|
- - "<"
|
|
66
69
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: 2.
|
|
70
|
+
version: 2.7.0
|
|
68
71
|
type: :runtime
|
|
69
72
|
prerelease: false
|
|
70
73
|
version_requirements: !ruby/object:Gem::Requirement
|
|
71
74
|
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: 2.4.13
|
|
72
78
|
- - "<"
|
|
73
79
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: 2.
|
|
80
|
+
version: 2.7.0
|
|
75
81
|
- !ruby/object:Gem::Dependency
|
|
76
82
|
name: httparty
|
|
77
83
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -212,7 +218,6 @@ executables:
|
|
|
212
218
|
extensions: []
|
|
213
219
|
extra_rdoc_files: []
|
|
214
220
|
files:
|
|
215
|
-
- ".codeclimate.yml"
|
|
216
221
|
- ".git-blame-ignore-revs"
|
|
217
222
|
- ".gitignore"
|
|
218
223
|
- ".gitlab-ci.yml"
|
data/.codeclimate.yml
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
engines:
|
|
3
|
-
bundler-audit:
|
|
4
|
-
enabled: true
|
|
5
|
-
duplication:
|
|
6
|
-
enabled: true
|
|
7
|
-
config:
|
|
8
|
-
languages:
|
|
9
|
-
- ruby
|
|
10
|
-
rubocop-gitlab:
|
|
11
|
-
enabled: true
|
|
12
|
-
ratings:
|
|
13
|
-
paths:
|
|
14
|
-
- Gemfile.lock
|
|
15
|
-
- "**.rb"
|
|
16
|
-
exclude_paths:
|
|
17
|
-
- spec/
|
|
18
|
-
- vendor/
|
|
19
|
-
- tmp/
|