gitlab-triage 1.10.1 → 1.14.1

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: 1242b945b940e1378f6bb8f66eb5f08f3adb387245c0425954aa7a6cc3e98f25
4
- data.tar.gz: 925cbb79cd61b2c5f4eeb87eed0596c674902aa6fd44bfe679b9740d660aede6
3
+ metadata.gz: fbce3d75b063c517b80bbbfa5a0cf8ef2cf0f6ffe9eabcebe743f7ea5e6fbc23
4
+ data.tar.gz: '09687c2d578d80f351a54a614113bcf36b70a15144f848409b8143be53c95f9b'
5
5
  SHA512:
6
- metadata.gz: 1caedc0d489bc0e45e66399aa9529cf3eaf9812a78cb99fddba5cc5a078b10ab748e763d2407e6911da9ba2cadbc56453040366c8bad1757a0ae07fbcbf25b1d
7
- data.tar.gz: 12de27a31f013916beee14fe3da467e1ee316774e8fea019c0735c68c81f0e5694a79cdcb5d4bc94522b620a94c4c4989c4367f51fd28d400f19a1dca63d48dc
6
+ metadata.gz: f88d9507fd36e15941fa9b1345887e138893e8e7e5ae1b872408cfea449c2565c48c4267c1df278fa0f1ddf3f1c857050e4868eef88268b4acf9c7078dda0a53
7
+ data.tar.gz: 77dfd58b8e61d3115e9057b688d7f2b91b8473dd4e7a177a32cad0585f1f42c115d371f9d10f10a0a27fd3ee1b4230ee757982dcc85207395ebef82274165e70
@@ -2,7 +2,7 @@ stages:
2
2
  - prepare
3
3
  - test
4
4
  - triage
5
- - release
5
+ - deploy
6
6
 
7
7
  default:
8
8
  image: ruby:2.7
@@ -38,6 +38,7 @@ workflow:
38
38
  services:
39
39
  - docker:${DOCKER_VERSION}-dind
40
40
  variables:
41
+ DOCKER_VERSION: "19.03.0"
41
42
  DOCKER_DRIVER: overlay2
42
43
  DOCKER_HOST: tcp://docker:2375
43
44
  DOCKER_TLS_CERTDIR: ""
@@ -118,7 +119,7 @@ dry-run:gitlab-triage:
118
119
  - gitlab-triage --version
119
120
  - gitlab-triage --help
120
121
  - gitlab-triage --init
121
- - gitlab-triage --dry-run --debug --token $API_TOKEN --source-id $CI_PROJECT_PATH
122
+ - gitlab-triage --dry-run --debug --token $GITLAB_API_TOKEN --source-id $CI_PROJECT_PATH
122
123
 
123
124
  # This job requires allows to override the `CI_PROJECT_PATH` variable when triggered.
124
125
  dry-run:custom:
@@ -127,26 +128,6 @@ dry-run:custom:
127
128
  - when: manual
128
129
  allow_failure: true
129
130
 
130
- ###################
131
- ## Release stage ##
132
- ###################
133
- release:
134
- stage: release
135
- rules:
136
- - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push"'
137
- changes: ["lib/gitlab/triage/version.rb"]
138
- - if: '$CI_MERGE_REQUEST_TITLE =~ /RELEASE/'
139
- when: manual
140
- before_script: []
141
- script:
142
- - version=$(ruby -r ./lib/gitlab/triage/version -e 'puts Gitlab::Triage::VERSION' | tr -d "\n")
143
- - tag="v${version}"
144
- - message="Version ${version}."
145
- # TODO: Add release notes from the Release MR?
146
- - 'curl --request POST --header "PRIVATE-TOKEN: ${API_TOKEN}" -d "tag_name=${tag}" -d "ref=${CI_COMMIT_SHA}" -d "message=${message}" "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/repository/tags"'
147
- - gem build gitlab-triage.gemspec
148
- - gem push "gitlab-triage-${version}.gem"
149
- artifacts:
150
- paths:
151
- - gitlab-triage*.gem
152
- expire_in: 30 days
131
+ include:
132
+ - project: 'gitlab-org/quality/pipeline-common'
133
+ file: '/ci/gem-release.yml'
@@ -1,2 +1,2 @@
1
1
  # The official maintainers
2
- * @rymai @godfat @markglenfletcher
2
+ * @rymai @godfat-gitlab @markglenfletcher
data/Guardfile CHANGED
@@ -24,7 +24,7 @@
24
24
  # * zeus: 'zeus rspec' (requires the server to be started separately)
25
25
  # * 'just' rspec: 'rspec'
26
26
 
27
- guard :rspec, cmd: "bundle exec rspec" do
27
+ guard :rspec, cmd: "bundle exec rspec -f doc" do
28
28
  require "guard/rspec/dsl"
