lazylead 0.10.2 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.githooks/commit-msg +13 -0
  3. data/.rultor.yml +1 -0
  4. data/Rakefile +9 -1
  5. data/lazylead.gemspec +8 -7
  6. data/lib/lazylead/cli/app.rb +1 -1
  7. data/lib/lazylead/model.rb +24 -3
  8. data/lib/lazylead/opts.rb +11 -0
  9. data/lib/lazylead/schedule.rb +8 -1
  10. data/lib/lazylead/system/jira.rb +4 -3
  11. data/lib/lazylead/task/accuracy/accuracy.rb +32 -4
  12. data/lib/lazylead/task/accuracy/has_label.rb +48 -0
  13. data/lib/lazylead/task/accuracy/logs_link.rb +2 -2
  14. data/lib/lazylead/task/accuracy/memes.rb +77 -0
  15. data/lib/lazylead/task/accuracy/records.rb +3 -1
  16. data/lib/lazylead/task/accuracy/records_link.rb +49 -0
  17. data/lib/lazylead/task/accuracy/wiki_url.rb +46 -0
  18. data/lib/lazylead/version.rb +1 -1
  19. data/lib/messages/svn_grep.erb +1 -0
  20. data/test/lazylead/exchange_test.rb +21 -14
  21. data/test/lazylead/opts_test.rb +16 -0
  22. data/test/lazylead/retry_test.rb +85 -0
  23. data/test/lazylead/system/jira_test.rb +13 -5
  24. data/test/lazylead/task/accuracy/accuracy_test.rb +5 -0
  25. data/test/lazylead/task/accuracy/has_label_test.rb +54 -0
  26. data/test/lazylead/task/accuracy/memes_test.rb +77 -0
  27. data/test/lazylead/task/accuracy/records_llink_test.rb +55 -0
  28. data/test/lazylead/task/accuracy/records_test.rb +16 -0
  29. data/test/lazylead/task/accuracy/score_test.rb +7 -0
  30. data/test/lazylead/task/accuracy/screenshots_test.rb +2 -0
  31. data/test/lazylead/task/accuracy/wiki_url_test.rb +58 -0
  32. data/test/lazylead/task/svn/grep_test.rb +1 -2
  33. data/test/lazylead/task/svn/touch_test.rb +1 -1
  34. data/test/sqlite_test.rb +1 -1
  35. data/test/test.rb +47 -0
  36. metadata +42 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1fa41cd073bf1df41fee51ab890e6fd72a379bc0f4fd3d6db118194bbbcad85f
4
- data.tar.gz: 310978fbc087cb95064a28c86e48f79cd03928217f03701df7976a74d8b0442b
3
+ metadata.gz: 25f9021629329ee9ec02db8d6d8d51a0daaffcfccdc6e63627e12644f5775aab
4
+ data.tar.gz: 5560dc06b0b2c67ce56ff918ebe3eeb1b28d0bf22457c3c539f0229cf7db8690
5
5
  SHA512:
6
- metadata.gz: bacaf9e2ba2a774e979e4957ca8e18623188a419b0a9f5363ea237692dc9977376e78411bbe0fcf0f575a8ac80352258aaa35236554c824302ffa99d4994ebf3
7
- data.tar.gz: 7df938335a3179983280f254194cb9f1e8e6b3b0de2cf890ed97a38b7056d5aa0a3ec073ec0a3e1c47a5480c7f3f003be1f07a328e5da90a5f4f278e8c5be2ba
6
+ metadata.gz: 1240d3399f5d17087f3e844dbebe1c838f152dac9ac11635dcb4697ba5ec9c391b69dc7ccf140966e8a83fa95f12607bb46e32f50b768187335f1c43ed9b1aed
7
+ data.tar.gz: 92da12e36cba95581298efc6a3899af2f24e61d0be558b40e919fa8a5a98f871dbb565c5f1e8b7d1ad5cc53768deec7e5cc0edec94a1510cc4663e8c0abf53b8
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ def check_message_format
4
+ regex = /^#\d+:\s\w+/
5
+ message_file = ARGV[0]
6
+ message = File.read(message_file)
7
+ unless regex.match(message)
8
+ puts "Your message is not formatted correctly"
9
+ puts "Message should have following format: '#issue_id: Commit message'"
10
+ exit 1
11
+ end
12
+ end
13
+ check_message_format
data/.rultor.yml CHANGED
@@ -25,6 +25,7 @@ release:
25
25
  curl -s -X POST --url "https://circleci.com/api/v2/project/gh/dgroup/lazylead/envvar" -H @../circleci.header -H "accept: application/json" -H "content-type: application/json" -d "{ \"name\": \"DOCKER_RELEASE_TAGS\", \"value\": \"${tag}\" }" -o /dev/null
