lazylead 0.3.0 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.0pdd.yml +4 -1
  3. data/.circleci/config.yml +2 -0
  4. data/.docs/accuracy.md +107 -0
  5. data/.docs/accuracy_email.jpg +0 -0
  6. data/.docs/accuracy_jira_comment.jpg +0 -0
  7. data/.docs/propagate_down.md +1 -1
  8. data/.github/dependabot.yml +6 -0
  9. data/.pdd +1 -1
  10. data/.rubocop.yml +6 -0
  11. data/bin/lazylead +9 -3
  12. data/lazylead.gemspec +5 -4
  13. data/lib/lazylead/cc.rb +12 -6
  14. data/lib/lazylead/cli/app.rb +4 -3
  15. data/lib/lazylead/exchange.rb +1 -1
  16. data/lib/lazylead/log.rb +30 -8
  17. data/lib/lazylead/model.rb +44 -18
  18. data/lib/lazylead/opts.rb +68 -0
  19. data/lib/lazylead/postman.rb +1 -1
  20. data/lib/lazylead/schedule.rb +6 -4
  21. data/lib/lazylead/smtp.rb +1 -1
  22. data/lib/lazylead/system/fake.rb +1 -1
  23. data/lib/lazylead/system/jira.rb +17 -15
  24. data/lib/lazylead/system/synced.rb +2 -1
  25. data/lib/lazylead/task/accuracy/accuracy.rb +140 -0
  26. data/lib/lazylead/task/accuracy/affected_build.rb +43 -0
  27. data/lib/lazylead/task/accuracy/requirement.rb +40 -0
  28. data/lib/lazylead/task/alert.rb +8 -6
  29. data/lib/lazylead/task/confluence_ref.rb +4 -3
  30. data/lib/lazylead/task/echo.rb +4 -0
  31. data/lib/lazylead/task/fix_version.rb +10 -6
  32. data/lib/lazylead/task/missing_comment.rb +7 -5
  33. data/lib/lazylead/task/propagate_down.rb +11 -3
  34. data/lib/lazylead/task/savepoint.rb +1 -1
  35. data/lib/lazylead/task/touch.rb +104 -0
  36. data/lib/lazylead/version.rb +1 -1
  37. data/lib/messages/accuracy.erb +118 -0
  38. data/lib/messages/svn_touch.erb +147 -0
  39. data/readme.md +19 -18
  40. data/test/lazylead/cc_test.rb +22 -2
  41. data/test/lazylead/cli/app_test.rb +2 -2
  42. data/test/lazylead/exchange_test.rb +3 -3
  43. data/test/lazylead/model_test.rb +4 -4
  44. data/test/lazylead/opts_test.rb +70 -0
  45. data/test/lazylead/postman_test.rb +1 -1
  46. data/test/lazylead/smtp_test.rb +1 -1
  47. data/test/lazylead/system/jira_test.rb +35 -1
  48. data/test/lazylead/task/accuracy/accuracy_test.rb +73 -0
  49. data/test/lazylead/task/accuracy/affected_build_test.rb +42 -0
  50. data/test/lazylead/task/assignee_alert_test.rb +2 -2
  51. data/test/lazylead/task/duedate_test.rb +37 -27
  52. data/test/lazylead/task/fix_version_test.rb +9 -6
  53. data/test/lazylead/task/missing_comment_test.rb +11 -9
  54. data/test/lazylead/task/propagate_down_test.rb +4 -2
  55. data/test/lazylead/task/touch_test.rb +61 -0
  56. data/test/test.rb +9 -0
  57. data/upgrades/sqlite/999.testdata.sql +2 -1
  58. metadata +40 -8
  59. data/todo.yml +0 -6
@@ -26,6 +26,7 @@ require "json"
26
26
  require "faraday"
27
27
  require_relative "../system/jira"
28
28
  require_relative "../log"
29
+ require_relative "../opts"
29
30
  require_relative "../confluence"
30
31
 
31
32
  module Lazylead
@@ -35,14 +36,14 @@ module Lazylead
35
36
  # @todo #/DEV Support sub-task for link search. Potentially, the issue
36
37
  # might have sub-tasks where discussion ongoing.
37
38
  class ConfluenceRef
38
- def initialize(log = Log::NOTHING)
39
+ def initialize(log = Log.new)
39
40
  @log = log
