gitlab-triage 1.18.0 → 1.22.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: '0609b18bd6e787fbf4e4c586b8631bc5f8c83c0a9c1a90047da3f953b558a6d6'
4
- data.tar.gz: dec00b0fe73a998726c0c42a845aa5fa48e5b590597df4f054ed26200d4a47ca
3
+ metadata.gz: '0490c10fa9f789e91502f3cb74a56bf4a830d200a58310f3aaa0475683f0717b'
4
+ data.tar.gz: 347c7aa604de9c1b042caaa21b7f8daf7e300c8aea7af175913174c51f90ed2d
5
5
  SHA512:
6
- metadata.gz: bb63d114b513c6a8705e47b432e5c7612fc4cbe9bf729d625286917b14341a0bfffc18ce70b06968a347b61ae8a2a629ed2c76f0e8d0c37b35a611c97b7d1045
7
- data.tar.gz: e0752eb15014713b4ef6f1ad64494a6b3607348be75107a93a59e11659a2fa8fca4a80b4e45c8a9eca9fb7fe4ecd2dcd0923fd27bfae8d09de9ee3bf45c8e2f8
6
+ metadata.gz: c428c46630d38c57e6c7e9db9b4d05e7d3751863163497c19f45b114a0d7b280cfe891c6bf595997ed45505881bc5f7eec4ad59934fe26a6f03f59fe3e533d63
7
+ data.tar.gz: 86b73ca21f856a51c9f4d5d79e40d2470003527aeae3910f5313df11121e705d5a25698b2e129e905cec2fd17e1e3cbe193a4f84dba2eb69e3a6c0db7d6964a4
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: |
@@ -159,6 +160,7 @@ Available condition types:
159
160
  - [`no_additional_labels` condition](#no-additional-labels-condition)
160
161
  - [`author_member` condition](#author-member-condition)
161
162
  - [`assignee_member` condition](#assignee-member-condition)
163
+ - [`draft` condition](#draft-condition)
162
164
  - [`source_branch` condition](#source-branch-condition)
163
165
  - [`target_branch` condition](#target-branch-condition)
164
166
  - [`weight` condition](#weight-condition)
@@ -191,7 +193,14 @@ conditions:
191
193
 
192
194
  ##### Milestone condition
193
195
 
194
- Accepts the name of a milestone to filter upon.
196
+ Accepts the name of a milestone to filter upon. Also accepts the following timebox values:
197
+
198
+ - `none`
199
+ - `any`
200
+ - `upcoming`
201
+ - `started`
202
+
203
+ See the [`milestone_id` API field documentation](https://docs.gitlab.com/ee/api/issues.html) for their meaning.
195
204
 
196
205
  Example:
197
206
 
@@ -204,11 +213,12 @@ conditions:
204
213
 
205
214
  Accepts a string.
206
215
 
207
- | State | Type | Value |
208
- | --------- | ---- | ------ |
209
- | Closed issues | string | `closed` |
210
- | Open issues | string | `opened` |
211
- | Merged merge requests | string | `merged` |
216
+ | State | Type | Value |
217
+ | --------- | ---- | ------ |
218
+ | Closed issues/MRs | string | `closed` |
219
+ | Open issues/MRs | string | `opened` |
220
+ | Locked issues | string | `locked` |
221
+ | Merged merge requests | string | `merged` |
212
222
 
213
223
  Example:
214
224
 
@@ -411,7 +421,6 @@ conditions:
411
421
 
412
422
  Accepts a boolean. If `true` the resource cannot have more labels than those specified by the `labels` condition.
413
423
 
414
-
415
424
  Example:
416
425
 
417
426
  ```yml
@@ -467,8 +476,23 @@ conditions:
467
476
  source_id: 9970
468
477
  ```
469
478
 
479
+ ##### Draft condition
480
+
481
+ **This condition is only applicable for merge requests.**
482
+
483
+ Accepts a boolean. If `true`, only draft MRs are returned. If `false`, only non-draft MRs are returned.
484
+
485
+ Example:
486
+
487
+ ```yml
488
+ conditions:
489
+ draft: true
490
+ ```
491
+
470
492
  ##### Source branch condition
471
493
 
494
+ **This condition is only applicable for merge requests.**
495
+
472
496
  Accepts the name of a source branch to filter upon.
473
497
 
474
498
  Example:
@@ -480,6 +504,8 @@ conditions:
480
504
 
481
505
  ##### Target branch condition
482
506
 
507
+ **This condition is only applicable for merge requests.**
508
+
483
509
  Accepts the name of a target branch to filter upon.
484
510
 
485
511
  Example:
@@ -491,8 +517,9 @@ conditions:
491
517
 
492
518
  ##### Weight condition
493
519
 
520
+ **This condition is only applicable for issues.**
521
+
494
522
  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
523
 
497
524
  | State | Type | Value |
498
525
  | --------- | ---- | ------ |
@@ -716,8 +743,8 @@ The following placeholders are supported:
716
743
  - `downvotes`: the resources's downvotes count
717
744
  - `title`: the resource's title
718
745
  - `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.
746
+ - `full_reference`: the full reference of the resource as `namespace/project#12`, `namespace/project!42`, `namespace/project&72`
747
+ - `type`: the type of the resources. For now, only `issues`, `merge_requests`, and `epics` are supported.
721
748
 
722
749
  If the resource doesn't respond to the placeholder, or if the field is `nil`,
723
750
  the placeholder is not replaced.
@@ -827,8 +854,7 @@ The following placeholders are supported for `summary`:
827
854
 
828
855
  - `title`: The title of the generated issue
829
856
  - `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`
857
+ - `type`: The resource type for the summary. For now `issues`, `merge_requests`, or `epics`,
832
858
 
833
859
  > **Note:**
834
860
  > - Both `item` and `summary` fields act like a [comment action](#comment-action),
@@ -957,17 +983,6 @@ resource_rules:
957
983
  issues:
958
984
  summaries:
959
985
  - 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
986
  rules:
972
987
  - name: New issues
973
988
  conditions:
@@ -993,6 +1008,17 @@ resource_rules:
993
1008
  Please triage the following old {{type}}:
994
1009
 
995
1010
  {{items}}
1011
+ actions:
1012
+ summarize:
1013
+ title: "Newest and oldest {{type}} summary"
1014
+ summary: |
1015
+ Please triage the following {{type}}:
1016
+
1017
+ {{items}}
1018
+
1019
+ Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
1020
+
1021
+ /label ~"needs attention"
996
1022
  ```
997
1023
 
998
1024
  Which could generate an issue like:
@@ -1032,7 +1058,7 @@ Here's a list of currently available Ruby expression API:
1032
1058
 
1033
1059
  | Name | Return type | Description |
1034
1060
  | ---- | ---- | ---- |
1035
- | resource | Hash | The hash containing the raw data of the resource |
1061
+ | 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
1062
  | author | String | The username of the resource author |
1037
1063
  | milestone | Milestone | The milestone attached to the resource |
1038
1064
  | labels | [Label] | A list of labels, having only names |
@@ -1041,7 +1067,7 @@ Here's a list of currently available Ruby expression API:
1041
1067
  | label_events | [LabelEvent] | A list of label events on the resource |
1042
1068
  | instance_version | InstanceVersion | The version for the GitLab instance we're triaging with |
1043
1069
  | 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 |
1070
+ | full_resource_reference | String | A full reference including project path to the issue or merge request |
1045
1071
 
1046
1072
  ##### Methods for `MergeRequest` (merge request context)
1047
1073
 
@@ -1130,7 +1156,7 @@ Usage: gitlab-triage [options]
1130
1156
  --init-ci Initialize the project with a .gitlab-ci.yml file
1131
1157
  ```
1132
1158
 
1133
- #### Running locally
1159
+ #### Running with the installed gem
1134
1160
 
1135
1161
  Triaging against a specific project:
1136
1162
 
@@ -1152,6 +1178,16 @@ gitlab-triage --dry-run --token $GITLAB_API_TOKEN --all-projects
1152
1178
 
1153
1179
  > **Note:** The `--all-projects` option will process all resources for all projects visible to the specified `$GITLAB_API_TOKEN`
1154
1180
 
1181
+ #### Running from source
1182
+
1183
+ Execute the `gitlab-triage` script from the `./bin` directory.
1184
+
1185
+ For example- after cloning this project, from the root `gitlab-triage` directory:
1186
+
1187
+ ```
1188
+ bundle exec bin/gitlab-triage --dry-run --token $GITLAB_API_TOKEN --source-id gitlab-org/triage
1189
+ ```
1190
+
1155
1191
  #### Running on GitLab CI pipeline
1156
1192
 
1157
1193
  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,7 @@ 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
40
41
  EpicsTriagingForProjectImpossibleError = Class.new(StandardError)
41
42
 
42
43
  def initialize(policies:, options:, network_adapter_class: DEFAULT_NETWORK_ADAPTER, graphql_network_adapter_class: DEFAULT_GRAPHQL_ADAPTER)
@@ -62,16 +63,16 @@ module Gitlab
62
63
  puts Gitlab::Triage::UI.header("Triaging the `#{options.source_id}` #{options.source.to_s.singularize}", char: '=')
63
64
  puts
64
65
 
65
- resource_rules.each do |resource_type, resource|
66
+ resource_rules.each do |resource_type, policy_definition|
66
67
  if resource_type == 'epics' && options.source != :groups
67
68
  raise(EpicsTriagingForProjectImpossibleError, "Epics can only be triaged at the group level. Please set the `--source groups` option.")
68
69
  end
69
70
 
70
- puts Gitlab::Triage::UI.header("Processing rules for #{resource_type}", char: '-')
71
+ puts Gitlab::Triage::UI.header("Processing summaries & rules for #{resource_type}", char: '-')
71
72
  puts
72
73
 
73
- process_summaries(resource_type, resource[:summaries])
74
- process_rules(resource_type, resource[:rules])
74
+ process_summaries(resource_type, policy_definition[:summaries])
75
+ process_rules(resource_type, policy_definition[:rules])
75
76
  end
76
77
  end
77
78
 
@@ -127,71 +128,165 @@ module Gitlab
127
128
  rule.fetch(:limits) { {} }
128
129
  end
129
130
 
130
- def process_summaries(resource_type, summaries)
131
- return if summaries.blank?
132
-
133
- summaries.each do |summary|
134
- process_summary(resource_type, summary)
131
+ # Process an array of +summary_definitions+.
132
+ #
133
+ # @example Example of an array of summary definitions (shown as YAML for readability).
134
+ #
135
+ # - name: Newest and oldest issues summary
136
+ # rules:
137
+ # - name: New issues
138
+ # conditions:
139
+ # state: opened
140
+ # limits:
141
+ # most_recent: 2
142
+ # actions:
143
+ # summarize:
144
+ # item: "- [ ] [{{title}}]({{web_url}}) {{labels}}"
145
+ # summary: |
146
+ # Please triage the following new {{type}}:
147
+ # {{items}}
148
+ # actions:
149
+ # summarize:
150
+ # title: "Newest and oldest {{type}} summary"
151
+ # summary: |
152
+ # Please triage the following {{type}}:
153
+ # {{items}}
154
+ # Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
155
+ # /label ~"needs attention"
156
+ #
157
+ # @param summary_definitions [Array<Hash>] An array usually given as YAML in a triage policy file.
158
+ #
159
+ # @return [nil]
160
+ def process_summaries(resource_type, summary_definitions)
161
+ return if summary_definitions.blank?
162
+
163
+ summary_definitions.each do |summary_definition|
164
+ process_summary(resource_type, summary_definition)
135
165
  end
136
166
  end
137
167
 
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|
168
+ # Process an array of +rule_definitions+.
169
+ #
170
+ # @example Example of an array of rule definitions.
171
+ #
172
+ # [{ name: "New issues", conditions: { state: opened }, limits: { most_recent: 2 }, actions: { labels: ["needs attention"] } }]
173
+ #
174
+ # @param rule_definitions [Array<Hash>] An array usually given as YAML in a triage policy file.
175
+ #
176
+ # @return [nil]
177
+ def process_rules(resource_type, rule_definitions)
178
+ return if rule_definitions.blank?
179
+
180
+ rule_definitions.each do |rule_definition|
181
+ resources_for_rule(resource_type, rule_definition) do |resources|
143
182
  policy = Policies::RulePolicy.new(
144
- resource_type, rule, resources, network)
183
+ resource_type, rule_definition, resources, network)
145
184
 
146
185
  process_action(policy)
147
186
  end
148
187
  end
149
188
  end
150
189
 
151
- def process_summary(resource_type, summary)
152
- puts Gitlab::Triage::UI.header("Processing summary: **#{summary[:name]}**", char: '~')
190
+ # Process a +summary_definition+.
191
+ #
192
+ # @example Example of a summary definition hash (shown as YAML for readability).
193
+ #
194
+ # name: Newest and oldest issues summary
195
+ # rules:
196
+ # - name: New issues
197
+ # conditions:
198
+ # state: opened
199
+ # limits:
200
+ # most_recent: 2
201
+ # actions:
202
+ # summarize:
203
+ # item: "- [ ] [{{title}}]({{web_url}}) {{labels}}"
204
+ # summary: |
205
+ # Please triage the following new {{type}}:
206
+ # {{items}}
207
+ # actions:
208
+ # summarize:
209
+ # title: "Newest and oldest {{type}} summary"
210
+ # summary: |
211
+ # Please triage the following {{type}}:
212
+ # {{items}}
213
+ # Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
214
+ # /label ~"needs attention"
215
+ #
216
+ # @param resource_type [String] The resource type, e.g. +issues+ or +merge_requests+.
217
+ # @param summary_definition [Hash] A hash usually given as YAML in a triage policy file:
218
+ #
219
+ # @return [nil]
220
+ def process_summary(resource_type, summary_definition)
221
+ puts Gitlab::Triage::UI.header("Processing summary: **#{summary_definition[:name]}**", char: '~')
153
222
  puts
154
223
 
155
- summary_parts_for_rules(resource_type, summary[:rules]) do |resources|
224
+ summary_parts_for_rules(resource_type, summary_definition[:rules]) do |summary_resources|
156
225
  policy = Policies::SummaryPolicy.new(
157
- resource_type, summary, resources, network)
226
+ resource_type, summary_definition, summary_resources, network)
158
227
 
159
228
  process_action(policy)
160
229
  end
161
230
  end
162
231
 
163
- def summary_parts_for_rules(resource_type, rules)
232
+ # Transform an array of +rule_definitions+ into a +PoliciesResources::SummaryResources.new(rule => rule_resources)+ object.
233
+ #
234
+ # @example Example of an array of rule definitions.
235
+ #
236
+ # [{ name: "New issues", conditions: { state: opened }, limits: { most_recent: 2 }, actions: { labels: ["needs attention"] } }]
237
+ #
238
+ # @param resource_type [String] The resource type, e.g. +issues+ or +merge_requests+.
239
+ # @param rule_definitions [Array<Hash>] An array of rule definitions, e.g.
240
+ # +[{ name: 'Foo', conditions: { milestone: 'v1' } }, { name: 'Foo', conditions: { state: 'opened' } }]+.
241
+ #
242
+ # @yieldparam summary_resources [PoliciesResources::SummaryResources] An object which contains a +{ rule_definition => resources }+ hash.
243
+ # @yieldreturn [nil]
244
+ #
245
+ # @return [nil]
246
+ def summary_parts_for_rules(resource_type, rule_definitions)
164
247
  # { 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
248
+ parts = rule_definitions.each_with_object({}) do |rule_definition, result|
249
+ to_enum(:resources_for_rule, resource_type, rule_definition).each do |rule_resources, expanded_conditions|
250
+ # We replace the non-expanded rule conditions with the expanded ones
251
+ result.merge!(rule_definition.merge(conditions: expanded_conditions) => rule_resources)
252
+ end
253
+
254
+ result
174
255
  end
175
256
 
176
257
  yield(PoliciesResources::SummaryResources.new(parts))
177
258
  end
178
259
 
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|
260
+ # Transform a non-expanded +rule_definition+ into a +PoliciesResources::RuleResources.new(resources)+ object.
261
+ #
262
+ # @example Example of a rule definition hash.
263
+ #
264
+ # { name: "New issues", conditions: { state: opened }, limits: { most_recent: 2 }, actions: { labels: ["needs attention"] } }
265
+ #
266
+ # @param resource_type [String] The resource type, e.g. +issues+ or +merge_requests+.
267
+ # @param rule_definition [Hash] A rule definition, e.g. +{ name: 'Foo', conditions: { milestone: 'v1' } }+.
268
+ #
269
+ # @yieldparam rule_resources [PoliciesResources::RuleResources] An object which contains an array of resources.
270
+ # @yieldparam expanded_conditions [Hash] A hash of expanded conditions.
271
+ # @yieldreturn [nil]
272
+ #
273
+ # @return [nil]
274
+ def resources_for_rule(resource_type, rule_definition)
275
+ puts Gitlab::Triage::UI.header("Gathering resources for rule: **#{rule_definition[:name]}**", char: '-')
276
+
277
+ ExpandCondition.perform(rule_conditions(rule_definition)) do |expanded_conditions|
183
278
  # retrieving the resources for every rule is inefficient
184
279
  # however, previous rules may affect those upcoming
185
280
  resources = []
186
281
 
187
- if rule[:api] == 'graphql'
188
- graphql_query = build_graphql_query(resource_type, conditions, true)
282
+ if rule_definition[:api] == 'graphql'
283
+ graphql_query = build_graphql_query(resource_type, expanded_conditions, true)
189
284
  resources = graphql_network.query(graphql_query, source: source_full_path)
190
285
  else
191
- resources = network.query_api(build_get_url(resource_type, conditions))
286
+ resources = network.query_api(build_get_url(resource_type, expanded_conditions))
192
287
  iids = resources.pluck('iid').map(&:to_s)
193
288
 
194
- graphql_query = build_graphql_query(resource_type, conditions)
289
+ graphql_query = build_graphql_query(resource_type, expanded_conditions)
195
290
  graphql_resources = graphql_network.query(graphql_query, source: source_full_path, iids: iids) if graphql_query.any?
196
291
 
197
292
  decorate_resources_with_graphql_data(resources, graphql_resources)
@@ -202,28 +297,19 @@ module Gitlab
202
297
 
203
298
  puts "\n\n* Found #{resources.count} resources..."
204
299
  print "* Filtering resources..."
205
- resources = filter_resources(resources, conditions)
300
+ resources = filter_resources(resources, expanded_conditions)
206
301
  puts "\n* Total after filtering: #{resources.count} resources"
207
302
  print "* Limiting resources..."
208
- resources = limit_resources(resources, rule_limits(rule))
303
+ resources = limit_resources(resources, rule_limits(rule_definition))
209
304
  puts "\n* Total after limiting: #{resources.count} resources"
210
305
  puts
211
306
 
212
- yield(PoliciesResources::RuleResources.new(resources), conditions)
307
+ yield(PoliciesResources::RuleResources.new(resources), expanded_conditions)
213
308
  end
214
309
  end
215
310
 
216
311
  def attach_resource_type(resources, resource_type)
217
312
  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
313
  end
228
314
 
229
315
  def decorate_resources_with_graphql_data(resources, graphql_resources)
@@ -308,19 +394,20 @@ module Gitlab
308
394
  allowed_values: ALLOWED_STATE_VALUES[resource_type])
309
395
  end
310
396
 
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]
397
+ condition_builders << milestone_condition_builder(resource_type, conditions[:milestone]) if conditions[:milestone]
314
398
 
315
399
  if conditions[:date] && APIQueryBuilders::DateQueryParamBuilder.applicable?(conditions[:date])
316
400
  condition_builders << APIQueryBuilders::DateQueryParamBuilder.new(conditions.delete(:date))
317
401
  end
318
402
 
319
- if conditions[:weight] && resource_type.to_sym == :issues
320
- condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('weight', conditions[:weight])
403
+ case resource_type&.to_sym
404
+ when :issues
405
+ condition_builders.concat(issues_resource_query(conditions))
406
+ when :merge_requests
407
+ condition_builders.concat(merge_requests_resource_query(conditions))
321
408
  end
322
409
 
323
- condition_builders.each do |condition_builder|
410
+ condition_builders.compact.each do |condition_builder|
324
411
  params[condition_builder.param_name] = condition_builder.param_content
325
412
  end
326
413
 
@@ -334,6 +421,50 @@ module Gitlab
334
421
  ).build
335
422
  end
336
423
 
424
+ def milestone_condition_builder(resource_type, milestone_condition)
425
+ milestone_value = Array(milestone_condition)[0].to_s # back-compatibility
426
+ return if milestone_value.empty?
427
+
428
+ # Issues API should use the `milestone_id` param for timebox values, and `milestone` for milestone title
429
+ args =
430
+ if resource_type.to_sym == :issues && MILESTONE_TIMEBOX_VALUES.include?(milestone_value.downcase)
431
+ ['milestone_id', milestone_value.titleize] # The API only accepts titleized values.
432
+ else
433
+ ['milestone', milestone_value]
434
+ end
435
+
436
+ APIQueryBuilders::SingleQueryParamBuilder.new(*args)
437
+ end
438
+
439
+ def issues_resource_query(conditions)
440
+ [].tap do |condition_builders|
441
+ condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('weight', conditions[:weight]) if conditions[:weight]
442
+ end
443
+ end
444
+
445
+ def merge_requests_resource_query(conditions)
446
+ [].tap do |condition_builders|
447
+ condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('source_branch', conditions[:source_branch]) if conditions[:source_branch]
448
+ condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('target_branch', conditions[:target_branch]) if conditions[:target_branch]
449
+ condition_builders << draft_condition_builder(conditions[:draft]) if conditions.key?(:draft)
450
+ end
451
+ end
452
+
453
+ def draft_condition_builder(draft_condittion)
454
+ # Issues API only accepts 'yes' and 'no' as strings: https://docs.gitlab.com/ee/api/merge_requests.html
455
+ wip =
456
+ case draft_condittion
457
+ when true
458
+ 'yes'
459
+ when false
460
+ 'no'
461
+ else
462
+ raise ArgumentError, 'The "draft" condition only accepts true or false.'
463
+ end
464
+
465
+ APIQueryBuilders::SingleQueryParamBuilder.new('wip', wip)
466
+ end
467
+
337
468
  def build_graphql_query(resource_type, conditions, graphql_only = false)
338
469
  Gitlab::Triage::GraphqlQueries::QueryBuilder
339
470
  .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,21 +82,64 @@ 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
- if resource_type == 'merge_requests'
86
- condition_queries << QueryParamBuilders::LabelsParamBuilder.new('labels', condition_params) if condition.to_s == 'labels'
87
- condition_queries << QueryParamBuilders::BaseParamBuilder.new('sourceBranch', condition_params) if condition.to_s == 'source_branch'
88
- condition_queries << QueryParamBuilders::BaseParamBuilder.new('targetBranch', condition_params) if condition.to_s == 'target_branch'
89
- end
90
-
91
- if resource_type == 'issues'
92
- condition_queries << QueryParamBuilders::LabelsParamBuilder.new('labelName', condition_params) if condition.to_s == 'labels'
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)
93
91
  end
94
92
  end
95
93
 
96
94
  condition_queries
95
+ .compact
97
96
  .map(&:build_param)
98
97
  .join
99
98
  end
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
+
114
+ def merge_requests_resource_query(condition, condition_params)
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)
128
+ end
129
+
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)
142
+ end
100
143
  end
101
144
  end
102
145
  end
@@ -5,18 +5,23 @@ module Gitlab
5
5
  module GraphqlQueries
6
6
  module QueryParamBuilders
7
7
  class BaseParamBuilder
8
- attr_reader :param_name, :param_contents, :with_quotes
8
+ attr_reader :param_name, :param_contents, :with_quotes, :negated
9
9
 
10
- def initialize(param_name, param_contents, with_quotes: true)
10
+ def initialize(param_name, param_contents, with_quotes: true, negated: false)
11
11
  @param_name = param_name
12
12
  @param_contents = param_contents.to_s.strip
13
13
  @with_quotes = with_quotes
14
+ @negated = negated
14
15
  end
15
16
 
16
17
  def build_param
17
18
  contents = with_quotes ? Utils.graphql_quote(param_contents) : param_contents
18
19
 
19
- ", #{param_name}: #{contents}"
20
+ if negated
21
+ ", not: { #{param_name}: #{contents} }"
22
+ else
23
+ ", #{param_name}: #{contents}"
24
+ end
20
25
  end
21
26
  end
22
27
  end
@@ -6,10 +6,10 @@ module Gitlab
6
6
  module GraphqlQueries
7
7
  module QueryParamBuilders
8
8
  class LabelsParamBuilder < BaseParamBuilder
9
- def initialize(param_name, labels)
9
+ def initialize(param_name, labels, negated: false)
10
10
  label_param_content = labels.map { |label| Utils.graphql_quote(label) }.join(', ').then { |content| "[#{content}]" }
11
11
 
12
- super(param_name, label_param_content, with_quotes: false)
12
+ super(param_name, label_param_content, with_quotes: false, negated: negated)
13
13
  end
14
14
  end
15
15
  end
@@ -49,7 +49,7 @@ module Gitlab
49
49
  when Hash
50
50
  resources << results
51
51
  else
52
- raise Errors::Network::UnexpectedResponse, "Unexpected response: #{results.inspect}"
52
+ raise_unexpected_response(results)
53
53
  end
54
54
 
55
55
  rate_limit_debug(response) if options.debug
@@ -73,7 +73,14 @@ module Gitlab
73
73
  rate_limit_debug(response) if options.debug
74
74
  rate_limit_wait(response)
75
75
 
76
- response.delete(:results).with_indifferent_access
76
+ results = response.delete(:results)
77
+
78
+ case results
79
+ when Hash
80
+ results.with_indifferent_access
81
+ else
82
+ raise_unexpected_response(results)
83
+ end
77
84
  rescue Net::ReadTimeout
78
85
  {}
79
86
  end
@@ -95,6 +102,10 @@ module Gitlab
95
102
  puts Gitlab::Triage::UI.debug "Rate limit almost exceeded, sleeping for #{response[:ratelimit_reset_at] - Time.now} seconds" if options.debug
96
103
  sleep(1) until Time.now >= response[:ratelimit_reset_at]
97
104
  end
105
+
106
+ def raise_unexpected_response(results)
107
+ raise Errors::Network::UnexpectedResponse, "Unexpected response: #{results.inspect}"
108
+ end
98
109
  end
99
110
  end
100
111
  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
@@ -8,10 +8,6 @@ module Gitlab
8
8
  module Resource
9
9
  class Issue < Base
10
10
  include Shared::Issuable
11
-
12
- def reference
13
- '#'
14
- end
15
11
  end
16
12
  end
17
13
  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]
@@ -32,6 +32,8 @@ module Gitlab
32
32
  @labels_with_details ||= label_events
33
33
  .select { |event| event.action == 'add' && event.label }
34
34
  .map(&:label)
35
+ .sort_by(&:added_at)
36
+ .reverse
35
37
  .uniq(&:name)
36
38
  .select { |label| resource[:labels].include?(label.name) }
37
39
  end
@@ -55,12 +57,7 @@ module Gitlab
55
57
  end
56
58
 
57
59
  def full_resource_reference
58
- @full_resource_reference ||=
59
- "#{project_path}#{reference}#{resource[:iid]}"
60
- end
61
-
62
- def reference
63
- raise NotImplementedError
60
+ @full_resource_reference ||= resource.dig(:references, :full)
64
61
  end
65
62
 
66
63
  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.18.0'
5
+ VERSION = '1.22.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.18.0
4
+ version: 1.22.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-05-31 00:00:00.000000000 Z
11
+ date: 2022-01-26 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
  - - ">="