jirametrics 2.22 → 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 +10 -2
- data/lib/jirametrics/aging_work_bar_chart.rb +26 -10
- data/lib/jirametrics/aging_work_in_progress_chart.rb +43 -11
- data/lib/jirametrics/aging_work_table.rb +9 -7
- data/lib/jirametrics/anonymizer.rb +74 -1
- data/lib/jirametrics/atlassian_document_format.rb +93 -93
- data/lib/jirametrics/blocked_stalled_change.rb +5 -3
- data/lib/jirametrics/board.rb +28 -8
- data/lib/jirametrics/board_feature.rb +14 -0
- data/lib/jirametrics/board_movement_calculator.rb +2 -2
- data/lib/jirametrics/cfd_data_builder.rb +108 -0
- data/lib/jirametrics/change_item.rb +4 -3
- data/lib/jirametrics/chart_base.rb +107 -3
- data/lib/jirametrics/css_variable.rb +1 -1
- data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
- data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +1 -2
- data/lib/jirametrics/cycletime_histogram.rb +15 -103
- data/lib/jirametrics/cycletime_scatterplot.rb +13 -98
- data/lib/jirametrics/daily_view.rb +38 -13
- data/lib/jirametrics/daily_wip_by_age_chart.rb +1 -1
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +1 -1
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -2
- data/lib/jirametrics/daily_wip_chart.rb +29 -7
- data/lib/jirametrics/data_quality_report.rb +38 -12
- data/lib/jirametrics/dependency_chart.rb +2 -2
- data/lib/jirametrics/download_config.rb +15 -0
- data/lib/jirametrics/downloader.rb +87 -5
- data/lib/jirametrics/downloader_for_cloud.rb +107 -22
- data/lib/jirametrics/downloader_for_data_center.rb +3 -2
- data/lib/jirametrics/estimate_accuracy_chart.rb +42 -4
- data/lib/jirametrics/examples/aggregated_project.rb +2 -2
- data/lib/jirametrics/examples/standard_project.rb +32 -19
- data/lib/jirametrics/expedited_chart.rb +3 -1
- data/lib/jirametrics/exporter.rb +15 -2
- data/lib/jirametrics/file_config.rb +9 -11
- data/lib/jirametrics/file_system.rb +35 -2
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +5 -1
- data/lib/jirametrics/github_gateway.rb +115 -0
- data/lib/jirametrics/groupable_issue_chart.rb +4 -0
- data/lib/jirametrics/grouping_rules.rb +26 -4
- data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -4
- data/lib/jirametrics/html/aging_work_table.erb +3 -0
- data/lib/jirametrics/html/cumulative_flow_diagram.erb +503 -0
- data/lib/jirametrics/html/daily_wip_chart.erb +38 -5
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -12
- data/lib/jirametrics/html/expedited_chart.erb +3 -13
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -8
- data/lib/jirametrics/html/index.css +228 -60
- data/lib/jirametrics/html/index.erb +6 -0
- data/lib/jirametrics/html/index.js +53 -3
- data/lib/jirametrics/html/legacy_colors.css +174 -0
- data/lib/jirametrics/html/sprint_burndown.erb +7 -13
- data/lib/jirametrics/html/throughput_chart.erb +40 -9
- data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +59 -59
- data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +11 -7
- data/lib/jirametrics/html/wip_by_column_chart.erb +250 -0
- data/lib/jirametrics/html_generator.rb +2 -1
- data/lib/jirametrics/html_report_config.rb +45 -33
- data/lib/jirametrics/issue.rb +197 -99
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/jira_gateway.rb +32 -10
- data/lib/jirametrics/mcp_server.rb +531 -0
- data/lib/jirametrics/project_config.rb +87 -8
- 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 +4 -0
- data/lib/jirametrics/settings.json +3 -1
- data/lib/jirametrics/sprint_burndown.rb +4 -2
- data/lib/jirametrics/status.rb +1 -1
- data/lib/jirametrics/stitcher.rb +7 -1
- 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/wip_by_column_chart.rb +236 -0
- data/lib/jirametrics.rb +58 -0
- metadata +52 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1f64f63f13e8cb59d3b18fb1e1ad90f77ca06d0e2f59a75ff7b7bae4db1870f
|
|
4
|
+
data.tar.gz: 9b7d6b8759102d7590e86c114d2ac8b1b2e7c4cc9f45a168002752196a6bf797
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8ec0bee468f8c34c001ea9151b0d78b1018d246cc86f9c2588a70ee55e3940b6263f69032884ec5c2dc596d3182909829f6ee63fe51b6c65444ce667bf70a6ca
|
|
7
|
+
data.tar.gz: 9b9f5337d0fc671639f9f651977cb2ecc60b2d27105cdce1b75a2a4188c093c111ba29ed880adccb515d548b8bd9209b4cf703482a0ede4ccea6b182212a3a57
|
data/bin/jirametrics-mcp
ADDED
|
@@ -65,8 +65,16 @@ class AggregateConfig
|
|
|
65
65
|
|
|
66
66
|
if issues.nil?
|
|
67
67
|
file_system.warning "No issues found for #{project_name}"
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
@project_config.add_issues issues
|
|
72
|
+
|
|
73
|
+
# Bring fix versions over
|
|
74
|
+
project.fix_versions.each do |fix_version|
|
|
75
|
+
unless @project_config.fix_versions.find { |fv| fv.id == fix_version.id }
|
|
76
|
+
@project_config.fix_versions << fix_version
|
|
77
|
+
end
|
|
70
78
|
end
|
|
71
79
|
end
|
|
72
80
|
|
|
@@ -15,7 +15,7 @@ class AgingWorkBarChart < ChartBase
|
|
|
15
15
|
newest at the bottom.
|
|
16
16
|
</p>
|
|
17
17
|
<p>
|
|
18
|
-
There are three bars for each issue, and hovering over any of the bars will provide more details.
|
|
18
|
+
There are <%= (aggregated_project? || current_board.scrum?) ? 'four' : 'three' %> bars for each issue, and hovering over any of the bars will provide more details.
|
|
19
19
|
<ol>
|
|
20
20
|
<li>Status: The status the issue was in at any time. The colour indicates the
|
|
21
21
|
status category, which will be one of #{color_block '--status-category-todo-color'} To Do,
|
|
@@ -25,6 +25,9 @@ class AgingWorkBarChart < ChartBase
|
|
|
25
25
|
or #{color_block '--stalled-color'} stalled.</li>
|
|
26
26
|
<li>Priority: This shows the priority over time. If one of these priorities is considered expedited
|
|
27
27
|
then it will be drawn with diagonal lines.</li>
|
|
28
|
+
<% if aggregated_project? || current_board.scrum? %>
|
|
29
|
+
<li>Sprints: The sprints that the issue was in.</li>
|
|
30
|
+
<% end %>
|
|
28
31
|
</ol>
|
|
29
32
|
</p>
|
|
30
33
|
#{describe_non_working_days}
|
|
@@ -63,7 +66,7 @@ class AgingWorkBarChart < ChartBase
|
|
|
63
66
|
|
|
64
67
|
def adjust_time_date_ranges_to_start_from_earliest_issue_start aging_issues
|
|
65
68
|
earliest_start_time = aging_issues.collect do |issue|
|
|
66
|
-
issue.
|
|
69
|
+
issue.started_stopped_times.first
|
|
67
70
|
end.min
|
|
68
71
|
return if earliest_start_time.nil? || earliest_start_time >= @time_range.begin
|
|
69
72
|
|
|
@@ -81,7 +84,9 @@ class AgingWorkBarChart < ChartBase
|
|
|
81
84
|
['blocked', collect_blocked_stalled_ranges(issue: issue, issue_start_time: issue_start_time)],
|
|
82
85
|
['priority', collect_priority_ranges(issue: issue)]
|
|
83
86
|
]
|
|
84
|
-
bar_data << ['sprints', collect_sprint_ranges(issue: issue)] if current_board.scrum?
|
|
87
|
+
bar_data << ['sprints', collect_sprint_ranges(issue: issue)] if aggregated_project? || current_board.scrum?
|
|
88
|
+
|
|
89
|
+
bar_data.each { |entry| clip_ranges_to_start_time(ranges: entry.last, issue_start_time: issue_start_time) }
|
|
85
90
|
|
|
86
91
|
issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
|
|
87
92
|
bar_data.collect do |stack, ranges|
|
|
@@ -97,7 +102,7 @@ class AgingWorkBarChart < ChartBase
|
|
|
97
102
|
|
|
98
103
|
def select_aging_issues issues:
|
|
99
104
|
issues.select do |issue|
|
|
100
|
-
started_time, stopped_time = issue.
|
|
105
|
+
started_time, stopped_time = issue.started_stopped_times
|
|
101
106
|
next false unless started_time && stopped_time.nil?
|
|
102
107
|
|
|
103
108
|
age = (date_range.end - started_time.to_date).to_i + 1
|
|
@@ -108,15 +113,22 @@ class AgingWorkBarChart < ChartBase
|
|
|
108
113
|
def grow_chart_height_if_too_many_issues aging_issue_count:
|
|
109
114
|
px_per_bar = 10
|
|
110
115
|
bars_per_issue = 3
|
|
111
|
-
bars_per_issue += 1 if current_board.scrum?
|
|
116
|
+
bars_per_issue += 1 if aggregated_project? || current_board.scrum?
|
|
112
117
|
|
|
113
118
|
preferred_height = aging_issue_count * px_per_bar * bars_per_issue
|
|
114
119
|
@canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
|
|
115
120
|
end
|
|
116
121
|
|
|
122
|
+
def clip_ranges_to_start_time ranges:, issue_start_time:
|
|
123
|
+
return if issue_start_time.nil?
|
|
124
|
+
|
|
125
|
+
ranges.each { |range| range.start = issue_start_time if range.start < issue_start_time }
|
|
126
|
+
ranges.reject! { |range| range.start >= range.stop }
|
|
127
|
+
end
|
|
128
|
+
|
|
117
129
|
def collect_status_ranges issue:, now:
|
|
118
130
|
ranges = []
|
|
119
|
-
issue_started_time = issue.
|
|
131
|
+
issue_started_time = issue.started_stopped_times.first
|
|
120
132
|
previous_start = nil
|
|
121
133
|
previous_status = nil
|
|
122
134
|
issue.status_changes.each do |change|
|
|
@@ -223,10 +235,12 @@ class AgingWorkBarChart < ChartBase
|
|
|
223
235
|
previous_change = change
|
|
224
236
|
end
|
|
225
237
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
238
|
+
if previous_change
|
|
239
|
+
results << create_range_for_priority(
|
|
240
|
+
previous_change: previous_change, stop_time: time_range.end,
|
|
241
|
+
expedited_priority_names: expedited_priority_names
|
|
242
|
+
)
|
|
243
|
+
end
|
|
230
244
|
results
|
|
231
245
|
end
|
|
232
246
|
|
|
@@ -263,6 +277,8 @@ class AgingWorkBarChart < ChartBase
|
|
|
263
277
|
end
|
|
264
278
|
|
|
265
279
|
open_sprints.each_value do |data|
|
|
280
|
+
next if data[:sprint].future?
|
|
281
|
+
|
|
266
282
|
stop = data[:sprint].completed_time || time_range.end
|
|
267
283
|
results << BarChartRange.new(
|
|
268
284
|
start: data[:start_time], stop: stop,
|
|
@@ -6,6 +6,7 @@ require 'jirametrics/board_movement_calculator'
|
|
|
6
6
|
|
|
7
7
|
class AgingWorkInProgressChart < ChartBase
|
|
8
8
|
include GroupableIssueChart
|
|
9
|
+
|
|
9
10
|
attr_accessor :possible_statuses, :board_id
|
|
10
11
|
attr_reader :board_columns
|
|
11
12
|
|
|
@@ -55,7 +56,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
55
56
|
def run
|
|
56
57
|
determine_board_columns
|
|
57
58
|
|
|
58
|
-
@header_text += " on board: #{
|
|
59
|
+
@header_text += " on board: #{current_board.name}"
|
|
59
60
|
data_sets = make_data_sets
|
|
60
61
|
|
|
61
62
|
adjust_visibility_of_unmapped_status_column data_sets: data_sets
|
|
@@ -76,7 +77,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
76
77
|
|
|
77
78
|
@fake_column = BoardColumn.new({
|
|
78
79
|
'name' => '[Unmapped Statuses]',
|
|
79
|
-
'statuses' => unmapped_statuses.collect { |id| { 'id' => id.to_s } }.uniq
|
|
80
|
+
'statuses' => unmapped_statuses.collect { |id| { 'id' => id.to_s } }.uniq
|
|
80
81
|
})
|
|
81
82
|
@board_columns = columns + [@fake_column]
|
|
82
83
|
end
|
|
@@ -114,14 +115,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
114
115
|
|
|
115
116
|
calculator = BoardMovementCalculator.new board: @all_boards[@board_id], issues: issues, today: date_range.end
|
|
116
117
|
|
|
117
|
-
column_indexes_to_remove =
|
|
118
|
-
unless @show_all_columns
|
|
119
|
-
column_indexes_to_remove = indexes_of_leading_and_trailing_zeros(calculator.age_data_for(percentage: 100))
|
|
120
|
-
|
|
121
|
-
column_indexes_to_remove.reverse_each do |index|
|
|
122
|
-
@board_columns.delete_at index
|
|
123
|
-
end
|
|
124
|
-
end
|
|
118
|
+
column_indexes_to_remove = trim_board_columns data_sets: data_sets, calculator: calculator
|
|
125
119
|
|
|
126
120
|
@row_index_offset = data_sets.size
|
|
127
121
|
|
|
@@ -177,6 +171,44 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
177
171
|
result
|
|
178
172
|
end
|
|
179
173
|
|
|
174
|
+
def trim_board_columns data_sets:, calculator:
|
|
175
|
+
return [] if @show_all_columns
|
|
176
|
+
|
|
177
|
+
columns_with_aging_items = data_sets.flat_map do |set|
|
|
178
|
+
set['data'].filter_map { |d| d['x'] if d.is_a? Hash }
|
|
179
|
+
end.uniq
|
|
180
|
+
|
|
181
|
+
# @fake_column is always the last element and is handled separately.
|
|
182
|
+
real_column_count = @board_columns.size - 1
|
|
183
|
+
|
|
184
|
+
# The last visible column always has artificially inflated age_data because
|
|
185
|
+
# ages_of_issues_when_leaving_column uses `today` as end_date when there is no
|
|
186
|
+
# next column. Exclude it from the right-boundary search so it is only kept when
|
|
187
|
+
# it has current aging items (handled by the last_aging fallback below).
|
|
188
|
+
age_data = calculator.age_data_for(percentage: 100)
|
|
189
|
+
last_data = (0...(real_column_count - 1)).to_a.reverse.find { |i| !age_data[i].zero? }
|
|
190
|
+
|
|
191
|
+
in_current = ->(i) { columns_with_aging_items.include?(@board_columns[i].name) }
|
|
192
|
+
first_aging = (0...real_column_count).find(&in_current)
|
|
193
|
+
last_aging = (0...real_column_count).to_a.reverse.find(&in_current)
|
|
194
|
+
|
|
195
|
+
# Combine: include any column with age_data (up to but not including the last visible
|
|
196
|
+
# column) and any column with current aging items.
|
|
197
|
+
first_data = (0...real_column_count).find { |i| !age_data[i].zero? }
|
|
198
|
+
left_bound = [first_data, first_aging].compact.min
|
|
199
|
+
right_bound = [last_data, last_aging].compact.max
|
|
200
|
+
|
|
201
|
+
indexes_to_remove =
|
|
202
|
+
if left_bound && right_bound
|
|
203
|
+
(0...left_bound).to_a + ((right_bound + 1)...real_column_count).to_a
|
|
204
|
+
else
|
|
205
|
+
(0...real_column_count).to_a
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
indexes_to_remove.reverse_each { |index| @board_columns.delete_at index }
|
|
209
|
+
indexes_to_remove
|
|
210
|
+
end
|
|
211
|
+
|
|
180
212
|
def column_for issue:
|
|
181
213
|
@board_columns.find do |board_column|
|
|
182
214
|
board_column.status_ids.include? issue.status.id
|
|
@@ -192,7 +224,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
|
192
224
|
end
|
|
193
225
|
end
|
|
194
226
|
|
|
195
|
-
if has_unmapped
|
|
227
|
+
if has_unmapped && @description_text
|
|
196
228
|
@description_text += "<p>The items shown in #{column_name.inspect} are not visible on the " \
|
|
197
229
|
'board but are still active. Most likely everyone has forgotten about them.</p>'
|
|
198
230
|
else
|
|
@@ -45,20 +45,21 @@ class AgingWorkTable < ChartBase
|
|
|
45
45
|
# This is its own method simply so the tests can initialize the calculator without doing a full run.
|
|
46
46
|
def initialize_calculator
|
|
47
47
|
@today = date_range.end
|
|
48
|
-
@
|
|
48
|
+
@calculators = @all_boards.transform_values do |board|
|
|
49
|
+
BoardMovementCalculator.new board: board, issues: issues, today: @today
|
|
50
|
+
end
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
def expedited_but_not_started
|
|
52
54
|
@issues.select do |issue|
|
|
53
|
-
started_time, stopped_time = issue.
|
|
55
|
+
started_time, stopped_time = issue.started_stopped_times
|
|
54
56
|
started_time.nil? && stopped_time.nil? && issue.expedited?
|
|
55
57
|
end.sort_by(&:created)
|
|
56
58
|
end
|
|
57
59
|
|
|
58
60
|
def select_aging_issues
|
|
59
61
|
aging_issues = @issues.select do |issue|
|
|
60
|
-
|
|
61
|
-
started, stopped = cycletime.started_stopped_times(issue)
|
|
62
|
+
started, stopped = issue.started_stopped_times
|
|
62
63
|
next false if started.nil? || stopped
|
|
63
64
|
next true if issue.blocked_on_date?(@today, end_time: time_range.end) || issue.expedited?
|
|
64
65
|
|
|
@@ -77,7 +78,7 @@ class AgingWorkTable < ChartBase
|
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
def blocked_text issue
|
|
80
|
-
started_time, _stopped_time = issue.
|
|
81
|
+
started_time, _stopped_time = issue.started_stopped_times
|
|
81
82
|
return nil if started_time.nil?
|
|
82
83
|
|
|
83
84
|
current = issue.blocked_stalled_changes(end_time: time_range.end)[-1]
|
|
@@ -124,7 +125,8 @@ class AgingWorkTable < ChartBase
|
|
|
124
125
|
due = issue.due_date
|
|
125
126
|
message = nil
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
calculator = @calculators[issue.board.id]
|
|
129
|
+
days_remaining, error = calculator.forecasted_days_remaining_and_message issue: issue, today: @today
|
|
128
130
|
|
|
129
131
|
unless error
|
|
130
132
|
if due
|
|
@@ -174,6 +176,6 @@ class AgingWorkTable < ChartBase
|
|
|
174
176
|
end
|
|
175
177
|
|
|
176
178
|
def priority_text issue
|
|
177
|
-
"<img src='#{issue.priority_url}' title='Priority: #{issue.priority_name}' />"
|
|
179
|
+
"<img src='#{issue.priority_url}' title='Priority: #{issue.priority_name}' style='max-width: 1em;'/>"
|
|
178
180
|
end
|
|
179
181
|
end
|
|
@@ -21,6 +21,10 @@ class Anonymizer < ChartBase
|
|
|
21
21
|
anonymize_column_names
|
|
22
22
|
# anonymize_issue_statuses
|
|
23
23
|
anonymize_board_names
|
|
24
|
+
anonymize_labels_and_components
|
|
25
|
+
anonymize_sprints
|
|
26
|
+
anonymize_fix_versions
|
|
27
|
+
anonymize_server_url
|
|
24
28
|
shift_all_dates unless @date_adjustment.zero?
|
|
25
29
|
@file_system.log 'Anonymize done'
|
|
26
30
|
end
|
|
@@ -38,13 +42,25 @@ class Anonymizer < ChartBase
|
|
|
38
42
|
|
|
39
43
|
def anonymize_issue_keys_and_titles issues: @issues
|
|
40
44
|
counter = 0
|
|
45
|
+
seen_author_raws = {}
|
|
41
46
|
issues.each do |issue|
|
|
42
47
|
new_key = "ANON-#{counter += 1}"
|
|
43
48
|
|
|
44
49
|
issue.raw['key'] = new_key
|
|
45
50
|
issue.raw['fields']['summary'] = random_phrase
|
|
51
|
+
issue.raw['fields']['description'] = nil
|
|
46
52
|
issue.raw['fields']['assignee']['displayName'] = random_name unless issue.raw['fields']['assignee'].nil?
|
|
47
53
|
|
|
54
|
+
anonymize_author_raw(issue.raw['fields']['creator'], seen_author_raws)
|
|
55
|
+
|
|
56
|
+
issue.changes.each do |change|
|
|
57
|
+
anonymize_author_raw(change.author_raw, seen_author_raws)
|
|
58
|
+
if change.comment? || change.description?
|
|
59
|
+
change.value = nil
|
|
60
|
+
change.old_value = nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
48
64
|
issue.issue_links.each do |link|
|
|
49
65
|
other_issue = link.other_issue
|
|
50
66
|
next if other_issue.key.match?(/^ANON-\d+$/) # Already anonymized?
|
|
@@ -55,6 +71,49 @@ class Anonymizer < ChartBase
|
|
|
55
71
|
end
|
|
56
72
|
end
|
|
57
73
|
|
|
74
|
+
def anonymize_labels_and_components
|
|
75
|
+
@issues.each do |issue|
|
|
76
|
+
issue.raw['fields']['labels'] = []
|
|
77
|
+
issue.raw['fields']['components'] = []
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def anonymize_sprints
|
|
82
|
+
sprint_counter = 0
|
|
83
|
+
sprint_name_map = {}
|
|
84
|
+
@all_boards.each_value do |board|
|
|
85
|
+
board.sprints.each do |sprint|
|
|
86
|
+
name = sprint.raw['name']
|
|
87
|
+
unless sprint_name_map[name]
|
|
88
|
+
sprint_counter += 1
|
|
89
|
+
sprint_name_map[name] = "Sprint-#{sprint_counter}"
|
|
90
|
+
end
|
|
91
|
+
sprint.raw['name'] = sprint_name_map[name]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def anonymize_fix_versions
|
|
97
|
+
version_counter = 0
|
|
98
|
+
version_name_map = {}
|
|
99
|
+
@issues.each do |issue|
|
|
100
|
+
issue.raw['fields']['fixVersions']&.each do |fix_version|
|
|
101
|
+
name = fix_version['name']
|
|
102
|
+
unless version_name_map[name]
|
|
103
|
+
version_counter += 1
|
|
104
|
+
version_name_map[name] = "Version-#{version_counter}"
|
|
105
|
+
end
|
|
106
|
+
fix_version['name'] = version_name_map[name]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def anonymize_server_url
|
|
112
|
+
@all_boards.each_value do |board|
|
|
113
|
+
board.raw['self'] = board.raw['self']&.sub(/^https?:\/\/[^\/]+/, 'https://anon.example.com')
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
58
117
|
def anonymize_column_names
|
|
59
118
|
@all_boards.each_key do |board_id|
|
|
60
119
|
@file_system.log "Anonymizing column names for board #{board_id}"
|
|
@@ -143,7 +202,7 @@ class Anonymizer < ChartBase
|
|
|
143
202
|
end
|
|
144
203
|
|
|
145
204
|
range = @project_config.time_range
|
|
146
|
-
@project_config.time_range = (range.begin +
|
|
205
|
+
@project_config.time_range = (range.begin + adjustment_in_seconds)..(range.end + adjustment_in_seconds)
|
|
147
206
|
end
|
|
148
207
|
|
|
149
208
|
def random_name
|
|
@@ -186,4 +245,18 @@ class Anonymizer < ChartBase
|
|
|
186
245
|
board.raw['name'] = "#{random_phrase} board"
|
|
187
246
|
end
|
|
188
247
|
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
def anonymize_author_raw author_raw, seen
|
|
252
|
+
return unless author_raw
|
|
253
|
+
return if seen[author_raw.object_id]
|
|
254
|
+
|
|
255
|
+
seen[author_raw.object_id] = true
|
|
256
|
+
name = random_name
|
|
257
|
+
author_raw['displayName'] = name
|
|
258
|
+
author_raw['name'] = name
|
|
259
|
+
author_raw.delete('emailAddress')
|
|
260
|
+
author_raw.delete('avatarUrls')
|
|
261
|
+
end
|
|
189
262
|
end
|
|
@@ -23,105 +23,95 @@ class AtlassianDocumentFormat
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
def to_text input
|
|
27
|
+
if input.is_a? String
|
|
28
|
+
input
|
|
29
|
+
elsif input&.[]('content')
|
|
30
|
+
input['content'].collect { |element| adf_node_to_text element }.join
|
|
31
|
+
else
|
|
32
|
+
''
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
26
36
|
# ADF is Atlassian Document Format
|
|
27
37
|
# https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/
|
|
28
38
|
def adf_node_to_html node # rubocop:disable Metrics/CyclomaticComplexity
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
when 'mediaSingle', 'mediaGroup'
|
|
72
|
-
result << '<div>'
|
|
73
|
-
closing_tag = '</div>'
|
|
74
|
-
when 'mention'
|
|
75
|
-
user = node_attrs['text']
|
|
76
|
-
result << "<b>#{user}</b>"
|
|
77
|
-
when 'orderedList'
|
|
78
|
-
result << '<ol>'
|
|
79
|
-
closing_tag = '</ol>'
|
|
80
|
-
when 'panel'
|
|
81
|
-
type = node_attrs['panelType']
|
|
82
|
-
result << "<div>#{type.upcase}</div>"
|
|
83
|
-
when 'paragraph'
|
|
84
|
-
result << '<p>'
|
|
85
|
-
closing_tag = '</p>'
|
|
86
|
-
when 'rule'
|
|
87
|
-
result << '<hr />'
|
|
88
|
-
when 'status'
|
|
89
|
-
text = node_attrs['text']
|
|
90
|
-
result << text
|
|
91
|
-
when 'table'
|
|
92
|
-
result << '<table>'
|
|
93
|
-
closing_tag = '</table>'
|
|
94
|
-
when 'tableCell'
|
|
95
|
-
result << '<td>'
|
|
96
|
-
closing_tag = '</td>'
|
|
97
|
-
when 'tableHeader'
|
|
98
|
-
result << '<th>'
|
|
99
|
-
closing_tag = '</th>'
|
|
100
|
-
when 'tableRow'
|
|
101
|
-
result << '<tr>'
|
|
102
|
-
closing_tag = '</tr>'
|
|
103
|
-
when 'text'
|
|
104
|
-
marks = adf_marks_to_html node['marks']
|
|
105
|
-
result << marks.collect(&:first).join
|
|
106
|
-
result << node['text']
|
|
107
|
-
result << marks.collect(&:last).join
|
|
108
|
-
when 'taskItem'
|
|
109
|
-
state = node_attrs['state'] == 'TODO' ? '☐' : '☑'
|
|
110
|
-
result << "<li>#{state} "
|
|
111
|
-
closing_tag = '</li>'
|
|
112
|
-
when 'taskList'
|
|
113
|
-
result << "<ul class='taskList'>"
|
|
114
|
-
closing_tag = '</ul>'
|
|
115
|
-
else
|
|
116
|
-
result << "<p>Unparseable section: #{node['type']}</p>"
|
|
39
|
+
adf_node_render(node) do |n|
|
|
40
|
+
node_attrs = n['attrs']
|
|
41
|
+
case n['type']
|
|
42
|
+
when 'blockquote' then ['<blockquote>', '</blockquote>']
|
|
43
|
+
when 'bulletList' then ['<ul>', '</ul>']
|
|
44
|
+
when 'codeBlock' then ['<code>', '</code>']
|
|
45
|
+
when 'date'
|
|
46
|
+
[Time.at(node_attrs['timestamp'].to_i / 1000, in: @timezone_offset).to_date.to_s, nil]
|
|
47
|
+
when 'decisionItem', 'listItem' then ['<li>', '</li>']
|
|
48
|
+
when 'decisionList' then ['<div>Decisions<ul>', '</ul></div>']
|
|
49
|
+
when 'emoji', 'status' then [node_attrs['text'], nil]
|
|
50
|
+
when 'expand' then ["<div>#{node_attrs['title']}</div>", nil]
|
|
51
|
+
when 'hardBreak' then ['<br />', nil]
|
|
52
|
+
when 'heading'
|
|
53
|
+
level = node_attrs['level']
|
|
54
|
+
["<h#{level}>", "</h#{level}>"]
|
|
55
|
+
when 'inlineCard'
|
|
56
|
+
url = node_attrs['url']
|
|
57
|
+
["[Inline card]: <a href='#{url}'>#{url}</a>", nil]
|
|
58
|
+
when 'media'
|
|
59
|
+
text = node_attrs['alt'] || node_attrs['id']
|
|
60
|
+
["Media: #{text}", nil]
|
|
61
|
+
when 'mediaSingle', 'mediaGroup' then ['<div>', '</div>']
|
|
62
|
+
when 'mention' then ["<b>#{node_attrs['text']}</b>", nil]
|
|
63
|
+
when 'orderedList' then ['<ol>', '</ol>']
|
|
64
|
+
when 'panel' then ["<div>#{node_attrs['panelType'].upcase}</div>", nil]
|
|
65
|
+
when 'paragraph' then ['<p>', '</p>']
|
|
66
|
+
when 'rule' then ['<hr />', nil]
|
|
67
|
+
when 'table' then ['<table>', '</table>']
|
|
68
|
+
when 'tableCell' then ['<td>', '</td>']
|
|
69
|
+
when 'tableHeader' then ['<th>', '</th>']
|
|
70
|
+
when 'tableRow' then ['<tr>', '</tr>']
|
|
71
|
+
when 'text'
|
|
72
|
+
marks = adf_marks_to_html(n['marks'])
|
|
73
|
+
[marks.collect(&:first).join + n['text'], marks.collect(&:last).join]
|
|
74
|
+
when 'taskItem'
|
|
75
|
+
state = node_attrs['state'] == 'TODO' ? '☐' : '☑'
|
|
76
|
+
["<li>#{state} ", '</li>']
|
|
77
|
+
when 'taskList' then ["<ul class='taskList'>", '</ul>']
|
|
78
|
+
else
|
|
79
|
+
["<p>Unparseable section: #{n['type']}</p>", nil]
|
|
80
|
+
end
|
|
117
81
|
end
|
|
82
|
+
end
|
|
118
83
|
|
|
119
|
-
|
|
120
|
-
|
|
84
|
+
def adf_node_to_text node # rubocop:disable Metrics/CyclomaticComplexity
|
|
85
|
+
adf_node_render(node) do |n|
|
|
86
|
+
node_attrs = n['attrs']
|
|
87
|
+
case n['type']
|
|
88
|
+
when 'blockquote', 'bulletList', 'codeBlock',
|
|
89
|
+
'mediaSingle', 'mediaGroup',
|
|
90
|
+
'orderedList', 'table', 'taskList' then ['', nil]
|
|
91
|
+
when 'date'
|
|
92
|
+
[Time.at(node_attrs['timestamp'].to_i / 1000, in: @timezone_offset).to_date.to_s, nil]
|
|
93
|
+
when 'decisionItem' then ['- ', "\n"]
|
|
94
|
+
when 'decisionList' then ["Decisions:\n", nil]
|
|
95
|
+
when 'emoji', 'mention', 'status' then [node_attrs['text'], nil]
|
|
96
|
+
when 'expand' then ["#{node_attrs['title']}\n", nil]
|
|
97
|
+
when 'hardBreak' then ["\n", nil]
|
|
98
|
+
when 'heading', 'paragraph', 'tableRow' then ['', "\n"]
|
|
99
|
+
when 'inlineCard' then [node_attrs['url'], nil]
|
|
100
|
+
when 'listItem' then ['- ', nil]
|
|
101
|
+
when 'media'
|
|
102
|
+
text = node_attrs['alt'] || node_attrs['id']
|
|
103
|
+
["Media: #{text}", nil]
|
|
104
|
+
when 'panel' then ["#{node_attrs['panelType'].upcase}\n", nil]
|
|
105
|
+
when 'rule' then ["---\n", nil]
|
|
106
|
+
when 'tableCell', 'tableHeader' then ['', "\t"]
|
|
107
|
+
when 'text' then [n['text'], nil]
|
|
108
|
+
when 'taskItem'
|
|
109
|
+
state = node_attrs['state'] == 'TODO' ? '☐' : '☑'
|
|
110
|
+
["#{state} ", "\n"]
|
|
111
|
+
else
|
|
112
|
+
["[Unparseable: #{n['type']}]\n", nil]
|
|
113
|
+
end
|
|
121
114
|
end
|
|
122
|
-
|
|
123
|
-
result << closing_tag if closing_tag
|
|
124
|
-
result
|
|
125
115
|
end
|
|
126
116
|
|
|
127
117
|
def adf_marks_to_html list
|
|
@@ -157,4 +147,14 @@ class AtlassianDocumentFormat
|
|
|
157
147
|
text = "@#{user.display_name}" if user
|
|
158
148
|
"<span class='account_id'>#{text}</span>"
|
|
159
149
|
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def adf_node_render node, &render_node
|
|
154
|
+
prefix, suffix = render_node.call(node)
|
|
155
|
+
result = +(prefix || '')
|
|
156
|
+
node['content']&.each { |child| result << adf_node_render(child, &render_node) }
|
|
157
|
+
result << suffix if suffix
|
|
158
|
+
result
|
|
159
|
+
end
|
|
160
160
|
end
|
|
@@ -4,10 +4,12 @@ require 'jirametrics/value_equality'
|
|
|
4
4
|
|
|
5
5
|
class BlockedStalledChange
|
|
6
6
|
include ValueEquality
|
|
7
|
-
attr_reader :time, :blocking_issue_keys, :flag, :status, :stalled_days, :status_is_blocking
|
|
7
|
+
attr_reader :time, :blocking_issue_keys, :flag, :flag_reason, :status, :stalled_days, :status_is_blocking
|
|
8
8
|
|
|
9
|
-
def initialize time:, flagged: nil,
|
|
9
|
+
def initialize time:, flagged: nil, flag_reason: nil, status: nil, status_is_blocking: true,
|
|
10
|
+
blocking_issue_keys: nil, stalled_days: nil
|
|
10
11
|
@flag = flagged
|
|
12
|
+
@flag_reason = flag_reason
|
|
11
13
|
@status = status
|
|
12
14
|
@status_is_blocking = status_is_blocking
|
|
13
15
|
@blocking_issue_keys = blocking_issue_keys
|
|
@@ -25,7 +27,7 @@ class BlockedStalledChange
|
|
|
25
27
|
def reasons
|
|
26
28
|
result = []
|
|
27
29
|
if blocked?
|
|
28
|
-
result << 'Blocked by flag' if @flag
|
|
30
|
+
result << (@flag_reason ? "Blocked by flag: #{@flag_reason}" : 'Blocked by flag') if @flag
|
|
29
31
|
result << "Blocked by status: #{@status}" if blocked_by_status?
|
|
30
32
|
result << "Blocked by issues: #{@blocking_issue_keys.join(', ')}" if @blocking_issue_keys
|
|
31
33
|
elsif stalled_by_status?
|