lazylead 0.5.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +49 -1
- data/.simplecov +1 -1
- data/Guardfile +1 -1
- data/Rakefile +4 -3
- data/bin/lazylead +9 -5
- data/lazylead.gemspec +20 -15
- data/lib/lazylead/cc.rb +21 -20
- data/lib/lazylead/cli/app.rb +13 -5
- data/lib/lazylead/confluence.rb +8 -1
- data/lib/lazylead/email.rb +0 -20
- data/lib/lazylead/exchange.rb +16 -28
- data/lib/lazylead/log.rb +2 -1
- data/lib/lazylead/model.rb +31 -16
- data/lib/lazylead/opts.rb +65 -2
- data/lib/lazylead/postman.rb +18 -16
- data/lib/lazylead/salt.rb +1 -0
- data/lib/lazylead/smtp.rb +3 -1
- data/lib/lazylead/system/jira.rb +31 -6
- data/lib/lazylead/task/accuracy/accuracy.rb +8 -10
- data/lib/lazylead/task/accuracy/attachment.rb +0 -4
- data/lib/lazylead/task/accuracy/logs.rb +8 -4
- data/lib/lazylead/task/accuracy/onlyll.rb +148 -0
- data/lib/lazylead/task/accuracy/records.rb +1 -1
- data/lib/lazylead/task/accuracy/servers.rb +16 -7
- data/lib/lazylead/task/accuracy/stacktrace.rb +50 -10
- data/lib/lazylead/task/accuracy/testcase.rb +23 -6
- data/lib/lazylead/task/assignment.rb +96 -0
- data/lib/lazylead/task/fix_version.rb +6 -0
- data/lib/lazylead/task/propagate_down.rb +1 -1
- data/lib/lazylead/task/svn/diff.rb +77 -0
- data/lib/lazylead/task/svn/grep.rb +139 -0
- data/lib/lazylead/task/svn/touch.rb +99 -0
- data/lib/lazylead/version.rb +1 -1
- data/lib/messages/illegal_assignee_change.erb +123 -0
- data/lib/messages/illegal_fixversion_change.erb +8 -0
- data/lib/messages/only_ll.erb +107 -0
- data/lib/messages/svn_diff.erb +110 -0
- data/lib/messages/{svn_log.erb → svn_diff_attachment.erb} +19 -9
- data/lib/messages/svn_grep.erb +114 -0
- data/test/lazylead/cc_test.rb +1 -0
- data/test/lazylead/model_test.rb +20 -0
- data/test/lazylead/opts_test.rb +47 -0
- data/test/lazylead/postman_test.rb +8 -5
- data/test/lazylead/smoke_test.rb +13 -0
- data/test/lazylead/system/jira_test.rb +6 -7
- data/test/lazylead/task/accuracy/logs_test.rb +62 -2
- data/test/lazylead/task/accuracy/onlyll_test.rb +138 -0
- data/test/lazylead/task/accuracy/servers_test.rb +2 -2
- data/test/lazylead/task/accuracy/stacktrace_test.rb +227 -0
- data/test/lazylead/task/accuracy/testcase_test.rb +49 -0
- data/test/lazylead/task/assignment_test.rb +53 -0
- data/test/lazylead/task/fix_version_test.rb +1 -0
- data/test/lazylead/task/propagate_down_test.rb +4 -3
- data/test/lazylead/task/savepoint_test.rb +7 -4
- data/test/lazylead/task/svn/diff_test.rb +97 -0
- data/test/lazylead/task/svn/grep_test.rb +103 -0
- data/test/lazylead/task/{touch_test.rb → svn/touch_test.rb} +7 -34
- data/test/test.rb +7 -6
- data/upgrades/sqlite/999.testdata.sql +3 -1
- metadata +120 -34
- data/lib/lazylead/task/touch.rb +0 -119
data/lib/lazylead/log.rb
CHANGED
@@ -23,6 +23,7 @@
|
|
23
23
|
# OR OTHER DEALINGS IN THE SOFTWARE.
|
24
24
|
|
25
25
|
require "logging"
|
26
|
+
require "colorize"
|
26
27
|
require "forwardable"
|
27
28
|
|
28
29
|
module Lazylead
|
@@ -68,7 +69,7 @@ module Lazylead
|
|
68
69
|
Logging.appenders.stdout(
|
69
70
|
"stdout",
|
70
71
|
layout: Logging.layouts.pattern(
|
71
|
-
pattern: "[%d] %-5l [%X{tid}] %m\n",
|
72
|
+
pattern: "[%d] %-5l #{'[%X{tid}]'.colorize(:light_green)} %m\n",
|
72
73
|
color_scheme: "bright"
|
73
74
|
)
|
74
75
|
)
|
data/lib/lazylead/model.rb
CHANGED
@@ -68,9 +68,7 @@ module Lazylead
|
|
68
68
|
opts.each_with_object({}) do |e, o|
|
69
69
|
k = e[0]
|
70
70
|
v = e[1]
|
71
|
-
if v.respond_to?
|
72
|
-
v = ENV[v.slice(2, v.length - 3)] if v.start_with? "${"
|
73
|
-
end
|
71
|
+
v = ENV[v.slice(2, v.length - 3)] if v.respond_to?(:start_with?) && v.start_with?("${")
|
74
72
|
o[k] = v
|
75
73
|
end
|
76
74
|
end
|
@@ -80,6 +78,13 @@ module Lazylead
|
|
80
78
|
JSON.parse(properties).to_h
|
81
79
|
end
|
82
80
|
|
81
|
+
def to_h?
|
82
|
+
return true unless to_hash.nil?
|
83
|
+
false
|
84
|
+
rescue StandardError => _e
|
85
|
+
false
|
86
|
+
end
|
87
|
+
|
83
88
|
def to_s
|
84
89
|
attributes.map { |k, v| "#{k}='#{v}'" }.join(", ")
|
85
90
|
end
|
@@ -99,6 +104,7 @@ module Lazylead
|
|
99
104
|
sys = system.connect
|
100
105
|
opts = props
|
101
106
|
opts = detect_cc(sys) if opts.key? "cc"
|
107
|
+
opts["system"] = second_sys if opts.numeric? "system"
|
102
108
|
action.constantize.new.run(sys, postman, opts)
|
103
109
|
end
|
104
110
|
|
@@ -130,12 +136,12 @@ module Lazylead
|
|
130
136
|
|
131
137
|
def props
|
132
138
|
@props ||= begin
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
+
if team.nil?
|
140
|
+
Opts.new(env(to_hash))
|
141
|
+
else
|
142
|
+
Opts.new(env(team.to_hash.merge(to_hash)))
|
143
|
+
end
|
144
|
+
end
|
139
145
|
end
|
140
146
|
|
141
147
|
def postman
|
@@ -146,17 +152,21 @@ module Lazylead
|
|
146
152
|
end
|
147
153
|
end
|
148
154
|
|
155
|
+
def second_sys
|
156
|
+
sys = System.find(props["system"])
|
157
|
+
raise "ll-014: No ticketing system found for #{self}" if sys.nil?
|
158
|
+
sys.connect
|
159
|
+
end
|
160
|
+
|
149
161
|
private
|
150
162
|
|
151
163
|
# Parse scheduling #type and #unit
|
152
164
|
def trigger
|
153
165
|
@trigger ||= begin
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
trg.map(&:strip).map(&:chomp)
|
159
|
-
end
|
166
|
+
trg = schedule.split(":")
|
167
|
+
raise "ll-007: illegal schedule format '#{schedule}'" unless trg.size == 2
|
168
|
+
trg.map(&:strip).map(&:chomp)
|
169
|
+
end
|
160
170
|
end
|
161
171
|
end
|
162
172
|
|
@@ -172,6 +182,10 @@ module Lazylead
|
|
172
182
|
@log = log
|
173
183
|
end
|
174
184
|
|
185
|
+
# @todo #/DEV Remove the suppression during next refactoring (or enhancements)
|
186
|
+
# for the method below
|
187
|
+
#
|
188
|
+
# rubocop:disable Metrics/AbcSize
|
175
189
|
def exec
|
176
190
|
Logging.mdc["tid"] = "task #{id}"
|
177
191
|
@log.debug "'#{name}' is started."
|
@@ -182,12 +196,13 @@ module Lazylead
|
|
182
196
|
rescue StandardError => e
|
183
197
|
msg = <<~MSG
|
184
198
|
ll-006: Task ##{id} #{e} (#{e.class}) at #{self}
|
185
|
-
#{Backtrace.new(e) if ARGV.include? '--trace'}
|
199
|
+
#{Backtrace.new(e) if ARGV.include? '--trace'}
|
186
200
|
MSG
|
187
201
|
@log.error msg
|
188
202
|
ensure
|
189
203
|
Logging.mdc["tid"] = ""
|
190
204
|
end
|
205
|
+
# rubocop:enable Metrics/AbcSize
|
191
206
|
end
|
192
207
|
|
193
208
|
# Ticketing systems to monitor.
|
data/lib/lazylead/opts.rb
CHANGED
@@ -34,7 +34,8 @@ module Lazylead
|
|
34
34
|
# License:: MIT
|
35
35
|
class Opts
|
36
36
|
extend Forwardable
|
37
|
-
def_delegators :@origin, :[], :[]=, :to_s, :key?, :fetch, :
|
37
|
+
def_delegators :@origin, :[], :[]=, :to_s, :key?, :fetch, :except, :each,
|
38
|
+
:each_pair, :sort_by
|
38
39
|
|
39
40
|
def initialize(origin = {})
|
40
41
|
@origin = origin
|
@@ -42,7 +43,14 @@ module Lazylead
|
|
42
43
|
|
43
44
|
# Split text value by delimiter, trim all spaces and reject blank items
|
44
45
|
def slice(key, delim)
|
45
|
-
|
46
|
+
return [] unless to_h.key? key
|
47
|
+
trim to_h[key].split(delim)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Trim all spaces and reject blank items in array
|
51
|
+
def trim(arr)
|
52
|
+
return [] if arr.nil?
|
53
|
+
arr.map(&:chomp).map(&:strip).reject(&:blank?)
|
46
54
|
end
|
47
55
|
|
48
56
|
def blank?(key)
|
@@ -76,5 +84,60 @@ module Lazylead
|
|
76
84
|
return Salt.new(sid).decrypt(text) if ENV.key? sid
|
77
85
|
text
|
78
86
|
end
|
87
|
+
|
88
|
+
def merge(args)
|
89
|
+
return self unless args.is_a? Hash
|
90
|
+
Opts.new @origin.merge(args)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Construct html document from template and binds.
|
94
|
+
def msg_body(template = "template")
|
95
|
+
Email.new(
|
96
|
+
to_h[template],
|
97
|
+
to_h.merge(version: Lazylead::VERSION)
|
98
|
+
).render
|
99
|
+
end
|
100
|
+
|
101
|
+
def msg_to(delim = ",")
|
102
|
+
sliced delim, :to, "to"
|
103
|
+
end
|
104
|
+
|
105
|
+
def msg_cc(delim = ",")
|
106
|
+
sliced delim, :cc, "cc"
|
107
|
+
end
|
108
|
+
|
109
|
+
def msg_from(delim = ",")
|
110
|
+
sliced delim, :from, "from"
|
111
|
+
end
|
112
|
+
|
113
|
+
def msg_attachments(delim = ",")
|
114
|
+
sliced(delim, :attachments, "attachments").select { |f| File.file? f }
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Find the option by key and split by delimiter
|
119
|
+
# Opts.new("key" => "a,b").sliced(",", "key") => [a, b]
|
120
|
+
# Opts.new(key: "a,b").sliced(",", :key) => [a, b]
|
121
|
+
# Opts.new(key: "a,b").sliced(",", "key", :key) => [a, b]
|
122
|
+
# Opts.new(key: "").sliced ",", :key) => []
|
123
|
+
#
|
124
|
+
def sliced(delim, *keys)
|
125
|
+
return [] if keys.empty?
|
126
|
+
key = keys.detect { |k| key? k }
|
127
|
+
val = to_h[key]
|
128
|
+
return [] if val.nil? || val.blank?
|
129
|
+
return val if val.is_a? Array
|
130
|
+
return [val] unless val.include? delim
|
131
|
+
slice key, delim
|
132
|
+
end
|
133
|
+
|
134
|
+
# Ensure that particular key from options is a positive numer
|
135
|
+
# Opts.new("key" => "1").numeric? "key" => true
|
136
|
+
# Opts.new("key" => "0").numeric? "key" => false
|
137
|
+
# Opts.new("key" => ".").numeric? "key" => false
|
138
|
+
# Opts.new("key" => "nil").numeric? "key" => false
|
139
|
+
def numeric?(key)
|
140
|
+
to_h[key].to_i.positive?
|
141
|
+
end
|
79
142
|
end
|
80
143
|
end
|
data/lib/lazylead/postman.rb
CHANGED
@@ -44,8 +44,6 @@ module Lazylead
|
|
44
44
|
# Copyright:: Copyright (c) 2019-2020 Yurii Dubinka
|
45
45
|
# License:: MIT
|
46
46
|
class Postman
|
47
|
-
include Emailing
|
48
|
-
|
49
47
|
def initialize(log = Log.new)
|
50
48
|
@log = log
|
51
49
|
end
|
@@ -53,25 +51,29 @@ module Lazylead
|
|
53
51
|
# Send an email.
|
54
52
|
# :opts :: the mail configuration like to, from, cc, subject, template.
|
55
53
|
def send(opts)
|
56
|
-
|
54
|
+
if opts.msg_to.empty?
|
55
|
+
@log.warn "ll-013: Email can't be sent to '#{opts.msg_to}," \
|
56
|
+
" more: '#{opts}'"
|
57
|
+
else
|
58
|
+
mail = make_email(opts)
|
59
|
+
mail.deliver
|
60
|
+
@log.debug "#{__FILE__} sent '#{mail.subject}' to '#{mail.to}'."
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Construct an email based on input arguments
|
65
|
+
def make_email(opts)
|
57
66
|
mail = Mail.new
|
58
|
-
mail.to opts
|
59
|
-
mail.from opts
|
60
|
-
mail.cc opts
|
67
|
+
mail.to opts.msg_to
|
68
|
+
mail.from opts.msg_from
|
69
|
+
mail.cc opts.msg_cc if opts.key? "cc"
|
61
70
|
mail.subject opts["subject"]
|
62
71
|
mail.html_part do
|
63
72
|
content_type "text/html; charset=UTF-8"
|
64
|
-
body
|
73
|
+
body opts.msg_body
|
65
74
|
end
|
66
|
-
|
67
|
-
mail
|
68
|
-
@log.debug "#{__FILE__} sent email based on #{opts}."
|
69
|
-
end
|
70
|
-
|
71
|
-
def add_attachments(mail, opts)
|
72
|
-
return unless opts.key? "attachments"
|
73
|
-
opts["attachments"].select { |a| File.file? a }
|
74
|
-
.each { |a| mail.add_file a }
|
75
|
+
opts.msg_attachments.each { |f| mail.add_file f }
|
76
|
+
mail
|
75
77
|
end
|
76
78
|
end
|
77
79
|
end
|
data/lib/lazylead/salt.rb
CHANGED
data/lib/lazylead/smtp.rb
CHANGED
@@ -23,6 +23,7 @@
|
|
23
23
|
# OR OTHER DEALINGS IN THE SOFTWARE.
|
24
24
|
|
25
25
|
require "mail"
|
26
|
+
require "colorize"
|
26
27
|
require_relative "log"
|
27
28
|
require_relative "salt"
|
28
29
|
|
@@ -45,7 +46,8 @@ module Lazylead
|
|
45
46
|
Mail.defaults do
|
46
47
|
delivery_method :test
|
47
48
|
end
|
48
|
-
@log.warn "SMTP connection enabled in
|
49
|
+
@log.warn "SMTP connection enabled in " \
|
50
|
+
"#{'test'.colorize(:light_yellow)} mode."
|
49
51
|
else
|
50
52
|
setup_smtp
|
51
53
|
end
|
data/lib/lazylead/system/jira.rb
CHANGED
@@ -25,6 +25,7 @@
|
|
25
25
|
require "jira-ruby"
|
26
26
|
require "forwardable"
|
27
27
|
require_relative "../salt"
|
28
|
+
require_relative "../opts"
|
28
29
|
|
29
30
|
module Lazylead
|
30
31
|
# Jira system for manipulation with issues.
|
@@ -45,16 +46,32 @@ module Lazylead
|
|
45
46
|
" and salt #{@salt.id} (found=#{@salt.specified?})"
|
46
47
|
end
|
47
48
|
|
48
|
-
|
49
|
+
# Find the jira issues by 'JQL'
|
50
|
+
# @param 'jql' - Jira search query
|
51
|
+
# @param 'opts' - Parameters for Jira search query.
|
52
|
+
# :max_results => maximum number of tickets per one iteration.
|
53
|
+
# :fields => ticket fields to be fetched like assignee, summary, etc.
|
54
|
+
def issues(jql, opts = { max_results: 50, fields: nil, expand: nil })
|
49
55
|
raw do |jira|
|
50
|
-
|
56
|
+
start = 0
|
57
|
+
tickets = []
|
58
|
+
total = jira.Issue.jql(jql, max_results: 0)
|
59
|
+
@log.debug "Found #{total} ticket(s) in '#{jql}'"
|
60
|
+
loop do
|
61
|
+
tickets.concat(jira.Issue.jql(jql, opts.merge(start_at: start))
|
62
|
+
.map { |i| Lazylead::Issue.new(i, jira) })
|
63
|
+
@log.debug "Fetched #{tickets.size}"
|
64
|
+
start += opts.fetch(:max_results, 50).to_i
|
65
|
+
break if start > total
|
66
|
+
end
|
67
|
+
tickets
|
51
68
|
end
|
52
69
|
end
|
53
70
|
|
54
71
|
# Execute request to the ticketing system using raw client.
|
55
72
|
# For Jira the raw client is 'jira-ruby' gem.
|
56
73
|
def raw
|
57
|
-
raise "ll-
|
74
|
+
raise "ll-009: No block given to method" unless block_given?
|
58
75
|
yield client
|
59
76
|
end
|
60
77
|
|
@@ -160,7 +177,7 @@ module Lazylead
|
|
160
177
|
end
|
161
178
|
|
162
179
|
def url
|
163
|
-
@issue.attrs[
|
180
|
+
"#{@issue.attrs['self'].split('/rest/api/').first}/browse/#{key}"
|
164
181
|
end
|
165
182
|
|
166
183
|
def duedate
|
@@ -237,15 +254,23 @@ module Lazylead
|
|
237
254
|
|
238
255
|
def add_label(label, *more)
|
239
256
|
lbl = labels
|
257
|
+
lbl = [] if lbl.nil?
|
240
258
|
lbl << label
|
241
259
|
lbl += more if more.size.positive?
|
242
|
-
|
260
|
+
labels! lbl
|
243
261
|
end
|
244
262
|
|
263
|
+
# Get the labels for a particular issue
|
245
264
|
def labels
|
246
265
|
fields["labels"]
|
247
266
|
end
|
248
267
|
|
268
|
+
# Update the labels for a particular issue
|
269
|
+
def labels!(lbl)
|
270
|
+
return if lbl.nil? || lbl.empty?
|
271
|
+
save!("fields" => { "labels" => lbl.uniq })
|
272
|
+
end
|
273
|
+
|
249
274
|
def save!(opts)
|
250
275
|
@issue.save(opts)
|
251
276
|
end
|
@@ -324,7 +349,7 @@ module Lazylead
|
|
324
349
|
|
325
350
|
# Execute request to the ticketing system using raw client.
|
326
351
|
def raw
|
327
|
-
raise "ll-
|
352
|
+
raise "ll-008: No block given to method" unless block_given?
|
328
353
|
yield(OpenStruct.new(Issue: self))
|
329
354
|
end
|
330
355
|
|
@@ -81,7 +81,7 @@ module Lazylead
|
|
81
81
|
def post
|
82
82
|
return if @opts.key? "silent"
|
83
83
|
@issue.post comment
|
84
|
-
@issue.add_label "LL.accuracy", grade(@accuracy)
|
84
|
+
@issue.add_label "LL.accuracy", "#{grade(@accuracy)}%"
|
85
85
|
end
|
86
86
|
|
87
87
|
# The jira comment in markdown format
|
@@ -114,9 +114,7 @@ module Lazylead
|
|
114
114
|
end
|
115
115
|
|
116
116
|
def color
|
117
|
-
if colors.nil? || !defined?(@score) || !@score.is_a?(Numeric)
|
118
|
-
return "#061306"
|
119
|
-
end
|
117
|
+
return "#061306" if colors.nil? || !defined?(@score) || !@score.is_a?(Numeric)
|
120
118
|
colors.reverse_each do |color|
|
121
119
|
return color.last if @accuracy >= color.first
|
122
120
|
end
|
@@ -125,12 +123,12 @@ module Lazylead
|
|
125
123
|
|
126
124
|
def colors
|
127
125
|
@colors ||= begin
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
126
|
+
JSON.parse(@opts["colors"])
|
127
|
+
.to_h
|
128
|
+
.to_a
|
129
|
+
.each { |e| e[0] = e[0].to_i }
|
130
|
+
.sort_by { |e| e[0] }
|
131
|
+
end
|
134
132
|
end
|
135
133
|
|
136
134
|
# Calculate grade for accuracy
|
@@ -27,10 +27,6 @@ require_relative "requirement"
|
|
27
27
|
module Lazylead
|
28
28
|
# Check that ticket has an attachment.
|
29
29
|
class Attachment < Lazylead::Requirement
|
30
|
-
def initialize(desc, score, field)
|
31
|
-
super(desc, score, field)
|
32
|
-
end
|
33
|
-
|
34
30
|
def passed(issue)
|
35
31
|
issue.attachments.any?(&method(:matching))
|
36
32
|
end
|
@@ -27,14 +27,18 @@ require_relative "attachment"
|
|
27
27
|
module Lazylead
|
28
28
|
# Check that ticket has log file(s) in attachment.
|
29
29
|
class Logs < Lazylead::Attachment
|
30
|
-
def initialize
|
30
|
+
def initialize(files = %w[log.zip logs.zip log.gz logs.gz log.tar.gz
|
31
|
+
logs.tar.gz log.7z logs.7z log.tar logs.tar])
|
31
32
|
super("Log files", 2, "Attachments")
|
33
|
+
@files = files
|
32
34
|
end
|
33
35
|
|
34
|
-
# Ensure that ticket has a '*.log' file more '
|
36
|
+
# Ensure that ticket has a '*.log' file more '5KB'
|
35
37
|
def matching(attachment)
|
36
|
-
attachment.attrs["
|
37
|
-
|
38
|
+
name = attachment.attrs["filename"].downcase
|
39
|
+
return false unless attachment.attrs["size"].to_i > 5120
|
40
|
+
return true if File.extname(name).start_with? ".log", ".txt", ".out"
|
41
|
+
@files.any? { |l| name.end_with? l }
|
38
42
|
end
|
39
43
|
end
|
40
44
|
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The MIT License
|
4
|
+
#
|
5
|
+
# Copyright (c) 2019-2020 Yurii Dubinka
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
# of this software and associated documentation files (the "Software"),
|
9
|
+
# to deal in the Software without restriction, including without limitation
|
10
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
11
|
+
# and/or sell copies of the Software, and to permit persons to whom
|
12
|
+
# the Software is furnished to do so, subject to the following conditions:
|
13
|
+
#
|
14
|
+
# The above copyright notice and this permission notice shall be included
|
15
|
+
# in all copies or substantial portions of the Software.
|
16
|
+
#
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
22
|
+
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
23
|
+
# OR OTHER DEALINGS IN THE SOFTWARE.
|
24
|
+
|
25
|
+
require_relative "../../log"
|
26
|
+
require_relative "../../opts"
|
27
|
+
|
28
|
+
module Lazylead
|
29
|
+
module Task
|
30
|
+
#
|
31
|
+
# Ensure that ticket accuracy evaluation labels are set only by LL, not by
|
32
|
+
# some person.
|
33
|
+
#
|
34
|
+
# The task supports the following features:
|
35
|
+
# - fetch issues from remote ticketing system by query
|
36
|
+
# - check the history of labels modification
|
37
|
+
# - remove evaluation labels if they set not by LL
|
38
|
+
class OnlyLL
|
39
|
+
def initialize(log = Log.new)
|
40
|
+
@log = log
|
41
|
+
end
|
42
|
+
|
43
|
+
def run(sys, postman, opts)
|
44
|
+
found = sys.issues(opts["jql"],
|
45
|
+
opts.jira_defaults.merge(expand: "changelog"))
|
46
|
+
.map { |i| Labels.new(i, opts) }
|
47
|
+
.select(&:exists?)
|
48
|
+
.reject(&:valid?)
|
49
|
+
.each(&:remove)
|
50
|
+
postman.send opts.merge(tickets: found) unless found.empty?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# The ticket with grid labels
|
56
|
+
class Labels
|
57
|
+
attr_reader :issue
|
58
|
+
|
59
|
+
def initialize(issue, opts)
|
60
|
+
@issue = issue
|
61
|
+
@opts = opts
|
62
|
+
end
|
63
|
+
|
64
|
+
# Ensure that issue has evaluation labels for accuracy rules
|
65
|
+
def exists?
|
66
|
+
return false if @issue.labels.nil? || @issue.labels.empty?
|
67
|
+
return false unless @issue.labels.is_a? Array
|
68
|
+
grid.any? { |g| @issue.labels.any? { |l| g.eql? l } }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Compare the score evaluated by LL and current ticket score.
|
72
|
+
# @return true if current score equal to LL evaluation
|
73
|
+
def valid?
|
74
|
+
score.eql?(@issue.labels.sort.find { |l| grid.any? { |g| l.eql? g } })
|
75
|
+
end
|
76
|
+
|
77
|
+
# Find expected ticket score evaluated by LL.
|
78
|
+
# If LL evaluated the same ticket several times (no matter why),
|
79
|
+
# then the last score would be returned.
|
80
|
+
def score
|
81
|
+
to_l(
|
82
|
+
@issue.history
|
83
|
+
.select { |h| h["author"]["key"].eql? @opts["author"] }
|
84
|
+
.select { |h| to_l(h) }
|
85
|
+
.reverse
|
86
|
+
.first
|
87
|
+
).fetch("toString", "").split.find { |l| grid.any? { |g| l.eql? g } }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Find history record with labels changes.
|
91
|
+
def to_l(history)
|
92
|
+
return {} if history.nil? || history.empty?
|
93
|
+
history["items"].find { |f| f["field"].eql? "labels" }
|
94
|
+
end
|
95
|
+
|
96
|
+
# Detect the percentage grid for tickets, by default its 0%, 10%, 20%, etc.
|
97
|
+
def grid
|
98
|
+
@grid ||= begin
|
99
|
+
if @opts.key? "grid"
|
100
|
+
@opts.slice("grid", ",")
|
101
|
+
else
|
102
|
+
%w[0% 10% 20% 30% 40% 50% 60% 70% 80% 90% 100%]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Remove score labels from the ticket.
|
108
|
+
def remove
|
109
|
+
@issue.labels!(@issue.labels - grid)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Detect the violators with their changes.
|
113
|
+
# .violators => ["Tom Hhhh set 40%", "Bob Mmmm set 50%"]
|
114
|
+
def violators
|
115
|
+
@issue.history
|
116
|
+
.reject { |h| h["author"]["key"].eql? @opts["author"] }
|
117
|
+
.select { |h| grid?(h) }
|
118
|
+
.group_by { |h| h["author"]["key"] }
|
119
|
+
.map do |a|
|
120
|
+
"#{a.last.first['author']['displayName']} set #{hacked(a.last)}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Ensure that history record has label change related to LL grid labels.
|
125
|
+
# @return true if LL grid labels added
|
126
|
+
def grid?(record)
|
127
|
+
diff(record).any? { |d| d.find { |l| grid.any? { |g| l.eql? g } } }
|
128
|
+
end
|
129
|
+
|
130
|
+
# Detect label diff in single history record from ticket's history.
|
131
|
+
def diff(record)
|
132
|
+
record["items"].select { |f| f["field"].eql? "labels" }
|
133
|
+
.reject { |f| f["toString"].nil? || f["toString"].blank? }
|
134
|
+
.map do |f|
|
135
|
+
from = []
|
136
|
+
from = f["fromString"].split unless f["fromString"].nil?
|
137
|
+
f["toString"].split - from
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Hacked score by violator in ticket's history.
|
142
|
+
def hacked(record)
|
143
|
+
diff(record.first)
|
144
|
+
.first
|
145
|
+
.find { |l| grid.any? { |g| l.eql? g } }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|