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.
- 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
|