40
41
  end
41
42
 
42
43
  def run(sys, _, opts)
43
44
  confluences = confluences(opts)
44
45
  return if confluences.empty?
45
- sys.issues(opts["jql"])
46
+ sys.issues(opts["jql"], opts.jira_defaults)
46
47
  .map { |i| Link.new(i, sys, confluences) }
47
48
  .each(&:fetch_links)
48
49
  .select(&:need_link?)
@@ -50,7 +51,7 @@ module Lazylead
50
51
  end
51
52
 
52
53
  def confluences(opts)
53
- return [] if opts["confluences"].nil? || opts["confluences"].blank?
54
+ return [] if opts.blank? "confluences"
54
55
  JSON.parse(opts["confluences"], object_class: OpenStruct)
55
56
  .map { |c| Confluence.new(c) }
56
57
  end
@@ -30,6 +30,10 @@ module Lazylead
30
30
  # Copyright:: Copyright (c) 2019-2020 Yurii Dubinka
31
31
  # License:: MIT
32
32
  class Echo
33
+ def initialize(log = Log.new)
34
+ @log = log
35
+ end
36
+
33
37
  def run(_, _, _)
34
38
  self.class.to_s
35
39
  end
@@ -23,24 +23,28 @@
23
23
  # OR OTHER DEALINGS IN THE SOFTWARE.
24
24
 
25
25
  require "date"
26
- require_relative "../system/jira"
27
26
  require_relative "../log"
27
+ require_relative "../opts"
28
+ require_relative "../system/jira"
28
29
 
29
30
  module Lazylead
30
31
  module Task
31
32
  # @todo #/DEV Each task should verify input arguments.
32
33
  # The common API should be provided for each task.
33
34
  class FixVersion
34
- def initialize(log = Log::NOTHING)
35
+ def initialize(log = Log.new)
35
36
  @log = log
36
37
  end
37
38
 
38
39
  def run(sys, postman, opts)
39
- allowed = opts["allowed"].split(",").map(&:strip).reject(&:blank?)
40
+ allowed = opts.slice("allowed", ",")
41
+ issues = sys.issues(
42
+ opts["jql"], opts.jira_defaults.merge(expand: "changelog")
43
+ )
44
+ return if issues.empty?
40
45
  postman.send opts.merge(
41
- versions: sys.issues(opts["jql"], expand: "changelog")
42
- .map { |i| Version.new(i, allowed) }
43
- .select(&:changed?)
46
+ versions: issues.map { |i| Version.new(i, allowed) }
47
+ .select(&:changed?)
44
48
  )
45
49
  end
46
50
  end
@@ -23,6 +23,7 @@
23
23
  # OR OTHER DEALINGS IN THE SOFTWARE.
24
24
 
25
25
  require_relative "../log"
26
+ require_relative "../opts"
26
27
  require_relative "../postman"
27
28
 
28
29
  module Lazylead
@@ -36,16 +37,17 @@ module Lazylead
36
37
  # nobody mentioned in comment the ftp location for recorded session.
37
38
  # Such cases needs to be reported.
38
39
  class MissingComment
39
- def initialize(log = Log::NOTHING)
40
+ def initialize(log = Log.new)
40
41
  @log = log
41
42
  end
42
43
 
43
44
  def run(sys, postman, opts)
44
- opts["details"] = "text '#{opts['text']}'" if opts["details"].blank?
45
+ opts["details"] = "text '#{opts['text']}'" if opts.blank? "details"
46
+ issues = sys.issues(opts["jql"], opts.jira_defaults)
47
+ return if issues.empty?
45
48
  postman.send opts.merge(
46
- comments: sys.issues(opts["jql"])
47
- .map { |i| Comments.new(i, sys) }
48
- .reject { |c| c.body? opts["text"] }
49
+ comments: issues.map { |i| Comments.new(i, sys) }
50
+ .reject { |c| c.body? opts["text"] }
49
51
  )
50
52
  end
51
53
  end
@@ -23,6 +23,7 @@
23
23
  # OR OTHER DEALINGS IN THE SOFTWARE.
24
24
 
25
25
  require_relative "../log"
26
+ require_relative "../opts"
26
27
  require_relative "../version"
27
28
  require_relative "../system/jira"
28
29
 
