gitlab-triage 0.0.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.
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