26
26
  curl -s -X POST --url https://circleci.com/api/v2/project/gh/dgroup/lazylead/pipeline -H @../circleci.header -H 'accept: application/json' -H 'content-type: application/json' -H 'x-attribution-login: dgroup' -o /dev/null
27
27
  merge:
28
+ squash: true
28
29
  script: |-
29
30
  bundle install
30
31
  bundle exec rake test rubocop sqlint xcop
data/Rakefile CHANGED
@@ -28,6 +28,7 @@ require "date"
28
28
  require "rdoc"
29
29
  require "colorize"
30
30
  require "rake/clean"
31
+ require "concurrent"
31
32
 
32
33
  # @todo #/DEV Investigate the possibility of using migrations from active_record
33
34
  # - Rake tasks https://gist.github.com/schickling/6762581
@@ -45,11 +46,18 @@ def version
45
46
  Gem::Specification.load(Dir["*.gemspec"].first).version
46
47
  end
47
48
 
48
- task default: %i[clean rubocop test sqlint xcop copyright docker]
49
+ task default: %i[init_hooks clean rubocop test sqlint xcop copyright docker]
50
+
51
+ task :init_hooks do
52
+ next if File.file?(".git/hooks/commit-msg")
53
+ FileUtils.copy(".githooks/commit-msg", ".git/hooks/commit-msg")
54
+ FileUtils.chmod("+x", ".git/hooks/commit-msg")
55
+ end
49
56
 
50
57
  require "rake/testtask"
51
58
  desc "Run all unit tests"
52
59
  Rake::TestTask.new(:test) do |t|
60
+ ENV["MT_CPU"] = Concurrent.processor_count.to_s
53
61
  t.libs << "test"
54
62
  t.libs << "lib"
55
63
  t.test_files = FileList["test/**/*_test.rb"]
data/lazylead.gemspec CHANGED
@@ -32,7 +32,7 @@ Gem::Specification.new do |s|
32
32
  s.rubygems_version = "2.2"
33
33
  s.required_ruby_version = ">=2.6.5"
34
34
  s.name = "lazylead"
35
- s.version = "0.10.2"
35
+ s.version = "0.11.0"
36
36
  s.license = "MIT"
37
37
  s.summary = "Eliminate the annoying work within bug-trackers."
38
38
  s.description = "Ticketing systems (Github, Jira, etc.) are strongly
@@ -45,7 +45,7 @@ tasks instead of solving technical problems."
45
45
  s.authors = ["Yurii Dubinka"]
46
46
  s.email = "yurii.dubinka@gmail.com"
47
47
  s.homepage = "http://github.com/dgroup/lazylead"
48
- s.post_install_message = "Thanks for installing Lazylead v0.10.2!
48
+ s.post_install_message = "Thanks for installing Lazylead v0.11.0!
49
49
  Read our blog posts: https://lazylead.org
50
50
  Stay in touch with the community in Telegram: https://t.me/lazylead
51
51
  Follow us on Twitter: https://twitter.com/lazylead
@@ -58,7 +58,7 @@ tasks instead of solving technical problems."
58
58
  s.add_runtime_dependency "activerecord", "6.1.3"
59
59
  s.add_runtime_dependency "backtrace", "0.3"
60
60
  s.add_runtime_dependency "colorize", "0.8.1"
61
- s.add_runtime_dependency "faraday", "1.5.1"
61
+ s.add_runtime_dependency "faraday", "1.7.1"
62
62
  s.add_runtime_dependency "get_process_mem", "0.2.7"
63
63
  s.add_runtime_dependency "inifile", "3.0.0"
64
64
  s.add_runtime_dependency "jira-ruby", "2.1.5"
@@ -66,6 +66,7 @@ tasks instead of solving technical problems."
66
66
  s.add_runtime_dependency "logging", "2.3.0"
67
67
  s.add_runtime_dependency "mail", "2.7.1"
68
68
  s.add_runtime_dependency "memory_profiler", "1.0.0"
