lazylead 0.8.1 → 0.9.2

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.
@@ -36,7 +36,7 @@ module Lazylead
36
36
  end
37
37
 
38
38
  def run(sys, postman, opts)
39
- assignments = sys.issues(opts["jql"])
39
+ assignments = sys.issues(opts["jql"], opts.jira_defaults)
40
40
  .group_by(&:assignee)
41
41
  .map { |user, tasks| [user.id, Teammate.new(user, tasks)] }
42
42
  .to_h
@@ -71,6 +71,11 @@ module Lazylead
71
71
  def to_s
72
72
  "#{id} has #{total} tasks"
73
73
  end
74
+
75
+ def sprints(*label)
76
+ return @tasks.group_by(&:sprint).sort if label.empty? || label.nil?
77
+ @tasks.group_by { |t| t.sprint(label) }.sort
78
+ end
74
79
  end
75
80
 
76
81
  # The teammate without tasks.
@@ -0,0 +1,111 @@
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 "date"
26
+ require_relative "../log"
27
+ require_relative "../opts"
28
+ require_relative "../system/jira"
29
+
30
+ module Lazylead
31
+ module Task
32
+ #
33
+ # Email alerts about due date modification by not-authorized persons.
34
+ #
35
+ # Author:: Yurii Dubinka (yurii.dubinka@gmail.com)
36
+ # Copyright:: Copyright (c) 2019-2020 Yurii Dubinka
37
+ # License:: MIT
38
+ class Micromanager
39
+ def initialize(log = Log.new)
40
+ @log = log
41
+ end
42
+
43
+ def run(sys, postman, opts)
44
+ allowed = opts.slice "allowed", ","
45
+ dues = sys.issues(opts["jql"], opts.jira_defaults.merge(expand: "changelog"))
46
+ .map { |i| Due.new(i, allowed, since(opts)) }
47
+ .select(&:illegal?)
48
+ .reject(&:obsolete?)
49
+ return if dues.empty?
50
+ postman.send opts.merge(dues: dues)
51
+ end
52
+
53
+ # Detect history period where search should start.
54
+ #
55
+ # opts["period"] The default period for past is 1 day (86400 seconds).
56
+ # So, if now 2017-04-06 15:50:58.674+0000
57
+ # it returns 2017-04-05 15:50:58 +0000
58
+ #
59
+ # opts["now"] The current time for unit tests.
60
+ # If absent the "Time.now" is used.
61
+ #
62
+ def since(opts)
63
+ @since ||= if opts.key? "now"
64
+ Time.parse(opts["now"])
65
+ else
66
+ Time.now - opts.fetch("period", "86400").to_i
67
+ end
68
+ end
69
+ end
70
+
71
+ # Instance of "Due" history item for the particular ticket.
72
+ class Due
73
+ attr_reader :issue, :when
74
+
75
+ def initialize(issue, allowed, since)
76
+ @issue = issue
77
+ @allowed = allowed
78
+ @since = since
79
+ end
80
+
81
+ # Gives true when last change of "Due Date" field was done
82
+ # by not authorized person.
83
+ def illegal?
84
+ return false if @issue.assignee.id.eql?(last.id)
85
+ @allowed.none? { |a| a.eql? last.id }
86
+ end
87
+
88
+ # Give true when "Due Date" changes happens in past and its alert already sent.
89
+ def obsolete?
90
+ @when < @since
91
+ end
92
+
93
+ # Detect details about last change of "Due Date" to non-null value
94
+ def last
95
+ @last ||= begin
96
+ dd = @issue.history
97
+ .reverse
98
+ .find { |h| h["items"].any? { |i| i["field"] == "duedate" } }
99
+ if dd.nil? && !@issue.duedate.nil?
100
+ @when = @issue["created"]
101
+ dd = @issue.reporter
102
+ else
103
+ @when = dd["created"]
104
+ dd = Lazylead::User.new(dd["author"])
105
+ end
106
+ dd
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -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 "zip"
25
26
  require "tempfile"
26
27
  require "nokogiri"
27
28
  require "backtrace"
@@ -49,6 +50,7 @@ module Lazylead
49
50
  "-r#{opts['since_rev']}:HEAD #{opts['svn_url']}"
50
51
  ]
