gitlab-triage 1.10.1 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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