lazylead 0.10.3 → 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
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