gitlab-triage 0.14.1 → 0.15.0
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 +4 -4
- data/README.md +69 -24
- data/bin/gitlab-triage +2 -74
- data/lib/gitlab/triage/action/base.rb +3 -3
- data/lib/gitlab/triage/action/comment.rb +5 -7
- data/lib/gitlab/triage/action/summarize.rb +4 -5
- data/lib/gitlab/triage/command_builders/base_command_builder.rb +1 -1
- data/lib/gitlab/triage/command_builders/text_content_builder.rb +54 -16
- data/lib/gitlab/triage/engine.rb +27 -34
- data/lib/gitlab/triage/entity_builders/issue_builder.rb +27 -17
- data/lib/gitlab/triage/filters/member_conditions_filter.rb +6 -8
- data/lib/gitlab/triage/filters/ruby_conditions_filter.rb +5 -3
- data/lib/gitlab/triage/network.rb +20 -8
- data/lib/gitlab/triage/network_adapters/base_adapter.rb +1 -1
- data/lib/gitlab/triage/option_parser.rb +72 -0
- data/lib/gitlab/triage/options.rb +21 -0
- data/lib/gitlab/triage/policies/base_policy.rb +21 -2
- data/lib/gitlab/triage/policies/rule_policy.rb +7 -1
- data/lib/gitlab/triage/policies/summary_policy.rb +25 -2
- data/lib/gitlab/triage/resource/base.rb +49 -10
- data/lib/gitlab/triage/resource/context.rb +13 -8
- data/lib/gitlab/triage/resource/instance_version.rb +3 -4
- data/lib/gitlab/triage/resource/issue.rb +14 -0
- data/lib/gitlab/triage/resource/label.rb +41 -0
- data/lib/gitlab/triage/resource/label_event.rb +45 -0
- data/lib/gitlab/triage/resource/merge_request.rb +14 -0
- data/lib/gitlab/triage/resource/milestone.rb +5 -5
- data/lib/gitlab/triage/resource/shared/issuable.rb +54 -0
- data/lib/gitlab/triage/url_builders/url_builder.rb +3 -2
- data/lib/gitlab/triage/version.rb +1 -1
- data/support/.triage-policies.example.yml +4 -4
- metadata +10 -4
data/lib/gitlab/triage/engine.rb
CHANGED
@@ -22,18 +22,18 @@ require_relative 'ui'
|
|
22
22
|
module Gitlab
|
23
23
|
module Triage
|
24
24
|
class Engine
|
25
|
-
attr_reader :
|
25
|
+
attr_reader :per_page, :policies, :options
|
26
26
|
|
27
27
|
def initialize(policies:, options:, network_adapter_class: Gitlab::Triage::NetworkAdapters::HttpartyAdapter)
|
28
|
-
|
29
|
-
|
28
|
+
options.host_url = policies.delete(:host_url) { options.host_url }
|
29
|
+
options.api_version = policies.delete(:api_version) { 'v4' }
|
30
|
+
options.dry_run = ENV['TEST'] == 'true' if options.dry_run.nil?
|
31
|
+
|
30
32
|
@per_page = policies.delete(:per_page) { 100 }
|
31
33
|
@policies = policies
|
32
34
|
@options = options
|
33
35
|
@network_adapter_class = network_adapter_class
|
34
36
|
|
35
|
-
options.dry_run = true if ENV['TEST'] == 'true'
|
36
|
-
|
37
37
|
assert_project_id!
|
38
38
|
assert_token!
|
39
39
|
end
|
@@ -53,6 +53,10 @@ module Gitlab
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
+
def network
|
57
|
+
@network ||= Network.new(network_adapter)
|
58
|
+
end
|
59
|
+
|
56
60
|
private
|
57
61
|
|
58
62
|
def assert_project_id!
|
@@ -71,21 +75,6 @@ module Gitlab
|
|
71
75
|
@resource_rules ||= policies.delete(:resource_rules) { {} }
|
72
76
|
end
|
73
77
|
|
74
|
-
def network
|
75
|
-
@network ||= Network.new(network_adapter, options)
|
76
|
-
end
|
77
|
-
|
78
|
-
def net
|
79
|
-
@net ||= {
|
80
|
-
host_url: host_url,
|
81
|
-
api_version: api_version,
|
82
|
-
token: options.token,
|
83
|
-
source_id: options.project_id,
|
84
|
-
debug: options.debug,
|
85
|
-
network: network
|
86
|
-
}
|
87
|
-
end
|
88
|
-
|
89
78
|
def network_adapter
|
90
79
|
@network_adapter ||= @network_adapter_class.new(options)
|
91
80
|
end
|
@@ -110,7 +99,7 @@ module Gitlab
|
|
110
99
|
return if rules.blank?
|
111
100
|
|
112
101
|
rules.each do |rule|
|
113
|
-
process_action(Policies::RulePolicy.new(resource_type, rule, resources_for_rule(resource_type, rule),
|
102
|
+
process_action(Policies::RulePolicy.new(resource_type, rule, resources_for_rule(resource_type, rule), network))
|
114
103
|
end
|
115
104
|
end
|
116
105
|
|
@@ -122,7 +111,7 @@ module Gitlab
|
|
122
111
|
# { summary_rule => resources }
|
123
112
|
summary_parts = Hash[summary[:rules].zip(resources)]
|
124
113
|
|
125
|
-
process_action(Policies::SummaryPolicy.new(resource_type, summary, summary_parts,
|
114
|
+
process_action(Policies::SummaryPolicy.new(resource_type, summary, summary_parts, network))
|
126
115
|
end
|
127
116
|
|
128
117
|
def resources_for_rules(resource_type, rules)
|
@@ -136,7 +125,10 @@ module Gitlab
|
|
136
125
|
ExpandCondition.perform(rule_conditions(rule)) do |conditions|
|
137
126
|
# retrieving the resources for every rule is inefficient
|
138
127
|
# however, previous rules may affect those upcoming
|
139
|
-
resources = network.query_api(
|
128
|
+
resources = network.query_api(build_get_url(resource_type, conditions))
|
129
|
+
# In some filters/actions we want to know which resource type it is
|
130
|
+
attach_resource_type(resources, resource_type)
|
131
|
+
|
140
132
|
puts "\n\n* Found #{resources.count} resources..."
|
141
133
|
print "* Filtering resources..."
|
142
134
|
resources = filter_resources(resources, conditions)
|
@@ -150,10 +142,16 @@ module Gitlab
|
|
150
142
|
resources
|
151
143
|
end
|
152
144
|
|
145
|
+
# We don't have to do this once the response will contain the type
|
146
|
+
# of the resource. For now let's just attach it.
|
147
|
+
def attach_resource_type(resources, resource_type)
|
148
|
+
resources.each { |resource| resource[:type] ||= resource_type }
|
149
|
+
end
|
150
|
+
|
153
151
|
def process_action(policy)
|
154
152
|
Action.process(
|
155
153
|
policy: policy,
|
156
|
-
|
154
|
+
network: network,
|
157
155
|
dry: options.dry_run)
|
158
156
|
puts
|
159
157
|
end
|
@@ -166,9 +164,9 @@ module Gitlab
|
|
166
164
|
results << Filters::VotesConditionsFilter.new(resource, conditions[:upvotes]).calculate if conditions[:upvotes]
|
167
165
|
results << Filters::ForbiddenLabelsConditionsFilter.new(resource, conditions[:forbidden_labels]).calculate if conditions[:forbidden_labels]
|
168
166
|
results << Filters::NoAdditionalLabelsConditionsFilter.new(resource, conditions.fetch(:labels) { [] }).calculate if conditions[:no_additional_labels]
|
169
|
-
results << Filters::AuthorMemberConditionsFilter.new(resource, conditions[:author_member],
|
170
|
-
results << Filters::AssigneeMemberConditionsFilter.new(resource, conditions[:assignee_member],
|
171
|
-
results << Filters::RubyConditionsFilter.new(resource, conditions,
|
167
|
+
results << Filters::AuthorMemberConditionsFilter.new(resource, conditions[:author_member], network).calculate if conditions[:author_member]
|
168
|
+
results << Filters::AssigneeMemberConditionsFilter.new(resource, conditions[:assignee_member], network).calculate if conditions[:assignee_member]
|
169
|
+
results << Filters::RubyConditionsFilter.new(resource, conditions, network).calculate if conditions[:ruby]
|
172
170
|
|
173
171
|
results.all?
|
174
172
|
end
|
@@ -198,17 +196,12 @@ module Gitlab
|
|
198
196
|
params[condition_builder.param_name] = condition_builder.param_content
|
199
197
|
end
|
200
198
|
|
201
|
-
|
202
|
-
|
203
|
-
api_version: api_version,
|
199
|
+
UrlBuilders::UrlBuilder.new(
|
200
|
+
network_options: options,
|
204
201
|
source_id: options.project_id,
|
205
202
|
resource_type: resource_type,
|
206
203
|
params: params
|
207
204
|
).build
|
208
|
-
|
209
|
-
puts Gitlab::Triage::UI.debug "get_url: #{get_url}" if options.debug
|
210
|
-
|
211
|
-
get_url
|
212
205
|
end
|
213
206
|
end
|
214
207
|
end
|
@@ -6,23 +6,25 @@ module Gitlab
|
|
6
6
|
module Triage
|
7
7
|
module EntityBuilders
|
8
8
|
class IssueBuilder
|
9
|
-
attr_reader :title
|
10
9
|
attr_writer :description, :items
|
11
10
|
|
12
|
-
def initialize(action
|
13
|
-
@
|
11
|
+
def initialize(type:, action:, resources:, network:)
|
12
|
+
@type = type
|
14
13
|
@item_template = action[:item]
|
14
|
+
@title_template = action[:title]
|
15
15
|
@summary_template = action[:summary]
|
16
|
+
@redact_confidentials =
|
17
|
+
action[:redact_confidential_resources] != false
|
16
18
|
@resources = resources
|
17
|
-
@
|
19
|
+
@network = network
|
18
20
|
end
|
19
21
|
|
20
|
-
def
|
21
|
-
|
22
|
+
def title
|
23
|
+
@title ||= build_text(title_resource, @title_template)
|
24
|
+
end
|
22
25
|
|
23
|
-
|
24
|
-
|
25
|
-
.build_command
|
26
|
+
def description
|
27
|
+
@description ||= build_text(description_resource, @summary_template)
|
26
28
|
end
|
27
29
|
|
28
30
|
def valid?
|
@@ -31,22 +33,30 @@ module Gitlab
|
|
31
33
|
|
32
34
|
private
|
33
35
|
|
34
|
-
def
|
35
|
-
@
|
36
|
-
'items' => items,
|
37
|
-
'title' => title
|
38
|
-
}
|
36
|
+
def title_resource
|
37
|
+
{ type: @type }
|
39
38
|
end
|
40
39
|
|
41
|
-
def
|
42
|
-
|
40
|
+
def description_resource
|
41
|
+
title_resource.merge(title: title, items: items)
|
42
|
+
end
|
43
43
|
|
44
|
+
def items
|
44
45
|
@items ||= @resources.map(&method(:build_item)).join("\n")
|
45
46
|
end
|
46
47
|
|
47
48
|
def build_item(resource)
|
49
|
+
build_text(resource, @item_template)
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_text(resource, template)
|
53
|
+
return '' unless template
|
54
|
+
|
48
55
|
CommandBuilders::TextContentBuilder.new(
|
49
|
-
|
56
|
+
template,
|
57
|
+
resource: resource,
|
58
|
+
network: @network,
|
59
|
+
redact_confidentials: @redact_confidentials)
|
50
60
|
.build_command.chomp
|
51
61
|
end
|
52
62
|
end
|
@@ -8,9 +8,8 @@ module Gitlab
|
|
8
8
|
SOURCES = %w[project group].freeze
|
9
9
|
CONDITIONS = %w[member_of not_member_of].freeze
|
10
10
|
|
11
|
-
def initialize(resource, condition,
|
12
|
-
@
|
13
|
-
@network = net[:network]
|
11
|
+
def initialize(resource, condition, network = nil)
|
12
|
+
@network = network
|
14
13
|
super(resource, condition)
|
15
14
|
end
|
16
15
|
|
@@ -61,19 +60,18 @@ module Gitlab
|
|
61
60
|
end
|
62
61
|
|
63
62
|
def members
|
64
|
-
@members ||= @network.query_api_cached(
|
63
|
+
@members ||= @network.query_api_cached(member_url)
|
65
64
|
end
|
66
65
|
|
67
66
|
def member_url
|
68
|
-
UrlBuilders::UrlBuilder.new(
|
67
|
+
UrlBuilders::UrlBuilder.new(url_opts).build
|
69
68
|
end
|
70
69
|
|
71
70
|
private
|
72
71
|
|
73
|
-
def
|
72
|
+
def url_opts
|
74
73
|
{
|
75
|
-
|
76
|
-
api_version: @net[:api_version],
|
74
|
+
network_options: @network.options,
|
77
75
|
resource_type: 'members',
|
78
76
|
source: @source == :group ? 'groups' : 'projects',
|
79
77
|
source_id: @source_id,
|
@@ -10,14 +10,16 @@ module Gitlab
|
|
10
10
|
[{ name: :ruby, type: String }]
|
11
11
|
end
|
12
12
|
|
13
|
-
def initialize(resource, condition,
|
13
|
+
def initialize(resource, condition, network = nil)
|
14
14
|
super(resource, condition)
|
15
15
|
|
16
|
-
@
|
16
|
+
@network = network
|
17
17
|
end
|
18
18
|
|
19
19
|
def calculate
|
20
|
-
|
20
|
+
context = Resource::Context.build(@resource, network: @network)
|
21
|
+
|
22
|
+
!!context.eval(@expression)
|
21
23
|
end
|
22
24
|
|
23
25
|
private
|
@@ -9,19 +9,21 @@ module Gitlab
|
|
9
9
|
class Network
|
10
10
|
include Retryable
|
11
11
|
|
12
|
-
|
12
|
+
TokenNotFound = Class.new(StandardError)
|
13
13
|
|
14
|
-
|
14
|
+
attr_reader :options, :adapter
|
15
|
+
|
16
|
+
def initialize(adapter)
|
15
17
|
@adapter = adapter
|
16
|
-
@options = options
|
17
|
-
@cache =
|
18
|
+
@options = adapter.options
|
19
|
+
@cache = {}
|
18
20
|
end
|
19
21
|
|
20
|
-
def query_api_cached(
|
21
|
-
@cache
|
22
|
+
def query_api_cached(url)
|
23
|
+
@cache[url] || @cache[url] = query_api(url)
|
22
24
|
end
|
23
25
|
|
24
|
-
def query_api(
|
26
|
+
def query_api(url)
|
25
27
|
response = {}
|
26
28
|
resources = []
|
27
29
|
|
@@ -29,6 +31,8 @@ module Gitlab
|
|
29
31
|
print '.'
|
30
32
|
|
31
33
|
response = execute_with_retry(Net::ReadTimeout) do
|
34
|
+
puts Gitlab::Triage::UI.debug "query_api: #{url}" if options.debug
|
35
|
+
|
32
36
|
@adapter.get(token, response.fetch(:next_page_url) { url })
|
33
37
|
end
|
34
38
|
|
@@ -47,14 +51,22 @@ module Gitlab
|
|
47
51
|
[]
|
48
52
|
end
|
49
53
|
|
50
|
-
def post_api(
|
54
|
+
def post_api(url, body)
|
51
55
|
execute_with_retry(Net::ReadTimeout) do
|
56
|
+
puts Gitlab::Triage::UI.debug "post_api: #{url}" if options.debug
|
57
|
+
|
52
58
|
@adapter.post(token, url, body)
|
53
59
|
end
|
54
60
|
|
55
61
|
rescue Net::ReadTimeout
|
56
62
|
false
|
57
63
|
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def token
|
68
|
+
options.token || raise(TokenNotFound)
|
69
|
+
end
|
58
70
|
end
|
59
71
|
end
|
60
72
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal
|
2
|
+
# rubocop:disable Rails/Exit
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require 'fileutils'
|
6
|
+
require_relative 'options'
|
7
|
+
|
8
|
+
module Gitlab
|
9
|
+
module Triage
|
10
|
+
class OptionParser
|
11
|
+
class << self
|
12
|
+
def parse(argv)
|
13
|
+
options = Options.new
|
14
|
+
options.host_url = 'https://gitlab.com'
|
15
|
+
|
16
|
+
parser = ::OptionParser.new do |opts|
|
17
|
+
opts.banner = "Usage: gitlab-triage [options]\n\n"
|
18
|
+
|
19
|
+
opts.on('-n', '--dry-run', "Don't actually update anything, just print") do |value|
|
20
|
+
options.dry_run = value
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on('-f', '--policies-file [string]', String, 'A valid policies YML file') do |value|
|
24
|
+
options.policies_file = value
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on('-p', '--project-id [string]', String, 'A project ID or path') do |value|
|
28
|
+
options.project_id = value
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on('-t', '--token [string]', String, 'A valid API token') do |value|
|
32
|
+
options.token = value
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on('-H', '--host-url [string]', String, 'A valid host url') do |value|
|
36
|
+
options.host_url = value
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on('-d', '--debug', 'Print debug information') do |value|
|
40
|
+
options.debug = value
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on('-h', '--help', 'Print help message') do
|
44
|
+
$stdout.puts opts
|
45
|
+
exit
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.on('--init', 'Initialize the project with a policy file') do
|
49
|
+
example_path =
|
50
|
+
File.expand_path('../../../support/.triage-policies.example.yml', __dir__)
|
51
|
+
|
52
|
+
FileUtils.cp(example_path, '.triage-policies.yml')
|
53
|
+
exit
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on('--init-ci', 'Initialize the project with a .gitlab-ci.yml file') do
|
57
|
+
example_path =
|
58
|
+
File.expand_path('../../../support/.gitlab-ci.example.yml', __dir__)
|
59
|
+
|
60
|
+
FileUtils.cp(example_path, '.gitlab-ci.yml')
|
61
|
+
exit
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
parser.parse!(argv)
|
66
|
+
|
67
|
+
options
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Gitlab
|
2
|
+
module Triage
|
3
|
+
Options = Struct.new(
|
4
|
+
:dry_run,
|
5
|
+
:policies_file,
|
6
|
+
:project_id,
|
7
|
+
:token,
|
8
|
+
:debug,
|
9
|
+
:host_url,
|
10
|
+
:api_version
|
11
|
+
) do
|
12
|
+
def initialize(*args)
|
13
|
+
super
|
14
|
+
|
15
|
+
# Defaults
|
16
|
+
self.host_url ||= 'https://gitlab.com'
|
17
|
+
self.api_version ||= 'v4'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -3,7 +3,17 @@
|
|
3
3
|
module Gitlab
|
4
4
|
module Triage
|
5
5
|
module Policies
|
6
|
-
BasePolicy
|
6
|
+
class BasePolicy
|
7
|
+
attr_reader :type, :policy_spec, :resources, :network
|
8
|
+
|
9
|
+
def initialize(type, policy_spec, resources, network)
|
10
|
+
@type = type
|
11
|
+
@policy_spec = policy_spec
|
12
|
+
# In some filters/actions we want to know which resource type it is
|
13
|
+
@resources = attach_resource_type(resources, type)
|
14
|
+
@network = network
|
15
|
+
end
|
16
|
+
|
7
17
|
def name
|
8
18
|
@name ||= (policy_spec[:name] || "#{type}-#{object_id}")
|
9
19
|
end
|
@@ -17,12 +27,21 @@ module Gitlab
|
|
17
27
|
end
|
18
28
|
|
19
29
|
def comment?
|
20
|
-
|
30
|
+
# The actual keys are strings
|
31
|
+
(actions.keys.map(&:to_sym) - [:summarize]).any?
|
21
32
|
end
|
22
33
|
|
23
34
|
def build_issue
|
24
35
|
raise NotImplementedError
|
25
36
|
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# We don't have to do this once the response will contain the type
|
41
|
+
# of the resource. For now let's just attach it.
|
42
|
+
def attach_resource_type(resources, type)
|
43
|
+
resources.map { |resource| resource.reverse_merge(type: type) }
|
44
|
+
end
|
26
45
|
end
|
27
46
|
end
|
28
47
|
end
|