51
52
  stdout = `#{cmd.join(" ")}`
53
+ stdout.scrub!
52
54
  send_email postman, opts.merge(stdout: stdout) unless stdout.blank?
53
55
  end
54
56
 
@@ -58,19 +60,39 @@ module Lazylead
58
60
  def send_email(postman, opts)
59
61
  Dir.mktmpdir do |dir|
60
62
  name = "svn-log-#{Date.today.strftime('%d-%b-%Y')}.html"
61
- f = File.open(File.join(dir, name), "w")
62
63
  begin
63
- f.write opts.msg_body("template-attachment")
64
- f.close
65
- postman.send opts.merge(attachments: [f.path])
64
+ postman.send opts.merge(attachments: [to_f(File.join(dir, name), opts)])
66
65
  ensure
67
- File.delete(f)
66
+ FileUtils.rm_rf("#{dir}/*")
68
67
  end
69
68
  rescue StandardError => e
70
69
  @log.error "ll-010: Can't send an email '#{opts['subject']}' to #{opts['to']} due to " \
71
70
  "#{Backtrace.new(e)}'"
72
71
  end
73
72
  end
73
+
74
+ # Wrap attachment content to a *.zip file and archive.
75
+ # to_f('my-content.html', opts) => my-content.html.zip
76
+ #
77
+ # You may disable archiving option by passing option *no_archive*
78
+ # to_f('my-content.html', "no_archive" => true)
79
+ def to_f(path, opts)
80
+ if opts.key? "no_archive"
81
+ f = File.open(path, "w")
82
+ body = opts.msg_body("template-attachment")
83
+ else
84
+ f = File.new("#{path}.zip", "wb")
85
+ bytes = Zip::OutputStream.write_buffer do |zio|
86
+ zio.put_next_entry(File.basename(path))
87
+ zio.write opts.msg_body("template-attachment")
88
+ end
89
+ bytes.rewind # reposition buffer pointer to the beginning
90
+ body = bytes.sysread
91
+ end
92
+ f.write body
93
+ f.close
94
+ f.path
95
+ end
74
96
  end
75
97
  end
76
98
  end
@@ -23,5 +23,5 @@
23
23
  # OR OTHER DEALINGS IN THE SOFTWARE.
24
24
 
25
25
  module Lazylead
26
- VERSION = "0.8.1"
26
+ VERSION = "0.9.2"
27
27
  end
