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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -24
  3. data/bin/gitlab-triage +2 -74
  4. data/lib/gitlab/triage/action/base.rb +3 -3
  5. data/lib/gitlab/triage/action/comment.rb +5 -7
  6. data/lib/gitlab/triage/action/summarize.rb +4 -5
  7. data/lib/gitlab/triage/command_builders/base_command_builder.rb +1 -1
  8. data/lib/gitlab/triage/command_builders/text_content_builder.rb +54 -16
  9. data/lib/gitlab/triage/engine.rb +27 -34
  10. data/lib/gitlab/triage/entity_builders/issue_builder.rb +27 -17
  11. data/lib/gitlab/triage/filters/member_conditions_filter.rb +6 -8
  12. data/lib/gitlab/triage/filters/ruby_conditions_filter.rb +5 -3
  13. data/lib/gitlab/triage/network.rb +20 -8
  14. data/lib/gitlab/triage/network_adapters/base_adapter.rb +1 -1
  15. data/lib/gitlab/triage/option_parser.rb +72 -0
  16. data/lib/gitlab/triage/options.rb +21 -0
  17. data/lib/gitlab/triage/policies/base_policy.rb +21 -2
  18. data/lib/gitlab/triage/policies/rule_policy.rb +7 -1
  19. data/lib/gitlab/triage/policies/summary_policy.rb +25 -2
  20. data/lib/gitlab/triage/resource/base.rb +49 -10
  21. data/lib/gitlab/triage/resource/context.rb +13 -8
  22. data/lib/gitlab/triage/resource/instance_version.rb +3 -4
  23. data/lib/gitlab/triage/resource/issue.rb +14 -0
  24. data/lib/gitlab/triage/resource/label.rb +41 -0
  25. data/lib/gitlab/triage/resource/label_event.rb +45 -0
  26. data/lib/gitlab/triage/resource/merge_request.rb +14 -0
  27. data/lib/gitlab/triage/resource/milestone.rb +5 -5
  28. data/lib/gitlab/triage/resource/shared/issuable.rb +54 -0
  29. data/lib/gitlab/triage/url_builders/url_builder.rb +3 -2
  30. data/lib/gitlab/triage/version.rb +1 -1
  31. data/support/.triage-policies.example.yml +4 -4
  32. metadata +10 -4
@@ -22,18 +22,18 @@ require_relative 'ui'
22
22
  module Gitlab
23
23
  module Triage
24
24
  class Engine
25
- attr_reader :host_url, :api_version, :per_page, :policies, :options
25
+ attr_reader :per_page, :policies, :options
26
26
 
27
27
  def initialize(policies:, options:, network_adapter_class: Gitlab::Triage::NetworkAdapters::HttpartyAdapter)
28
- @host_url = policies.delete(:host_url) { options.host_url }
29
- @api_version = policies.delete(:api_version) { 'v4' }
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), net))
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, net))
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(options.token, build_get_url(resource_type, conditions))
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
- net: net,
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], net).calculate if conditions[:author_member]
170
- results << Filters::AssigneeMemberConditionsFilter.new(resource, conditions[:assignee_member], net).calculate if conditions[:assignee_member]
171
- results << Filters::RubyConditionsFilter.new(resource, conditions, net).calculate if conditions[:ruby]
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
- get_url = UrlBuilders::UrlBuilder.new(
202
- host_url: host_url,
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, resources, net)
13
- @title = action[:title]
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
- @net = net
19
+ @network = network
18
20
  end
19
21
 
20
- def description
21
- return '' unless @summary_template
22
+ def title
23
+ @title ||= build_text(title_resource, @title_template)
24
+ end
22
25
 
23
- @description ||= CommandBuilders::TextContentBuilder.new(
24
- @summary_template, resource: summary_resource, net: @net)
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 summary_resource
35
- @summary_resource ||= {
36
- 'items' => items,
37
- 'title' => title
38
- }
36
+ def title_resource
37
+ { type: @type }
39
38
  end
40
39
 
41
- def items
42
- return '' unless @item_template
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
- @item_template, resource: resource, net: @net)
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, net = {})
12
- @net = net
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(@net[:token], member_url)
63
+ @members ||= @network.query_api_cached(member_url)
65
64
  end
66
65
 
67
66
  def member_url
68
- UrlBuilders::UrlBuilder.new(net_opts).build
67
+ UrlBuilders::UrlBuilder.new(url_opts).build
69
68
  end
70
69
 
71
70
  private
72
71
 
73
- def net_opts
72
+ def url_opts
74
73
  {
75
- host_url: @net[:host_url],
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, net = {})
13
+ def initialize(resource, condition, network = nil)
14
14
  super(resource, condition)
15
15
 
16
- @net = net
16
+ @network = network
17
17
  end
18
18
 
19
19
  def calculate
20
- !!Resource::Context.new(@resource, @net).eval(@expression)
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
- attr_reader :options
12
+ TokenNotFound = Class.new(StandardError)
13
13
 
14
- def initialize(adapter, options = {})
14
+ attr_reader :options, :adapter
15
+
16
+ def initialize(adapter)
15
17
  @adapter = adapter
16
- @options = options
17
- @cache = Hash.new { |hash, key| hash[key] = {} }
18
+ @options = adapter.options
19
+ @cache = {}
18
20
  end
19
21
 
20
- def query_api_cached(token, url)
21
- @cache.dig(token, url) || @cache[token][url] = query_api(token, url)
22
+ def query_api_cached(url)
23
+ @cache[url] || @cache[url] = query_api(url)
22
24
  end
23
25
 
24
- def query_api(token, url)
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(token, url, body)
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
@@ -6,7 +6,7 @@ module Gitlab
6
6
  class BaseAdapter
7
7
  attr_reader :options
8
8
 
9
- def initialize(options = {})
9
+ def initialize(options)
10
10
  @options = options
11
11
  end
12
12
  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 = Struct.new(:type, :policy_spec, :resources, :net) do
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
- (actions.keys - [:summarize]).any?
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