29
29
  dsl = Guard::RSpec::Dsl.new(self)
30
30
 
data/README.md CHANGED
@@ -141,6 +141,8 @@ Available condition types:
141
141
  - [`assignee_member` condition](#assignee-member-condition)
142
142
  - [`source_branch` condition](#source-branch-condition)
143
143
  - [`target_branch` condition](#target-branch-condition)
144
+ - [`weight` condition](#weight-condition)
145
+ - [`discussions` condition](#discussions-condition)
144
146
  - [`ruby` condition](#ruby-condition)
145
147
 
146
148
  ##### Date condition
@@ -467,6 +469,44 @@ conditions:
467
469
  target_branch: 'master'
468
470
  ```
469
471
 
472
+ ##### Weight condition
473
+
474
+ Accepts a string per the [API documentation](https://docs.gitlab.com/ee/api/issues.html#list-issues).
475
+ This condition is only applicable for issues (not merge requests).
476
+
477
+ | State | Type | Value |
478
+ | --------- | ---- | ------ |
479
+ | Any weight | string | `Any` |
480
+ | No weight | string | `None` |
481
+ | Specific weight | integer | integer |
482
+
483
+ Example:
484
+
485
+ ```yml
486
+ conditions:
487
+ weight: Any
488
+ ```
489
+
490
+ ##### Discussions condition
491
+
492
+ Accepts a hash of fields.
493
+
494
+ | Field | Type | Values | Required |
495
+ | --------- | ---- | ---- | -------- |
496
+ | `attribute` | string | `threads`, `notes` | yes |
497
+ | `condition` | string | `less_than`, `greater_than` | yes |
498
+ | `threshold` | integer | integer | yes |
499
+
500
+ Example:
501
+
502
+ ```yml
503
+ conditions:
504
+ discussions:
505
+ attribute: threads
506
+ condition: greater_than
507
+ threshold: 15
508
+ ```
509
+
470
510
  ##### Ruby condition
471
511
 
472
512
  This condition allows users to write a Ruby expression to be evaluated for
@@ -552,6 +592,10 @@ Adds a number of labels to the resource.
552
592
 
553
593
  Accepts an array of strings. Each element is the name of a label to add.
554
594
 
595
+ If any of the labels doesn't exist, the automation will stop immediately so
596
+ that if a label is renamed or deleted, you'll have to explicitly update or remove
597
+ it in your policy file.
598
+
555
599
  Example:
556
600
 
557
601
  ```yml
@@ -567,6 +611,10 @@ Removes a number of labels from the resource.
567
611
 
568
612
  Accepts an array of strings. Each element is the name of a label to remove.
569
613
 
614
+ If any of the labels doesn't exist, the automation will stop immediately so
615
+ that if a label is renamed or deleted, you'll have to explicitly update or remove
616
+ it in your policy file.
617
+
570
618
  Example:
571
619
 
572
620
  ```yml
@@ -1016,22 +1064,22 @@ Usage: gitlab-triage [options]
1016
1064
  Triaging against a specific project:
1017
1065
 
1018
1066
  ```
1019
- gitlab-triage --dry-run --token $API_TOKEN --source-id gitlab-org/triage
1067
+ gitlab-triage --dry-run --token $GITLAB_API_TOKEN --source-id gitlab-org/triage
1020
1068
  ```
1021
1069
 
1022
1070
  Triaging against a whole group:
1023
1071
 
1024
1072
  ```
1025
- gitlab-triage --dry-run --token $API_TOKEN --source-id gitlab-org --source groups
1073
+ gitlab-triage --dry-run --token $GITLAB_API_TOKEN --source-id gitlab-org --source groups
1026
1074
  ```
1027
1075
 
1028
1076
  Triaging against an entire instance:
1029
1077
 
1030
1078
  ```
1031
- gitlab-triage --dry-run --token $API_TOKEN --all-projects
1079
+ gitlab-triage --dry-run --token $GITLAB_API_TOKEN --all-projects
1032
1080
  ```
1033
1081
 
1034
- > **Note:** The `--all-projects` option will process all resources for all projects visible to the specified `$API_TOKEN`
1082
+ > **Note:** The `--all-projects` option will process all resources for all projects visible to the specified `$GITLAB_API_TOKEN`
1035
1083
 
1036
1084
  #### Running on GitLab CI pipeline
1037
1085
 
@@ -1042,7 +1090,7 @@ run:triage:triage:
1042
1090
  stage: triage
1043
1091
  script:
1044
1092
  - gem install gitlab-triage
1045
- - gitlab-triage --token $API_TOKEN --source-id $CI_PROJECT_PATH
1093
+ - gitlab-triage --token $GITLAB_API_TOKEN --source-id $CI_PROJECT_PATH
1046
1094
  only:
1047
1095
  - schedules
1048
1096
  ```
@@ -1056,7 +1104,7 @@ Yes, you can override the host url using the following options:
1056
1104
  ##### CLI
1057
1105
 
1058
1106
  ```
1059
- gitlab-triage --dry-run --token $API_TOKEN --source-id gitlab-org/triage --host-url https://gitlab.host.com
1107
+ gitlab-triage --dry-run --token $GITLAB_API_TOKEN --source-id gitlab-org/triage --host-url https://gitlab.host.com
1060
1108
  ```
1061
1109
 
1062
1110
  ##### Policy file
@@ -1093,7 +1141,7 @@ Gitlab::Triage::Resource::Context.include MyPlugin
1093
1141
  And then run it with:
1094
1142
 
1095
1143
  ```shell
1096
- gitlab-triage -r ./my_plugin.rb --token $API_TOKEN --source-id gitlab-org/triage
1144
+ gitlab-triage -r ./my_plugin.rb --token $GITLAB_API_TOKEN --source-id gitlab-org/triage
1097
1145
  ```
1098
1146
 
1099
1147
  This allows you to use `has_severity_label?` in the Ruby condition:
@@ -20,6 +20,8 @@ Gem::Specification.new do |spec|
20
20
  spec.require_paths = ['lib']
21
21
 
22
22
  spec.add_dependency 'activesupport', '~> 5.1'
23
+ spec.add_dependency 'globalid', '~> 0.4'
24
+ spec.add_dependency 'graphql-client', '~> 0.16'
23
25
  spec.add_dependency 'httparty', '~> 0.17'
24
26
 
25
27
  spec.add_development_dependency 'bundler'
@@ -41,8 +41,8 @@ module Gitlab
41
41
  CommandBuilders::CommentCommandBuilder.new(
42
42
  [
43
43
  CommandBuilders::TextContentBuilder.new(policy.actions[:comment], resource: resource, network: network).build_command,
44
- CommandBuilders::LabelCommandBuilder.new(policy.actions[:labels]).build_command,
45
- CommandBuilders::RemoveLabelCommandBuilder.new(policy.actions[:remove_labels]).build_command,
44
+ CommandBuilders::LabelCommandBuilder.new(policy.actions[:labels], resource: resource, network: network).build_command,
45
+ CommandBuilders::RemoveLabelCommandBuilder.new(policy.actions[:remove_labels], resource: resource, network: network).build_command,
46
46
  CommandBuilders::CcCommandBuilder.new(policy.actions[:mention]).build_command,
47
47
  CommandBuilders::MoveCommandBuilder.new(policy.actions[:move]).build_command,
48
48
  CommandBuilders::StatusCommandBuilder.new(policy.actions[:status]).build_command
@@ -2,13 +2,15 @@ module Gitlab
2
2
  module Triage
3
3
  module CommandBuilders
4
4
  class BaseCommandBuilder
5
- def initialize(items)
5
+ def initialize(items, resource: nil, network: nil)
6
6
  @items = Array.wrap(items)
7
7
  @items.delete('')
8
+ @resource = resource&.with_indifferent_access
9
+ @network = network
8
10
  end
9
11
 
10
12
  def build_command
11
- if @items.any?
13
+ if items.any?
12
14
  [slash_command_string, content_string].compact.join(separator)
13
15
  else
14
16
  ""
@@ -17,6 +19,8 @@ module Gitlab
17
19
 
18
20
  private
19
21
 
22
+ attr_reader :items, :resource, :network
23
+
20
24
  def separator
21
25
  ' '
22
26
  end
@@ -26,7 +30,7 @@ module Gitlab
26
30
  end
27
31
 
28
32
  def content_string
29
- @items.map do |item|
33
+ items.map do |item|
30
34
  format_item(item)
31
35
  end.join(separator)
32
36
  end
@@ -4,8 +4,25 @@ module Gitlab
4
4
  module Triage
5
5
  module CommandBuilders
6
6
  class LabelCommandBuilder < BaseCommandBuilder
7
+ def build_command
8
+ ensure_labels_exist!
9
+
10
+ super
11
+ end
12
+
7
13
  private
8
14
 
15
+ def ensure_labels_exist!
16
+ items.each do |label|
17
+ label_opts = { project_id: resource[:project_id], name: label }
18
+
19
+ unless Resource::Label.new(label_opts, network: network).exist?
20
+ raise Resource::Label::LabelDoesntExistError,
21
+ "Label `#{label}` doesn't exist!"
22
+ end
23
+ end
24
+ end
25
+
9
26
  def slash_command_string
10
27
  "/label"
11
28
  end
@@ -34,13 +34,9 @@ module Gitlab
34
34
  }.freeze
35
35
  PLACEHOLDER_REGEX = /{{([\w\.]+)}}/.freeze
36
36
 
37
- attr_reader :resource, :network
38
-
39
37
  def initialize(
40
38
  items, resource: nil, network: nil, redact_confidentials: true)
41
- super(items)
42
- @resource = resource&.with_indifferent_access
43
- @network = network
39
+ super(items, resource: resource, network: network)
44
40
  @redact_confidentials = redact_confidentials
45
41
  end
46
42
 
@@ -4,10 +4,10 @@ require 'active_support/inflector'
4
4
  require_relative 'expand_condition'
5
5
  require_relative 'filters/merge_request_date_conditions_filter'
6
6
  require_relative 'filters/votes_conditions_filter'
7
- require_relative 'filters/forbidden_labels_conditions_filter'
8
7
  require_relative 'filters/no_additional_labels_conditions_filter'
9
8
  require_relative 'filters/author_member_conditions_filter'
10
9
  require_relative 'filters/assignee_member_conditions_filter'
10
+ require_relative 'filters/discussions_conditions_filter'
11
11
  require_relative 'filters/ruby_conditions_filter'
12
12
  require_relative 'limiters/date_field_limiter'
13
13
  require_relative 'action'
@@ -20,7 +20,10 @@ require_relative 'api_query_builders/single_query_param_builder'
20
20
  require_relative 'api_query_builders/multi_query_param_builder'
21
21
  require_relative 'url_builders/url_builder'
22
22
  require_relative 'network'
23
+ require_relative 'graphql_network'
23
24
  require_relative 'network_adapters/httparty_adapter'
25
+ require_relative 'network_adapters/graphql_adapter'
26
+ require_relative 'graphql_queries/query_builder'
24
27
  require_relative 'ui'
25
28
 
26
29
  module Gitlab
@@ -28,7 +31,10 @@ module Gitlab
28
31
  class Engine
29
32
  attr_reader :per_page, :policies, :options
30
33
 
31
- def initialize(policies:, options:, network_adapter_class: Gitlab::Triage::NetworkAdapters::HttpartyAdapter)
34
+ DEFAULT_NETWORK_ADAPTER = Gitlab::Triage::NetworkAdapters::HttpartyAdapter
35
+ DEFAULT_GRAPHQL_ADAPTER = Gitlab::Triage::NetworkAdapters::GraphqlAdapter
36
+
37
+ def initialize(policies:, options:, network_adapter_class: DEFAULT_NETWORK_ADAPTER, graphql_network_adapter_class: DEFAULT_GRAPHQL_ADAPTER)
32
38
  options.host_url = policies.delete(:host_url) { options.host_url }
33
39
  options.api_version = policies.delete(:api_version) { 'v4' }
34
40
  options.dry_run = ENV['TEST'] == 'true' if options.dry_run.nil?
@@ -37,6 +43,7 @@ module Gitlab
37
43
  @policies = policies
38
44
  @options = options
39
45
  @network_adapter_class = network_adapter_class
46
+ @graphql_network_adapter_class = graphql_network_adapter_class
40
47
 
41
48
  assert_all!
42
49
  assert_project_id!
@@ -63,6 +70,10 @@ module Gitlab
63
70
  @network ||= Network.new(network_adapter)
64
71
  end
65
72
 
73
+ def graphql_network
74
+ @graphql_network ||= GraphqlNetwork.new(graphql_network_adapter)
75
+ end
76
+
66
77
  private
67
78
 
68
79
  def assert_project_id!
@@ -95,6 +106,10 @@ module Gitlab
95
106
  @network_adapter ||= @network_adapter_class.new(options)
96
107
  end
97
108
 
109
+ def graphql_network_adapter
110
+ @graphql_network_adapter ||= @graphql_network_adapter_class.new(options)
111
+ end
112
+
98
113
  def rule_conditions(rule)
99
114
  rule.fetch(:conditions) { {} }
100
115
  end
@@ -159,8 +174,13 @@ module Gitlab
159
174
  # retrieving the resources for every rule is inefficient
160
175
  # however, previous rules may affect those upcoming
161
176
  resources = network.query_api(build_get_url(resource_type, conditions))
177
+ iids = resources.pluck('iid').map(&:to_s)
178
+
179
+ graphql_query = build_graphql_query(resource_type, conditions)
180
+ graphql_resources = graphql_network.query(graphql_query, source: options.source_id, iids: iids) if graphql_query.present?
162
181
  # In some filters/actions we want to know which resource type it is
163
182
  attach_resource_type(resources, resource_type)
183
+ decorate_resources_with_graphql_data(resources, graphql_resources)
164
184
 
165
185
  puts "\n\n* Found #{resources.count} resources..."
166
186
  print "* Filtering resources..."
@@ -181,6 +201,13 @@ module Gitlab
181
201
  resources.each { |resource| resource[:type] ||= resource_type }
182
202
  end
183
203
 
204
+ def decorate_resources_with_graphql_data(resources, graphql_resources)
205
+ return if graphql_resources.nil?
206
+
207
+ graphql_resources_by_id = graphql_resources.to_h { |resource| [resource[:id], resource] }
208
+ resources.each { |resource| resource.merge!(graphql_resources_by_id[resource[:id]].to_h) }
209
+ end
210
+
184
211
  def process_action(policy)
185
212
  Action.process(
186
213
  policy: policy,
@@ -202,10 +229,6 @@ module Gitlab
202
229
  results << Filters::VotesConditionsFilter.new(resource, conditions[:upvotes]).calculate
203
230
  end
204
231
 
205
- if conditions[:forbidden_labels]
206
- results << Filters::ForbiddenLabelsConditionsFilter.new(resource, conditions[:forbidden_labels]).calculate
207
- end
208
-
209
232
  if conditions[:no_additional_labels]
210
233
  results << Filters::NoAdditionalLabelsConditionsFilter.new(resource, conditions.fetch(:labels) { [] }).calculate
211
234
  end
@@ -218,6 +241,10 @@ module Gitlab
218
241
  results << Filters::AssigneeMemberConditionsFilter.new(resource, conditions[:assignee_member], network).calculate
219
242
  end
220
243
 
244
+ if conditions[:discussions]
245
+ results << Filters::DiscussionsConditionsFilter.new(resource, conditions[:discussions]).calculate
246
+ end
247
+
221
248
  if conditions[:ruby]
222
249
  results << Filters::RubyConditionsFilter.new(resource, conditions, network).calculate
223
250
  end
@@ -244,6 +271,11 @@ module Gitlab
244
271
 
245
272
  condition_builders = []
246
273
  condition_builders << APIQueryBuilders::MultiQueryParamBuilder.new('labels', conditions[:labels], ',') if conditions[:labels]
274
+
275
+ if conditions[:forbidden_labels]
276
+ condition_builders << APIQueryBuilders::MultiQueryParamBuilder.new('not[labels]', conditions[:forbidden_labels], ',')
277
+ end
278
+
247
279
  condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('state', conditions[:state]) if conditions[:state]
248
280
  condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('milestone', Array(conditions[:milestone])[0]) if conditions[:milestone]
249
281
  condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('source_branch', conditions[:source_branch]) if conditions[:source_branch]
@@ -253,6 +285,10 @@ module Gitlab
253
285
  condition_builders << APIQueryBuilders::DateQueryParamBuilder.new(conditions.delete(:date))
254
286
  end
255
287
 
288
+ if conditions[:weight] && resource_type.to_sym == :issues
289
+ condition_builders << APIQueryBuilders::SingleQueryParamBuilder.new('weight', conditions[:weight])
290
+ end
291
+
256
292
  condition_builders.each do |condition_builder|
257
293
  params[condition_builder.param_name] = condition_builder.param_content
258
294
  end
@@ -266,6 +302,11 @@ module Gitlab
266
302
  params: params
267
303
  ).build
268
304
  end
305
+
306
+ def build_graphql_query(resource_type, conditions)
307
+ Gitlab::Triage::GraphqlQueries::QueryBuilder
308
+ .new(options.source, resource_type, conditions)
309
+ end
269
310
  end
270
311
  end
271
312
  end
@@ -0,0 +1,60 @@
1
+ require_relative 'base_conditions_filter'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module Filters
6
+ class DiscussionsConditionsFilter < BaseConditionsFilter
7
+ ATTRIBUTES = %w[notes threads].freeze
8
+ CONDITIONS = %w[greater_than less_than].freeze
9
+
10
+ def self.filter_parameters
11
+ [
12
+ {
13
+ name: :attribute,
14
+ type: String,
15
+ values: ATTRIBUTES
16
+ },
17
+ {
18
+ name: :condition,
19
+ type: String,
20
+ values: CONDITIONS
21
+ },
22
+ {
23
+ name: :threshold,
24
+ type: Numeric
25
+ }
26
+ ]
27
+ end
28
+
29
+ def initialize_variables(condition)
30
+ @attribute = condition[:attribute].to_sym
31
+ @condition = condition[:condition].to_sym
32
+ @threshold = condition[:threshold]
33
+ end
34
+
35
+ def resource_value
36
+ if @attribute == :notes
37
+ @resource[:user_notes_count]
38
+ else
39
+ @resource.dig(:discussions, :nodes)&.count do |node|
40
+ !node&.dig(:notes, :nodes, 0, :system)
41
+ end
42
+ end
43
+ end
44
+
45
+ def condition_value
46
+ @threshold
47
+ end
48
+
49
+ def calculate
50
+ case @condition
51
+ when :greater_than
52
+ resource_value.to_i > condition_value
53
+ when :less_than
54
+ resource_value.to_i < condition_value
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,54 @@
1
+ require 'active_support/all'
2
+ require 'net/protocol'
3
+ require 'globalid'
4
+
5
+ require_relative 'retryable'
6
+ require_relative 'ui'
7
+ require_relative 'errors'
8
+
9
+ module Gitlab
10
+ module Triage
11
+ class GraphqlNetwork
12
+ attr_reader :options, :adapter
13
+
14
+ def initialize(adapter)
15
+ @adapter = adapter
16
+ @options = adapter.options
17
+ end
18
+
19
+ def query(graphql_query, variables = {})
20
+ return if graphql_query.blank?
21
+
22
+ response = {}
23
+ resources = []
24
+
25
+ parsed_graphql_query = adapter.parse(graphql_query.query)
26
+
27
+ begin
28
+ print '.'
29
+
30
+ response = adapter.query(
31
+ parsed_graphql_query,
32
+ resource_path: graphql_query.resource_path,
33
+ variables: variables.merge(after: response.delete(:end_cursor))
34
+ )
35
+
36
+ resources.concat(Array.wrap(response.delete(:results)))
37
+ end while response.delete(:more_pages)
38
+
39
+ resources
40
+ .map { |resource| resource.deep_transform_keys(&:underscore) }
41
+ .map(&:with_indifferent_access)
42
+ .map { |resource| resource.merge(id: extract_id_from_global_id(resource[:id])) }
43
+ end
44
+
45
+ private
46
+
47
+ def extract_id_from_global_id(global_id)
48
+ return if global_id.blank?
49
+
50
+ GlobalID.parse(global_id).model_id.to_i
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,47 @@
1
+ require_relative 'user_notes_query'
2
+ require_relative 'threads_query'
3
+
4
+ module Gitlab
5
+ module Triage
6
+ module GraphqlQueries
7
+ class QueryBuilder
8
+ def initialize(source_type, resource_type, conditions)
9
+ @source_type = source_type.to_s.singularize
10
+ @resource_type = resource_type
11
+ @conditions = conditions
12
+ end
13
+
14
+ def resource_path
15
+ [source_type, resource_type]
16
+ end
17
+
18
+ def query
19
+ return if query_template.nil?
20
+
21
+ format(query_template, source_type: source_type, resource_type: resource_type.to_s.camelize(:lower), group_query: group_query)
22
+ end
23
+
24
+ delegate :present?, to: :query_template
25
+
26
+ private
27
+
28
+ attr_reader :source_type, :resource_type, :conditions
29
+
30
+ def query_template
31
+ case conditions.dig(:discussions, :attribute).to_s
32
+ when 'notes'
33
+ UserNotesQuery
34
+ when 'threads'
35
+ ThreadsQuery
36
+ end
37
+ end
38
+
39
+ def group_query
40
+ return if source_type != 'group'
41
+
42
+ ', includeSubgroups: true'
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,31 @@
1
+ module Gitlab
2
+ module Triage
3
+ module GraphqlQueries
4
+ ThreadsQuery = <<-GRAPHQL.freeze # rubocop:disable Naming/ConstantName
5
+ query($source: ID!, $after: String, $iids: [String!]) {
6
+ %{source_type}(fullPath: $source) {
7
+ id
8
+ %{resource_type}(after: $after, iids: $iids%{group_query}) {
9
+ pageInfo {
10
+ hasNextPage
11
+ endCursor
12
+ }
13
+ nodes {
14
+ id
15
+ discussions {
16
+ nodes {
17
+ notes {
18
+ nodes {
19
+ system
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ GRAPHQL
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ module Gitlab
2
+ module Triage
3
+ module GraphqlQueries
4
+ UserNotesQuery = <<-GRAPHQL.freeze # rubocop:disable Naming/ConstantName
5
+ query($source: ID!, $after: String, $iids: [String!]) {
6
+ %{source_type}(fullPath: $source) {
7
+ id
8
+ %{resource_type}(after: $after, iids: $iids%{group_query}) {
9
+ pageInfo {
10
+ hasNextPage
11
+ endCursor
12
+ }
13
+ nodes {
14
+ id
15
+ userNotesCount
16
+ }
17
+ }
18
+ }
19
+ }
20
+ GRAPHQL
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,69 @@
1
+ require 'graphql/client'
2
+ require 'graphql/client/http'
3
+
4
+ require_relative 'base_adapter'
5
+ require_relative '../ui'
6
+ require_relative '../errors'
7
+
8
+ module Gitlab
9
+ module Triage
10
+ module NetworkAdapters
11
+ class GraphqlAdapter < BaseAdapter
12
+ Client = GraphQL::Client
13
+
14
+ def query(graphql_query, resource_path: [], variables: {})
15
+ response = client.query(graphql_query, variables: variables, context: { token: options.token })
16
+
17
+ raise_on_error!(response)
18
+
19
+ parsed_response = parse_response(response, resource_path)
20
+
21
+ return { results: {} } if parsed_response.nil?
22
+ return { results: parsed_response.map(&:to_h) } if parsed_response.is_a?(Client::List)
23
+ return { results: parsed_response.to_h } unless parsed_response.nodes?
24
+
25
+ {
26
+ more_pages: parsed_response.page_info.has_next_page,
27
+ end_cursor: parsed_response.page_info.end_cursor,
28
+ results: parsed_response.nodes.map(&:to_h)
29
+ }
30
+ end
31
+
32
+ delegate :parse, to: :client
33
+
34
+ private
35
+
36
+ def parse_response(response, resource_path)
37
+ resource_path.reduce(response.data) { |data, resource| data&.send(resource) }
38
+ end
39
+
40
+ def raise_on_error!(response)
41
+ return if response.errors.blank?
42
+
43
+ puts Gitlab::Triage::UI.debug response.inspect if options.debug
44
+
45
+ raise "There was an error: #{response.errors.messages.to_json}"
46
+ end
47
+
48
+ def http_client
49
+ Client::HTTP.new("#{options.host_url}/api/graphql") do
50
+ def headers(context) # rubocop:disable Lint/NestedMethodDefinition
51
+ {
52
+ 'Content-type' => 'application/json',
53
+ 'PRIVATE-TOKEN' => context[:token]
54
+ }
55
+ end
56
+ end
57
+ end
58
+
59
+ def schema
60
+ @schema ||= Client.load_schema(http_client)
61
+ end
62
+
63
+ def client
64
+ @client ||= Client.new(schema: schema, execute: http_client).tap { |client| client.allow_dynamic_queries = true }
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,20 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
3
+ require 'delegate'
4
4
 
5
5
  module Gitlab
6
6
  module Triage
7
7
  module PoliciesResources
8
- class RuleResources
9
- include Enumerable
10
- extend Forwardable
11
-
12
- def initialize(new_resources)
13
- @resources = new_resources
14
- end
15
-
16
- def_delegator :@resources, :each
17
- end
8
+ RuleResources = Class.new(SimpleDelegator)
18
9
  end
19
10
  end
20
11
  end
@@ -1,20 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
3
+ require 'delegate'
4
4
 
5
5
  module Gitlab
6
6
  module Triage
7
7
  module PoliciesResources
8
- class SummaryResources
9
- include Enumerable
10
- extend Forwardable
11
-
12
- def initialize(new_rule_to_resources)
13
- @rule_to_resources = new_rule_to_resources
14
- end
15
-
16
- def_delegator :@rule_to_resources, :each
17
- end
8
+ SummaryResources = Class.new(SimpleDelegator)
18
9
  end
19
10
  end
20
11
  end
@@ -58,11 +58,15 @@ module Gitlab
58
58
  build_url(params: params)
59
59
  end
60
60
 
61
+ def resource_id
62
+ resource[:iid]
63
+ end
64
+
61
65
  def resource_url(params: {}, sub_resource_type: nil)
62
66
  build_url(
63
67
  params: params,
64
68
  options: {
65
- resource_id: resource[:iid],
69
+ resource_id: resource_id,
66
70
  sub_resource_type: sub_resource_type
67
71
  }
68
72
  )
@@ -8,6 +8,8 @@ module Gitlab
8
8
  module Triage
9
9
  module Resource
10
10
  class Label < Base
11
+ LabelDoesntExistError = Class.new(StandardError)
12
+
11
13
  FIELDS = %i[
12
14
  id
13
15
  project_id
@@ -35,6 +37,19 @@ module Gitlab
35
37
  Time.parse(value) if value
36
38
  end
37
39
  end
40
+
41
+ def exist?
42
+ label = network.query_api_cached(resource_url).first
43
+ return false unless label
44
+
45
+ label[:name] == name
46
+ end
47
+
48
+ private
49
+
50
+ def resource_id
51
+ name
52
+ end
38
53
  end
39
54
  end
40
55
  end
@@ -17,7 +17,7 @@ module Gitlab
17
17
 
18
18
  def build
19
19
  url = base_url
20
- url << "/#{@resource_id}" if @resource_id
20
+ url << "/#{percent_encode(@resource_id.to_s)}" if @resource_id
21
21
  url << "/#{@sub_resource_type}" if @sub_resource_type
22
22
  url << params_string if @params
23
23
  url
@@ -31,16 +31,20 @@ module Gitlab
31
31
 
32
32
  def base_url
33
33
  url = host_with_api_url
34
- url << "/#{@source}/#{CGI.escape(@source_id.to_s)}" unless @all
34
+ url << "/#{@source}/#{percent_encode(@source_id.to_s)}" unless @all
35
35
  url << "/#{@resource_type}" if @resource_type
36
36
  url
37
37
  end
38
38
 
39
39
  def params_string
40
40
  "?" << @params.map do |k, v|
41
- "#{k}=#{v}"
41
+ "#{percent_encode(k.to_s)}=#{percent_encode(v.to_s)}"
42
42
  end.join("&")
43
43
  end
44
+
45
+ def percent_encode(str)
46
+ CGI.escape(str).gsub('+', '%20')
47
+ end
44
48
  end
45
49
  end
46
50
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
- VERSION = '1.10.1'
5
+ VERSION = '1.14.1'
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 $API_TOKEN --source projects --source-id $CI_PROJECT_PATH
11
+ - gitlab-triage --dry-run --token $GITLAB_API_TOKEN --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 $API_TOKEN --source projects --source-id $CI_PROJECT_PATH
20
+ - gitlab-triage --token $GITLAB_API_TOKEN --source projects --source-id $CI_PROJECT_PATH
21
21
  only:
22
22
  - schedules
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.10.1
4
+ version: 1.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-08 00:00:00.000000000 Z
11
+ date: 2020-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '5.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: globalid
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: graphql-client
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.16'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.16'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: httparty
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -158,17 +186,22 @@ files:
158
186
  - lib/gitlab/triage/filters/assignee_member_conditions_filter.rb
159
187
  - lib/gitlab/triage/filters/author_member_conditions_filter.rb
160
188
  - lib/gitlab/triage/filters/base_conditions_filter.rb
161
- - lib/gitlab/triage/filters/forbidden_labels_conditions_filter.rb
189
+ - lib/gitlab/triage/filters/discussions_conditions_filter.rb
162
190
  - lib/gitlab/triage/filters/member_conditions_filter.rb
163
191
  - lib/gitlab/triage/filters/merge_request_date_conditions_filter.rb
164
192
  - lib/gitlab/triage/filters/name_conditions_filter.rb
165
193
  - lib/gitlab/triage/filters/no_additional_labels_conditions_filter.rb
166
194
  - lib/gitlab/triage/filters/ruby_conditions_filter.rb
167
195
  - lib/gitlab/triage/filters/votes_conditions_filter.rb
196
+ - lib/gitlab/triage/graphql_network.rb
197
+ - lib/gitlab/triage/graphql_queries/query_builder.rb
198
+ - lib/gitlab/triage/graphql_queries/threads_query.rb
199
+ - lib/gitlab/triage/graphql_queries/user_notes_query.rb
168
200
  - lib/gitlab/triage/limiters/base_limiter.rb
169
201
  - lib/gitlab/triage/limiters/date_field_limiter.rb
170
202
  - lib/gitlab/triage/network.rb
171
203
  - lib/gitlab/triage/network_adapters/base_adapter.rb
204
+ - lib/gitlab/triage/network_adapters/graphql_adapter.rb
172
205
  - lib/gitlab/triage/network_adapters/httparty_adapter.rb
173
206
  - lib/gitlab/triage/network_adapters/test_adapter.rb
174
207
  - lib/gitlab/triage/option_parser.rb
@@ -214,7 +247,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
214
247
  - !ruby/object:Gem::Version
215
248
  version: '0'
216
249
  requirements: []
217
- rubygems_version: 3.1.2
250
+ rubygems_version: 3.1.4
218
251
  signing_key:
219
252
  specification_version: 4
220
253
  summary: GitLab triage automation project.
@@ -1,32 +0,0 @@
1
- require_relative 'base_conditions_filter'
2
-
3
- module Gitlab
4
- module Triage
5
- module Filters
6
- class ForbiddenLabelsConditionsFilter < BaseConditionsFilter
7
- def validate_condition(condition)
8
- raise ArgumentError, 'condition must be an array containing forbidden label values' unless condition.is_a?(Array)
9
- end
10
-
11
- def initialize_variables(forbidden_labels)
12
- @attribute = :labels
13
- @forbidden_labels = forbidden_labels
14
- end
15
-
16
- def resource_value
17
- @resource[@attribute]
18
- end
19
-
20
- def calculate
21
- label_intersection.empty?
22
- end
23
-
24
- private
25
-
26
- def label_intersection
27
- resource_value & @forbidden_labels
28
- end
29
- end
30
- end
31
- end
32
- end