lazylead 0.10.3 → 0.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2a3f17c4f0160f62b3faa0dd25179669f902d359961612701ab5b123ef2a7a2
4
- data.tar.gz: d2a4ba8aa4d282362268a4b8911c1af9eaf425f83db9643f10dddefb8564eb53
3
+ metadata.gz: 333915b333e6a6ed286d3ee7e5b984b30ec77745195d0a522b38453c8946bddd
4
+ data.tar.gz: 3ad06509a4714b6317a0f013f61a74e3c3de05b58c424de6faf435fe877c7079
5
5
  SHA512:
6
- metadata.gz: 8093e705ee5fe376e98755ec187b3e981dce7b55243c497debb1b1f5384df8c6cab62f60f01949affe0cf8218d0d1acc7a21c62cc935a538e18df80efe7b0d15
7
- data.tar.gz: c73d5edeede58fb972d219681456832f089ed2b08b75893c657dda86ef3c4c1d9eae47fde49e8335ea0f193b94e053744da47cea44c0c804d0e4145b95abcbab
6
+ metadata.gz: db6709844e1f8175a32fadef567cc661d469ce2fd6fbd69050726bb9d0368e41133765b730c4efb5da353c074db2d9a875e51dfffa3e408b65279a13e7f33b33
7
+ data.tar.gz: 2189335ff6852e008d489d582653de84653413396283e92724e7ae2c058549dc8a7d0d11d13f431020ec62517179bcab758b05571aa5067a614f64a4f86116d2
@@ -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.3"
35
+ s.version = "0.11.1"
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.3!
48
+ s.post_install_message = "Thanks for installing Lazylead v0.11.1!
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,12 +91,12 @@ 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"
99
100
  s.add_development_dependency "tempfile", "0.1.1"
100
- s.add_development_dependency "xcop", "0.6.2"
101
+ s.add_development_dependency "xcop", "0.6.3"
101
102
  end
@@ -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
@@ -64,7 +64,8 @@ module Lazylead
64
64
  def jira_defaults
65
65
  {
66
66
  max_results: fetch("max_results", 50),
67
- fields: jira_fields
67
+ fields: jira_fields,
68
+ limit: to_h["limit"]
68
69
  }
69
70
  end
70
71
 
@@ -142,5 +143,16 @@ module Lazylead
142
143
  def construct(field, delim: ",")
143
144
  slice(field, delim).map(&:constantize).map(&:new)
144
145
  end
146
+
147
+ # Get the value by key
148
+ # @param key
149
+ # The key to fetch value. The key might be a plain text or symbol
150
+ # @param default
151
+ # The default/alternative value if key not found
152
+ # @return The value by key or alternative/default value.
153
+ def find(key, default = nil)
154
+ return default if key.nil?
155
+ to_h[key.to_s] || to_h[key.to_sym] || default
156
+ end
145
157
  end
146
158
  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,17 +57,18 @@ 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 = []
63
64
  total = jira.Issue.jql(jql, max_results: 0)
64
65
  @log.debug "Found #{total} ticket(s) in '#{jql}'"
65
66
  loop do
66
- tickets.concat(jira.Issue.jql(jql, opts.merge(start_at: start))
67
+ tickets.concat(jira.Issue.jql(jql, range(start, opts))
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).to_i
71
+ break if (start > total) || (start >= opts.find(:limit, total).to_i)
71
72
  end
72
73
  tickets
73
74
  end
@@ -82,6 +83,26 @@ module Lazylead
82
83
 
83
84
  private
84
85
 
86
+ # Detect tickets range in remote search result.
87
+ #
88
+ # In jira you may navigate through the search result using following parameters:
89
+ # https://docs.atlassian.com/jira-software/REST/7.3.1/#agile/1.0/epic-getIssuesForEpic
90
+ # :start_at
91
+ # The starting index of the returned issues. Base index: 0. See the 'Pagination' section at
92
+ # the top of this page for more details.
93
+ # :max_results
94
+ # The maximum number of issues to return per page. Default: 50. See the 'Pagination' section
95
+ # at the top of this page for more details. Note, the total number of issues returned is
96
+ # limited by the property 'jira.search.views.default.max' in your JIRA instance. If you
97
+ # exceed this limit, your results will be truncated.
98
+ def range(start, opts)
99
+ limit = opts.find(:limit, 0).to_i
100
+ max_results = opts.find(:max_results, 50).to_i
101
+ opts[:start_at] = start
102
+ opts[:max_results] = [limit, max_results].min if limit.positive?
103
+ opts
104
+ end
105
+
85
106
  def client
86
107
  return @client if defined? @client
87
108
  @opts[:auth_type] = :basic if @opts[:auth_type].nil?
@@ -326,7 +347,7 @@ module Lazylead
326
347
  # or dashboards.
327
348
  class NoAuthJira
328
349
  extend Forwardable
329
- def_delegators :@jira, :issues, :raw
350
+ def_delegators :@jira, :issues, :raw, :max_results, :limit
330
351
 
331
352
  def initialize(url, path = "", log = Log.new)
332
353
  @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,14 +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?
54
+ opts
55
+ end
56
+
57
+ # Initialize accuracy rules and configuration for tickets verification.
58
+ def init_rules(opts)
59
+ Requires.new(__dir__).load
60
+ opts[:rules] = opts.construct("rules")
61
+ opts[:total] = opts[:rules].sum(&:score)
62
+ opts[:memes] = Memes.new(opts["memes"])
55
63
  end
56
64
  end
57
65
  end
@@ -93,7 +101,7 @@ module Lazylead
93
101
  comment << "|#{r.desc}|#{r.passed(@issue) ? '(/)' : '(-)'}|#{r.field}|"
94
102
  end
95
103
  comment << docs_link
96
- comment << ""
104
+ comment << reaction
97
105
  comment << "Posted by [lazylead v#{Lazylead::VERSION}|https://bit.ly/2NjdndS]."
98
106
  comment.join("\r\n")
99
107
  end
@@ -144,5 +152,26 @@ module Lazylead
144
152
  return @issue.reporter.id if first.nil?
145
153
  first["author"]["key"]
146
154
  end
155
+
156
+ # Add reaction meme to the ticket comment based on score.
157
+ # The meme details are represented as array, where each element is a separate line in future
158
+ # comment in jira.
159
+ #
160
+ # @todo #339/DEV Seems jira doesn't support the rendering of external images by url, thus so far
161
+ # we might have several options:
162
+ # - attach meme to ticket and make rendering using [^attach.jpg!thumbnail] option
163
+ # - have a link to meme (like it implemented now)
164
+ # The 1st option with attachment might generate multiple events in jira and spam ticket
165
+ # watchers, thus, some research & UX testing needed how to make it better.
166
+ def reaction
167
+ return [] if @opts[:memes].nil? && !@opts[:memes].enabled?
168
+ url = @opts[:memes].find(@accuracy)
169
+ return [] if url.blank?
170
+ [
171
+ "",
172
+ "Our reaction when we got the ticket with triage accuracy #{@accuracy}% is [here|#{url}].",
173
+ ""
174
+ ]
175
+ end
147
176
  end
148
177
  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