69
+ s.add_runtime_dependency "nokogiri", "1.11.3"
69
70
  s.add_runtime_dependency "openssl", "2.1.2"
70
71
  s.add_runtime_dependency "railties", "6.1.3"
71
72
  s.add_runtime_dependency "require_all", "3.0.0"
@@ -79,7 +80,7 @@ tasks instead of solving technical problems."
79
80
  s.add_runtime_dependency "tzinfo-data", "1.2021.1"
80
81
  s.add_runtime_dependency "vcs4sql", "0.1.1"
81
82
  s.add_runtime_dependency "viewpoint", "1.1.1"
82
- s.add_development_dependency "codecov", "0.5.2"
83
+ s.add_development_dependency "codecov", "0.6.0"
83
84
  s.add_development_dependency "guard", "2.18.0"
84
85
  s.add_development_dependency "guard-minitest", "2.4.6"
85
86
  s.add_development_dependency "minitest", "5.14.4"
@@ -90,9 +91,9 @@ tasks instead of solving technical problems."
90
91
  s.add_development_dependency "rake", "13.0.6"
91
92
  s.add_development_dependency "random-port", "0.5.1"
92
93
  s.add_development_dependency "rdoc", "6.3.2"
93
- s.add_development_dependency "rubocop", "1.18.3"
94
- s.add_development_dependency "rubocop-minitest", "0.14.0"
95
- s.add_development_dependency "rubocop-performance", "1.11.4"
94
+ s.add_development_dependency "rubocop", "1.20.0"
95
+ s.add_development_dependency "rubocop-minitest", "0.15.0"
96
+ s.add_development_dependency "rubocop-performance", "1.11.5"
96
97
  s.add_development_dependency "rubocop-rake", "0.6.0"
97
98
  s.add_development_dependency "rubocop-rspec", "2.4.0"
98
99
  s.add_development_dependency "sqlint", "0.2.0"
@@ -70,7 +70,7 @@ module Lazylead
70
70
  ActiveRecord::Base.establish_connection(
71
71
  adapter: "sqlite3",
72
72
  database: @db,
73
- pool: opts[:max_connections]
73
+ pool: opts[:max_connections] || ENV["MT_CPU"]
74
74
  )
75
75
  @log.debug "Database connection established"
76
76
  end
@@ -170,10 +170,9 @@ module Lazylead
170
170
 
171
171
  # A task with extended logging
172
172
  # @see Lazylead::ORM::Task
173
- class VerboseTask
173
+ class Verbose
174
174
  extend Forwardable
175
- def_delegators :@orig, :id, :name, :team, :to_s, :inspect, :props, :type,
176
- :unit
175
+ def_delegators :@orig, :id, :name, :team, :to_s, :inspect, :props, :type, :unit
177
176
 
178
177
  def initialize(orig, log = Log.new)
179
178
  @orig = orig
@@ -203,6 +202,28 @@ module Lazylead
203
202
  # rubocop:enable Metrics/AbcSize
204
203
  end
205
204
 
205
+ # A task which support retry in case of failure.
206
+ # @see Lazylead::ORM::Task
207
+ class Retry
208
+ extend Forwardable
209
+ def_delegators :@orig, :id, :name, :team, :to_s, :inspect, :props, :type, :unit
210
+
211
+ def initialize(orig, log = Log.new)
212
+ @orig = orig
213
+ @log = log
214
+ end
215
+
216
+ def exec
217
+ retries ||= 0
218
+ @orig.exec
219
+ @orig
220
+ rescue StandardError
221
+ sleep(props.fetch("attempt_wait", 0).to_f) if props.key? "attempt_wait"
222
+ retry if (retries += 1) < props.fetch("attempt", 0).to_i
223
+ @orig
224
+ end
225
+ end
226
+
206
227
  # Ticketing systems to monitor.
207
228
  class System < ActiveRecord::Base
208
229
  include ORM
data/lib/lazylead/opts.rb CHANGED
@@ -142,5 +142,16 @@ module Lazylead
142
142
  def construct(field, delim: ",")
143
143
  slice(field, delim).map(&:constantize).map(&:new)
144
144
  end
