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