gitlab-triage 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +19 -0
  3. data/.gitignore +15 -0
  4. data/.gitlab-ci.yml +101 -0
  5. data/.gitlab/issue_templates/Policy.md +32 -0
  6. data/.rubocop.yml +6 -0
  7. data/Gemfile +4 -0
  8. data/Guardfile +70 -0
  9. data/README.md +13 -0
  10. data/Rakefile +6 -0
  11. data/bin/gitlab-triage +63 -0
  12. data/gitlab-triage.gemspec +29 -0
  13. data/lib/gitlab/triage.rb +6 -0
  14. data/lib/gitlab/triage/command_builders/base_command_builder.rb +32 -0
  15. data/lib/gitlab/triage/command_builders/cc_command_builder.rb +19 -0
  16. data/lib/gitlab/triage/command_builders/comment_command_builder.rb +23 -0
  17. data/lib/gitlab/triage/command_builders/label_command_builder.rb +19 -0
  18. data/lib/gitlab/triage/command_builders/status_command_builder.rb +23 -0
  19. data/lib/gitlab/triage/engine.rb +146 -0
  20. data/lib/gitlab/triage/filter_builders/base_filter_builder.rb +18 -0
  21. data/lib/gitlab/triage/filter_builders/multi_filter_builder.rb +20 -0
  22. data/lib/gitlab/triage/filter_builders/single_filter_builder.rb +13 -0
  23. data/lib/gitlab/triage/limiters/base_conditions_limiter.rb +65 -0
  24. data/lib/gitlab/triage/limiters/date_conditions_limiter.rb +63 -0
  25. data/lib/gitlab/triage/limiters/name_conditions_limiter.rb +30 -0
  26. data/lib/gitlab/triage/limiters/votes_conditions_limiter.rb +54 -0
  27. data/lib/gitlab/triage/network.rb +37 -0
  28. data/lib/gitlab/triage/network_adapters/httparty_adapter.rb +36 -0
  29. data/lib/gitlab/triage/network_adapters/test_adapter.rb +31 -0
  30. data/lib/gitlab/triage/retryable.rb +31 -0
  31. data/lib/gitlab/triage/ui.rb +15 -0
  32. data/lib/gitlab/triage/version.rb +5 -0
  33. data/policies.yml +232 -0
  34. metadata +160 -0