145
+
146
+ # Get the value by key
147
+ # @param key
148
+ # The key to fetch value. The key might be a plain text or symbol
149
+ # @param default
150
+ # The default/alternative value if key not found
151
+ # @return The value by key or alternative/default value.
152
+ def find(key, default = nil)
153
+ return default if key.nil?
154
+ to_h[key.to_s] || to_h[key.to_sym] || default
155
+ end
145
156
  end
146
157
  end
@@ -47,7 +47,14 @@ module Lazylead
47
47
  raise "ll-002: task can't be a null" if task.nil?
48
48
  @trigger.method(task.type).call(task.unit) do
49
49
  ActiveRecord::Base.connection_pool.with_connection do
50
- ORM::VerboseTask.new(task, @log).exec
50
+ if task.props.key? "no_logs"
51
+ ORM::Retry.new(task, @log).exec
52
+ else
53
+ ORM::Verbose.new(
54
+ ORM::Retry.new(task, @log),
55
+ @log
56
+ ).exec
57
+ end
51
58
  end
52
59
  end
53
60
  @log.debug "Task scheduled: #{task}"
@@ -57,6 +57,7 @@ module Lazylead
57
57
  # :max_results => maximum number of tickets per one iteration.
58
58
  # :fields => ticket fields to be fetched like assignee, summary, etc.
59
59
  def issues(jql, opts = { max_results: 50, fields: nil, expand: nil })
60
+ opts = Opts.new(opts)
60
61
  raw do |jira|
61
62
  start = 0
62
63
  tickets = []
@@ -66,8 +67,8 @@ module Lazylead
66
67
  tickets.concat(jira.Issue.jql(jql, opts.merge(start_at: start))
67
68
  .map { |i| Lazylead::Issue.new(i, jira) })
68
69
  @log.debug "Fetched #{tickets.size}"
69
- start += opts.fetch(:max_results, 50).to_i
70
- break if start > total
70
+ start += opts.find(:max_results, 50)
71
+ break if (start > total) || (start >= opts.find(:limit, total))
71
72
  end
72
73
  tickets
73
74
  end
@@ -326,7 +327,7 @@ module Lazylead
326
327
  # or dashboards.
327
328
  class NoAuthJira
328
329
  extend Forwardable
329
- def_delegators :@jira, :issues, :raw
330
+ def_delegators :@jira, :issues, :raw, :max_results, :limit
330
331
 
331
332
  def initialize(url, path = "", log = Log.new)
