jirametrics 2.4 → 2.30
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 +4 -4
- data/bin/jirametrics-mcp +5 -0
- data/lib/jirametrics/aggregate_config.rb +16 -3
- data/lib/jirametrics/aging_work_bar_chart.rb +193 -133
- data/lib/jirametrics/aging_work_in_progress_chart.rb +138 -42
- data/lib/jirametrics/aging_work_table.rb +63 -19
- data/lib/jirametrics/anonymizer.rb +81 -6
- data/lib/jirametrics/atlassian_document_format.rb +160 -0
- data/lib/jirametrics/bar_chart_range.rb +17 -0
- data/lib/jirametrics/blocked_stalled_change.rb +6 -4
- data/lib/jirametrics/board.rb +74 -22
- data/lib/jirametrics/board_config.rb +11 -3
- data/lib/jirametrics/board_feature.rb +14 -0
- data/lib/jirametrics/board_movement_calculator.rb +155 -0
- data/lib/jirametrics/cfd_data_builder.rb +108 -0
- data/lib/jirametrics/change_item.rb +54 -18
- data/lib/jirametrics/chart_base.rb +203 -30
- data/lib/jirametrics/css_variable.rb +2 -2
- data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
- data/lib/jirametrics/cycle_time_config.rb +137 -0
- data/lib/jirametrics/cycletime_histogram.rb +17 -38
- data/lib/jirametrics/cycletime_scatterplot.rb +18 -87
- data/lib/jirametrics/daily_view.rb +306 -0
- data/lib/jirametrics/daily_wip_by_age_chart.rb +5 -8
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +15 -5
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -6
- data/lib/jirametrics/daily_wip_chart.rb +36 -16
- data/lib/jirametrics/data_quality_report.rb +251 -42
- data/lib/jirametrics/dependency_chart.rb +42 -12
- data/lib/jirametrics/download_config.rb +27 -0
- data/lib/jirametrics/downloader.rb +185 -110
- data/lib/jirametrics/downloader_for_cloud.rb +287 -0
- data/lib/jirametrics/downloader_for_data_center.rb +95 -0
- data/lib/jirametrics/estimate_accuracy_chart.rb +75 -14
- data/lib/jirametrics/estimation_configuration.rb +25 -0
- data/lib/jirametrics/examples/aggregated_project.rb +9 -23
- data/lib/jirametrics/examples/standard_project.rb +57 -58
- data/lib/jirametrics/expedited_chart.rb +11 -10
- data/lib/jirametrics/exporter.rb +51 -14
- data/lib/jirametrics/file_config.rb +21 -6
- data/lib/jirametrics/file_system.rb +96 -4
- data/lib/jirametrics/fix_version.rb +13 -0
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +115 -0
- data/lib/jirametrics/github_gateway.rb +115 -0
- data/lib/jirametrics/groupable_issue_chart.rb +12 -4
- data/lib/jirametrics/grouping_rules.rb +26 -4
- data/lib/jirametrics/html/aging_work_bar_chart.erb +8 -17
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +24 -5
- data/lib/jirametrics/html/aging_work_table.erb +13 -4
- data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
- data/lib/jirametrics/html/cumulative_flow_diagram.erb +503 -0
- data/lib/jirametrics/html/daily_wip_chart.erb +41 -15
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +4 -12
- data/lib/jirametrics/html/expedited_chart.erb +7 -24
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +81 -0
- data/lib/jirametrics/html/hierarchy_table.erb +1 -1
- data/lib/jirametrics/html/index.css +336 -62
- data/lib/jirametrics/html/index.erb +16 -21
- data/lib/jirametrics/html/index.js +164 -0
- data/lib/jirametrics/html/legacy_colors.css +174 -0
- data/lib/jirametrics/html/sprint_burndown.erb +18 -25
- data/lib/jirametrics/html/throughput_chart.erb +43 -21
- data/lib/jirametrics/html/time_based_histogram.erb +123 -0
- data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +16 -21
- data/lib/jirametrics/html/wip_by_column_chart.erb +250 -0
- data/lib/jirametrics/html_generator.rb +32 -0
- data/lib/jirametrics/html_report_config.rb +83 -76
- data/lib/jirametrics/issue.rb +499 -91
- data/lib/jirametrics/issue_collection.rb +33 -0
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/jira_gateway.rb +96 -16
- data/lib/jirametrics/mcp_server.rb +531 -0
- data/lib/jirametrics/project_config.rb +374 -130
- data/lib/jirametrics/pull_request.rb +30 -0
- data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
- data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
- data/lib/jirametrics/pull_request_review.rb +13 -0
- data/lib/jirametrics/raw_javascript.rb +17 -0
- data/lib/jirametrics/rules.rb +2 -2
- data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
- data/lib/jirametrics/settings.json +10 -2
- data/lib/jirametrics/sprint.rb +13 -0
- data/lib/jirametrics/sprint_burndown.rb +47 -39
- data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
- data/lib/jirametrics/status.rb +84 -19
- data/lib/jirametrics/status_collection.rb +83 -38
- data/lib/jirametrics/stitcher.rb +81 -0
- data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
- data/lib/jirametrics/throughput_chart.rb +73 -23
- data/lib/jirametrics/time_based_histogram.rb +139 -0
- data/lib/jirametrics/time_based_scatterplot.rb +107 -0
- data/lib/jirametrics/user.rb +12 -0
- data/lib/jirametrics/value_equality.rb +2 -2
- data/lib/jirametrics/wip_by_column_chart.rb +236 -0
- data/lib/jirametrics.rb +101 -66
- metadata +72 -16
- data/lib/jirametrics/cycletime_config.rb +0 -69
- data/lib/jirametrics/discard_changes_before.rb +0 -37
- data/lib/jirametrics/html/cycletime_histogram.erb +0 -47
- data/lib/jirametrics/html/data_quality_report.erb +0 -126
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'jirametrics/chart_base'
|
|
4
|
+
|
|
5
|
+
class WipByColumnChart < ChartBase
|
|
6
|
+
attr_accessor :possible_statuses, :board_id
|
|
7
|
+
|
|
8
|
+
ColumnStats = Struct.new(:name, :min_wip_limit, :max_wip_limit, :wip_history, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
def initialize block
|
|
11
|
+
super()
|
|
12
|
+
header_text 'WIP by column'
|
|
13
|
+
description_text <<-HTML
|
|
14
|
+
<p>
|
|
15
|
+
This chart shows how much time each board column has spent at different WIP (Work in Progress) levels.
|
|
16
|
+
</p>
|
|
17
|
+
<p>
|
|
18
|
+
Each row on the Y axis is a WIP level (the number of items in that column at the same time).
|
|
19
|
+
Each column on the X axis is a board column.
|
|
20
|
+
The horizontal bars show what percentage of the total time that column spent at that WIP level —
|
|
21
|
+
a wider bar means more time was spent there.
|
|
22
|
+
</p>
|
|
23
|
+
<p>
|
|
24
|
+
A column whose widest bar is at WIP 1 was almost always working on one item at a time, often called
|
|
25
|
+
single-piece-flow. This team is likely collaborating very well and might have been
|
|
26
|
+
<a href="https://blog.mikebowler.ca/2021/06/19/pair-programming/">pairing</a> or
|
|
27
|
+
<a href="https://blog.mikebowler.ca/2023/04/22/ensemble-programming/">mobbing/ensembling</a>
|
|
28
|
+
and these teams tend to be very effective.
|
|
29
|
+
</p>
|
|
30
|
+
<p>
|
|
31
|
+
A column with wide bars at high WIP levels usually indicates a team that is highly siloed. Where each person
|
|
32
|
+
is working by themselves.
|
|
33
|
+
</p>
|
|
34
|
+
<p>
|
|
35
|
+
The dashed lines show the minimum and maximum WIP limits configured on the board.
|
|
36
|
+
If the widest bar sits well above the maximum limit, the limit may be set too low or not being respected.
|
|
37
|
+
If the widest bar sits below the minimum limit, consider whether that limit is still meaningful.
|
|
38
|
+
</p>
|
|
39
|
+
<p>
|
|
40
|
+
Hover over any bar to see the exact percentage.
|
|
41
|
+
</p>
|
|
42
|
+
<% if @all_boards[@board_id].team_managed_kanban? %>
|
|
43
|
+
<p>
|
|
44
|
+
If the data looks a bit off then that's probably because you're using a Team Managed project in "kanban mode".
|
|
45
|
+
For this specific case, we are unable to tell if an item is actually visible on the board and so we may
|
|
46
|
+
be reporting more items started than you actually see on the board. See
|
|
47
|
+
<a href="https://jirametrics.org/faq/#team-managed-kanban-backlog">the FAQ</a>.
|
|
48
|
+
</p>
|
|
49
|
+
<% end %>
|
|
50
|
+
HTML
|
|
51
|
+
|
|
52
|
+
instance_eval(&block)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def show_recommendations
|
|
56
|
+
@show_recommendations = true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def run
|
|
60
|
+
@header_text += " on board: #{current_board.name}"
|
|
61
|
+
stats = column_stats
|
|
62
|
+
@column_names = stats.collect(&:name)
|
|
63
|
+
@wip_data = stats.collect do |stat|
|
|
64
|
+
total = stat.wip_history.sum { |_wip, seconds| seconds }.to_f
|
|
65
|
+
next [] if total.zero?
|
|
66
|
+
|
|
67
|
+
stat.wip_history.collect { |wip, seconds| { 'wip' => wip, 'pct' => format_pct(seconds, total) } }
|
|
68
|
+
end
|
|
69
|
+
@max_wip = stats.flat_map { |s| s.wip_history.collect { |wip, _| wip } }.max || 0
|
|
70
|
+
@wip_limits = stats.collect { |s| { 'min' => s.min_wip_limit, 'max' => s.max_wip_limit } }
|
|
71
|
+
@recommendations = @show_recommendations ? compute_recommendations(stats) : Array.new(stats.size)
|
|
72
|
+
|
|
73
|
+
trim_zero_end_columns
|
|
74
|
+
@recommendation_texts = @show_recommendations ? build_recommendation_texts : []
|
|
75
|
+
|
|
76
|
+
wrap_and_render(binding, __FILE__)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def column_stats
|
|
80
|
+
board = current_board
|
|
81
|
+
columns = board.visible_columns
|
|
82
|
+
status_to_column = build_status_to_column_map(columns)
|
|
83
|
+
relevant_issues = @issues.select { |issue| issue.board.id == @board_id }
|
|
84
|
+
|
|
85
|
+
current_column = initial_column_state(relevant_issues, status_to_column)
|
|
86
|
+
events = events_within_range(relevant_issues, status_to_column)
|
|
87
|
+
column_wip_seconds = compute_wip_seconds(columns, current_column, events)
|
|
88
|
+
|
|
89
|
+
columns.collect.with_index do |column, index|
|
|
90
|
+
ColumnStats.new(
|
|
91
|
+
name: column.name,
|
|
92
|
+
min_wip_limit: column.min,
|
|
93
|
+
max_wip_limit: column.max,
|
|
94
|
+
wip_history: column_wip_seconds[index].sort.to_a
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def trim_zero_end_columns
|
|
102
|
+
all_zero = @wip_data.map { |col| col.none? { |e| e['wip'].positive? } }
|
|
103
|
+
first = all_zero.index(false)
|
|
104
|
+
return unless first
|
|
105
|
+
|
|
106
|
+
last = all_zero.rindex(false)
|
|
107
|
+
@column_names = @column_names[first..last]
|
|
108
|
+
@wip_data = @wip_data[first..last]
|
|
109
|
+
@wip_limits = @wip_limits[first..last]
|
|
110
|
+
@recommendations = @recommendations[first..last]
|
|
111
|
+
@max_wip = @wip_data.flat_map { |col| col.map { |e| e['wip'] } }.max || 0
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def compute_recommendations stats
|
|
115
|
+
stats.collect do |stat|
|
|
116
|
+
next nil if stat.wip_history.empty?
|
|
117
|
+
|
|
118
|
+
total = stat.wip_history.sum { |_wip, seconds| seconds }.to_f
|
|
119
|
+
next nil if total.zero?
|
|
120
|
+
|
|
121
|
+
cumulative = 0
|
|
122
|
+
stat.wip_history.sort.find do |_wip, seconds|
|
|
123
|
+
cumulative += seconds
|
|
124
|
+
cumulative / total >= 0.85
|
|
125
|
+
end&.first
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_recommendation_texts
|
|
130
|
+
@column_names.each_with_index.filter_map do |name, i|
|
|
131
|
+
rec = @recommendations[i]
|
|
132
|
+
next if rec.nil?
|
|
133
|
+
|
|
134
|
+
next "Almost nothing passes through column '#{name}'. Do we still need it?" if rec.zero?
|
|
135
|
+
|
|
136
|
+
max = @wip_limits[i]['max']
|
|
137
|
+
if max.nil?
|
|
138
|
+
"Add a WIP limit to column '#{name}' — suggested maximum: #{rec}"
|
|
139
|
+
elsif rec < max
|
|
140
|
+
"Lower the WIP limit for '#{name}' from #{max} to #{rec}"
|
|
141
|
+
elsif rec > max
|
|
142
|
+
"Raise the WIP limit for '#{name}' from #{max} to #{rec}"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def format_pct seconds, total
|
|
148
|
+
raw = seconds / total * 100.0
|
|
149
|
+
(1..10).each do |decimals|
|
|
150
|
+
rounded = raw.round(decimals)
|
|
151
|
+
next if rounded.zero? && raw.positive?
|
|
152
|
+
next if rounded >= 100.0 && raw < 100.0
|
|
153
|
+
|
|
154
|
+
return rounded
|
|
155
|
+
end
|
|
156
|
+
raw
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def build_status_to_column_map columns
|
|
160
|
+
columns.each_with_object({}).with_index do |(column, map), index|
|
|
161
|
+
column.status_ids.each { |id| map[id] = index }
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def initial_column_state relevant_issues, status_to_column
|
|
166
|
+
relevant_issues.each_with_object({}) do |issue, hash|
|
|
167
|
+
started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
|
|
168
|
+
in_wip = started_time &&
|
|
169
|
+
started_time <= time_range.begin &&
|
|
170
|
+
(stopped_time.nil? || stopped_time > time_range.begin)
|
|
171
|
+
unless in_wip
|
|
172
|
+
hash[issue] = nil
|
|
173
|
+
next
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
last_change = issue.status_changes.reverse.find { |c| c.time <= time_range.begin }
|
|
177
|
+
hash[issue] = last_change ? status_to_column[last_change.value_id] : nil
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def events_within_range relevant_issues, status_to_column
|
|
182
|
+
events = []
|
|
183
|
+
relevant_issues.each do |issue|
|
|
184
|
+
started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
|
|
185
|
+
next unless started_time
|
|
186
|
+
|
|
187
|
+
# Issue starts within the window: add an explicit event to enter WIP in its current column
|
|
188
|
+
if started_time > time_range.begin && started_time <= time_range.end
|
|
189
|
+
last_change = issue.status_changes.reverse.find { |c| c.time <= started_time }
|
|
190
|
+
events << [started_time, issue, last_change ? status_to_column[last_change.value_id] : nil]
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Status changes while the issue is actively in WIP and within the window
|
|
194
|
+
issue.status_changes.each do |change|
|
|
195
|
+
next unless change.time > time_range.begin
|
|
196
|
+
next if change.time > time_range.end
|
|
197
|
+
next unless change.time >= started_time
|
|
198
|
+
next if stopped_time && change.time >= stopped_time
|
|
199
|
+
|
|
200
|
+
events << [change.time, issue, status_to_column[change.value_id]]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Issue stops within the window: add an explicit event to exit WIP
|
|
204
|
+
if stopped_time && stopped_time > time_range.begin && stopped_time <= time_range.end
|
|
205
|
+
events << [stopped_time, issue, nil]
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
events.sort_by!(&:first)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def compute_wip_seconds columns, current_column, events
|
|
212
|
+
wip_counts = Array.new(columns.size, 0)
|
|
213
|
+
current_column.each_value { |col| wip_counts[col] += 1 unless col.nil? }
|
|
214
|
+
|
|
215
|
+
column_wip_seconds = Array.new(columns.size) { Hash.new(0) }
|
|
216
|
+
prev_time = time_range.begin
|
|
217
|
+
|
|
218
|
+
events.each do |time, issue, new_col|
|
|
219
|
+
elapsed = (time - prev_time).to_i
|
|
220
|
+
if elapsed.positive?
|
|
221
|
+
wip_counts.each_with_index { |wip, idx| column_wip_seconds[idx][wip] += elapsed }
|
|
222
|
+
prev_time = time
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
old_col = current_column[issue]
|
|
226
|
+
wip_counts[old_col] -= 1 unless old_col.nil?
|
|
227
|
+
wip_counts[new_col] += 1 unless new_col.nil?
|
|
228
|
+
current_column[issue] = new_col
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
elapsed = (time_range.end - prev_time).to_i
|
|
232
|
+
wip_counts.each_with_index { |wip, idx| column_wip_seconds[idx][wip] += elapsed } if elapsed.positive?
|
|
233
|
+
|
|
234
|
+
column_wip_seconds
|
|
235
|
+
end
|
|
236
|
+
end
|
data/lib/jirametrics.rb
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'English'
|
|
3
4
|
require 'thor'
|
|
5
|
+
require 'require_all'
|
|
6
|
+
|
|
7
|
+
# This one does need to be loaded early. The rest will be loaded later.
|
|
8
|
+
require 'jirametrics/file_system'
|
|
4
9
|
|
|
5
10
|
class JiraMetrics < Thor
|
|
11
|
+
def self.exit_on_failure?
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
map %w[--version -v] => :__print_version
|
|
16
|
+
|
|
17
|
+
desc '--version, -v', 'print the version'
|
|
18
|
+
def __print_version
|
|
19
|
+
puts Gem.loaded_specs['jirametrics'].version
|
|
20
|
+
end
|
|
21
|
+
|
|
6
22
|
option :config
|
|
7
23
|
option :name
|
|
8
|
-
desc 'export
|
|
24
|
+
desc 'export', "Export data into either reports or CSV's as per the configuration"
|
|
9
25
|
def export
|
|
10
26
|
load_config options[:config]
|
|
11
27
|
Exporter.instance.export(name_filter: options[:name] || '*')
|
|
@@ -13,7 +29,7 @@ class JiraMetrics < Thor
|
|
|
13
29
|
|
|
14
30
|
option :config
|
|
15
31
|
option :name
|
|
16
|
-
desc 'download
|
|
32
|
+
desc 'download', 'Download data from Jira'
|
|
17
33
|
def download
|
|
18
34
|
load_config options[:config]
|
|
19
35
|
Exporter.instance.download(name_filter: options[:name] || '*')
|
|
@@ -21,7 +37,7 @@ class JiraMetrics < Thor
|
|
|
21
37
|
|
|
22
38
|
option :config
|
|
23
39
|
option :name
|
|
24
|
-
desc '
|
|
40
|
+
desc 'go', 'Same as running download, followed by export'
|
|
25
41
|
def go
|
|
26
42
|
load_config options[:config]
|
|
27
43
|
Exporter.instance.download(name_filter: options[:name] || '*')
|
|
@@ -30,73 +46,92 @@ class JiraMetrics < Thor
|
|
|
30
46
|
Exporter.instance.export(name_filter: options[:name] || '*')
|
|
31
47
|
end
|
|
32
48
|
|
|
33
|
-
|
|
49
|
+
option :config
|
|
50
|
+
desc 'info', 'Dump information about one issue'
|
|
51
|
+
def info key
|
|
52
|
+
load_config options[:config]
|
|
53
|
+
Exporter.instance.info(key, name_filter: options[:name] || '*')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
option :config
|
|
57
|
+
option :name
|
|
58
|
+
desc 'mcp', 'Start in MCP (Model Context Protocol) server mode'
|
|
59
|
+
def mcp
|
|
60
|
+
# Redirect stdout to stderr for the entire startup phase so that any
|
|
61
|
+
# incidental output (from config files, gem loading, etc.) does not
|
|
62
|
+
# corrupt the JSON-RPC channel before the MCP transport takes over.
|
|
63
|
+
original_stdout = $stdout.dup
|
|
64
|
+
$stdout.reopen($stderr)
|
|
65
|
+
|
|
66
|
+
load_config options[:config]
|
|
67
|
+
require 'jirametrics/mcp_server'
|
|
68
|
+
|
|
69
|
+
Exporter.instance.file_system.log_only = true
|
|
34
70
|
|
|
35
|
-
|
|
36
|
-
|
|
71
|
+
projects = {}
|
|
72
|
+
aggregates = {}
|
|
73
|
+
Exporter.instance.each_project_config(name_filter: options[:name] || '*') do |project|
|
|
74
|
+
project.evaluate_next_level
|
|
75
|
+
project.run load_only: true
|
|
76
|
+
projects[project.name || 'default'] = {
|
|
77
|
+
issues: project.issues,
|
|
78
|
+
today: project.time_range.end.to_date,
|
|
79
|
+
end_time: project.time_range.end
|
|
80
|
+
}
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
if e.message.start_with? 'This is an aggregated project'
|
|
83
|
+
names = project.aggregate_project_names
|
|
84
|
+
aggregates[project.name] = names if names.any?
|
|
85
|
+
next
|
|
86
|
+
end
|
|
87
|
+
next if e.message.start_with? 'No data found'
|
|
37
88
|
|
|
38
|
-
|
|
39
|
-
# The fact that File.exist can see the file does not mean that require will be
|
|
40
|
-
# able to load it. Convert this to an absolute pathname now for require.
|
|
41
|
-
config_file = File.absolute_path(config_file).to_s
|
|
42
|
-
else
|
|
43
|
-
puts "Cannot find configuration file #{config_file.inspect}"
|
|
44
|
-
exit 1
|
|
89
|
+
raise
|
|
45
90
|
end
|
|
46
91
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
require 'jirametrics/grouping_rules'
|
|
51
|
-
require 'jirametrics/daily_wip_chart'
|
|
52
|
-
require 'jirametrics/groupable_issue_chart'
|
|
53
|
-
require 'jirametrics/discard_changes_before'
|
|
54
|
-
require 'jirametrics/css_variable'
|
|
55
|
-
|
|
56
|
-
require 'jirametrics/aggregate_config'
|
|
57
|
-
require 'jirametrics/expedited_chart'
|
|
58
|
-
require 'jirametrics/board_config'
|
|
59
|
-
require 'jirametrics/file_config'
|
|
60
|
-
require 'jirametrics/jira_gateway'
|
|
61
|
-
require 'jirametrics/trend_line_calculator'
|
|
62
|
-
require 'jirametrics/status'
|
|
63
|
-
require 'jirametrics/issue_link'
|
|
64
|
-
require 'jirametrics/estimate_accuracy_chart'
|
|
65
|
-
require 'jirametrics/status_collection'
|
|
66
|
-
require 'jirametrics/sprint'
|
|
67
|
-
require 'jirametrics/issue'
|
|
68
|
-
require 'jirametrics/daily_wip_by_age_chart'
|
|
69
|
-
require 'jirametrics/daily_wip_by_parent_chart'
|
|
70
|
-
require 'jirametrics/aging_work_in_progress_chart'
|
|
71
|
-
require 'jirametrics/cycletime_scatterplot'
|
|
72
|
-
require 'jirametrics/sprint_issue_change_data'
|
|
73
|
-
require 'jirametrics/cycletime_histogram'
|
|
74
|
-
require 'jirametrics/daily_wip_by_blocked_stalled_chart'
|
|
75
|
-
require 'jirametrics/html_report_config'
|
|
76
|
-
require 'jirametrics/data_quality_report'
|
|
77
|
-
require 'jirametrics/aging_work_bar_chart'
|
|
78
|
-
require 'jirametrics/change_item'
|
|
79
|
-
require 'jirametrics/project_config'
|
|
80
|
-
require 'jirametrics/dependency_chart'
|
|
81
|
-
require 'jirametrics/cycletime_config'
|
|
82
|
-
require 'jirametrics/tree_organizer'
|
|
83
|
-
require 'jirametrics/aging_work_table'
|
|
84
|
-
require 'jirametrics/sprint_burndown'
|
|
85
|
-
require 'jirametrics/self_or_issue_dispatcher'
|
|
86
|
-
require 'jirametrics/throughput_chart'
|
|
87
|
-
require 'jirametrics/exporter'
|
|
88
|
-
require 'jirametrics/file_system'
|
|
89
|
-
require 'jirametrics/blocked_stalled_change'
|
|
90
|
-
require 'jirametrics/board_column'
|
|
91
|
-
require 'jirametrics/anonymizer'
|
|
92
|
-
require 'jirametrics/downloader'
|
|
93
|
-
require 'jirametrics/fix_version'
|
|
94
|
-
require 'jirametrics/download_config'
|
|
95
|
-
require 'jirametrics/columns_config'
|
|
96
|
-
require 'jirametrics/hierarchy_table'
|
|
97
|
-
require 'jirametrics/board'
|
|
98
|
-
load config_file
|
|
92
|
+
$stdout.reopen(original_stdout)
|
|
93
|
+
original_stdout.close
|
|
94
|
+
McpServer.new(projects: projects, aggregates: aggregates, timezone_offset: Exporter.instance.timezone_offset).run
|
|
99
95
|
end
|
|
100
96
|
|
|
101
|
-
|
|
97
|
+
option :config
|
|
98
|
+
desc 'stitch', 'Dump information about one issue'
|
|
99
|
+
def stitch stitch_file = 'stitcher.erb'
|
|
100
|
+
load_config options[:config]
|
|
101
|
+
Exporter.instance.stitch stitch_file
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.log_uncaught_exception exception, file_system: nil
|
|
105
|
+
return unless exception && !exception.is_a?(SystemExit)
|
|
106
|
+
|
|
107
|
+
begin
|
|
108
|
+
file_system ||= Exporter.instance.file_system
|
|
109
|
+
return if file_system.logfile == $stdout
|
|
110
|
+
|
|
111
|
+
file_system.logfile.puts "#{exception.class}: #{exception.message}"
|
|
112
|
+
exception.backtrace&.each { |line| file_system.logfile.puts "\t#{line}" }
|
|
113
|
+
rescue StandardError
|
|
114
|
+
# Exporter may not be initialized, or the logfile may already be closed
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
at_exit { JiraMetrics.log_uncaught_exception $ERROR_INFO }
|
|
119
|
+
|
|
120
|
+
no_commands do
|
|
121
|
+
def load_config config_file, file_system: FileSystem.new
|
|
122
|
+
config_file = './config.rb' if config_file.nil?
|
|
123
|
+
|
|
124
|
+
if file_system.file_exist? config_file
|
|
125
|
+
# The fact that File.exist can see the file does not mean that require will be
|
|
126
|
+
# able to load it. Convert this to an absolute pathname now for require.
|
|
127
|
+
config_file = File.absolute_path(config_file).to_s
|
|
128
|
+
else
|
|
129
|
+
file_system.error "Cannot find configuration file #{config_file.inspect}"
|
|
130
|
+
exit 1
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
require_rel 'jirametrics'
|
|
134
|
+
load config_file
|
|
135
|
+
end
|
|
136
|
+
end
|
|
102
137
|
end
|
metadata
CHANGED
|
@@ -1,15 +1,42 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jirametrics
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: '2.
|
|
4
|
+
version: '2.30'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Bowler
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: mutant-rspec
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: mcp
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
13
40
|
- !ruby/object:Gem::Dependency
|
|
14
41
|
name: random-word
|
|
15
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -52,42 +79,52 @@ dependencies:
|
|
|
52
79
|
- - "~>"
|
|
53
80
|
- !ruby/object:Gem::Version
|
|
54
81
|
version: 1.2.2
|
|
55
|
-
description:
|
|
56
|
-
CSV files
|
|
82
|
+
description: Extract metrics from Jira and export to either a report or to CSV files
|
|
57
83
|
email: mbowler@gargoylesoftware.com
|
|
58
84
|
executables:
|
|
59
85
|
- jirametrics
|
|
86
|
+
- jirametrics-mcp
|
|
60
87
|
extensions: []
|
|
61
88
|
extra_rdoc_files: []
|
|
62
89
|
files:
|
|
63
90
|
- bin/jirametrics
|
|
91
|
+
- bin/jirametrics-mcp
|
|
64
92
|
- lib/jirametrics.rb
|
|
65
93
|
- lib/jirametrics/aggregate_config.rb
|
|
66
94
|
- lib/jirametrics/aging_work_bar_chart.rb
|
|
67
95
|
- lib/jirametrics/aging_work_in_progress_chart.rb
|
|
68
96
|
- lib/jirametrics/aging_work_table.rb
|
|
69
97
|
- lib/jirametrics/anonymizer.rb
|
|
98
|
+
- lib/jirametrics/atlassian_document_format.rb
|
|
99
|
+
- lib/jirametrics/bar_chart_range.rb
|
|
70
100
|
- lib/jirametrics/blocked_stalled_change.rb
|
|
71
101
|
- lib/jirametrics/board.rb
|
|
72
102
|
- lib/jirametrics/board_column.rb
|
|
73
103
|
- lib/jirametrics/board_config.rb
|
|
104
|
+
- lib/jirametrics/board_feature.rb
|
|
105
|
+
- lib/jirametrics/board_movement_calculator.rb
|
|
106
|
+
- lib/jirametrics/cfd_data_builder.rb
|
|
74
107
|
- lib/jirametrics/change_item.rb
|
|
75
108
|
- lib/jirametrics/chart_base.rb
|
|
76
109
|
- lib/jirametrics/columns_config.rb
|
|
77
110
|
- lib/jirametrics/css_variable.rb
|
|
78
|
-
- lib/jirametrics/
|
|
111
|
+
- lib/jirametrics/cumulative_flow_diagram.rb
|
|
112
|
+
- lib/jirametrics/cycle_time_config.rb
|
|
79
113
|
- lib/jirametrics/cycletime_histogram.rb
|
|
80
114
|
- lib/jirametrics/cycletime_scatterplot.rb
|
|
115
|
+
- lib/jirametrics/daily_view.rb
|
|
81
116
|
- lib/jirametrics/daily_wip_by_age_chart.rb
|
|
82
117
|
- lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb
|
|
83
118
|
- lib/jirametrics/daily_wip_by_parent_chart.rb
|
|
84
119
|
- lib/jirametrics/daily_wip_chart.rb
|
|
85
120
|
- lib/jirametrics/data_quality_report.rb
|
|
86
121
|
- lib/jirametrics/dependency_chart.rb
|
|
87
|
-
- lib/jirametrics/discard_changes_before.rb
|
|
88
122
|
- lib/jirametrics/download_config.rb
|
|
89
123
|
- lib/jirametrics/downloader.rb
|
|
124
|
+
- lib/jirametrics/downloader_for_cloud.rb
|
|
125
|
+
- lib/jirametrics/downloader_for_data_center.rb
|
|
90
126
|
- lib/jirametrics/estimate_accuracy_chart.rb
|
|
127
|
+
- lib/jirametrics/estimation_configuration.rb
|
|
91
128
|
- lib/jirametrics/examples/aggregated_project.rb
|
|
92
129
|
- lib/jirametrics/examples/standard_project.rb
|
|
93
130
|
- lib/jirametrics/expedited_chart.rb
|
|
@@ -95,6 +132,8 @@ files:
|
|
|
95
132
|
- lib/jirametrics/file_config.rb
|
|
96
133
|
- lib/jirametrics/file_system.rb
|
|
97
134
|
- lib/jirametrics/fix_version.rb
|
|
135
|
+
- lib/jirametrics/flow_efficiency_scatterplot.rb
|
|
136
|
+
- lib/jirametrics/github_gateway.rb
|
|
98
137
|
- lib/jirametrics/groupable_issue_chart.rb
|
|
99
138
|
- lib/jirametrics/grouping_rules.rb
|
|
100
139
|
- lib/jirametrics/hierarchy_table.rb
|
|
@@ -102,22 +141,35 @@ files:
|
|
|
102
141
|
- lib/jirametrics/html/aging_work_in_progress_chart.erb
|
|
103
142
|
- lib/jirametrics/html/aging_work_table.erb
|
|
104
143
|
- lib/jirametrics/html/collapsible_issues_panel.erb
|
|
105
|
-
- lib/jirametrics/html/
|
|
106
|
-
- lib/jirametrics/html/cycletime_scatterplot.erb
|
|
144
|
+
- lib/jirametrics/html/cumulative_flow_diagram.erb
|
|
107
145
|
- lib/jirametrics/html/daily_wip_chart.erb
|
|
108
|
-
- lib/jirametrics/html/data_quality_report.erb
|
|
109
146
|
- lib/jirametrics/html/estimate_accuracy_chart.erb
|
|
110
147
|
- lib/jirametrics/html/expedited_chart.erb
|
|
148
|
+
- lib/jirametrics/html/flow_efficiency_scatterplot.erb
|
|
111
149
|
- lib/jirametrics/html/hierarchy_table.erb
|
|
112
150
|
- lib/jirametrics/html/index.css
|
|
113
151
|
- lib/jirametrics/html/index.erb
|
|
152
|
+
- lib/jirametrics/html/index.js
|
|
153
|
+
- lib/jirametrics/html/legacy_colors.css
|
|
114
154
|
- lib/jirametrics/html/sprint_burndown.erb
|
|
115
155
|
- lib/jirametrics/html/throughput_chart.erb
|
|
156
|
+
- lib/jirametrics/html/time_based_histogram.erb
|
|
157
|
+
- lib/jirametrics/html/time_based_scatterplot.erb
|
|
158
|
+
- lib/jirametrics/html/wip_by_column_chart.erb
|
|
159
|
+
- lib/jirametrics/html_generator.rb
|
|
116
160
|
- lib/jirametrics/html_report_config.rb
|
|
117
161
|
- lib/jirametrics/issue.rb
|
|
162
|
+
- lib/jirametrics/issue_collection.rb
|
|
118
163
|
- lib/jirametrics/issue_link.rb
|
|
164
|
+
- lib/jirametrics/issue_printer.rb
|
|
119
165
|
- lib/jirametrics/jira_gateway.rb
|
|
166
|
+
- lib/jirametrics/mcp_server.rb
|
|
120
167
|
- lib/jirametrics/project_config.rb
|
|
168
|
+
- lib/jirametrics/pull_request.rb
|
|
169
|
+
- lib/jirametrics/pull_request_cycle_time_histogram.rb
|
|
170
|
+
- lib/jirametrics/pull_request_cycle_time_scatterplot.rb
|
|
171
|
+
- lib/jirametrics/pull_request_review.rb
|
|
172
|
+
- lib/jirametrics/raw_javascript.rb
|
|
121
173
|
- lib/jirametrics/rules.rb
|
|
122
174
|
- lib/jirametrics/self_or_issue_dispatcher.rb
|
|
123
175
|
- lib/jirametrics/settings.json
|
|
@@ -126,19 +178,24 @@ files:
|
|
|
126
178
|
- lib/jirametrics/sprint_issue_change_data.rb
|
|
127
179
|
- lib/jirametrics/status.rb
|
|
128
180
|
- lib/jirametrics/status_collection.rb
|
|
181
|
+
- lib/jirametrics/stitcher.rb
|
|
182
|
+
- lib/jirametrics/throughput_by_completed_resolution_chart.rb
|
|
129
183
|
- lib/jirametrics/throughput_chart.rb
|
|
184
|
+
- lib/jirametrics/time_based_histogram.rb
|
|
185
|
+
- lib/jirametrics/time_based_scatterplot.rb
|
|
130
186
|
- lib/jirametrics/tree_organizer.rb
|
|
131
187
|
- lib/jirametrics/trend_line_calculator.rb
|
|
188
|
+
- lib/jirametrics/user.rb
|
|
132
189
|
- lib/jirametrics/value_equality.rb
|
|
133
|
-
|
|
190
|
+
- lib/jirametrics/wip_by_column_chart.rb
|
|
191
|
+
homepage: https://jirametrics.org
|
|
134
192
|
licenses:
|
|
135
193
|
- Apache-2.0
|
|
136
194
|
metadata:
|
|
137
195
|
rubygems_mfa_required: 'true'
|
|
138
196
|
bug_tracker_uri: https://github.com/mikebowler/jirametrics/issues
|
|
139
|
-
changelog_uri: https://
|
|
140
|
-
documentation_uri: https://
|
|
141
|
-
post_install_message:
|
|
197
|
+
changelog_uri: https://jirametrics.org/changes
|
|
198
|
+
documentation_uri: https://jirametrics.org
|
|
142
199
|
rdoc_options: []
|
|
143
200
|
require_paths:
|
|
144
201
|
- lib
|
|
@@ -153,8 +210,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
153
210
|
- !ruby/object:Gem::Version
|
|
154
211
|
version: '0'
|
|
155
212
|
requirements: []
|
|
156
|
-
rubygems_version:
|
|
157
|
-
signing_key:
|
|
213
|
+
rubygems_version: 4.0.10
|
|
158
214
|
specification_version: 4
|
|
159
215
|
summary: Extract Jira metrics
|
|
160
216
|
test_files: []
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'jirametrics/self_or_issue_dispatcher'
|
|
4
|
-
require 'date'
|
|
5
|
-
|
|
6
|
-
class CycleTimeConfig
|
|
7
|
-
include SelfOrIssueDispatcher
|
|
8
|
-
|
|
9
|
-
attr_reader :label, :parent_config
|
|
10
|
-
|
|
11
|
-
def initialize parent_config:, label:, block:, today: Date.today
|
|
12
|
-
@parent_config = parent_config
|
|
13
|
-
@label = label
|
|
14
|
-
@today = today
|
|
15
|
-
instance_eval(&block) unless block.nil?
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def start_at block = nil
|
|
19
|
-
@start_at = block unless block.nil?
|
|
20
|
-
@start_at
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def stop_at block = nil
|
|
24
|
-
@stop_at = block unless block.nil?
|
|
25
|
-
@stop_at
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def in_progress? issue
|
|
29
|
-
started_time(issue) && stopped_time(issue).nil?
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def done? issue
|
|
33
|
-
stopped_time(issue)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def started_time issue
|
|
37
|
-
@start_at.call(issue)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def stopped_time issue
|
|
41
|
-
@stop_at.call(issue)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def cycletime issue
|
|
45
|
-
start = started_time(issue)
|
|
46
|
-
stop = stopped_time(issue)
|
|
47
|
-
return nil if start.nil? || stop.nil?
|
|
48
|
-
|
|
49
|
-
(stop.to_date - start.to_date).to_i + 1
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def age issue, today: nil
|
|
53
|
-
start = started_time(issue)
|
|
54
|
-
stop = today || @today || Date.today
|
|
55
|
-
return nil if start.nil? || stop.nil?
|
|
56
|
-
|
|
57
|
-
(stop.to_date - start.to_date).to_i + 1
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def possible_statuses
|
|
61
|
-
if parent_config.is_a? BoardConfig
|
|
62
|
-
project_config = parent_config.project_config
|
|
63
|
-
else
|
|
64
|
-
# TODO: This will go away when cycletimes are no longer supported inside html_reports
|
|
65
|
-
project_config = parent_config.file_config.project_config
|
|
66
|
-
end
|
|
67
|
-
project_config.possible_statuses
|
|
68
|
-
end
|
|
69
|
-
end
|