@@ -0,0 +1,19 @@
1
+ require_relative 'base_command_builder'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module CommandBuilders
6
+ class LabelCommandBuilder < BaseCommandBuilder
7
+ private
8
+
9
+ def slash_command_string
10
+ "/label"
11
+ end
12
+
13
+ def format_item(item)
14
+ "~\"#{item}\""
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ require_relative 'base_command_builder'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module CommandBuilders
6
+ class StatusCommandBuilder < BaseCommandBuilder
7
+ private
8
+
9
+ def separator
10
+ ''
11
+ end
12
+
13
+ def slash_command_string
14
+ "/"
15
+ end
16
+
17
+ def format_item(item)
18
+ item
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,146 @@
1
+ require 'active_support/all'
2
+
3
+ require_relative 'limiters/date_conditions_limiter'
4
+ require_relative 'limiters/votes_conditions_limiter'
5
+ require_relative 'command_builders/comment_command_builder'
6
+ require_relative 'command_builders/label_command_builder'
7
+ require_relative 'command_builders/cc_command_builder'
8
+ require_relative 'command_builders/status_command_builder'
9
+ require_relative 'filter_builders/single_filter_builder'
10
+ require_relative 'filter_builders/multi_filter_builder'
11
+ require_relative 'network'
12
+ require_relative 'network_adapters/httparty_adapter'
13
+ require_relative 'ui'
14
+
15
+ module Gitlab
16
+ module Triage
17
+ class Engine
18
+ attr_reader :host_url, :api_version, :per_page, :policies, :options
19
+
20
+ def initialize(policies:, options:, network_adapter: Gitlab::Triage::NetworkAdapters::HttpartyAdapter.new)
21
+ @host_url = policies.delete(:host_url) { 'https://gitlab.com' }
22
+ @api_version = policies.delete(:api_version) { 'v4' }
23
+ @per_page = policies.delete(:per_page) { 100 }
24
+ @policies = policies
25
+ @options = options
26
+ @network_adapter = network_adapter
27
+
28
+ options.dry_run = true if ENV['TEST'] == 'true'
29
+
30
+ assert_project_id!
31
+ assert_token!
32
+ end
33
+
34
+ def perform
35
+ puts "Performing a dry run.\n\n" if options.dry_run
36
+
37
+ resource_rules.each do |type, resource|
38
+ puts Gitlab::Triage::UI.header("Processing rules for #{type}")
39
+ puts
40
+ resource[:rules].each do |rule|
41
+ puts Gitlab::Triage::UI.header("Processing rule: #{rule[:name]}", char: '~')
42
+ puts
43
+ process_rule(type, rule)
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def assert_project_id!
51
+ return if options.project_id
52
+
53
+ raise ArgumentError, 'A project_id is needed (pass it with the `--project-id` option)!'
54
+ end
55
+
56
+ def assert_token!
57
+ return if options.token
58
+
59
+ raise ArgumentError, 'A token is needed (pass it with the `--token` option)!'
60
+ end
61
+
62
+ def resource_rules
63
+ @resource_rules ||= policies.delete(:resource_rules) { {} }
64
+ end
65
+
66
+ def network
67
+ @network ||= Network.new(@network_adapter)
68
+ end
69
+
70
+ def rule_conditions(rule)
71
+ rule.fetch(:conditions) { {} }
72
+ end
73
+
74
+ def rule_actions(rule)
75
+ rule.fetch(:actions) { {} }
76
+ end
77
+
78
+ def process_rule(resource_type, rule)
79
+ # retrieving the resources for every rule is inefficient
80
+ # however, previous rules may affect those upcoming
81
+ resources = network.query_api(options.token, build_get_url(resource_type, rule_conditions(rule)))
82
+ puts "\n\nFound #{resources.count} resources..."
83
+ puts "Limiting resources..."
84
+ resources = limit_resources(resources, rule_conditions(rule))
85
+ puts "Total resource after limiting: #{resources.count} resources"
86
+ process_resources(resource_type, resources, rule)
87
+ end
88
+
89
+ def limit_resources(resources, conditions)
90
+ resources.select do |resource|
91
+ results = []
92
+ results << Limiters::DateConditionsLimiter.new(resource, conditions[:date]).calculate if conditions[:date]
93
+ results << Limiters::VotesConditionsLimiter.new(resource, conditions[:upvotes]).calculate if conditions[:upvotes]
94
+ !results.uniq.include?(false)
95
+ end
96
+ end
97
+
98
+ def process_resources(resource_type, resources, rule)
99
+ comment = build_comment(rule_actions(rule))
100
+
101
+ if options.dry_run && resources.any?
102
+ puts "\nThe following comment would be posted for the rule '#{rule[:name]}':\n\n"
103
+ puts ">>>\n#{comment}\n>>>"
104
+ return
105
+ end
106
+
107
+ resources.each do |resource|
108
+ network.post_api(options.token, build_post_url(resource_type, resource), comment)
109
+ end
110
+ end
111
+
112
+ def build_comment(actions)
113
+ CommandBuilders::CommentCommandBuilder.new(
114
+ [
115
+ actions[:comment],
116
+ CommandBuilders::LabelCommandBuilder.new(actions[:labels]).build_command,
117
+ CommandBuilders::CcCommandBuilder.new(actions[:mention]).build_command,
118
+ CommandBuilders::StatusCommandBuilder.new(actions[:status]).build_command
119
+ ]
120
+ ).build_command
121
+ end
122
+
123
+ def build_get_url(resource_type, conditions)
124
+ # Example issues query with state and labels
125
+ # https://gitlab.com/api/v4/projects/test-triage%2Fissue-project/issues?state=open&labels=project%20label%20with%20spaces,group_label_no_spaces
126
+ query_base = ["#{host_url}/api/#{api_version}/projects/#{CGI.escape(options.project_id.to_s)}/#{resource_type}/?per_page=#{per_page}"]
127
+ query_base << FilterBuilders::MultiFilterBuilder.new('labels', conditions[:labels], ',').build_filter if conditions[:labels]
128
+ query_base << FilterBuilders::SingleFilterBuilder.new('state', conditions[:state]).build_filter if conditions[:state]
129
+ query_base << FilterBuilders::MultiFilterBuilder.new('milestone', conditions[:milestone], ',').build_filter if conditions[:milestone]
130
+
131
+ get_url = query_base.join
132
+ puts Gitlab::Triage::UI.debug "get_url: #{get_url}" if options.debug
133
+
134
+ get_url
135
+ end
136
+
137
+ def build_post_url(resource_type, resource)
138
+ # POST /projects/:id/issues/:issue_iid/notes
139
+ post_url = "#{host_url}/api/#{api_version}/projects/#{options.project_id}/#{resource_type}/#{resource['iid']}/notes/"
140
+ puts Gitlab::Triage::UI.debug "post_url: #{post_url}" if options.debug
141
+
142
+ post_url
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,18 @@
1
+ module Gitlab
2
+ module Triage
3
+ module FilterBuilders
4
+ class BaseFilterBuilder
5
+ attr_reader :filter_name, :filter_contents
6
+
7
+ def initialize(filter_name, filter_contents)
8
+ @filter_name = filter_name
9
+ @filter_contents = filter_contents
10
+ end
11
+
12
+ def build_filter
13
+ "&#{filter_name}=#{filter_content}"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'base_filter_builder'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module FilterBuilders
6
+ class MultiFilterBuilder < BaseFilterBuilder
7
+ attr_reader :separator
8
+
9
+ def initialize(filter_name, filter_contents, separator)
10
+ @separator = separator
11
+ super(filter_name, filter_contents)
12
+ end
13
+
14
+ def filter_content
15
+ filter_contents.join(separator)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'base_filter_builder'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module FilterBuilders
6
+ class SingleFilterBuilder < BaseFilterBuilder
7
+ def filter_content
8
+ filter_contents
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,65 @@
1
+ module Gitlab
2
+ module Triage
3
+ module Limiters
4
+ class BaseConditionsLimiter
5
+ def initialize(resource, condition)
6
+ @resource = resource
7
+ validate_condition(condition)
8
+ initialize_variables(condition)
9
+ end
10
+
11
+ def self.params_limiter_names(params = nil)
12
+ params ||= limiter_parameters
13
+
14
+ params.map do |param|
15
+ param[:name]
16
+ end
17
+ end
18
+
19
+ def self.all_params_limiter_names
20
+ params_limiter_names
21
+ end
22
+
23
+ def self.params_checking_condition_value
24
+ params_limiter_names params_check_for_field(:values)
25
+ end
26
+
27
+ def self.params_checking_condition_type
28
+ params_limiter_names params_check_for_field(:type)
29
+ end
30
+
31
+ def self.params_check_for_field(field)
32
+ limiter_parameters.select do |param|
33
+ param[field].present?
34
+ end
35
+ end
36
+
37
+ def validate_condition(condition)
38
+ validate_required_parameters(condition)
39
+ validate_parameter_types(condition)
40
+ validate_parameter_content(condition)
41
+ end
42
+
43
+ def validate_required_parameters(condition)
44
+ self.class.limiter_parameters.each do |param|
45
+ raise ArgumentError, "#{param[:name]} is a required parameter" unless condition[param[:name]]
46
+ end
47
+ end
48
+
49
+ def validate_parameter_types(condition)
50
+ self.class.limiter_parameters.each do |param|
51
+ raise ArgumentError, "#{param[:name]} must be of type #{param[:type]}" unless condition[param[:name]].is_a?(param[:type])
52
+ end
53
+ end
54
+
55
+ def validate_parameter_content(condition)
56
+ self.class.limiter_parameters.each do |param|
57
+ if param[:values]
58
+ raise ArgumentError, "#{param[:name]} must be of one of #{param[:values].join(',')}" unless param[:values].include?(condition[param[:name]])
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,63 @@
1
+ require 'active_support/all'
2
+
3
+ require_relative 'base_conditions_limiter'
4
+
5
+ module Gitlab
6
+ module Triage
7
+ module Limiters
8
+ class DateConditionsLimiter < BaseConditionsLimiter
9
+ ATTRIBUTES = %w[updated_at created_at].freeze
10
+ CONDITIONS = %w[older_than newer_than].freeze
11
+ INTERVAL_TYPES = %w[days weeks months years].freeze
12
+
13
+ def self.limiter_parameters
14
+ [
15
+ {
16
+ name: :attribute,
17
+ type: String,
18
+ values: ATTRIBUTES
19
+ },
20
+ {
21
+ name: :condition,
22
+ type: String,
23
+ values: CONDITIONS
24
+ },
25
+ {
26
+ name: :interval_type,
27
+ type: String,
28
+ values: INTERVAL_TYPES
29
+ },
30
+ {
31
+ name: :interval,
32
+ type: Numeric
33
+ }
34
+ ]
35
+ end
36
+
37
+ def initialize_variables(condition)
38
+ @attribute = condition[:attribute].to_sym
39
+ @condition = condition[:condition].to_sym
40
+ @interval_type = condition[:interval_type].to_sym
41
+ @interval = condition[:interval]
42
+ end
43
+
44
+ def resource_value
45
+ @resource[@attribute].to_date
46
+ end
47
+
48
+ def condition_value
49
+ @interval.public_send(@interval_type).ago.to_date # rubocop:disable GitlabSecurity/PublicSend
50
+ end
51
+
52
+ def calculate
53
+ case @condition
54
+ when :older_than
55
+ resource_value < condition_value
56
+ when :newer_than
57
+ resource_value > condition_value
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,30 @@
1
+ require_relative 'base_conditions_limiter'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module Limiters
6
+ class NameConditionsLimiter < BaseConditionsLimiter
7
+ def self.limiter_parameters
8
+ []
9
+ end
10
+
11
+ def initialize_variables(matching_name)
12
+ @attribute = :name
13
+ @matching_name = matching_name
14
+ end
15
+
16
+ def resource_value
17
+ @resource[@attribute]
18
+ end
19
+
20
+ def condition_value
21
+ @matching_name
22
+ end
23
+
24
+ def calculate
25
+ resource_value == condition_value
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,54 @@
1
+ require_relative 'base_conditions_limiter'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module Limiters
6
+ class VotesConditionsLimiter < BaseConditionsLimiter
7
+ ATTRIBUTES = %w[upvotes downvotes].freeze
8
+ CONDITIONS = %w[greater_than less_than].freeze
9
+
10
+ def self.limiter_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
+ @resource[@attribute]
37
+ end
38
+
39
+ def condition_value
40
+ @threshold
41
+ end
42
+
43
+ def calculate
44
+ case @condition
45
+ when :greater_than
46
+ resource_value > condition_value
47
+ when :less_than
48
+ resource_value < condition_value
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end