gitlab-triage 1.20.0 → 1.23.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84173833ef78959cc6fdb59fa9dbe7e69ad7061265d22a1941c57a36b2f8e42b
4
- data.tar.gz: 1293b7b39aca995152acfbed956af54c35d4897eee90f9db586bb318c20bd18c
3
+ metadata.gz: f64813b8d681fea2e5b23b4e53682089f4642a38037019658a3798456d219a77
4
+ data.tar.gz: 626bfa2d49c630ddab0f28c032b7665a96f07a65faa9b20da3812c538f3fcb42
5
5
  SHA512:
6
- metadata.gz: 73b18f51c5386dc91946fe55531057d04908b74f42b16830458c8af11efdbac16a13791191310eddb5732764e2ffa49a89500d5fbbd8ca10a63dbae9f89acef8
7
- data.tar.gz: ab7cefaa8a5574fb8f0e9b1c73875441975976275e28eb0f2e57194f6b26cf69323f60c3094b9d10cad88120df4abdbe112516c8c5e6a3c9f1e5e1efaad46a6f
6
+ metadata.gz: 5114d8ccdeccf7da91c2326f0b25c8ab3fe2c478ac4a90051f0757b545a90fc7087b16f573eae22e6ca85f0c3e9ded9e3e8d9299f8aad806325edee83b72753b
7
+ data.tar.gz: e03e119075fb170f5cfd424b6ec07c6739efc0975c928aeeea04c60cb01e2e89e0f596b1a84cd0d446073d0d20bcb6c9cc9bad7c65f590fd1ec9c15093b9556b
data/.gitignore CHANGED
@@ -9,6 +9,7 @@
9
9
  /pkg/
10
10
  /spec/reports/
11
11
  /tmp/
12
+ /doc/
12
13
 
13
14
  # rspec failure tracking
14
15
  .rspec_status
@@ -0,0 +1,13 @@
1
+ ---
2
+ # Settings for generating changelogs using the GitLab API. See
3
+ # https://docs.gitlab.com/ee/api/repositories.html#generate-changelog-data for
4
+ # more information.
5
+ categories:
6
+ added: Added
7
+ fixed: Fixed
8
+ changed: Changed
9
+ deprecated: Deprecated
10
+ removed: Removed
11
+ security: Security
12
+ performance: Performance
13
+ other: Other
@@ -1,35 +1,13 @@
1
- <!-- Replace `v4.5.0` with the previous release here, and `e18d76b309e42888759c1effe96767f13e34ae55`
2
- with the latest commit from https://gitlab.com/gitlab-org/gitlab-triage/commits/master that will be included in the release. -->
3
- - Diff: https://gitlab.com/gitlab-org/gitlab-triage/compare/v4.5.0...e18d76b309e42888759c1effe96767f13e34ae55
1
+ <!-- Replace `<PREVIOUS_VERSION>` with the previous version number here, `<COMMIT_UPDATING_VERSION>` with the latest
2
+ commit from this merge request, and `<NEW_VERSION>` with the upcoming version number. -->
3
+ ## Diff
4
4
 
5
- - Release notes:
5
+ https://gitlab.com/gitlab-org/ruby/gems/gitlab-triage/compare/v<PREVIOUS_VERSION>...<COMMIT_UPDATING_VERSION>
6
6
 
7
- <!-- Keep the sections order but remove the empty sections -->
7
+ ## Checklist
8
8
 