@@ -0,0 +1,122 @@
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
+ }
9
+
10
+ table tr {
11
+ border-bottom-width: 2.16px
12
+ }
13
+
14
+ table tr th {
15
+ border-bottom-width: 2.16px
16
+ }
17
+
18
+ table tr td, table tr th {
19
+ overflow: hidden;
20
+ padding: 5.4px 3.6px
21
+ }
22
+
23
+ a {
24
+ color: #275a90;
25
+ text-decoration: none
26
+ }
27
+
28
+ a:hover {
29
+ text-decoration: underline
30
+ }
31
+
32
+ pre, code, kbd, samp, var, output {
33
+ font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
34
+ font-size: 13px
35
+ }
36
+
37
+ pre code {
38
+ background: none;
39
+ border: 0;
40
+ line-height: 29.7px;
41
+ padding: 0
42
+ }
43
+
44
+ code, kbd {
45
+ background: #daf1e0;
46
+ border-radius: 3.6px;
47
+ color: #2a6f3b;
48
+ display: inline-block;
49
+ line-height: 18px;
50
+ padding: 3.6px 6.3px 2.7px
51
+ }
52
+
53
+ * {
54
+ border: 0;
55
+ border-collapse: separate;
56
+ border-spacing: 0;
57
+ box-sizing: border-box;
58
+ margin: 0;
59
+ max-width: 100%;
60
+ padding: 0;
61
+ vertical-align: baseline;
62
+ font-family: system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
63
+ font-size: 13px;
64
+ font-stretch: normal;
65
+ font-style: normal;
66
+ font-weight: 400;
67
+ line-height: 29.7px
68
+ }
69
+
70
+ html, body {
71
+ width: 100%
72
+ }
73
+
74
+ html {
75
+ height: 100%
76
+ }
77
+
78
+ body {
79
+ background: #fff;
80
+ color: #1a1919;
81
+ padding: 36px
82
+ }
83
+ </style>
84
+ <title>Not authorized "Assignee" change</title>
85
+ </head>
86
+ <body>
87
+ <p>Hi,</p>
88
+
89
+ <p>The <span style='font-weight:bold'>'Duedate'</span> for the following
90
+ ticket(s) changed by not authorized person(s):</p>
91
+ <table summary="ticket(s) where duedate changed">
92
+ <tr>
93
+ <th id="key">Key</th>
94
+ <th id="priority">Priority</th>
95
+ <th id="when">When</th>
96
+ <th id="who">Who</th>
97
+ <th id="to">To</th>
98
+ <th id="assignee">Assignee</th>
99
+ <th id="reporter">Reporter</th>
100
+ <th id="summary">Summary</th>
101
+ </tr>
102
+ <% dues.each do |d| %>
103
+ <tr>
104
+ <td><a href='<%= d.issue.url %>'><%= d.issue.key %></a></td>
105
+ <td><%= d.issue.priority %></td>
106
+ <td><%= d.when.to_date %></td>
107
+ <td><span style='color: red'><%= d.last.name %></span> (<%= d.last.id %>)</td>
108
+ <td><span style='color: red'><%= d.issue.duedate %></span></td>
109
+ <td><%= d.issue.assignee.name %></td>
110
+ <td><%= d.issue.reporter.name %></td>
111
+ <td><%= d.issue.summary %></td>
112
+ </tr>
113
+ <% end %>
114
+ </table>
115
+
116
+ <p>Authorized person(s) are: <code><%= allowed %></code>.</p>
117
+
118
+ <p>Posted by
119
+ <a href="https://github.com/dgroup/lazylead">lazylead v<%= version %></a>.
120
+ </p>
121
+ </body>
122
+ </html>
@@ -69,10 +69,9 @@
69
69
  <body>
70
70
  <table summary="tickets">
71
71
  <tr>
72
- <th id="uid">ID</th>
73
72
  <th id="name">User</th>
74
73
  <th id="total">
75
- <a href="https://jira.spring.io/issues/?jql=<%= CGI.escape(jql) %>">Assigned From</a>
74
+ <a href="<%= search_link %><%= CGI.escape(jql) %>">Assigned From</a>
76
75
  </th>
77
76
  <th id="duedate">Next Due date</th>
78
77
  </tr>
@@ -83,17 +82,19 @@
83
82
  <tr>
84
83
  <% end %>
85
84
  <td>
86
- <div class="auto"><a href="<%= user_link %>"><%= teammate %></a></div>
87
- </td>
88
- <td>
89
- <div class="auto"><%= assignment.name %></div>
85
+ <div class="auto"><a href="<%= user_link %><%= teammate %>"><%= assignment.name %></a></div>
90
86
  </td>
91
87
  <td>
92
88
  <div class="auto">
93
89
  <% if assignment.free? %>
94
90
  <span style="color: red">0</span>
95
91
  <% else %>
96
- <a href="https://jira.spring.io/issues/?jql=<%= CGI.escape("#{jql} and assignee=#{teammate}") %>"><%= assignment.size %></a>
92
+ <% assignment.sprints(defined?(sprint) ? sprint : "customfield_10480").each do |s| %>
93
+ <a href="<%= search_link %><%= CGI.escape("#{jql} and assignee=#{teammate}") %>">
94
+ <%= s.first.blank? ? "No sprint" : s.first %>: <%= s.last.size %>
95
+ </a>
96
+ <br/>
97
+ <% end %>
97
98
  <% end %>
98
99
  </div>
99
100
  </td>