@@ -39,15 +40,22 @@ module Lazylead
39
40
  # - apply diff to sub-tasks
40
41
  # - make a comment to sub-task with clarification.
41
42
  class PropagateDown
42
- def initialize(log = Log::NOTHING)
43
+ def initialize(log = Log.new)
43
44
  @log = log
44
45
  end
45
46
 
46
47
  # @todo #/DEV Define a new module Lazylead::Task with basic methods like
47
48
  # split, groupBy(assignee, reporter, etc), blank?
48
49
  def run(sys, _, opts)
49
- fields = opts["fields"].split(",").map(&:strip).reject(&:blank?)
50
- sys.issues(opts["jql"], fields: ["subtasks"] + fields)
50
+ fields = opts.slice("propagate", ",")
51
+ sys.issues(
52
+ opts["jql"],
53
+ {
54
+ expand: "changelog",
55
+ max_results: opts.fetch("max_results", 50),
56
+ fields: ["subtasks"] + opts.jira_fields
57
+ }
58
+ )
51
59
  .map { |i| Parent.new(i, sys, fields) }
52
60
  .select(&:subtasks?)
53
61
  .each(&:fetch)
@@ -34,7 +34,7 @@ module Lazylead
34
34
  # Send current configuration to admin user.
35
35
  #
36
36
  class Savepoint
37
- def initialize(log = Log::NOTHING)
37
+ def initialize(log = Log.new)
38
38
  @log = log
39
39
  end
40
40
 
@@ -0,0 +1,104 @@
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 "nokogiri"
26
+ require "active_support/core_ext/hash/conversions"
27
+ require_relative "../salt"
28
+ require_relative "../opts"
29
+
30
+ module Lazylead
31
+ module Task
32
+ #
33
+ # Send notification about modification of critical files in svn repo.
34
+ #
35
+ class SvnTouch
36
+ def initialize(log = Log.new)
37
+ @log = log
38
+ end
39
+
40
+ def run(_, postman, opts)
41
+ files = opts.slice("files", ",")
42
+ commits = touch(files, opts)
43
+ postman.send(opts.merge(entries: commits)) unless commits.empty?
44
+ end
45
+
46
+ # Return all svn commits for a particular date range, which are touching
47
+ # somehow the critical files within the svn repo.
48
+ def touch(files, opts)
49
+ xpath = files.map { |f| "contains(text(),\"#{f}\")" }.join(" or ")
50
+ svn_log(opts).xpath("//logentry[paths/path[#{xpath}]]")
51
+ .map(&method(:to_entry))
52
+ .each do |e|
53
+ if e.paths.path.respond_to? :delete_if
54
+ e.paths.path.delete_if { |p| files.none? { |f| p.include? f } }
55
+ end
56
+ end
57
+ end
58
+
59
+ # Return all svn commits for particular date range in repo
60
+ def svn_log(opts)
61
+ now = if opts.key? "now"
62
+ DateTime.parse(opts["now"])
63
+ else
64
+ DateTime.now
65
+ end
66
+ start = (now.to_time - opts["period"].to_i).to_datetime
67
+ cmd = [
68
+ "svn log --no-auth-cache",
69
+ "--username #{decrypt(opts['svn_user'])}",
70
+ "--password #{decrypt(opts['svn_password'])}",
71
+ "--xml -v -r {#{start}}:{#{now}} #{opts['svn_url']}"
72
+ ]
73
+ raw = `#{cmd.join(" ")}`
74
+ Nokogiri.XML(raw, nil, "UTF-8")
75
+ end
76
+
77
+ # Decrypt text using cryptography salt
78
+ def decrypt(text, sid = "svn_salt")
79
+ return Salt.new(sid).decrypt(text) if ENV.key? sid
80
+ text
81
+ end
82
+
83
+ # Convert single revision(XML text) to entry object.
84
+ # Entry object is a simple ruby struct object.
85
+ def to_entry(xml)
86
+ e = to_struct(Hash.from_xml(xml.to_s.strip)).logentry
87
+ if e.paths.path.respond_to? :each
88
+ e.paths.path.each(&:strip!)
89
+ else
90
+ e.paths.path.strip!
91
+ end
92
+ e
93
+ end
94
+
95
+ # Make a simple ruby struct object from hash hierarchically,
96
+ # considering nested hash(es) (if applicable)
97
+ def to_struct(hsh)
98
+ OpenStruct.new(
99
+ hsh.transform_values { |v| v.is_a?(Hash) ? to_struct(v) : v }
100
+ )
101
+ end
102
+ end
103
+ end
104
+ end
@@ -23,5 +23,5 @@
23
23
  # OR OTHER DEALINGS IN THE SOFTWARE.
