gitlab-triage 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +19 -0
- data/.gitignore +15 -0
- data/.gitlab-ci.yml +101 -0
- data/.gitlab/issue_templates/Policy.md +32 -0
- data/.rubocop.yml +6 -0
- data/Gemfile +4 -0
- data/Guardfile +70 -0
- data/README.md +13 -0
- data/Rakefile +6 -0
- data/bin/gitlab-triage +63 -0
- data/gitlab-triage.gemspec +29 -0
- data/lib/gitlab/triage.rb +6 -0
- data/lib/gitlab/triage/command_builders/base_command_builder.rb +32 -0
- data/lib/gitlab/triage/command_builders/cc_command_builder.rb +19 -0
- data/lib/gitlab/triage/command_builders/comment_command_builder.rb +23 -0
- data/lib/gitlab/triage/command_builders/label_command_builder.rb +19 -0
- data/lib/gitlab/triage/command_builders/status_command_builder.rb +23 -0
- data/lib/gitlab/triage/engine.rb +146 -0
- data/lib/gitlab/triage/filter_builders/base_filter_builder.rb +18 -0
- data/lib/gitlab/triage/filter_builders/multi_filter_builder.rb +20 -0
- data/lib/gitlab/triage/filter_builders/single_filter_builder.rb +13 -0
- data/lib/gitlab/triage/limiters/base_conditions_limiter.rb +65 -0
- data/lib/gitlab/triage/limiters/date_conditions_limiter.rb +63 -0
- data/lib/gitlab/triage/limiters/name_conditions_limiter.rb +30 -0
- data/lib/gitlab/triage/limiters/votes_conditions_limiter.rb +54 -0
- data/lib/gitlab/triage/network.rb +37 -0
- data/lib/gitlab/triage/network_adapters/httparty_adapter.rb +36 -0
- data/lib/gitlab/triage/network_adapters/test_adapter.rb +31 -0
- data/lib/gitlab/triage/retryable.rb +31 -0
- data/lib/gitlab/triage/ui.rb +15 -0
- data/lib/gitlab/triage/version.rb +5 -0
- data/policies.yml +232 -0
- 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,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
|