jirametrics 2.4 → 2.5pre1
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/lib/jirametrics/board.rb +1 -2
- data/lib/jirametrics/board_config.rb +2 -2
- data/lib/jirametrics/dependency_chart.rb +35 -7
- data/lib/jirametrics/download_config.rb +12 -0
- data/lib/jirametrics/downloader.rb +9 -3
- data/lib/jirametrics/examples/aggregated_project.rb +4 -15
- data/lib/jirametrics/examples/standard_project.rb +5 -22
- data/lib/jirametrics/expedited_chart.rb +1 -1
- data/lib/jirametrics/exporter.rb +3 -1
- data/lib/jirametrics/issue.rb +28 -4
- data/lib/jirametrics/project_config.rb +1 -1
- data/lib/jirametrics/rules.rb +2 -2
- data/lib/jirametrics/settings.json +4 -2
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 36ab7f032ba0b995401c3fc0ca9a11c353b3390284a668af73770ebd82b4eb77
|
4
|
+
data.tar.gz: cf843b3245b89cd7af6c00a0d82449bb5e10d957e3af997b7288d0b9360b8867
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 305166ab16e9abde2dbde0a01b5c60b46ff8f7654d4d4cb4b222777bb6222193d165cfcb9bc863e129eb599b6f63ee6e3ef6c19800cb22ffa39d9ca1933b5978
|
7
|
+
data.tar.gz: f72db2f5f5f408d51457e09d5fc43188487c8ba8dbefa6ace9dc1e90ec428a0f65e1c7828808f41d1d697365bcacdbd0faec51452a01344d7ec0a32fd6cb5a02
|
data/lib/jirametrics/board.rb
CHANGED
@@ -2,14 +2,13 @@
|
|
2
2
|
|
3
3
|
class Board
|
4
4
|
attr_reader :visible_columns, :raw, :possible_statuses, :sprints, :backlog_statuses
|
5
|
-
attr_accessor :cycletime, :project_config
|
5
|
+
attr_accessor :cycletime, :project_config
|
6
6
|
|
7
7
|
def initialize raw:, possible_statuses: StatusCollection.new
|
8
8
|
@raw = raw
|
9
9
|
@board_type = raw['type']
|
10
10
|
@possible_statuses = possible_statuses
|
11
11
|
@sprints = []
|
12
|
-
@expedited_priority_names = []
|
13
12
|
|
14
13
|
columns = raw['columnConfig']['columns']
|
15
14
|
|
@@ -11,7 +11,6 @@ class BoardConfig
|
|
11
11
|
|
12
12
|
def run
|
13
13
|
@board = @project_config.all_boards[id]
|
14
|
-
@board.expedited_priority_names = []
|
15
14
|
|
16
15
|
instance_eval(&@block)
|
17
16
|
end
|
@@ -26,6 +25,7 @@ class BoardConfig
|
|
26
25
|
end
|
27
26
|
|
28
27
|
def expedited_priority_names *priority_names
|
29
|
-
|
28
|
+
deprecated date: '2024-09-15', message: 'Expedited priority names are now specified in settings'
|
29
|
+
@project_config.settings['expedited_priority_names'] = priority_names
|
30
30
|
end
|
31
31
|
end
|
@@ -42,17 +42,13 @@ class DependencyChart < ChartBase
|
|
42
42
|
HTML
|
43
43
|
|
44
44
|
@rules_block = rules_block
|
45
|
-
@link_rules_block = ->(link_name, link_rules) {}
|
46
45
|
|
47
|
-
issue_rules
|
48
|
-
|
49
|
-
key = "<S>#{key} </S> " if issue.status.category_name == 'Done'
|
50
|
-
rules.label = "<#{key} [#{issue.type}]<BR/>#{word_wrap issue.summary}>"
|
51
|
-
end
|
46
|
+
issue_rules(&default_issue_rules)
|
47
|
+
link_rules(&default_link_rules)
|
52
48
|
end
|
53
49
|
|
54
50
|
def run
|
55
|
-
instance_eval(&@rules_block)
|
51
|
+
instance_eval(&@rules_block) if @rules_block
|
56
52
|
|
57
53
|
dot_graph = build_dot_graph
|
58
54
|
return "<h1>#{@header_text}</h1>No data matched the selected criteria. Nothing to show." if dot_graph.nil?
|
@@ -219,4 +215,36 @@ class DependencyChart < ChartBase
|
|
219
215
|
end
|
220
216
|
end.join(separator)
|
221
217
|
end
|
218
|
+
|
219
|
+
def default_issue_rules
|
220
|
+
chart = self
|
221
|
+
lambda do |issue, rules|
|
222
|
+
is_done = issue.done?
|
223
|
+
|
224
|
+
key = issue.key
|
225
|
+
key = "<S>#{key} </S> " if is_done
|
226
|
+
line2 = +'<BR/>'
|
227
|
+
if issue.artificial?
|
228
|
+
line2 << '(unknown state)' # Shouldn't happen if we've done a full download but is still possible.
|
229
|
+
elsif is_done
|
230
|
+
line2 << 'Done'
|
231
|
+
else
|
232
|
+
started_at = issue.board.cycletime.started_time(issue)
|
233
|
+
if started_at.nil?
|
234
|
+
line2 << 'Not started'
|
235
|
+
else
|
236
|
+
line2 << "Age: #{issue.board.cycletime.age(issue, today: chart.date_range.end)} days"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
rules.label = "<#{key} [#{issue.type}]#{line2}<BR/>#{word_wrap issue.summary}>"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def default_link_rules
|
244
|
+
lambda do |link, rules|
|
245
|
+
rules.ignore if link.origin.done? && link.other_issue.done?
|
246
|
+
rules.ignore if link.name == 'Cloners'
|
247
|
+
rules.merge_bidirectional keep: 'outward'
|
248
|
+
end
|
249
|
+
end
|
222
250
|
end
|
@@ -19,4 +19,16 @@ class DownloadConfig
|
|
19
19
|
@rolling_date_count = count unless count.nil?
|
20
20
|
@rolling_date_count
|
21
21
|
end
|
22
|
+
|
23
|
+
def no_earlier_than date = nil
|
24
|
+
@no_earlier_than = Date.parse(date) unless date.nil?
|
25
|
+
@no_earlier_than
|
26
|
+
end
|
27
|
+
|
28
|
+
def start_date today:
|
29
|
+
date = today.to_date - @rolling_date_count if @rolling_date_count
|
30
|
+
date = [date, @no_earlier_than].max if date && @no_earlier_than
|
31
|
+
date = @no_earlier_than if date.nil? && @no_earlier_than
|
32
|
+
date
|
33
|
+
end
|
22
34
|
end
|
@@ -199,7 +199,11 @@ class Downloader
|
|
199
199
|
)
|
200
200
|
is_last = json['isLast']
|
201
201
|
max_results = json['maxResults']
|
202
|
-
|
202
|
+
if json['values']
|
203
|
+
start_at += json['values'].size
|
204
|
+
else
|
205
|
+
log " No sprints found for board #{board_id}"
|
206
|
+
end
|
203
207
|
end
|
204
208
|
end
|
205
209
|
|
@@ -271,8 +275,10 @@ class Downloader
|
|
271
275
|
segments = []
|
272
276
|
segments << "filter=#{filter_id}"
|
273
277
|
|
274
|
-
|
275
|
-
|
278
|
+
start_date = @download_config.start_date today: today
|
279
|
+
|
280
|
+
if start_date
|
281
|
+
@download_date_range = start_date..today.to_date
|
276
282
|
|
277
283
|
# For an incremental download, we want to query from the end of the previous one, not from the
|
278
284
|
# beginning of the full range.
|
@@ -64,25 +64,14 @@ class Exporter
|
|
64
64
|
|
65
65
|
# By default, the issue doesn't show what board it's on and this is important for an
|
66
66
|
# aggregated view
|
67
|
+
chart = self
|
67
68
|
issue_rules do |issue, rules|
|
68
|
-
|
69
|
-
|
70
|
-
rules.label = "<#{key} [#{issue.type}]<BR/>#{issue.board.name}<BR/>#{word_wrap issue.summary}>"
|
69
|
+
chart.default_issue_rules.call(issue, rules)
|
70
|
+
rules.label = rules.label.split('<BR/>').insert(1, "Board: #{issue.board.name}").join('<BR/>')
|
71
71
|
end
|
72
72
|
|
73
73
|
link_rules do |link, rules|
|
74
|
-
|
75
|
-
case link.name
|
76
|
-
when 'Cloners'
|
77
|
-
# We don't want to see any clone links at all.
|
78
|
-
rules.ignore
|
79
|
-
when 'Blocks'
|
80
|
-
# For blocks, by default Jira will have links going both
|
81
|
-
# ways and we want them only going one way. Also make the
|
82
|
-
# link red.
|
83
|
-
rules.merge_bidirectional keep: 'outward'
|
84
|
-
rules.line_color = 'red'
|
85
|
-
end
|
74
|
+
chart.default_link_rules.call(link, rules)
|
86
75
|
|
87
76
|
# Because this is the aggregated view, let's hide any link that doesn't cross boards.
|
88
77
|
rules.ignore if link.origin.board == link.other_issue.board
|
@@ -6,13 +6,13 @@
|
|
6
6
|
# See https://github.com/mikebowler/jirametrics/wiki/Examples-folder for more
|
7
7
|
class Exporter
|
8
8
|
def standard_project name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {},
|
9
|
-
default_board: nil, anonymize: false, settings: {}, status_category_mappings: {}
|
9
|
+
default_board: nil, anonymize: false, settings: {}, status_category_mappings: {},
|
10
|
+
rolling_date_count: 90, no_earlier_than: nil
|
10
11
|
|
11
12
|
project name: name do
|
12
13
|
puts name
|
13
14
|
self.anonymize if anonymize
|
14
15
|
|
15
|
-
settings['blocked_link_text'] = ['is blocked by']
|
16
16
|
self.settings.merge! settings
|
17
17
|
|
18
18
|
status_category_mappings.each do |status, category|
|
@@ -21,7 +21,8 @@ class Exporter
|
|
21
21
|
|
22
22
|
file_prefix file_prefix
|
23
23
|
download do
|
24
|
-
rolling_date_count
|
24
|
+
self.rolling_date_count(rolling_date_count) if rolling_date_count
|
25
|
+
self.no_earlier_than(no_earlier_than) if no_earlier_than
|
25
26
|
end
|
26
27
|
|
27
28
|
boards.each_key do |board_id|
|
@@ -34,7 +35,6 @@ class Exporter
|
|
34
35
|
end
|
35
36
|
board id: board_id do
|
36
37
|
cycletime(&block)
|
37
|
-
expedited_priority_names 'Critical', 'Highest', 'Immediate Gating'
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
@@ -87,24 +87,7 @@ class Exporter
|
|
87
87
|
expedited_chart
|
88
88
|
sprint_burndown
|
89
89
|
estimate_accuracy_chart
|
90
|
-
|
91
|
-
dependency_chart do
|
92
|
-
link_rules do |link, rules|
|
93
|
-
case link.name
|
94
|
-
when 'Cloners'
|
95
|
-
rules.ignore
|
96
|
-
when 'Dependency', 'Blocks', 'Parent/Child', 'Cause', 'Satisfy Requirement', 'Relates'
|
97
|
-
rules.merge_bidirectional keep: 'outward'
|
98
|
-
rules.merge_bidirectional keep: 'outward'
|
99
|
-
when 'Sync'
|
100
|
-
rules.use_bidirectional_arrows
|
101
|
-
else
|
102
|
-
# This is a link type that we don't recognize. Dump it to standard out to draw attention
|
103
|
-
# to it.
|
104
|
-
puts "name=#{link.name}, label=#{link.label}"
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
90
|
+
dependency_chart
|
108
91
|
end
|
109
92
|
end
|
110
93
|
end
|
@@ -57,7 +57,7 @@ class ExpeditedChart < ChartBase
|
|
57
57
|
def prepare_expedite_data issue
|
58
58
|
expedite_start = nil
|
59
59
|
result = []
|
60
|
-
expedited_priority_names = issue.board.expedited_priority_names
|
60
|
+
expedited_priority_names = issue.board.project_config.settings['expedited_priority_names']
|
61
61
|
|
62
62
|
issue.changes.each do |change|
|
63
63
|
next unless change.priority?
|
data/lib/jirametrics/exporter.rb
CHANGED
data/lib/jirametrics/issue.rb
CHANGED
@@ -371,7 +371,9 @@ class Issue
|
|
371
371
|
end
|
372
372
|
|
373
373
|
def expedited?
|
374
|
-
|
374
|
+
return false unless @board&.project_config
|
375
|
+
|
376
|
+
names = @board.project_config.settings['expedited_priority_names']
|
375
377
|
return false unless names
|
376
378
|
|
377
379
|
current_priority = raw['fields']['priority']&.[]('name')
|
@@ -380,7 +382,9 @@ class Issue
|
|
380
382
|
|
381
383
|
def expedited_on_date? date
|
382
384
|
expedited_start = nil
|
383
|
-
|
385
|
+
return false unless @board&.project_config
|
386
|
+
|
387
|
+
expedited_names = @board.project_config.settings['expedited_priority_names']
|
384
388
|
|
385
389
|
changes.each do |change|
|
386
390
|
next unless change.priority?
|
@@ -433,6 +437,10 @@ class Issue
|
|
433
437
|
@fix_versions
|
434
438
|
end
|
435
439
|
|
440
|
+
def looks_like_issue_key? key
|
441
|
+
!!(key.is_a?(String) && key =~ /^[^-]+-\d+$/)
|
442
|
+
end
|
443
|
+
|
436
444
|
def parent_key project_config: @board.project_config
|
437
445
|
# Although Atlassian is trying to standardize on one way to determine the parent, today it's a mess.
|
438
446
|
# We try a variety of ways to get the parent and hopefully one of them will work. See this link:
|
@@ -454,8 +462,13 @@ class Issue
|
|
454
462
|
|
455
463
|
custom_field_names&.each do |field_name|
|
456
464
|
parent = fields[field_name]
|
457
|
-
|
458
|
-
|
465
|
+
next if parent.nil?
|
466
|
+
break if looks_like_issue_key? parent
|
467
|
+
|
468
|
+
project_config.file_system.log(
|
469
|
+
"Custom field #{field_name.inspect} should point to a parent id but found #{parent.inspect}"
|
470
|
+
)
|
471
|
+
parent = nil
|
459
472
|
end
|
460
473
|
end
|
461
474
|
|
@@ -505,6 +518,17 @@ class Issue
|
|
505
518
|
result
|
506
519
|
end
|
507
520
|
|
521
|
+
def done?
|
522
|
+
if artificial? || board.cycletime.nil?
|
523
|
+
# This was probably loaded as a linked issue, which means we don't know what board it really
|
524
|
+
# belonged to. The best we can do is look at the status category. This case should be rare but
|
525
|
+
# it can happen.
|
526
|
+
status.category_name == 'Done'
|
527
|
+
else
|
528
|
+
board.cycletime.done? self
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
508
532
|
private
|
509
533
|
|
510
534
|
def assemble_author raw
|
@@ -180,7 +180,7 @@ class ProjectConfig
|
|
180
180
|
|
181
181
|
board_id = $1.to_i
|
182
182
|
timezone_offset = exporter.timezone_offset
|
183
|
-
JSON.parse(file_system.load("#{target_path}#{file}"))['values']
|
183
|
+
JSON.parse(file_system.load("#{target_path}#{file}"))['values']&.each do |json|
|
184
184
|
@all_boards[board_id].sprints << Sprint.new(raw: json, timezone_offset: timezone_offset)
|
185
185
|
end
|
186
186
|
end
|
data/lib/jirametrics/rules.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jirametrics
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.5pre1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Bowler
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-09-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: random-word
|
@@ -153,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
153
153
|
- !ruby/object:Gem::Version
|
154
154
|
version: '0'
|
155
155
|
requirements: []
|
156
|
-
rubygems_version: 3.5.
|
156
|
+
rubygems_version: 3.5.18
|
157
157
|
signing_key:
|
158
158
|
specification_version: 4
|
159
159
|
summary: Extract Jira metrics
|