332
333
  @jira = Jira.new(
@@ -22,6 +22,7 @@
22
22
  # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23
23
  # OR OTHER DEALINGS IN THE SOFTWARE.
24
24
 
25
+ require_relative "memes"
25
26
  require_relative "../../log"
26
27
  require_relative "../../opts"
27
28
  require_relative "../../email"
@@ -44,15 +45,21 @@ module Lazylead
44
45
  end
45
46
 
46
47
  def run(sys, postman, opts)
47
- Requires.new(__dir__).load
48
- opts[:rules] = opts.construct("rules")
49
- opts[:total] = opts[:rules].sum(&:score)
48
+ init_rules(opts)
50
49
  opts[:tickets] = sys.issues(opts["jql"], opts.jira_defaults.merge(expand: "changelog"))
51
50
  .map { |i| Score.new(i, opts) }
52
51
  .each(&:evaluate)
53
52
  .each(&:post)
54
53
  postman.send(opts) unless opts[:tickets].empty?
55
54
  end
55
+
56
+ # Initialize accuracy rules and configuration for tickets verification.
57
+ def init_rules(opts)
58
+ Requires.new(__dir__).load
59
+ opts[:rules] = opts.construct("rules")
60
+ opts[:total] = opts[:rules].sum(&:score)
61
+ opts[:memes] = Memes.new(opts["memes"])
62
+ end
56
63
  end
57
64
  end
58
65
 
@@ -93,7 +100,7 @@ module Lazylead
93
100
  comment << "|#{r.desc}|#{r.passed(@issue) ? '(/)' : '(-)'}|#{r.field}|"
94
101
  end
95
102
  comment << docs_link
96
- comment << ""
103
+ comment << reaction
97
104
  comment << "Posted by [lazylead v#{Lazylead::VERSION}|https://bit.ly/2NjdndS]."
98
105
  comment.join("\r\n")
99
106
  end
@@ -144,5 +151,26 @@ module Lazylead
144
151
  return @issue.reporter.id if first.nil?
145
152
  first["author"]["key"]
146
153
  end
154
+
155
+ # Add reaction meme to the ticket comment based on score.
156
+ # The meme details are represented as array, where each element is a separate line in future
157
+ # comment in jira.
158
+ #
159
+ # @todo #339/DEV Seems jira doesn't support the rendering of external images by url, thus so far
160
+ # we might have several options:
161
+ # - attach meme to ticket and make rendering using [^attach.jpg!thumbnail] option
162
+ # - have a link to meme (like it implemented now)
163
+ # The 1st option with attachment might generate multiple events in jira and spam ticket
164
+ # watchers, thus, some research & UX testing needed how to make it better.
165
+ def reaction
166
+ return [] if @opts[:memes].nil? && !@opts[:memes].enabled?
167
+ url = @opts[:memes].find(@accuracy)
168
+ return [] if url.blank?
169
+ [
170
+ "",
171
+ "Our reaction when we got the ticket with triage accuracy #{@accuracy}% is [here|#{url}].",
172
+ ""
173
+ ]
174
+ end
147
175
  end
148
176
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License
4
+ #
5
+ # Copyright (c) 2019-2021 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 "requirement"
26
+
27
+ module Lazylead
28
+ # Check that ticket has specified label.
29
+ class HasLabel < Lazylead::Requirement
30
+ #
31
+ # @param label which should be present in ticket.
32
+ #
33
+ def initialize(label, desc: "Has label", score: 0.5)
34
+ super(desc, score, "Labels")
35
+ @label = label
36
+ end
37
+
38
+ def passed(issue)
39
+ return false if issue.labels.empty?
40
+ issue.labels.any? { |l| match?(l) }
41
+ end
42
+
43
+ # Ensure that particular label matches expected label.
44
+ def match?(label)
45
+ @label.eql? label
46
+ end
47
+ end
48
+ end
@@ -30,7 +30,7 @@ module Lazylead
30
30
  # or attachment with plain log file
31
31
  class LogsLink < Lazylead::Logs
32
32
  def initialize(link)
33
- super
33
+ super()
34
34
  @link = link
35
35
  end
36
36
 
@@ -48,7 +48,7 @@ module Lazylead
48
48
  .flat_map(&:split)
49
49
  .reject(&:blank?)
50
50
  .any? do |word|
51
- @link.is_a?(Array) ? @link.any? { |l| word.start_with? l } : word.start_with?(@link)
51
+ @link.is_a?(Array) ? @link.any? { |l| word.include? l } : word.include?(@link)
52
52
  end
53
53
  end
54
54
  end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License
4
+ #
5
+ # Copyright (c) 2019-2021 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 "json"
26
+ require_relative "../../log"
27
+ require_relative "../../opts"
28
+
29
+ module Lazylead
30
+ # Meme generator based on tickets accuracy.
31
+ # For each range of scores there might be several available memes to be chosen randomly.
32
+ # If no meme found for score, empty url should be provided.
33
+ #
34
+ # @todo #339/DEV Prepare the meme list and attach them to the github, section .docs/accuracy/memes
35
+ # As url they will be required for configuration of the each task.
36
+ class Memes
37
+ def initialize(memes, log: Log.new)
38
+ @log = log
39
+ @memes = memes
40
+ end
41
+
42
+ def enabled?
43
+ !@memes.nil? && !range.empty?
44
+ end
45
+
46
+ # Detect meme url based on accuracy score.
47
+ # @param score
48
+ # Accuracy score in percentage
49
+ def find(score)
50
+ r = range.find { |m| score >= m[0] && score <= m[1] }
51
+ return "" if r.nil?
52
+ r.last.sample
53
+ end
54
+
55
+ # Parse json-based memes configuration.
56
+ # Expected format is
57
+ # "memes": {
58
+ # "0-9.9": "https://meme.com?id=awful1.gif,https://meme.com?id=awful2.gif",
59
+ # "70-89.9": "https://meme.com?id=nice.gif",
60
+ # "90-100": "https://meme.com?id=wow.gif"
61
+ # }
62
+ def range
63
+ @range ||= if @memes.nil?
64
+ []
65
+ else
66
+ rng = JSON.parse(@memes).to_h
67
+ return [] if rng.empty?
68
+ rng.map do |k, v|
69
+ next unless k.include? "-"
70
+ row = k.split("-").map(&:to_f)
71
+ row << v.split(",")
72
+ row
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end