data/readme.md CHANGED
@@ -48,13 +48,15 @@ Join our telegram group [lazylead.org](https://t.me/lazyleads) for discussions.
48
48
  | Create a meeting(s) automatically in case some tickets appeared (group by assignee/reporters/component/ticket type/etc) | ⌛ | ⌛ | ⌛ | ❌ |
49
49
  | Propogate fields from parent tickets to sub-tasks | ✅ | ⌛ | ⌛ | ❌ |
50
50
  | Notify about tickets without comments with expected text | ✅ | ⌛ | ⌛ | ❌ |
51
- | Notify about team loading (no tasks on teammates) | ✅ | ⌛ | ⌛ | ❌ |
51
+ | [Notify about team loading (no tasks on teammates)](lib/lazylead/task/loading.rb) | ✅ | ⌛ | ⌛ | ❌ |
52
52
  | Notify about tickets matches predefined multiple conditions | ✅ | ⌛ | ⌛ | ❌ |
53
53
  | Link automatically the ticket and Confluence page if link found in ticket's comments/description | ✅ | ⌛ | ⌛ | ❌ |
54
54
  | Notify about tickets assigned to your team members not by effective managers| ✅ | ⌛ | ⌛ | ❌ |
55
55
  | Notify about modifications of important files in VCS | ❌ | ⌛ | ❌ | ✅ |
56
56
  | Notify about diff changes for past X period in VCS | ❌ | ⌛ | ❌ | ✅ |
57
57
  | Notify about changes with some text for past X period in VCS | ❌ | ⌛ | ❌ | ✅ |
58
+ | [Notify when someone outside of your team changed the due date on tickets for your team](lib/lazylead/task/micromanager.rb)| ✅ | ⌛ | ❌ | ❌ |
59
+ | [Notify when someone outside of your team assigned a ticket directly to the developer](lib/lazylead/task/assignment.rb)| ✅ | ⌛ | ❌ | ❌ |
58
60
 
59
61
  | Integration | Type | Status |
60
62
  | :---------------------------------------------------- | :-----------: | :----: |
@@ -65,7 +67,7 @@ Join our telegram group [lazylead.org](https://t.me/lazyleads) for discussions.
65
67
  | calendar.google.com | Calendar | ⌛ |
66
68
  | slack.com | Notifications | ⌛ |
67
69
 
68
- ✅ - implemented, ⌛ - planned, 🌵 - implemented, but not tested, ❌ - not supported by ticketing system.
70
+ ✅ - implemented, ⌛ - planned, 🌵 - implemented, but not tested, ❌ - not supported/planned.
69
71
 
70
72
  New ideas, bugs, suggestions or questions are welcome [via GitHub issues](https://github.com/dgroup/lazylead/issues/new)!
71
73
 
@@ -23,6 +23,7 @@
23
23
  # OR OTHER DEALINGS IN THE SOFTWARE.
24
24
 
25
25
  require_relative "../../../test"
26
+ require_relative "../../../../lib/lazylead/system/jira"
26
27
  require_relative "../../../../lib/lazylead/task/accuracy/accuracy"
27
28
  require_relative "../../../../lib/lazylead/task/accuracy/affected_build"
28
29
 
@@ -69,5 +70,15 @@ module Lazylead
69
70
  )
70
71
  ).evaluate.comment
71
72
  end
73
+
74
+ test "detect non-system reporter" do
75
+ assert_equal "grussell",
76
+ Score.new(
77
+ NoAuthJira.new("https://jira.spring.io")
78
+ .issues("key=INT-4116", expand: "changelog")
79
+ .first,
80
+ Opts.new("system-users" => "abilan")
81
+ ).reporter
82
+ end
72
83
  end
73
84
  end
@@ -0,0 +1,140 @@
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 "../../../test"
26
+ require_relative "../../../../lib/lazylead/task/accuracy/screenshots"
27
+ require_relative "../../../../lib/lazylead/system/jira"
28
+
29
+ module Lazylead
30
+ class ScreenshotsTest < Lazylead::Test
31
+ test "issue has two .png files with reference in description" do
32
+ assert Screenshots.new.passed(
33
+ OpenStruct.new(
34
+ description: "Hi,\n here are snapshots !img1.jpg|thumbnail!\n!img2.jpg|thumbnail!\n",
35
+ fields: {
36
+ "description" => "Hi,\n here are snapshots !img1.jpg|thumbnail!\n!img2.jpg|thumbnail!\n"
37
+ },
38
+ attachments: [
39
+ OpenStruct.new("filename" => "img1.jpg"),
40
+ OpenStruct.new("filename" => "img2.jpg")
41
+ ]
42
+ )
43
+ )
44
+ end
45
+
46
+ test "issue has several .png attachments mentioned using !xxx|thumbnail! option" do
47
+ assert Screenshots.new.passed(
48
+ NoAuthJira.new("https://jira.spring.io")
49
+ .issues("key=SPR-15729", fields: %w[attachment description])
50
+ .first
51
+ )
52
+ end
53
+
54
+ test "issue has no .png file however minimum 1 are required" do
55
+ refute Screenshots.new.passed(
56
+ OpenStruct.new(
57
+ description: "Hi,\n here are snapshots !img1.zip!\n",
58
+ fields: { "description" => "Hi,\n here are snapshots !img1.zip!\n" },
59
+ attachments: [
60
+ OpenStruct.new("filename" => "img1.jpg"),
61
+ OpenStruct.new("filename" => "img2.jpg")
62
+ ]
63
+ )
64
+ )
65
+ end
66
+
67
+ test "issue has two .png files with reference in description but with extension mismatch" do
68
+ refute Screenshots.new.passed(
69
+ OpenStruct.new(
70
+ description: "Hi,\n here are snapshots !img1.jpg|thumbnail!\n!img2.jpg|thumbnail!\n",
71
+ fields: {
72
+ "description" => "Hi,\n here are snapshots !img1.jpg|thumbnail!\n!img2.jpg|thumbnail!\n"
73
+ },
74
+ attachments: [
75
+ OpenStruct.new("filename" => "img1.JPG"),
76
+ OpenStruct.new("filename" => "img2.jpg")
77
+ ]
78
+ )
79
+ )
80
+ end
81
+
82
+ test "issue has two .png file in description but three .png in attachments" do
83
+ assert Screenshots.new.passed(
84
+ OpenStruct.new(
85
+ description: "Hi,\n here are snapshots !img1.JPG|thumbnail!\n!img2.jpg|thumbnail!\n",
86
+ fields: {
87
+ "description" => "Hi,\n here are snapshots !img1.JPG|thumbnail!\n!img2.jpg|thumbnail!\n"
88
+ },
89
+ attachments: [
90
+ OpenStruct.new("filename" => "img1.JPG"),
91
+ OpenStruct.new("filename" => "img2.jpg"),
92
+ OpenStruct.new("filename" => "img3.jpg")
93
+ ]
94
+ )
95
+ )
96
+ end
97
+
98
+ test "issue has two .png files with reference in description without thumbnail" do
99
+ assert Screenshots.new.passed(
100
+ OpenStruct.new(
101
+ description: "Hi,\n here are snapshots !img1.jpg!\n!img2.jpg!\n",
102
+ fields: { "description" => "Hi,\n here are snapshots !img1.jpg!\n!img2.jpg!\n" },
103
+ attachments: [
104
+ OpenStruct.new("filename" => "img1.jpg"),
105
+ OpenStruct.new("filename" => "img2.jpg")
106
+ ]
107
+ )
108
+ )
109
+ end
110
+
111
+ test "issue has two .png files with reference in description but absent in attachments" do
112
+ refute Screenshots.new.passed(
113
+ OpenStruct.new(
114
+ description: "Hi,\n here are snapshots !img1.jpg!\n!img2.jpg!\n",
115
+ fields: {
116
+ "description" => "Hi,\n here are snapshots !img1.jpg!\n!img2.jpg!\n"
117
+ },
118
+ attachments: [
119
+ OpenStruct.new("filename" => "img3.jpg"),
120
+ OpenStruct.new("filename" => "img4.jpg")
121
+ ]
122
+ )
123
+ )
124
+ end
125
+
126
+ test "two screenshots with docx attach" do
127
+ assert Screenshots.new.passed(
128
+ OpenStruct.new(
129
+ description: "Hi,\n here are snapshots !img1.jpg!\n!img2.jpg!\n[example.docx^!https://jira.com/images/icons/link_attachment_5.gif|width=7,height=7! ^|https://jira.com/secure/attachment/23/23_example.docx]",
130
+ fields: { "description" => "-" },
131
+ attachments: [
132
+ OpenStruct.new("filename" => "img1.jpg"),
133
+ OpenStruct.new("filename" => "img2.jpg"),
134
+ OpenStruct.new("filename" => "example.docx")
135
+ ]
136
+ )
137
+ )
138
+ end
139
+ end
140
+ end