9
- ```markdown
10
- ### New features and features updates
9
+ - [ ] Diff link is up-to-date.
10
+ - [ ] Check the release notes: https://gitlab.com/api/v4/projects/3430480/repository/changelog?version=<NEW_VERSION>
11
+ - [ ] Based on the diff and the release notes, `version.rb` is updated, according to [SemVer](https://semver.org).
11
12
 
12
- - !aaa <Title of the aaa MR>.
13
-
14
- ### Fixes
15
-
16
- - !bbb <Title of the bbb MR>.
17
-
18
- ### Doc changes
19
-
20
- - !ccc <Title of the ccc MR>.
21
-
22
- ### Other changes (tooling, technical debt)
23
-
24
- - !ddd <Title of the ddd MR>.
25
- ```
26
-
27
- - Checklist before merging:
28
- - [ ] Diff link is up-to-date.
29
- - [ ] Based on the diff, `lib/gitlab/triage/version.rb` is updated, according to [SemVer](https://semver.org).
30
- - [ ] Release notes are accurate.
31
-
32
- - Checklist after merging:
33
- - [ ] [Update the release notes for the newly created tag](docs/release_process.md#how-to).
34
-
35
- /label ~"Engineering Productivity" ~"ep::triage" ~tooling ~"tooling::workflow"
13
+ /label ~"type::tooling" ~"tooling::workflow" ~"ep::triage"
data/.gitlab-ci.yml CHANGED
@@ -129,5 +129,11 @@ dry-run:custom:
129
129
  allow_failure: true
130
130
 
131
131
  include:
132
+ - template: Security/Dependency-Scanning.gitlab-ci.yml
133
+ - template: Security/License-Scanning.gitlab-ci.yml
134
+ - template: Security/SAST.gitlab-ci.yml
135
+ - template: Security/Secret-Detection.gitlab-ci.yml
132
136
  - project: 'gitlab-org/quality/pipeline-common'
133
- file: '/ci/gem-release.yml'
137
+ file:
138
+ - '/ci/danger-review.yml'
139
+ - '/ci/gem-release.yml'
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.4
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 2.7.4
data/.yardopts ADDED
@@ -0,0 +1,4 @@
1
+ --protected
2
+ --private
3
+ -
4
+ LICENSE.md
data/Dangerfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gitlab-dangerfiles'
4
+
5
+ Gitlab::Dangerfiles.for_project(self, &:import_defaults)
data/Gemfile CHANGED
@@ -3,6 +3,13 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in gitlab-rubocop.gemspec
4
4
  gemspec
5
5
 
6
+ gem "yard"
7
+
6
8
  group :test do
7
9
  gem 'gitlab-styles', '~> 3.3.0'
8
10
  end
11
+
12
+ group :development, :test, :danger do
13
+ gem 'gitlab-dangerfiles', '~> 2.6.1', require: false
14
+ gem 'guard-rspec', '~> 4.7.3', require: false
15
+ end
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![pipeline status](https://gitlab.com/gitlab-org/gitlab-triage/badges/master/pipeline.svg)](https://gitlab.com/gitlab-org/gitlab-triage/commits/master)
1
+ [![pipeline status](https://gitlab.com/gitlab-org/ruby/gems/gitlab-triage/badges/master/pipeline.svg)](https://gitlab.com/gitlab-org/ruby/gems/gitlab-triage/-/commits/master)
2
2
 
3
3
  # GitLab Triage Project
4
4
 
@@ -12,12 +12,13 @@ The `gitlab-triage` gem aims to enable project managers and maintainers to
12
12
  automatically triage Issues and Merge Requests in GitLab projects or groups
13
13
  based on defined policies.
14
14
 
15
- See [Running locally](#running-locally) for how to specify a project or a
15
+ See [Running with the installed gem](#running-with-the-installed-gem) for how to specify a project or a
16
16
  group.
17
17
 
18
18
  ### What is a triage policy?
19
19
 
20
20
  Triage policies are defined on a resource level basis, resources being:
21
+ - Epics
21
22
  - Issues
22
23
  - Merge Requests
23
24
 
@@ -90,7 +91,7 @@ resource_rules:
90
91
  comment: |
91
92
  {{author}} This issue is unlabelled after 5 days. It needs attention. Please take care of this before the end of #{2.days.from_now.strftime('%Y-%m-%d')}
92
93
  summarize:
93
- destination: gitlab-org/gitlab-triage
94
+ destination: gitlab-org/ruby/gems/gitlab-triage
94
95
  title: |
95
96
  #{resource[:type].capitalize} require labels
96
97
  item: |
@@ -152,6 +153,7 @@ Used to declare a condition that must be satisfied by a resource before actions
152
153
  Available condition types:
153
154
  - [`date` condition](#date-condition)
154
155
  - [`milestone` condition](#milestone-condition)
156
+ - [`iteration` condition](#iteration-condition)
155
157
  - [`state` condition](#state-condition)
156
158
  - [`upvotes` condition](#upvotes-condition)
157
159
  - [`labels` condition](#labels-condition)
@@ -159,6 +161,7 @@ Available condition types:
159
161
  - [`no_additional_labels` condition](#no-additional-labels-condition)
160
162
  - [`author_member` condition](#author-member-condition)
161
163
  - [`assignee_member` condition](#assignee-member-condition)
164
+ - [`draft` condition](#draft-condition)
162
165
  - [`source_branch` condition](#source-branch-condition)
163
166
  - [`target_branch` condition](#target-branch-condition)
164
167
  - [`weight` condition](#weight-condition)
@@ -191,7 +194,14 @@ conditions:
191
194
 
192
195
  ##### Milestone condition
193
196
 
194
- Accepts the name of a milestone to filter upon.
197
+ Accepts the name of a milestone to filter upon. Also accepts the following timebox values:
198
+
199
+ - `none`
200
+ - `any`
201
+ - `upcoming`
202
+ - `started`
203
+
204
+ See the [`milestone_id` API field documentation](https://docs.gitlab.com/ee/api/issues.html) for their meaning.
195
205
 
196
206
  Example:
197
207
 
@@ -200,15 +210,35 @@ conditions:
200
210
  milestone: v1
201
211
  ```
202
212
 
213
+ ##### Iteration condition
214
+
215
+ Accepts the name of an iteration to filter upon. Also accepts the following
216
+ timebox values:
217
+
218
+ - `none`
219
+ - `any`
220
+
221
+ See the [`iteration_id` API field documentation](https://docs.gitlab.com/ee/api/issues.html) for their meaning.
222
+
223
+ Example:
224
+
225
+ ```yml
226
+ conditions:
227
+ iteration: none
228
+ ```
229
+
230
+ > **Note:** This query is not supported using GraphQL yet.
231
+
203
232
  ##### State condition
204
233
 
205
234
  Accepts a string.
206
235
 
207
- | State | Type | Value |
208
- | --------- | ---- | ------ |
209
- | Closed issues | string | `closed` |
210
- | Open issues | string | `opened` |
211
- | Merged merge requests | string | `merged` |
236
+ | State | Type | Value |
237
+ | --------- | ---- | ------ |
238
+ | Closed issues/MRs | string | `closed` |
239
+ | Open issues/MRs | string | `opened` |
240
+ | Locked issues | string | `locked` |
241
+ | Merged merge requests | string | `merged` |
212
242
 
213
243
  Example:
214
244
 
@@ -411,7 +441,6 @@ conditions:
411
441
 
412
442
  Accepts a boolean. If `true` the resource cannot have more labels than those specified by the `labels` condition.
413
443
 
414
-
415
444
  Example:
416
445
 
417
446
  ```yml
@@ -467,8 +496,23 @@ conditions:
467
496
  source_id: 9970
468
497
  ```
469
498
 
499
+ ##### Draft condition
500
+
501
+ **This condition is only applicable for merge requests.**
502
+
503
+ Accepts a boolean. If `true`, only draft MRs are returned. If `false`, only non-draft MRs are returned.
504
+
505
+ Example:
506
+
507
+ ```yml
508
+ conditions:
509
+ draft: true
510
+ ```
511
+
470
512
  ##### Source branch condition
471
513
 
514
+ **This condition is only applicable for merge requests.**
515
+
472
516
  Accepts the name of a source branch to filter upon.
473
517
 
474
518
  Example:
@@ -480,6 +524,8 @@ conditions:
480
524
 
481
525
  ##### Target branch condition
482
526
 
527
+ **This condition is only applicable for merge requests.**
528
+
483
529
  Accepts the name of a target branch to filter upon.
484
530
 
485
531
  Example:
@@ -491,8 +537,9 @@ conditions:
491
537
 
492
538
  ##### Weight condition
493
539
 
540
+ **This condition is only applicable for issues.**
541
+
494
542
  Accepts a string per the [API documentation](https://docs.gitlab.com/ee/api/issues.html#list-issues).
495
- This condition is only applicable for issues (not merge requests).
496
543
 
497
544
  | State | Type | Value |
498
545
  | --------- | ---- | ------ |
@@ -716,8 +763,8 @@ The following placeholders are supported:
716
763
  - `downvotes`: the resources's downvotes count
717
764
  - `title`: the resource's title
718
765
  - `web_url`: the web URL pointing to the resource
719
- - `type`: the type of the resources. For now, only `issues` and
720
- `merge_requests` are supported.
766
+ - `full_reference`: the full reference of the resource as `namespace/project#12`, `namespace/project!42`, `namespace/project&72`
767
+ - `type`: the type of the resources. For now, only `issues`, `merge_requests`, and `epics` are supported.
721
768
 
722
769
  If the resource doesn't respond to the placeholder, or if the field is `nil`,
723
770
  the placeholder is not replaced.
@@ -827,8 +874,7 @@ The following placeholders are supported for `summary`:
827
874
 
828
875
  - `title`: The title of the generated issue
829
876
  - `items`: Concatenated markdown separated by a newline for each `item`
830
- - `type`: The resource type for the summary. For now `issues` or
831
- `merge_requests`
877
+ - `type`: The resource type for the summary. For now `issues`, `merge_requests`, or `epics`,
832
878
 
833
879
  > **Note:**
834
880
  > - Both `item` and `summary` fields act like a [comment action](#comment-action),
@@ -957,17 +1003,6 @@ resource_rules:
957
1003
  issues:
958
1004
  summaries:
959
1005
  - name: Newest and oldest issues summary
960
- actions:
961
- summarize:
962
- title: "Newest and oldest {{type}} summary"
963
- summary: |
964
- Please triage the following {{type}}:
965
-
966
- {{items}}
967
-
968
- Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
969
-
970
- /label ~"needs attention"
971
1006
  rules:
972
1007
  - name: New issues
973
1008
  conditions:
@@ -993,6 +1028,17 @@ resource_rules:
993
1028
  Please triage the following old {{type}}:
994
1029
 
995
1030
  {{items}}
1031
+ actions:
1032
+ summarize:
1033
+ title: "Newest and oldest {{type}} summary"
1034
+ summary: |
1035
+ Please triage the following {{type}}:
1036
+
1037
+ {{items}}
1038
+
1039
+ Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
1040
+
1041
+ /label ~"needs attention"
996
1042
  ```
997
1043
 
998
1044
  Which could generate an issue like:
@@ -1032,8 +1078,9 @@ Here's a list of currently available Ruby expression API:
1032
1078
 
1033
1079
  | Name | Return type | Description |
1034
1080
  | ---- | ---- | ---- |
1035
- | resource | Hash | The hash containing the raw data of the resource |
1081
+ | resource | Hash | The hash containing the raw data of the resource. Note that `resource[:type]` is the type of the policy (`issues`, `merge_requests`, or `epics`), not the API `type` field. |
1036
1082
  | author | String | The username of the resource author |
1083
+ | state | String | The state of the resource |
1037
1084
  | milestone | Milestone | The milestone attached to the resource |
1038
1085
  | labels | [Label] | A list of labels, having only names |
1039
1086
  | labels_with_details | [Label] | A list of labels which has more information loaded from another API request |
@@ -1041,7 +1088,15 @@ Here's a list of currently available Ruby expression API:
1041
1088
  | label_events | [LabelEvent] | A list of label events on the resource |
1042
1089
  | instance_version | InstanceVersion | The version for the GitLab instance we're triaging with |
1043
1090
  | project_path | String | The path with namespace to the issues or merge requests project |
1044
- | full_resource_reference | String | A full reference incuding project path to the issue or merge request |
1091
+ | full_resource_reference | String | A full reference including project path to the issue or merge request |
1092
+
1093
+ ##### Methods for `Issue` (issue context)
1094
+
1095
+ | Name | Return type | Description |
1096
+ | ---- | ---- | ---- |
1097
+ | merge_requests_count | Integer | The number of merge requests related to the issue |
1098
+ | related_merge_requests | [MergeRequest] | The list of merge requests related to the issue |
1099
+ | closed_by | [MergeRequest] | The list of merge requests that close the issue |
1045
1100
 
1046
1101
  ##### Methods for `MergeRequest` (merge request context)
1047
1102
 
@@ -1130,7 +1185,7 @@ Usage: gitlab-triage [options]
1130
1185
  --init-ci Initialize the project with a .gitlab-ci.yml file
1131
1186
  ```
1132
1187
 
1133
- #### Running locally
1188
+ #### Running with the installed gem
1134
1189
 
1135
1190
  Triaging against a specific project:
1136
1191
 
@@ -1152,6 +1207,16 @@ gitlab-triage --dry-run --token $GITLAB_API_TOKEN --all-projects
1152
1207
 
1153
1208
  > **Note:** The `--all-projects` option will process all resources for all projects visible to the specified `$GITLAB_API_TOKEN`
1154
1209
 
1210
+ #### Running from source
1211
+
1212
+ Execute the `gitlab-triage` script from the `./bin` directory.
1213
+
1214
+ For example- after cloning this project, from the root `gitlab-triage` directory:
1215
+
1216
+ ```
1217
+ bundle exec bin/gitlab-triage --dry-run --token $GITLAB_API_TOKEN --source-id gitlab-org/triage
1218
+ ```
1219
+
1155
1220
  #### Running on GitLab CI pipeline
1156
1221
 
1157
1222
  You can enforce policies using a scheduled pipeline:
@@ -9,17 +9,26 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ['gitlab_rubygems@gitlab.com']
10
10
 
11
11
  spec.summary = 'GitLab triage automation project.'
12
- spec.homepage = 'https://gitlab.com/gitlab-org/gitlab-triage'
12
+ spec.homepage = 'https://gitlab.com/gitlab-org/ruby/gems/gitlab-triage'
13
13
  spec.license = 'MIT'
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
14
15
 
15
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
- f.match(%r{^(docs|test|spec|features)/})
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
+
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "https://gitlab.com/gitlab-org/ruby/gems/gitlab-triage"
20
+ spec.metadata["changelog_uri"] = "https://gitlab.com/gitlab-org/ruby/gems/gitlab-triage/-/releases"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match?(%r{^(docs?|spec|tmp)/}) }
17
26
  end
18
27
  spec.bindir = 'bin'
19
28
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
29
  spec.require_paths = ['lib']
21
30
 
22
- spec.add_dependency 'activesupport', '~> 5.1'
31
+ spec.add_dependency 'activesupport', '>= 5.1'
23
32
  spec.add_dependency 'globalid', '~> 0.4'
24
33
  spec.add_dependency 'graphql-client', '~> 0.16'
25
34
  spec.add_dependency 'httparty', '~> 0.17'
@@ -29,6 +29,7 @@ module Gitlab
29
29
  downvotes: "{{downvotes}}",
30
30
  title: "{{title}}",
31
31
  web_url: "{{web_url}}",
32
+ full_reference: "{{references.full}}",
32
33
  type: "{{type}}",
33
34
  items: "{{items}}"
34
35
  }.freeze
@@ -120,7 +121,7 @@ module Gitlab
120
121
  return attributes unless redact_confidential_attributes?
121
122
 
122
123
  case path
123
- when 'web_url', 'items', 'type'
124
+ when 'web_url', 'items', 'type', 'references.full'
124
125
  attributes # No need to redact them
125
126
  else
126
127
  [Resource::Base::CONFIDENTIAL_TEXT]
@@ -37,6 +37,8 @@ module Gitlab
37
37
  issues: %w[opened closed],
38
38
  merge_requests: %w[opened closed merged]
39
39
  }.with_indifferent_access.freeze
40
+ MILESTONE_TIMEBOX_VALUES = %w[none any upcoming started].freeze
41
+ ITERATION_SELECTION_VALUES = %w[none any].freeze
40
42
  EpicsTriagingForProjectImpossibleError = Class.new(StandardError)
41
43
 
42
44
  def initialize(policies:, options:, network_adapter_class: DEFAULT_NETWORK_ADAPTER, graphql_network_adapter_class: DEFAULT_GRAPHQL_ADAPTER)
@@ -62,16 +64,16 @@ module Gitlab
62
64
  puts Gitlab::Triage::UI.header("Triaging the `#{options.source_id}` #{options.source.to_s.singularize}", char: '=')
63
65
  puts
64
66
 
65
- resource_rules.each do |resource_type, resource|
67
+ resource_rules.each do |resource_type, policy_definition|
66
68
  if resource_type == 'epics' && options.source != :groups
67
69
  raise(EpicsTriagingForProjectImpossibleError, "Epics can only be triaged at the group level. Please set the `--source groups` option.")
68
70
  end
69
71
 
70
- puts Gitlab::Triage::UI.header("Processing rules for #{resource_type}", char: '-')
72
+ puts Gitlab::Triage::UI.header("Processing summaries & rules for #{resource_type}", char: '-')
71
73
  puts
72
74
 
73
- process_summaries(resource_type, resource[:summaries])
74
- process_rules(resource_type, resource[:rules])
75
+ process_summaries(resource_type, policy_definition[:summaries])
76
+ process_rules(resource_type, policy_definition[:rules])
75
77
  end
76
78
  end
77
79
 
@@ -127,71 +129,165 @@ module Gitlab
127
129
  rule.fetch(:limits) { {} }
128
130
  end
129
131
 
130
- def process_summaries(resource_type, summaries)
131
- return if summaries.blank?
132
-
133
- summaries.each do |summary|
134
- process_summary(resource_type, summary)
132
+ # Process an array of +summary_definitions+.
133
+ #
134
+ # @example Example of an array of summary definitions (shown as YAML for readability).
135
+ #
136
+ # - name: Newest and oldest issues summary
137
+ # rules:
138
+ # - name: New issues
139
+ # conditions:
140
+ # state: opened
141
+ # limits:
142
+ # most_recent: 2
143
+ # actions:
144
+ # summarize:
145
+ # item: "- [ ] [{{title}}]({{web_url}}) {{labels}}"
146
+ # summary: |
147
+ # Please triage the following new {{type}}:
148
+ # {{items}}
149
+ # actions:
150
+ # summarize:
151
+ # title: "Newest and oldest {{type}} summary"
152
+ # summary: |
153
+ # Please triage the following {{type}}:
154
+ # {{items}}
155
+ # Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
156
+ # /label ~"needs attention"
157
+ #
158
+ # @param summary_definitions [Array<Hash>] An array usually given as YAML in a triage policy file.
159
+ #
160
+ # @return [nil]
161
+ def process_summaries(resource_type, summary_definitions)
162
+ return if summary_definitions.blank?
163
+
164
+ summary_definitions.each do |summary_definition|
165
+ process_summary(resource_type, summary_definition)
135
166
  end
136
167
  end
137
168
 
138
- def process_rules(resource_type, rules)
139
- return if rules.blank?
140
-
141
- rules.each do |rule|
142
- resources_for_rule(resource_type, rule) do |resources|
169
+ # Process an array of +rule_definitions+.
170
+ #
171
+ # @example Example of an array of rule definitions.
172
+ #
173
+ # [{ name: "New issues", conditions: { state: opened }, limits: { most_recent: 2 }, actions: { labels: ["needs attention"] } }]
174
+ #
175
+ # @param rule_definitions [Array<Hash>] An array usually given as YAML in a triage policy file.
176
+ #
177
+ # @return [nil]
178
+ def process_rules(resource_type, rule_definitions)
179
+ return if rule_definitions.blank?
180
+
181
+ rule_definitions.each do |rule_definition|
182
+ resources_for_rule(resource_type, rule_definition) do |resources|
143
183
  policy = Policies::RulePolicy.new(
144
- resource_type, rule, resources, network)
184
+ resource_type, rule_definition, resources, network)
145
185
 
146
186
  process_action(policy)
147
187
  end
148
188
  end
149
189
  end
150
190
 
151
- def process_summary(resource_type, summary)
152
- puts Gitlab::Triage::UI.header("Processing summary: **#{summary[:name]}**", char: '~')
191
+ # Process a +summary_definition+.
192
+ #
193
+ # @example Example of a summary definition hash (shown as YAML for readability).
194
+ #
195
+ # name: Newest and oldest issues summary
196
+ # rules:
197
+ # - name: New issues
198
+ # conditions:
199
+ # state: opened
200
+ # limits:
201
+ # most_recent: 2
202
+ # actions:
203
+ # summarize:
204
+ # item: "- [ ] [{{title}}]({{web_url}}) {{labels}}"
205
+ # summary: |
206
+ # Please triage the following new {{type}}:
207
+ # {{items}}
208
+ # actions:
209
+ # summarize:
210
+ # title: "Newest and oldest {{type}} summary"
211
+ # summary: |
212
+ # Please triage the following {{type}}:
213
+ # {{items}}
214
+ # Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
215
+ # /label ~"needs attention"
216
+ #
217
+ # @param resource_type [String] The resource type, e.g. +issues+ or +merge_requests+.
218
+ # @param summary_definition [Hash] A hash usually given as YAML in a triage policy file:
219
+ #
220
+ # @return [nil]
221
+ def process_summary(resource_type, summary_definition)
222
+ puts Gitlab::Triage::UI.header("Processing summary: **#{summary_definition[:name]}**", char: '~')
153
223
  puts
154
224
 
155
- summary_parts_for_rules(resource_type, summary[:rules]) do |resources|
225
+ summary_parts_for_rules(resource_type, summary_definition[:rules]) do |summary_resources|
156
226
  policy = Policies::SummaryPolicy.new(
157
- resource_type, summary, resources, network)
227
+ resource_type, summary_definition, summary_resources, network)
158
228
 
159
229
  process_action(policy)
160
230
  end
161
231
  end
162
232
 
163
- def summary_parts_for_rules(resource_type, rules)
233
+ # Transform an array of +rule_definitions+ into a +PoliciesResources::SummaryResources.new(rule => rule_resources)+ object.
234
+ #
235
+ # @example Example of an array of rule definitions.
236
+ #
237
+ # [{ name: "New issues", conditions: { state: opened }, limits: { most_recent: 2 }, actions: { labels: ["needs attention"] } }]
238
+ #
239
+ # @param resource_type [String] The resource type, e.g. +issues+ or +merge_requests+.
240
+ # @param rule_definitions [Array<Hash>] An array of rule definitions, e.g.
241
+ # +[{ name: 'Foo', conditions: { milestone: 'v1' } }, { name: 'Foo', conditions: { state: 'opened' } }]+.
242
+ #
243
+ # @yieldparam summary_resources [PoliciesResources::SummaryResources] An object which contains a +{ rule_definition => resources }+ hash.
244
+ # @yieldreturn [nil]
245
+ #
246
+ # @return [nil]
247
+ def summary_parts_for_rules(resource_type, rule_definitions)
164
248
  # { summary_rule => resources }
165
- parts = rules.inject({}) do |result, rule|
166
- resources_and_conditions =
167
- to_enum(:resources_for_rule, resource_type, rule)
168
-
169
- resources_and_conditions
170
- .inject(result) do |result, (resources, conditions)|
171
- # { expanded_summary_rule => resources }
172
- result.merge(rule.merge(conditions: conditions) => resources)
173
- end
249
+ parts = rule_definitions.each_with_object({}) do |rule_definition, result|
250
+ to_enum(:resources_for_rule, resource_type, rule_definition).each do |rule_resources, expanded_conditions|
251
+ # We replace the non-expanded rule conditions with the expanded ones
252
+ result.merge!(rule_definition.merge(conditions: expanded_conditions) => rule_resources)
253
+ end
254
+
255
+ result
174
256
  end
175
257
 
176
258
  yield(PoliciesResources::SummaryResources.new(parts))
177
259
  end
178
260
 
179
- def resources_for_rule(resource_type, rule)
180
- puts Gitlab::Triage::UI.header("Gathering resources for rule: **#{rule[:name]}**", char: '-')
181
-
182
- ExpandCondition.perform(rule_conditions(rule)) do |conditions|
261
+ # Transform a non-expanded +rule_definition+ into a +PoliciesResources::RuleResources.new(resources)+ object.
262
+ #
263
+ # @example Example of a rule definition hash.
264
+ #
265
+ # { name: "New issues", conditions: { state: opened }, limits: { most_recent: 2 }, actions: { labels: ["needs attention"] } }
266
+ #
267
+ # @param resource_type [String] The resource type, e.g. +issues+ or +merge_requests+.
268
+ # @param rule_definition [Hash] A rule definition, e.g. +{ name: 'Foo', conditions: { milestone: 'v1' } }+.
269
+ #
270
+ # @yieldparam rule_resources [PoliciesResources::RuleResources] An object which contains an array of resources.
271
+ # @yieldparam expanded_conditions [Hash] A hash of expanded conditions.
272
+ # @yieldreturn [nil]
273
+ #
274
+ # @return [nil]
275
+ def resources_for_rule(resource_type, rule_definition)
276
+ puts Gitlab::Triage::UI.header("Gathering resources for rule: **#{rule_definition[:name]}**", char: '-')
277
+
278
+ ExpandCondition.perform(rule_conditions(rule_definition)) do |expanded_conditions|
183
279
  # retrieving the resources for every rule is inefficient
184
280
  # however, previous rules may affect those upcoming
185
281
  resources = []
186
282
 
187
- if rule[:api] == 'graphql'
188
- graphql_query = build_graphql_query(resource_type, conditions, true)
283
+ if rule_definition[:api] == 'graphql'
284
+ graphql_query = build_graphql_query(resource_type, expanded_conditions, true)
189
285
  resources = graphql_network.query(graphql_query, source: source_full_path)
190
286
  else
191
- resources = network.query_api(build_get_url(resource_type, conditions))
287
+ resources = network.query_api(build_get_url(resource_type, expanded_conditions))
192
288
  iids = resources.pluck('iid').map(&:to_s)
193
289
 
194
- graphql_query = build_graphql_query(resource_type, conditions)
290
+ graphql_query = build_graphql_query(resource_type, expanded_conditions)
195
291
  graphql_resources = graphql_network.query(graphql_query, source: source_full_path, iids: iids) if graphql_query.any?
196
292
 
197
293
  decorate_resources_with_graphql_data(resources, graphql_resources)
@@ -202,28 +298,19 @@ module Gitlab
202
298
 
203
299
  puts "\n\n* Found #{resources.count} resources..."
204
300
  print "* Filtering resources..."
205
- resources = filter_resources(resources, conditions)
301
+ resources = filter_resources(resources, expanded_conditions)
206
302
  puts "\n* Total after filtering: #{resources.count} resources"
207
303
  print "* Limiting resources..."
208
- resources = limit_resources(resources, rule_limits(rule))
304
+ resources = limit_resources(resources, rule_limits(rule_definition))
209
305
  puts "\n* Total after limiting: #{resources.count} resources"
210
306
  puts
211
307
 
212
- yield(PoliciesResources::RuleResources.new(resources), conditions)
308
+ yield(PoliciesResources::RuleResources.new(resources), expanded_conditions)
213
309
  end
214
310
  end
215
311
 
216
312
  def attach_resource_type(resources, resource_type)
217
313
  resources.each { |resource| resource[:type] = resource_type }
218
- # TODO: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
219
- # We should not overwrite the attribute here, but we need to
220
- # fix it first. We should instead use something like
221
- # gitlab_triage_resource_type so it won't conflict with the
222
- # existing fields.
223
- # And we need to retain the backward compatibility that using
224
- # {{type}} will give us this value, rather than from the REST API,
225
- # which will give us ISSUE from:
226
- # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59648
227
314
  end
228
315
 
229
316
  def decorate_resources_with_graphql_data(resources, graphql_resources)
@@ -308,19 +395,20 @@ module Gitlab
308
395
  allowed_values: ALLOWED_STATE_VALUES[resource_type])
309
396
  end
310
397
 
311
- condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('milestone', Array(conditions[:milestone])[0]) if conditions[:milestone]
312
- condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('source_branch', conditions[:source_branch]) if conditions[:source_branch]
313
- condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('target_branch', conditions[:target_branch]) if conditions[:target_branch]
398
+ condition_builders << milestone_condition_builder(resource_type, conditions[:milestone]) if conditions[:milestone]
314
399
 
315
400
  if conditions[:date] && APIQueryBuilders::DateQueryParamBuilder.applicable?(conditions[:date])
316
401
  condition_builders << APIQueryBuilders::DateQueryParamBuilder.new(conditions.delete(:date))
317
402
  end
318
403
 
319
- if conditions[:weight] && resource_type.to_sym == :issues
320
- condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('weight', conditions[:weight])
404
+ case resource_type&.to_sym
405
+ when :issues
406
+ condition_builders.concat(issues_resource_query(conditions))
407
+ when :merge_requests
408
+ condition_builders.concat(merge_requests_resource_query(conditions))
321
409
  end
322
410
 
323
- condition_builders.each do |condition_builder|
411
+ condition_builders.compact.each do |condition_builder|
324
412
  params[condition_builder.param_name] = condition_builder.param_content
325
413
  end
326
414
 
@@ -334,6 +422,62 @@ module Gitlab
334
422
  ).build
335
423
  end
336
424
 
425
+ def milestone_condition_builder(resource_type, milestone_condition)
426
+ milestone_value = Array(milestone_condition)[0].to_s # back-compatibility
427
+ return if milestone_value.empty?
428
+
429
+ # Issues API should use the `milestone_id` param for timebox values, and `milestone` for milestone title
430
+ args =
431
+ if resource_type.to_sym == :issues && MILESTONE_TIMEBOX_VALUES.include?(milestone_value.downcase)
432
+ ['milestone_id', milestone_value.titleize] # The API only accepts titleized values.
433
+ else
434
+ ['milestone', milestone_value]
435
+ end
436
+
437
+ APIQueryBuilders::SingleQueryParamBuilder.new(*args)
438
+ end
439
+
440
+ def iteration_condition_builder(iteration_value)
441
+ # Issues API should use the `iteration_id` param for timebox values, and `iteration_title` for iteration title
442
+ args =
443
+ if ITERATION_SELECTION_VALUES.include?(iteration_value.downcase)
444
+ ['iteration_id', iteration_value.titleize] # The API only accepts titleized values.
445
+ else
446
+ ['iteration_title', iteration_value]
447
+ end
448
+ APIQueryBuilders::SingleQueryParamBuilder.new(*args)
449
+ end
450
+
451
+ def issues_resource_query(conditions)
452
+ [].tap do |condition_builders|
453
+ condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('weight', conditions[:weight]) if conditions[:weight]
454
+ condition_builders << iteration_condition_builder(conditions[:iteration]) if conditions[:iteration]
455
+ end
456
+ end
457
+
458
+ def merge_requests_resource_query(conditions)
459
+ [].tap do |condition_builders|
460
+ condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('source_branch', conditions[:source_branch]) if conditions[:source_branch]
461
+ condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('target_branch', conditions[:target_branch]) if conditions[:target_branch]
462
+ condition_builders << draft_condition_builder(conditions[:draft]) if conditions.key?(:draft)
463
+ end
464
+ end
465
+
466
+ def draft_condition_builder(draft_condittion)
467
+ # Issues API only accepts 'yes' and 'no' as strings: https://docs.gitlab.com/ee/api/merge_requests.html
468
+ wip =
469
+ case draft_condittion
470
+ when true
471
+ 'yes'
472
+ when false
473
+ 'no'
474
+ else
475
+ raise ArgumentError, 'The "draft" condition only accepts true or false.'
476
+ end
477
+
478
+ APIQueryBuilders::SingleQueryParamBuilder.new('wip', wip)
479
+ end
480
+
337
481
  def build_graphql_query(resource_type, conditions, graphql_only = false)
338
482
  Gitlab::Triage::GraphqlQueries::QueryBuilder
339
483
  .new(options.source, resource_type, conditions, graphql_only: graphql_only)
@@ -49,7 +49,8 @@ module Gitlab
49
49
  end
50
50
 
51
51
  def description_resource
52
- title_resource.merge(title: title, items: items)
52
+ title_resource.merge(
53
+ title: title, items: items, resources: @resources)
53
54
  end
54
55
 
55
56
  def items
@@ -66,7 +66,7 @@ module Gitlab
66
66
  fields << 'assignees { nodes { id name username } }' if conditions.key?(:assignee_member)
67
67
  fields << 'upvotes' if conditions.dig(:upvotes, :attribute).to_s == 'upvotes'
68
68
  fields << 'downvotes' if conditions.dig(:upvotes, :attribute).to_s == 'downvotes'
69
- fields << 'mergedAt' if resource_type == 'merge_requests'
69
+ fields.push('draft', 'mergedAt') if resource_type == 'merge_requests'
70
70
  end
71
71
 
72
72
  fields
@@ -82,8 +82,13 @@ module Gitlab
82
82
  condition_queries << QueryParamBuilders::BaseParamBuilder.new('milestoneTitle', condition_params) if condition.to_s == 'milestone'
83
83
  condition_queries << QueryParamBuilders::BaseParamBuilder.new('state', condition_params, with_quotes: false) if condition.to_s == 'state'
84
84
 
85
- condition_queries << merge_requests_resource_query(condition, condition_params) if resource_type == 'merge_requests'
86
- condition_queries << issues_resource_query(condition, condition_params) if resource_type == 'issues'
85
+ case resource_type
86
+ when 'issues'
87
+ condition_queries << issues_label_query(condition, condition_params)
88
+ when 'merge_requests'
89
+ condition_queries << merge_requests_label_query(condition, condition_params)
90
+ condition_queries << merge_requests_resource_query(condition, condition_params)
91
+ end
87
92
  end
88
93
 
89
94
  condition_queries
@@ -92,26 +97,48 @@ module Gitlab
92
97
  .join
93
98
  end
94
99
 
100
+ def issues_label_query(condition, condition_params)
101
+ args =
102
+ case condition.to_s
103
+ when 'forbidden_labels'
104
+ ['labelName', condition_params, { negated: true }]
105
+ when 'labels'
106
+ ['labelName', condition_params]
107
+ else
108
+ return nil
109
+ end
110
+
111
+ QueryParamBuilders::LabelsParamBuilder.new(*args)
112
+ end
113
+
95
114
  def merge_requests_resource_query(condition, condition_params)
96
- case condition.to_s
97
- when 'forbidden_labels'
98
- QueryParamBuilders::LabelsParamBuilder.new('labels', condition_params, negated: true)
99
- when 'labels'
100
- QueryParamBuilders::LabelsParamBuilder.new('labels', condition_params)
101
- when 'source_branch'
102
- QueryParamBuilders::BaseParamBuilder.new('sourceBranch', condition_params)
103
- when 'target_branch'
104
- QueryParamBuilders::BaseParamBuilder.new('targetBranch', condition_params)
105
- end
115
+ args =
116
+ case condition.to_s
117
+ when 'source_branch'
118
+ ['sourceBranches', condition_params]
119
+ when 'target_branch'
120
+ ['targetBranches', condition_params]
121
+ when 'draft'
122
+ ['draft', condition_params, { with_quotes: false }]
123
+ else
124
+ return nil
125
+ end
126
+
127
+ QueryParamBuilders::BaseParamBuilder.new(*args)
106
128
  end
107
129
 
108
- def issues_resource_query(condition, condition_params)
109
- case condition.to_s
110
- when 'forbidden_labels'
111
- QueryParamBuilders::LabelsParamBuilder.new('labelName', condition_params, negated: true)
112
- when 'labels'
113
- QueryParamBuilders::LabelsParamBuilder.new('labelName', condition_params)
114
- end
130
+ def merge_requests_label_query(condition, condition_params)
131
+ args =
132
+ case condition.to_s
133
+ when 'forbidden_labels'
134
+ ['labels', condition_params, { negated: true }]
135
+ when 'labels'
136
+ ['labels', condition_params]
137
+ else
138
+ return nil
139
+ end
140
+
141
+ QueryParamBuilders::LabelsParamBuilder.new(*args)
115
142
  end
116
143
  end
117
144
  end
@@ -14,10 +14,6 @@ module Gitlab
14
14
  request_group(resource[:group_id])[:full_path]
15
15
  end
16
16
  alias_method :group_path, :project_path
17
-
18
- def reference
19
- '&'
20
- end
21
17
  end
22
18
  end
23
19
  end
@@ -9,8 +9,20 @@ module Gitlab
9
9
  class Issue < Base
10
10
  include Shared::Issuable
11
11
 
12
- def reference
13
- '#'
12
+ def merge_requests_count
13
+ @merge_requests_count ||= resource.dig(:merge_requests_count)
14
+ end
15
+
16
+ def related_merge_requests
17
+ @related_merge_requests ||= network.query_api_cached(
18
+ resource_url(sub_resource_type: 'related_merge_requests'))
19
+ .map { |merge_request| MergeRequest.new(merge_request, parent: self) }
20
+ end
21
+
22
+ def closed_by
23
+ @closed_by ||= network.query_api_cached(
24
+ resource_url(sub_resource_type: 'closed_by'))
25
+ .map { |merge_request| MergeRequest.new(merge_request, parent: self) }
14
26
  end
15
27
  end
16
28
  end
@@ -9,10 +9,6 @@ module Gitlab
9
9
  class MergeRequest < Base
10
10
  include Shared::Issuable
11
11
 
12
- def reference
13
- '!'
14
- end
15
-
16
12
  def first_contribution?
17
13
  if resource.key?(:first_contribution)
18
14
  resource[:first_contribution]
@@ -47,6 +47,10 @@ module Gitlab
47
47
  @labels_chronologically ||= labels_with_details.sort_by(&:added_at)
48
48
  end
49
49
 
50
+ def state
51
+ @state ||= resource.dig(:state)
52
+ end
53
+
50
54
  def author
51
55
  @author ||= resource.dig(:author, :username)
52
56
  end
@@ -57,12 +61,7 @@ module Gitlab
57
61
  end
58
62
 
59
63
  def full_resource_reference
60
- @full_resource_reference ||=
61
- "#{project_path}#{reference}#{resource[:iid]}"
62
- end
63
-
64
- def reference
65
- raise NotImplementedError
64
+ @full_resource_reference ||= resource.dig(:references, :full)
66
65
  end
67
66
 
68
67
  def root_id(
@@ -13,6 +13,8 @@ module Gitlab
13
13
  @sub_resource_type = options.fetch(:sub_resource_type, nil)
14
14
  @resource_id = options.fetch(:resource_id, nil)
15
15
  @params = options.fetch(:params, [])
16
+
17
+ @params = @params.merge(scope: :all) if @all
16
18
  end
17
19
 
18
20
  def build
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
- VERSION = '1.20.0'
5
+ VERSION = '1.23.0'
6
6
  end
7
7
  end
@@ -8,7 +8,7 @@ dry-run:triage:
8
8
  script:
9
9
  - gem install gitlab-triage
10
10
  - gitlab-triage --help
11
- - gitlab-triage --dry-run --token $GITLAB_API_TOKEN --source projects --source-id $CI_PROJECT_PATH
11
+ - gitlab-triage --dry-run --token $GITLAB_API_TOKEN --host-url $CI_SERVER_URL --source projects --source-id $CI_PROJECT_PATH
12
12
  when: manual
13
13
  except:
14
14
  - schedules
@@ -17,6 +17,6 @@ run:triage:
17
17
  stage: triage
18
18
  script:
19
19
  - gem install gitlab-triage
20
- - gitlab-triage --token $GITLAB_API_TOKEN --source projects --source-id $CI_PROJECT_PATH
20
+ - gitlab-triage --token $GITLAB_API_TOKEN --host-url $CI_SERVER_URL --source projects --source-id $CI_PROJECT_PATH
21
21
  only:
22
22
  - schedules
metadata CHANGED
@@ -1,27 +1,27 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-triage
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.20.0
4
+ version: 1.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-14 00:00:00.000000000 Z
11
+ date: 2022-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '5.1'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '5.1'
27
27
  - !ruby/object:Gem::Dependency
@@ -148,9 +148,14 @@ files:
148
148
  - ".gitignore"
149
149
  - ".gitlab-ci.yml"
150
150
  - ".gitlab/CODEOWNERS"
151
+ - ".gitlab/changelog_config.yml"
151
152
  - ".gitlab/merge_request_templates/Release.md"
152
153
  - ".rubocop.yml"
154
+ - ".ruby-version"
155
+ - ".tool-versions"
156
+ - ".yardopts"
153
157
  - CONTRIBUTING.md
158
+ - Dangerfile
154
159
  - Gemfile
155
160
  - Guardfile
156
161
  - LICENSE.md
@@ -233,10 +238,14 @@ files:
233
238
  - lib/gitlab/triage/version.rb
234
239
  - support/.gitlab-ci.example.yml
235
240
  - support/.triage-policies.example.yml
236
- homepage: https://gitlab.com/gitlab-org/gitlab-triage
241
+ homepage: https://gitlab.com/gitlab-org/ruby/gems/gitlab-triage
237
242
  licenses:
238
243
  - MIT
239
- metadata: {}
244
+ metadata:
245
+ allowed_push_host: https://rubygems.org
246
+ homepage_uri: https://gitlab.com/gitlab-org/ruby/gems/gitlab-triage
247
+ source_code_uri: https://gitlab.com/gitlab-org/ruby/gems/gitlab-triage
248
+ changelog_uri: https://gitlab.com/gitlab-org/ruby/gems/gitlab-triage/-/releases
240
249
  post_install_message:
241
250
  rdoc_options: []
242
251
  require_paths:
@@ -245,7 +254,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
245
254
  requirements:
246
255
  - - ">="
247
256
  - !ruby/object:Gem::Version
248
- version: '0'
257
+ version: 2.5.0
249
258
  required_rubygems_version: !ruby/object:Gem::Requirement
250
259
  requirements:
251
260
  - - ">="