jira_reporting 0.0.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 +7 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +52 -0
- data/Rakefile +6 -0
- data/auth.yml.example +10 -0
- data/bin/jira_reporting +50 -0
- data/bin/jira_sla_update +54 -0
- data/bin/kpi_report +204 -0
- data/bin/maint_vs_enh +127 -0
- data/bin/platform_stability_kpi_report +215 -0
- data/bin/pr-report.rb +23 -0
- data/bin/quarter_report +65 -0
- data/bin/sla_update_closed_issues +38 -0
- data/bin/sla_warning +36 -0
- data/bin/time_in_dev +56 -0
- data/jira_reporting.gemspec +37 -0
- data/lib/auto_hash.rb +21 -0
- data/lib/code_climate.rb +35 -0
- data/lib/jira_issue.rb +288 -0
- data/lib/jira_reporting.rb +27 -0
- data/lib/jira_reporting/connection.rb +16 -0
- data/lib/jira_reporting/sla_report.rb +103 -0
- data/lib/jira_reporting/sla_tracker.rb +183 -0
- data/lib/jira_reporting/triage_tracker.rb +69 -0
- data/lib/jira_reporting/version.rb +3 -0
- data/lib/pull_request.rb +104 -0
- data/optoro_holidays.yaml +71 -0
- data/spec/jira_reporting_spec.rb +11 -0
- data/spec/spec_helper.rb +2 -0
- metadata +313 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
require "jira_reporting/version"
|
2
|
+
require "jira_reporting/connection"
|
3
|
+
require "jira_reporting/sla_report"
|
4
|
+
require "jira_reporting/sla_tracker"
|
5
|
+
require "jira_reporting/triage_tracker"
|
6
|
+
require 'jira_issue'
|
7
|
+
require 'jiralicious'
|
8
|
+
require 'business_time'
|
9
|
+
require 'holidays'
|
10
|
+
|
11
|
+
PROJECT_ROOT = "#{File.dirname(__FILE__)}/../"
|
12
|
+
|
13
|
+
module JiraReporting
|
14
|
+
def self.connect!(user, pass)
|
15
|
+
Connection.instance.connect!(user, pass)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
BusinessTime::Config.beginning_of_workday = "8:00 am"
|
20
|
+
BusinessTime::Config.end_of_workday = "6:00 pm"
|
21
|
+
# init business time
|
22
|
+
Holidays.load_custom(File.join(PROJECT_ROOT,'optoro_holidays.yaml'))
|
23
|
+
Holidays.between(Time.now, 2.years.from_now, :optorolandia, :observed).map do |holiday|
|
24
|
+
BusinessTime::Config.holidays << holiday[:date]
|
25
|
+
# Implement long weekends if they apply to the region, eg:
|
26
|
+
# BusinessTime::Config.holidays << holiday[:date].next_week if !holiday[:date].weekday?
|
27
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module JiraReporting
|
4
|
+
class Connection
|
5
|
+
include Singleton
|
6
|
+
def connect!(user, pass)
|
7
|
+
Jiralicious.configure do |c|
|
8
|
+
c.username = user
|
9
|
+
c.password = pass
|
10
|
+
c.uri = "https://optoro.atlassian.net"
|
11
|
+
c.api_version = 'latest'
|
12
|
+
c.auth_type = :basic
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module JiraReporting
|
2
|
+
class SLAReport
|
3
|
+
|
4
|
+
def self.show_report
|
5
|
+
self.new.show_report
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@conn = Connection.instance
|
10
|
+
@debug = true
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :conn
|
14
|
+
|
15
|
+
def log(str)
|
16
|
+
puts "#{Time.now} #{str}" if @debug
|
17
|
+
end
|
18
|
+
|
19
|
+
def show_report
|
20
|
+
|
21
|
+
log 'querying'
|
22
|
+
#query = %q{project = "Tech Support" AND ("Support xt Can't Reproduce" > startOfDay(-7d) OR "Support xt Not Accepted" > startOfDay(-7d) OR "Support xt Resolved" > startOfDay(-7d)) AND status not in ("Not Accepted", "Can't Reproduce")}
|
23
|
+
query = %q{
|
24
|
+
project = "Tech Support"
|
25
|
+
AND status not in ("Not Accepted", "Can't Reproduce")}
|
26
|
+
#issues = Jiralicious.search(query).issues_raw
|
27
|
+
issues = find_all(query)
|
28
|
+
|
29
|
+
#log 'unmapping custom fields'
|
30
|
+
#unmap_custom_fields(issues.first)
|
31
|
+
rept = issues.map{|i| ReportIssue.new(i, custom_fields) }
|
32
|
+
prios = rept.group_by(&:priority)
|
33
|
+
|
34
|
+
prio_report("P1", prios[:p1])
|
35
|
+
prio_report("P2", prios[:p2])
|
36
|
+
prio_report("P3", prios[:p3])
|
37
|
+
prio_report("P4", prios[:p4])
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_all(query, offset = 0)
|
41
|
+
ask = 100
|
42
|
+
result = Jiralicious.search(query, start_at: offset, max_results: ask)
|
43
|
+
issues = result.issues_raw
|
44
|
+
if issues.count == ask && result.num_results > issues.count
|
45
|
+
issues += find_all(query, offset + ask)
|
46
|
+
end
|
47
|
+
# puts "found #{issues.count}"
|
48
|
+
issues
|
49
|
+
end
|
50
|
+
|
51
|
+
def prio_report(name, group)
|
52
|
+
puts "\n#{name}:"
|
53
|
+
|
54
|
+
cur_week = Time.now.to_date.cweek
|
55
|
+
week_groups = group.group_by{|t| t.created_at.to_date.cweek }
|
56
|
+
wgs = week_groups.to_a.select{|week, set| week >= (cur_week - 27) }
|
57
|
+
wgs.sort!{|a,b| a[0] <=> b[0] }
|
58
|
+
|
59
|
+
puts "Week: #{wgs.map{|w,s| "%12i" % w }.join(" | ")}"
|
60
|
+
|
61
|
+
o = []
|
62
|
+
wgs.each do |week, set|
|
63
|
+
sla_rate = set.select{|t| t.sla_diff <= 0 }.length.to_f / set.length
|
64
|
+
o << "%11.2f%" % (sla_rate*100)
|
65
|
+
end
|
66
|
+
puts "SLA Hit Rate: #{o.join " | "}"
|
67
|
+
|
68
|
+
o = []
|
69
|
+
wgs.each do |week, set|
|
70
|
+
over_under = set.map(&:sla_diff).sum
|
71
|
+
o << "%12i" % over_under
|
72
|
+
end
|
73
|
+
puts "Over Under: #{o.join " | "}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def custom_fields
|
77
|
+
@custom_fields ||= Hash[[
|
78
|
+
[10812, "Support xt Can't Reproduce"],
|
79
|
+
[10808, 'Support xt In Progress'],
|
80
|
+
[10809, 'Support xt Not Accepted'],
|
81
|
+
[10817, 'Support xt Requester Denied'],
|
82
|
+
[10814, 'Support xt Requester Review'],
|
83
|
+
[10816, 'Support xt Resolved'],
|
84
|
+
[10815, 'Support xt Reverify'],
|
85
|
+
[10813, 'Support xt Review'],
|
86
|
+
[10810, 'Support xt Triaged'],
|
87
|
+
[10811, 'Support xt Verified'],
|
88
|
+
].map{|k,v| ["customfield_#{k}", v]}]
|
89
|
+
end
|
90
|
+
|
91
|
+
#def unmap_custom_fields(sample_issue)
|
92
|
+
# sample_issue['fields'].keys.select {|f| f =~ /^customfield_/ }.each do |f|
|
93
|
+
# begin
|
94
|
+
# id = f.sub(/customfield_/,'')
|
95
|
+
# field = Jiralicious::CustomFieldOption.find(id)
|
96
|
+
# custom_fields[f] = field['value']
|
97
|
+
# rescue Jiralicious::IssueNotFound
|
98
|
+
# custom_fields[f] = 'Unknown'
|
99
|
+
# end
|
100
|
+
# end
|
101
|
+
#end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
class JiraIssue
|
2
|
+
module SLATracker
|
3
|
+
def set_sla_due_time!(due_time = nil)
|
4
|
+
due_time ||= sla_target
|
5
|
+
return unless due_time
|
6
|
+
jira_issue = jiralicious_issue
|
7
|
+
jira_issue.fields.set(SLA_DUE_TIME_FIELD, due_time.iso8601)
|
8
|
+
jira_issue.save!
|
9
|
+
end
|
10
|
+
|
11
|
+
def set_over_sla!(over = nil)
|
12
|
+
over ||= over_sla?
|
13
|
+
return if over.nil? || over == false
|
14
|
+
jira_issue = jiralicious_issue
|
15
|
+
jira_issue.fields.set_id(OVER_SLA_FIELD, OVER_SLA_FIELD_YES)
|
16
|
+
jira_issue.save!
|
17
|
+
end
|
18
|
+
|
19
|
+
def set_sla_due_warning!(almost_over_sla = nil)
|
20
|
+
almost_over_sla ||= approaching_sla_due_time?
|
21
|
+
return if almost_over_sla.nil? || almost_over_sla == false
|
22
|
+
jira_issue = jiralicious_issue
|
23
|
+
jira_issue.fields.set_id(SLA_DUE_WARNING_FIELD, SLA_DUE_WARNING_FIELD_SET)
|
24
|
+
jira_issue.save!
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_sla_closed_at!(sla_closed_at = nil)
|
28
|
+
sla_closed_at ||= closed_at
|
29
|
+
return if sla_closed_at.nil? || open?
|
30
|
+
jira_issue = jiralicious_issue
|
31
|
+
jira_issue.fields.set(SLA_CLOSED_AT_FIELD, sla_closed_at.iso8601)
|
32
|
+
jira_issue.save!
|
33
|
+
end
|
34
|
+
|
35
|
+
def set_total_time_over_sla!(time_over_sla = nil)
|
36
|
+
time_over_sla ||= sum_total_time_over_sla
|
37
|
+
return unless over_sla == API_OVER_SLA_FIELD_YES || closed? || time_over_sla.present?
|
38
|
+
jira_issue = jiralicious_issue
|
39
|
+
jira_issue.fields.set(TOTAL_TIME_OVER_SLA_FIELD, time_over_sla)
|
40
|
+
jira_issue.save!
|
41
|
+
end
|
42
|
+
|
43
|
+
def sla_base
|
44
|
+
created_at
|
45
|
+
end
|
46
|
+
|
47
|
+
def sla_target
|
48
|
+
case priority
|
49
|
+
when :p1
|
50
|
+
sla_base + 2.hours
|
51
|
+
when :p2
|
52
|
+
sla_base + 24.hours
|
53
|
+
when :p3
|
54
|
+
if sla_base <= FIVE_P_DATE
|
55
|
+
5.business_days.after(sla_base)
|
56
|
+
else
|
57
|
+
3.business_days.after(sla_base)
|
58
|
+
end
|
59
|
+
when :p4
|
60
|
+
if sla_base <= FIVE_P_DATE
|
61
|
+
nil
|
62
|
+
else
|
63
|
+
5.business_days.after(sla_base).change(:hour => BusinessTime::Config.end_of_workday.hour)
|
64
|
+
end
|
65
|
+
else
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def sla_time_ratio(from = Time.now)
|
71
|
+
return 0 if priority == :p5
|
72
|
+
from = closed_at if closed?
|
73
|
+
available_time = sla_target - sla_base
|
74
|
+
used_time = from - sla_base
|
75
|
+
used_time.to_f / available_time
|
76
|
+
end
|
77
|
+
|
78
|
+
def sla_total_available_time
|
79
|
+
sla_due_time - sla_base
|
80
|
+
end
|
81
|
+
|
82
|
+
def sla_remaining_time
|
83
|
+
sla_due_time - Time.now
|
84
|
+
end
|
85
|
+
|
86
|
+
def sla_warning_time
|
87
|
+
sla_total_available_time*0.20
|
88
|
+
end
|
89
|
+
|
90
|
+
def approaching_sla_due_time?
|
91
|
+
return false if sla_due_time.nil? || closed?
|
92
|
+
sla_remaining_time < sla_warning_time
|
93
|
+
end
|
94
|
+
|
95
|
+
def over_sla?
|
96
|
+
case
|
97
|
+
when over_sla == API_OVER_SLA_FIELD_NO
|
98
|
+
false
|
99
|
+
when sla_due_time.nil?
|
100
|
+
false
|
101
|
+
when closed? && sla_closed_at > sla_due_time
|
102
|
+
true
|
103
|
+
when open? && Time.now > sla_due_time
|
104
|
+
true
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def sum_total_time_over_sla # => difference in hours
|
109
|
+
if over_sla == API_OVER_SLA_FIELD_YES
|
110
|
+
((sla_closed_at - sla_due_time) / 60) / 60
|
111
|
+
else
|
112
|
+
0
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def sla_diff(now = Time.now)
|
117
|
+
t = sla_target
|
118
|
+
b = resolution_base
|
119
|
+
if t && b
|
120
|
+
b - t
|
121
|
+
elsif t
|
122
|
+
now - t
|
123
|
+
else
|
124
|
+
0
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def time_to_start_work
|
129
|
+
if verified
|
130
|
+
if in_progress
|
131
|
+
in_progress - verified
|
132
|
+
else
|
133
|
+
Time.now - verified
|
134
|
+
end
|
135
|
+
elsif triaged && in_progress
|
136
|
+
in_progress - triaged
|
137
|
+
elsif in_progress
|
138
|
+
in_progress - created
|
139
|
+
else
|
140
|
+
nil
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def time_to_finish_work
|
145
|
+
if in_progress
|
146
|
+
if pr_review
|
147
|
+
pr_review - in_progress
|
148
|
+
elsif requester_review
|
149
|
+
requester_review - in_progress
|
150
|
+
end
|
151
|
+
else
|
152
|
+
nil
|
153
|
+
#elsif resolved
|
154
|
+
# # wtf?
|
155
|
+
# resolved - created_at
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def resolution_base
|
160
|
+
if requester_review
|
161
|
+
requester_review
|
162
|
+
elsif resolved
|
163
|
+
resolved
|
164
|
+
elsif cant_reproduce
|
165
|
+
cant_reproduce
|
166
|
+
elsif not_accepted
|
167
|
+
not_accepted
|
168
|
+
else
|
169
|
+
# not resolved
|
170
|
+
nil
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def total_time_to_resolution
|
175
|
+
b = resolution_base
|
176
|
+
if b
|
177
|
+
resolution_base - created_at
|
178
|
+
else
|
179
|
+
nil
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
class JiraIssue
|
2
|
+
module TriageTracker
|
3
|
+
def set_sla_triaged_at!(sla_triaged = nil)
|
4
|
+
sla_triaged ||= triaged
|
5
|
+
return if sla_triaged.nil?
|
6
|
+
jira_issue = jiralicious_issue
|
7
|
+
jira_issue.fields.set(SLA_TRIAGED_AT_FIELD, sla_triaged.iso8601)
|
8
|
+
jira_issue.save!
|
9
|
+
end
|
10
|
+
|
11
|
+
def set_over_triage_sla!(over_triage = nil)
|
12
|
+
over_triage ||= over_triage_sla?
|
13
|
+
return if over_triage.nil? || over_triage == false
|
14
|
+
jira_issue = jiralicious_issue
|
15
|
+
jira_issue.fields.set_id(OVER_TRIAGE_SLA_FIELD, OVER_TRIAGE_SLA_FIELD_YES)
|
16
|
+
jira_issue.save!
|
17
|
+
end
|
18
|
+
|
19
|
+
def time_to_triage
|
20
|
+
if triaged
|
21
|
+
triaged - sla_base
|
22
|
+
else
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def triage_sla_target
|
28
|
+
case priority
|
29
|
+
when :p1
|
30
|
+
sla_base + 0.5.hours
|
31
|
+
when :p2
|
32
|
+
sla_base + 1.hours
|
33
|
+
when :p3
|
34
|
+
2.business_hours.after(sla_base)
|
35
|
+
when :p4
|
36
|
+
1.business_day.after(sla_base)
|
37
|
+
else
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def triage_sla_total_available_time
|
43
|
+
triage_sla_target - sla_base
|
44
|
+
end
|
45
|
+
|
46
|
+
def triage_sla_remaining_time
|
47
|
+
triage_sla_target - Time.now
|
48
|
+
end
|
49
|
+
|
50
|
+
def over_triage_sla?
|
51
|
+
case
|
52
|
+
when over_triage_sla == API_OVER_TRIAGE_SLA_FIELD_NO
|
53
|
+
false
|
54
|
+
when triage_sla_target.nil?
|
55
|
+
false
|
56
|
+
when triaged.nil? && Time.now > triage_sla_target
|
57
|
+
true
|
58
|
+
when triaged.nil? && Time.now < triage_sla_target
|
59
|
+
false
|
60
|
+
when sla_triaged_at.nil? && triaged < triage_sla_target
|
61
|
+
false
|
62
|
+
when sla_triaged_at.nil? && triaged > triage_sla_target
|
63
|
+
true
|
64
|
+
when triaged && sla_triaged_at > triage_sla_target
|
65
|
+
true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/pull_request.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'octokit'
|
2
|
+
require 'csv'
|
3
|
+
require 'io/console'
|
4
|
+
require 'pry-byebug'
|
5
|
+
require 'andand'
|
6
|
+
require 'jira_reporting'
|
7
|
+
|
8
|
+
class PullRequest
|
9
|
+
|
10
|
+
attr_accessor :pr_number, :jira, :title, :created, :requester, :cl, :ci,
|
11
|
+
:code_reviewed, :needs_testing, :project, :issue, :issue_current_sprint,
|
12
|
+
:issue_status, :pr_raw
|
13
|
+
|
14
|
+
def initialize(params)
|
15
|
+
params.each do |key, value|
|
16
|
+
instance_variable_set("@#{key}", value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_csv
|
21
|
+
[:pr_number, :jira, :title, :created, :requester, :cl, :ci, :code_reviewed, :needs_testing, :projec, :issue_current_sprint, :issue_status]
|
22
|
+
.map{|col| instance_variable_get("@#{col}")}
|
23
|
+
.to_csv
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.client
|
27
|
+
@@client
|
28
|
+
end
|
29
|
+
|
30
|
+
@@client = nil
|
31
|
+
def self.connect(token = nil)
|
32
|
+
return @@client if @@client
|
33
|
+
client = nil
|
34
|
+
if token
|
35
|
+
client = Octokit::Client.new(access_token: token)
|
36
|
+
else
|
37
|
+
print "Github Login: "
|
38
|
+
login = gets.chomp
|
39
|
+
print "Github Password: "
|
40
|
+
passwd = STDIN.noecho(&:gets).chomp
|
41
|
+
puts "\nBuilding report..."
|
42
|
+
client = Octokit::Client.new(login: login, password: passwd)
|
43
|
+
end
|
44
|
+
client.auto_paginate = true
|
45
|
+
@@client = client
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.get_prs(project, options = {})
|
49
|
+
|
50
|
+
pulls = client.pulls("optoro/#{project}", options)
|
51
|
+
|
52
|
+
prs = []
|
53
|
+
|
54
|
+
pulls.each do |pull|
|
55
|
+
pr = PullRequest.new(
|
56
|
+
pr_number: pull.number,
|
57
|
+
title: pull.title,
|
58
|
+
created: pull.created_at,
|
59
|
+
requester: pull.user.login,
|
60
|
+
ci: pull.rels[:statuses].get.data.first.andand.state,
|
61
|
+
pr_raw: pull,
|
62
|
+
)
|
63
|
+
|
64
|
+
if pr.title =~ /([A-Z]{2,}-[0-9]+)/
|
65
|
+
pr.jira = $1
|
66
|
+
end
|
67
|
+
|
68
|
+
labels = pr.labels
|
69
|
+
if labels.member? "CL - Low"
|
70
|
+
pr.cl = "Low"
|
71
|
+
end
|
72
|
+
if labels.member? "CL - Medium"
|
73
|
+
pr.cl = "Medium"
|
74
|
+
end
|
75
|
+
if labels.member? "CL - High"
|
76
|
+
pr.cl = "High"
|
77
|
+
end
|
78
|
+
if labels.member? "CL - System Impacting"
|
79
|
+
pr.cl = "System"
|
80
|
+
end
|
81
|
+
pr.needs_testing = labels.member?("Needs External Testing") ? "Yes" : "No"
|
82
|
+
pr.code_reviewed = labels.member?("Review Passed") ? "Yes" : "No"
|
83
|
+
pr.project = labels.member?("Part of Project") ? "Yes" : "No"
|
84
|
+
|
85
|
+
if pr.jira
|
86
|
+
issue = JiraIssue.find("issueKey = #{pr.jira}").first
|
87
|
+
if issue
|
88
|
+
pr.issue = issue
|
89
|
+
pr.issue_current_sprint = !!issue.sprints.detect{|s|s["state"] == "ACTIVE"}
|
90
|
+
pr.issue_status = issue.status
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
prs << pr
|
95
|
+
end
|
96
|
+
prs
|
97
|
+
end
|
98
|
+
|
99
|
+
def labels
|
100
|
+
pr_raw.rels[:issue].get.data.labels.map(&:name).to_set
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|