24
24
 
25
25
  module Lazylead
26
- VERSION = "0.3.0"
26
+ VERSION = "0.4.3"
27
27
  end
@@ -0,0 +1,118 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <style> /* CSS styles taken from https://github.com/yegor256/tacit */
5
+ th {
6
+ font-weight: 600
7
+ }
8
+
9
+ table tr {
10
+ border-bottom-width: 2.16px
11
+ }
12
+
13
+ table tr th {
14
+ border-bottom-width: 2.16px
15
+ }
16
+
17
+ table tr td, table tr th {
18
+ overflow: hidden;
19
+ padding: 5.4px 3.6px;
20
+ line-height: 14px;
21
+ }
22
+
23
+ #summary {
24
+ text-align: left;
25
+ }
26
+
27
+ .auto {
28
+ min-width: auto;
29
+ white-space: nowrap;
30
+ }
31
+
32
+ a {
33
+ color: #275a90;
34
+ text-decoration: none
35
+ }
36
+
37
+ a:hover {
38
+ text-decoration: underline
39
+ }
40
+
41
+ * {
42
+ border: 0;
43
+ border-collapse: separate;
44
+ border-spacing: 0;
45
+ box-sizing: border-box;
46
+ margin: 0;
47
+ max-width: 100%;
48
+ padding: 0;
49
+ vertical-align: baseline;
50
+ font-family: system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
51
+ font-size: 13px;
52
+ font-stretch: normal;
53
+ font-style: normal;
54
+ font-weight: 400;
55
+ line-height: 29.7px
56
+ }
57
+
58
+ html, body {
59
+ width: 100%
60
+ }
61
+
62
+ html {
63
+ height: 100%
64
+ }
65
+
66
+ body {
67
+ background: #fff;
68
+ color: #1a1919;
69
+ padding: 36px
70
+ }
71
+ </style>
72
+ <title>Accuracy</title>
73
+ </head>
74
+ <body>
75
+ <p>Hi,</p>
76
+ <p>The triage score and accuracy posted to the following tickets::</p>
77
+ <table summary="table with tickets triage score">
78
+ <tr>
79
+ <th id="key">Key</th>
80
+ <th id="duedate">Due date</th>
81
+ <th id="priority">Priority</th>
82
+ <th id="score">Score</th>
83
+ <th id="accuracy">Accuracy</th>
84
+ <th id="reporter">Reporter</th>
85
+ <th id="summary">Summary</th>
86
+ </tr>
87
+ <% tickets.sort_by { |s| s.issue.fields["priority"]["id"].to_i }
88
+ .each do |score| %>
89
+ <tr>
90
+ <td>
91
+ <div class="auto">
92
+ <a href="<%= score.issue.url %>"><%= score.issue.key %></a>
93
+ </div>
94
+ </td>
95
+ <td>
96
+ <div class="auto"><%= score.issue.duedate %></div>
97
+ </td>
98
+ <td><%= score.issue.priority %></td>
99
+ <td>
100
+ <span style="color: <%= score.color %>"><%= score.score %></span>
101
+ </td>
102
+ <td>
103
+ <span style="color: <%= score.color %>"><%= score.accuracy %>%</span>
104
+ </td>
105
+ <td>
106
+ <div class="auto">
107
+ <%= score.issue.reporter.name %>
108
+ </div>
109
+ </td>
110
+ <td><%= score.issue.summary %></td>
111
+ </tr>
112
+ <% end %>
113
+ </table>
114
+ <p>Posted by
115
+ <a href="https://github.com/dgroup/lazylead">lazylead v<%= version %></a>.
116
+ </p>
117
+ </body>
118
+ </html>
@@ -0,0 +1,147 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <style>
5
+ /* CSS styles taken from https://github.com/yegor256/tacit */
6
+ th {
7
+ font-weight: 600;
8
+ text-align: left;
9
+ }
10
+
11
+ td {
12
+ vertical-align: top;
13
+ }
14
+
15
+ table tr {
16
+ border-bottom-width: 2.16px
17
+ }
18
+
19
+ table tr th {
20
+ border-bottom-width: 2.16px
21
+ }
22
+
23
+ table tr td, table tr th {
24
+ overflow: hidden; /*padding: 5.4px 3.6px*/
25
+ padding-left: 2px;
26
+ padding-right: 2px;
27
+ }
28
+
29
+ td:nth-child(3) {
30
+ min-width: 120px;
31
+ max-width: 200px;
32
+ }
33
+
34
+ td:nth-child(4) {
35
+ min-width: 300px;
36
+ max-width: 500px;
37
+ }
38
+
39
+ pre, code, kbd, samp, var, output {
40
+ font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
41
+ font-size: 14.4px
42
+ }
43
+
44
+ pre code {
45
+ background: none;
46
+ border: 0;
47
+ line-height: 29.7px;
48
+ padding: 0
49
+ }
50
+
51
+ code, kbd {
52
+ background: #daf1e0;
53
+ border-radius: 3.6px;
54
+ color: #2a6f3b;
55
+ display: inline-block;
56
+ line-height: 18px;
57
+ padding: 3.6px 6.3px 2.7px
58
+ }
59
+
60
+ a {
61
+ color: #275a90;
62
+ text-decoration: none
63
+ }
64
+
65
+ a:hover {
66
+ text-decoration: underline
67
+ }
68
+
69
+ * {
70
+ border: 0;
71
+ border-collapse: separate;
72
+ border-spacing: 0;
73
+ box-sizing: border-box;
74
+ margin: 0;
75
+ max-width: 100%;
76
+ padding: 0;
77
+ vertical-align: baseline;
78
+ font-family: system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
79
+ font-size: 13px;
80
+ font-stretch: normal;
81
+ font-style: normal;
82
+ font-weight: 400;
83
+ line-height: 29.7px
84
+ }
85
+
86
+ html, body {
87
+ width: 100%
88
+ }
89
+
90
+ html {
91
+ height: 100%
92
+ }
93
+
94
+ body {
95
+ background: #fff;
96
+ color: #1a1919;
97
+ padding: 36px
98
+ }
99
+
100
+ .msg {
101
+ background: #eff6e8;
102
+ border-radius: 3.6px;
103
+ color: #2a6f3b;
104
+ display: inline-block;
105
+ line-height: 18px;
106
+ padding: 3.6px 6.3px 2.7px
107
+ }
108
+ </style>
109
+ <title>SVN touch</title>
110
+ </head>
111
+ <body>
112
+ <p>Hi,</p>
113
+ <p>The critical file(s) <code><%= files %></code> have been changed recently:</p>
114
+ <table summary="table with svn commits where critical files have been changed">
115
+ <tr>
116
+ <th id="rev">Revision</th>
117
+ <th id="author">Author</th>
118
+ <th id="when">When</th>
119
+ <th id="files">File(s)</th>
120
+ <th id="commit_msg">Commit</th>
121
+ </tr>
122
+ <% entries.each do |e| %>
123
+ <tr>
124
+ <td><a href="<%= commit_url %><%= e.revision %>"><%= e.revision %></a></td>
125
+ <td><a href="<%= user %><%= e.author %>"><%= e.author %></a></td>
126
+ <td><%= Time.strptime(e.date, "%Y-%m-%dT%H:%M").strftime("%Y-%m-%d %H:%M") %></td>
127
+ <td>
128
+ <% if e.paths.path.respond_to? :join %>
129
+ <%= e.paths
130
+ .path
131
+ .map { |file| "<a href='#{commit_url}#{e.revision}'>#{file}</a>" }
132
+ .join("<br/>") %>
133
+ <% else %>
134
+ <a href="<%= commit_url %><%= e.revision %>"><%= e.paths.path %></a>
135
+ <% end %>
136
+ </td>
137
+ <td>
138
+ <div class="msg"><%= e.msg %></div>
139
+ </td>
140
+ </tr>
141
+ <% end %>
142
+ </table>
143
+ <p>Posted by
144
+ <a href="https://github.com/dgroup/lazylead">lazylead v<%= version %></a>.
145
+ </p>
146
+ </body>
147
+ </html>