jirametrics 2.12.1 → 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 +191 -133
- 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 +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 +5 -3
- data/lib/jirametrics/board.rb +32 -8
- data/lib/jirametrics/board_config.rb +4 -1
- 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 +15 -6
- data/lib/jirametrics/chart_base.rb +141 -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} +21 -4
- data/lib/jirametrics/cycletime_histogram.rb +15 -101
- data/lib/jirametrics/cycletime_scatterplot.rb +17 -83
- data/lib/jirametrics/daily_view.rb +90 -61
- data/lib/jirametrics/daily_wip_by_age_chart.rb +4 -5
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +14 -4
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -2
- data/lib/jirametrics/daily_wip_chart.rb +30 -8
- data/lib/jirametrics/data_quality_report.rb +43 -12
- data/lib/jirametrics/dependency_chart.rb +6 -3
- data/lib/jirametrics/download_config.rb +15 -0
- data/lib/jirametrics/downloader.rb +117 -71
- 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 +42 -4
- data/lib/jirametrics/examples/aggregated_project.rb +2 -2
- data/lib/jirametrics/examples/standard_project.rb +41 -28
- data/lib/jirametrics/expedited_chart.rb +3 -1
- data/lib/jirametrics/exporter.rb +26 -6
- data/lib/jirametrics/file_config.rb +9 -11
- data/lib/jirametrics/file_system.rb +59 -3
- data/lib/jirametrics/fix_version.rb +13 -0
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +5 -1
- data/lib/jirametrics/github_gateway.rb +115 -0
- data/lib/jirametrics/groupable_issue_chart.rb +11 -1
- data/lib/jirametrics/grouping_rules.rb +26 -4
- data/lib/jirametrics/html/aging_work_bar_chart.erb +5 -5
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +3 -1
- data/lib/jirametrics/html/aging_work_table.erb +5 -0
- 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 +40 -5
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +4 -12
- data/lib/jirametrics/html/expedited_chart.erb +6 -14
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +4 -8
- data/lib/jirametrics/html/index.css +249 -69
- data/lib/jirametrics/html/index.erb +11 -37
- data/lib/jirametrics/html/index.js +164 -0
- data/lib/jirametrics/html/legacy_colors.css +174 -0
- data/lib/jirametrics/html/sprint_burndown.erb +17 -15
- data/lib/jirametrics/html/throughput_chart.erb +42 -11
- data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +61 -59
- data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +15 -11
- 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 +52 -57
- data/lib/jirametrics/issue.rb +305 -102
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/jira_gateway.rb +81 -17
- data/lib/jirametrics/mcp_server.rb +531 -0
- data/lib/jirametrics/project_config.rb +128 -12
- 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/settings.json +5 -1
- data/lib/jirametrics/sprint.rb +12 -0
- data/lib/jirametrics/sprint_burndown.rb +10 -4
- data/lib/jirametrics/status.rb +1 -1
- data/lib/jirametrics/status_collection.rb +1 -0
- 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/wip_by_column_chart.rb +236 -0
- data/lib/jirametrics.rb +83 -68
- metadata +61 -6
|
@@ -45,6 +45,7 @@ class DataQualityReport < ChartBase
|
|
|
45
45
|
scan_for_completed_issues_without_a_start_time entry: entry
|
|
46
46
|
scan_for_status_change_after_done entry: entry
|
|
47
47
|
scan_for_backwards_movement entry: entry, backlog_statuses: backlog_statuses
|
|
48
|
+
scan_for_issue_not_in_active_sprint entry: entry
|
|
48
49
|
scan_for_issues_not_created_in_a_backlog_status entry: entry, backlog_statuses: backlog_statuses
|
|
49
50
|
scan_for_stopped_before_started entry: entry
|
|
50
51
|
scan_for_issues_not_started_with_subtasks_that_have entry: entry
|
|
@@ -68,7 +69,7 @@ class DataQualityReport < ChartBase
|
|
|
68
69
|
result << render_problem_type(:status_changes_after_done)
|
|
69
70
|
result << render_problem_type(:backwards_through_status_categories)
|
|
70
71
|
result << render_problem_type(:backwords_through_statuses)
|
|
71
|
-
result << render_problem_type(:
|
|
72
|
+
result << render_problem_type(:issue_not_visible_on_board)
|
|
72
73
|
result << render_problem_type(:created_in_wrong_status)
|
|
73
74
|
result << render_problem_type(:stopped_before_started)
|
|
74
75
|
result << render_problem_type(:issue_not_started_but_subtasks_have)
|
|
@@ -120,7 +121,7 @@ class DataQualityReport < ChartBase
|
|
|
120
121
|
|
|
121
122
|
def initialize_entries
|
|
122
123
|
@entries = @issues.filter_map do |issue|
|
|
123
|
-
started, stopped = issue.
|
|
124
|
+
started, stopped = issue.started_stopped_times
|
|
124
125
|
next if stopped && stopped < time_range.begin
|
|
125
126
|
next if started && started > time_range.end
|
|
126
127
|
|
|
@@ -194,7 +195,7 @@ class DataQualityReport < ChartBase
|
|
|
194
195
|
# If it's been moved back to backlog then it's on a different report. Ignore it here.
|
|
195
196
|
detail = nil if backlog_statuses.any? { |s| s.name == change.value }
|
|
196
197
|
|
|
197
|
-
entry.report(problem_key: :
|
|
198
|
+
entry.report(problem_key: :issue_not_visible_on_board, detail: detail) unless detail.nil?
|
|
198
199
|
elsif change.old_value.nil?
|
|
199
200
|
# Do nothing
|
|
200
201
|
elsif index < last_index
|
|
@@ -223,6 +224,29 @@ class DataQualityReport < ChartBase
|
|
|
223
224
|
end
|
|
224
225
|
end
|
|
225
226
|
|
|
227
|
+
def scan_for_issue_not_in_active_sprint entry:
|
|
228
|
+
issue = entry.issue
|
|
229
|
+
return unless issue.board.scrum?
|
|
230
|
+
return if issue.sprints.any?(&:active?)
|
|
231
|
+
|
|
232
|
+
entry.report(problem_key: :issue_not_visible_on_board, detail: 'Issue is not in an active sprint')
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def scan_for_issue_never_visible_on_board entry:
|
|
236
|
+
issue = entry.issue
|
|
237
|
+
ever_visible = issue.changes.any? do |change|
|
|
238
|
+
next unless change.status?
|
|
239
|
+
|
|
240
|
+
issue.board.visible_columns.any? { |col| col.status_ids.include?(change.value_id) }
|
|
241
|
+
end
|
|
242
|
+
return if ever_visible
|
|
243
|
+
|
|
244
|
+
entry.report(
|
|
245
|
+
problem_key: :issue_not_visible_on_board,
|
|
246
|
+
detail: 'Issue has never been in a status mapped to a visible column on the board'
|
|
247
|
+
)
|
|
248
|
+
end
|
|
249
|
+
|
|
226
250
|
def scan_for_issues_not_created_in_a_backlog_status entry:, backlog_statuses:
|
|
227
251
|
creation_change = entry.issue.changes.find { |issue| issue.status? }
|
|
228
252
|
|
|
@@ -250,7 +274,7 @@ class DataQualityReport < ChartBase
|
|
|
250
274
|
|
|
251
275
|
started_subtasks = []
|
|
252
276
|
entry.issue.subtasks.each do |subtask|
|
|
253
|
-
started_subtasks << subtask if subtask.
|
|
277
|
+
started_subtasks << subtask if subtask.started_stopped_times.first
|
|
254
278
|
end
|
|
255
279
|
|
|
256
280
|
return if started_subtasks.empty?
|
|
@@ -266,8 +290,10 @@ class DataQualityReport < ChartBase
|
|
|
266
290
|
|
|
267
291
|
def scan_for_items_blocked_on_closed_tickets entry:
|
|
268
292
|
entry.issue.issue_links.each do |link|
|
|
293
|
+
next unless settings['blocked_link_text'].include?(link.label)
|
|
294
|
+
|
|
269
295
|
this_active = !entry.stopped
|
|
270
|
-
other_active = !link.other_issue.
|
|
296
|
+
other_active = !link.other_issue.started_stopped_times.last
|
|
271
297
|
next unless this_active && !other_active
|
|
272
298
|
|
|
273
299
|
entry.report(
|
|
@@ -293,14 +319,14 @@ class DataQualityReport < ChartBase
|
|
|
293
319
|
return "#{delta} hours" if delta < 24
|
|
294
320
|
|
|
295
321
|
delta /= 24
|
|
296
|
-
|
|
322
|
+
label_days delta
|
|
297
323
|
end
|
|
298
324
|
|
|
299
325
|
def scan_for_incomplete_subtasks_when_issue_done entry:
|
|
300
326
|
return unless entry.stopped
|
|
301
327
|
|
|
302
328
|
subtask_labels = entry.issue.subtasks.filter_map do |subtask|
|
|
303
|
-
subtask_started, subtask_stopped = subtask.
|
|
329
|
+
subtask_started, subtask_stopped = subtask.started_stopped_times
|
|
304
330
|
|
|
305
331
|
if !subtask_started && !subtask_stopped
|
|
306
332
|
"#{subtask_label subtask} (Not even started)"
|
|
@@ -407,17 +433,22 @@ class DataQualityReport < ChartBase
|
|
|
407
433
|
HTML
|
|
408
434
|
end
|
|
409
435
|
|
|
410
|
-
def
|
|
436
|
+
def render_issue_not_visible_on_board problems
|
|
437
|
+
unique_issue_count = problems.map(&:first).uniq.size
|
|
411
438
|
<<-HTML
|
|
412
|
-
#{
|
|
413
|
-
|
|
439
|
+
#{problems.size} #{'time'.then { |w| problems.size == 1 ? w : "#{w}s" }} across #{label_issues unique_issue_count},
|
|
440
|
+
an item was not visible on the board. This may impact
|
|
441
|
+
timings as the work was likely to have been forgotten if it wasn't visible. An issue can be not visible
|
|
442
|
+
for two reasons: the issue was in a status that is not mapped to any visible column on the board
|
|
443
|
+
(look in "unmapped statuses" on your board), or for scrum boards, the issue was not in an active sprint.
|
|
414
444
|
HTML
|
|
415
445
|
end
|
|
416
446
|
|
|
417
447
|
def render_created_in_wrong_status problems
|
|
418
448
|
<<-HTML
|
|
419
|
-
#{label_issues problems.size} were created in a status not
|
|
420
|
-
|
|
449
|
+
#{label_issues problems.size} were created in a status that is not considered to be some varient
|
|
450
|
+
of To Do. Most likely this means that the issue was created from one of the columns on the board,
|
|
451
|
+
rather than in the backlog. Why Jira allows this is still a mystery.
|
|
421
452
|
HTML
|
|
422
453
|
end
|
|
423
454
|
|
|
@@ -51,10 +51,13 @@ class DependencyChart < ChartBase
|
|
|
51
51
|
instance_eval(&@rules_block) if @rules_block
|
|
52
52
|
|
|
53
53
|
dot_graph = build_dot_graph
|
|
54
|
-
|
|
54
|
+
if dot_graph.nil?
|
|
55
|
+
return "<h1 class='foldable'>#{@header_text}</h1>" \
|
|
56
|
+
'<div>No data matched the selected criteria. Nothing to show.</div>'
|
|
57
|
+
end
|
|
55
58
|
|
|
56
59
|
svg = execute_graphviz(dot_graph.join("\n"))
|
|
57
|
-
"<h1>#{@header_text}</h1><div>#{@description_text}
|
|
60
|
+
"<h1 class='foldable'>#{@header_text}</h1><div>#{@description_text}#{shrink_svg svg}</div>"
|
|
58
61
|
end
|
|
59
62
|
|
|
60
63
|
def link_rules &block
|
|
@@ -228,7 +231,7 @@ class DependencyChart < ChartBase
|
|
|
228
231
|
elsif is_done
|
|
229
232
|
line2 << 'Done'
|
|
230
233
|
else
|
|
231
|
-
started_at = issue.
|
|
234
|
+
started_at = issue.started_stopped_times.first
|
|
232
235
|
if started_at.nil?
|
|
233
236
|
line2 << 'Not started'
|
|
234
237
|
else
|
|
@@ -25,10 +25,25 @@ class DownloadConfig
|
|
|
25
25
|
@no_earlier_than
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def github_repos
|
|
29
|
+
@github_repos ||= []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def github_repo *repos
|
|
33
|
+
github_repos.concat(repos.map { |r| normalize_github_repo(r) })
|
|
34
|
+
end
|
|
35
|
+
|
|
28
36
|
def start_date today:
|
|
29
37
|
date = today.to_date - @rolling_date_count if @rolling_date_count
|
|
30
38
|
date = [date, @no_earlier_than].max if date && @no_earlier_than
|
|
31
39
|
date = @no_earlier_than if date.nil? && @no_earlier_than
|
|
32
40
|
date
|
|
33
41
|
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def normalize_github_repo repo
|
|
46
|
+
match = repo.match(%r{github\.com/([^/]+/[^/]+?)/?$})
|
|
47
|
+
match ? match[1] : repo
|
|
48
|
+
end
|
|
34
49
|
end
|
|
@@ -3,8 +3,29 @@
|
|
|
3
3
|
require 'cgi'
|
|
4
4
|
require 'json'
|
|
5
5
|
|
|
6
|
+
class DownloadIssueData
|
|
7
|
+
attr_accessor :key, :found_in_primary_query, :last_modified,
|
|
8
|
+
:up_to_date, :cache_path, :issue
|
|
9
|
+
|
|
10
|
+
def initialize(
|
|
11
|
+
key:,
|
|
12
|
+
found_in_primary_query: true,
|
|
13
|
+
last_modified: nil,
|
|
14
|
+
up_to_date: true,
|
|
15
|
+
cache_path: nil,
|
|
16
|
+
issue: nil
|
|
17
|
+
)
|
|
18
|
+
@key = key
|
|
19
|
+
@found_in_primary_query = found_in_primary_query
|
|
20
|
+
@last_modified = last_modified
|
|
21
|
+
@up_to_date = up_to_date
|
|
22
|
+
@cache_path = cache_path
|
|
23
|
+
@issue = issue
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
6
27
|
class Downloader
|
|
7
|
-
CURRENT_METADATA_VERSION =
|
|
28
|
+
CURRENT_METADATA_VERSION = 5
|
|
8
29
|
|
|
9
30
|
attr_accessor :metadata
|
|
10
31
|
attr_reader :file_system
|
|
@@ -12,12 +33,23 @@ class Downloader
|
|
|
12
33
|
# For testing only
|
|
13
34
|
attr_reader :start_date_in_query, :board_id_to_filter_id
|
|
14
35
|
|
|
15
|
-
def
|
|
36
|
+
def self.create download_config:, file_system:, jira_gateway:, github_pr_cache: {}
|
|
37
|
+
is_cloud = jira_gateway.settings['jira_cloud'] || jira_gateway.cloud?
|
|
38
|
+
(is_cloud ? DownloaderForCloud : DownloaderForDataCenter).new(
|
|
39
|
+
download_config: download_config,
|
|
40
|
+
file_system: file_system,
|
|
41
|
+
jira_gateway: jira_gateway,
|
|
42
|
+
github_pr_cache: github_pr_cache
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize download_config:, file_system:, jira_gateway:, github_pr_cache: {}
|
|
16
47
|
@metadata = {}
|
|
17
48
|
@download_config = download_config
|
|
18
49
|
@target_path = @download_config.project_config.target_path
|
|
19
50
|
@file_system = file_system
|
|
20
51
|
@jira_gateway = jira_gateway
|
|
52
|
+
@github_pr_cache = github_pr_cache
|
|
21
53
|
@board_id_to_filter_id = {}
|
|
22
54
|
|
|
23
55
|
@issue_keys_downloaded_in_current_run = []
|
|
@@ -28,7 +60,6 @@ class Downloader
|
|
|
28
60
|
log '', both: true
|
|
29
61
|
log @download_config.project_config.name, both: true
|
|
30
62
|
|
|
31
|
-
init_gateway
|
|
32
63
|
load_metadata
|
|
33
64
|
|
|
34
65
|
if @metadata['no-download']
|
|
@@ -43,85 +74,41 @@ class Downloader
|
|
|
43
74
|
download_statuses
|
|
44
75
|
find_board_ids.each do |id|
|
|
45
76
|
board = download_board_configuration board_id: id
|
|
77
|
+
board.project_config = @download_config.project_config
|
|
46
78
|
download_issues board: board
|
|
47
79
|
end
|
|
48
80
|
download_users
|
|
49
81
|
|
|
50
82
|
save_metadata
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def init_gateway
|
|
54
|
-
@jira_gateway.load_jira_config(@download_config.project_config.jira_config)
|
|
55
|
-
@jira_gateway.ignore_ssl_errors = @download_config.project_config.settings['ignore_ssl_errors']
|
|
83
|
+
download_github_prs if @download_config.github_repos.any?
|
|
56
84
|
end
|
|
57
85
|
|
|
58
86
|
def log text, both: false
|
|
59
87
|
@file_system.log text, also_write_to_stderr: both
|
|
60
88
|
end
|
|
61
89
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
raise 'Board ids must be specified' if ids.empty?
|
|
65
|
-
|
|
66
|
-
ids
|
|
90
|
+
def log_start text
|
|
91
|
+
@file_system.log_start text
|
|
67
92
|
end
|
|
68
93
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
path = File.join(@target_path, "#{file_prefix}_issues/")
|
|
72
|
-
unless Dir.exist?(path)
|
|
73
|
-
log " Creating path #{path}"
|
|
74
|
-
Dir.mkdir(path)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
filter_id = @board_id_to_filter_id[board.id]
|
|
78
|
-
jql = make_jql(filter_id: filter_id)
|
|
79
|
-
jira_search_by_jql(jql: jql, initial_query: true, board: board, path: path)
|
|
80
|
-
|
|
81
|
-
log " Downloading linked issues for board #{board.id}", both: true
|
|
82
|
-
loop do
|
|
83
|
-
@issue_keys_pending_download.reject! { |key| @issue_keys_downloaded_in_current_run.include? key }
|
|
84
|
-
break if @issue_keys_pending_download.empty?
|
|
85
|
-
|
|
86
|
-
keys_to_request = @issue_keys_pending_download[0..99]
|
|
87
|
-
@issue_keys_pending_download.reject! { |key| keys_to_request.include? key }
|
|
88
|
-
jql = "key in (#{keys_to_request.join(', ')})"
|
|
89
|
-
jira_search_by_jql(jql: jql, initial_query: false, board: board, path: path)
|
|
90
|
-
end
|
|
94
|
+
def start_progress
|
|
95
|
+
@file_system.start_progress
|
|
91
96
|
end
|
|
92
97
|
|
|
93
|
-
def
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
log " JQL: #{jql}"
|
|
98
|
-
escaped_jql = CGI.escape jql
|
|
99
|
-
|
|
100
|
-
max_results = 100
|
|
101
|
-
start_at = 0
|
|
102
|
-
total = 1
|
|
103
|
-
while start_at < total
|
|
104
|
-
json = @jira_gateway.call_url relative_url: '/rest/api/2/search' \
|
|
105
|
-
"?jql=#{escaped_jql}&maxResults=#{max_results}&startAt=#{start_at}&expand=changelog&fields=*all"
|
|
106
|
-
|
|
107
|
-
json['issues'].each do |issue_json|
|
|
108
|
-
issue_json['exporter'] = {
|
|
109
|
-
'in_initial_query' => initial_query
|
|
110
|
-
}
|
|
111
|
-
identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
|
|
112
|
-
file = "#{issue_json['key']}-#{board.id}.json"
|
|
113
|
-
|
|
114
|
-
@file_system.save_json(json: issue_json, filename: File.join(path, file))
|
|
115
|
-
end
|
|
98
|
+
def progress_dot message = nil
|
|
99
|
+
@file_system.log message if message
|
|
100
|
+
@file_system.progress_dot
|
|
101
|
+
end
|
|
116
102
|
|
|
117
|
-
|
|
118
|
-
|
|
103
|
+
def end_progress
|
|
104
|
+
@file_system.end_progress
|
|
105
|
+
end
|
|
119
106
|
|
|
120
|
-
|
|
121
|
-
|
|
107
|
+
def find_board_ids
|
|
108
|
+
ids = @download_config.project_config.board_configs.collect(&:id)
|
|
109
|
+
raise 'Board ids must be specified' if ids.empty?
|
|
122
110
|
|
|
123
|
-
|
|
124
|
-
end
|
|
111
|
+
ids
|
|
125
112
|
end
|
|
126
113
|
|
|
127
114
|
def identify_other_issues_to_be_downloaded raw_issue:, board:
|
|
@@ -149,6 +136,8 @@ class Downloader
|
|
|
149
136
|
end
|
|
150
137
|
|
|
151
138
|
def download_users
|
|
139
|
+
return unless @jira_gateway.cloud?
|
|
140
|
+
|
|
152
141
|
log ' Downloading all users', both: true
|
|
153
142
|
json = @jira_gateway.call_url relative_url: '/rest/api/2/users'
|
|
154
143
|
|
|
@@ -197,11 +186,28 @@ class Downloader
|
|
|
197
186
|
# actually look at the returned json.
|
|
198
187
|
@board_id_to_filter_id[board_id] = json['filter']['id'].to_i
|
|
199
188
|
|
|
189
|
+
if json['type'] == 'simple'
|
|
190
|
+
features_json = download_features board_id: board_id
|
|
191
|
+
if BoardFeature.from_raw(features_json).any? { |f| f.name == 'jsw.agility.sprints' && f.enabled? }
|
|
192
|
+
download_sprints board_id: board_id
|
|
193
|
+
end
|
|
194
|
+
end
|
|
200
195
|
download_sprints board_id: board_id if json['type'] == 'scrum'
|
|
201
196
|
# TODO: Should be passing actual statuses, not empty list
|
|
202
197
|
Board.new raw: json, possible_statuses: StatusCollection.new
|
|
203
198
|
end
|
|
204
199
|
|
|
200
|
+
def download_features board_id:
|
|
201
|
+
log " Downloading features for board #{board_id}", both: true
|
|
202
|
+
json = @jira_gateway.call_url relative_url: "/rest/agile/1.0/board/#{board_id}/features"
|
|
203
|
+
|
|
204
|
+
@file_system.save_json(
|
|
205
|
+
json: json,
|
|
206
|
+
filename: File.join(@target_path, "#{file_prefix}_board_#{board_id}_features.json")
|
|
207
|
+
)
|
|
208
|
+
json
|
|
209
|
+
end
|
|
210
|
+
|
|
205
211
|
def download_sprints board_id:
|
|
206
212
|
log " Downloading sprints for board #{board_id}", both: true
|
|
207
213
|
max_results = 100
|
|
@@ -243,19 +249,29 @@ class Downloader
|
|
|
243
249
|
value = Date.parse(value) if value.is_a?(String) && value =~ /^\d{4}-\d{2}-\d{2}$/
|
|
244
250
|
@metadata[key] = value
|
|
245
251
|
end
|
|
252
|
+
|
|
246
253
|
end
|
|
247
254
|
|
|
248
255
|
# Even if this is the old format, we want to obey this one tag
|
|
249
256
|
@metadata['no-download'] = hash['no-download'] if hash['no-download']
|
|
250
257
|
end
|
|
251
258
|
|
|
259
|
+
def timezone_offset
|
|
260
|
+
@download_config.project_config.exporter.timezone_offset
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def today_in_project_timezone
|
|
264
|
+
Time.now.getlocal(timezone_offset).to_date
|
|
265
|
+
end
|
|
266
|
+
|
|
252
267
|
def save_metadata
|
|
253
268
|
@metadata['version'] = CURRENT_METADATA_VERSION
|
|
269
|
+
@metadata['rolling_date_count'] = @download_config.rolling_date_count
|
|
254
270
|
@metadata['date_start_from_last_query'] = @start_date_in_query if @start_date_in_query
|
|
255
271
|
|
|
256
272
|
if @download_date_range.nil?
|
|
257
273
|
log "Making up a date range in meta since one wasn't specified. You'll want to change that.", both: true
|
|
258
|
-
today =
|
|
274
|
+
today = today_in_project_timezone
|
|
259
275
|
@download_date_range = (today - 7)..today
|
|
260
276
|
end
|
|
261
277
|
|
|
@@ -290,7 +306,8 @@ class Downloader
|
|
|
290
306
|
end
|
|
291
307
|
end
|
|
292
308
|
|
|
293
|
-
def make_jql filter_id:, today:
|
|
309
|
+
def make_jql filter_id:, today: nil
|
|
310
|
+
today ||= today_in_project_timezone
|
|
294
311
|
segments = []
|
|
295
312
|
segments << "filter=#{filter_id}"
|
|
296
313
|
|
|
@@ -298,11 +315,7 @@ class Downloader
|
|
|
298
315
|
|
|
299
316
|
if start_date
|
|
300
317
|
@download_date_range = start_date..today.to_date
|
|
301
|
-
|
|
302
|
-
# For an incremental download, we want to query from the end of the previous one, not from the
|
|
303
|
-
# beginning of the full range.
|
|
304
|
-
@start_date_in_query = metadata['date_end'] || @download_date_range.begin
|
|
305
|
-
log " Incremental download only. Pulling from #{@start_date_in_query}", both: true if metadata['date_end']
|
|
318
|
+
@start_date_in_query = @download_date_range.begin
|
|
306
319
|
|
|
307
320
|
# Catch-all to pick up anything that's been around since before the range started but hasn't
|
|
308
321
|
# had an update during the range.
|
|
@@ -319,6 +332,39 @@ class Downloader
|
|
|
319
332
|
segments.join ' AND '
|
|
320
333
|
end
|
|
321
334
|
|
|
335
|
+
def download_github_prs
|
|
336
|
+
project_keys = extract_project_keys_from_downloaded_issues
|
|
337
|
+
if project_keys.empty?
|
|
338
|
+
log ' No project keys found in downloaded issues, skipping GitHub PR download', both: true
|
|
339
|
+
return
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
prs = @download_config.github_repos.flat_map do |repo|
|
|
343
|
+
GithubGateway.new(
|
|
344
|
+
repo: repo,
|
|
345
|
+
project_keys: project_keys,
|
|
346
|
+
file_system: @file_system,
|
|
347
|
+
raw_pr_cache: @github_pr_cache
|
|
348
|
+
).fetch_pull_requests(since: @download_date_range&.begin)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
@file_system.save_json(
|
|
352
|
+
json: prs.map(&:raw),
|
|
353
|
+
filename: File.join(@target_path, "#{file_prefix}_github_prs.json")
|
|
354
|
+
)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def extract_project_keys_from_downloaded_issues
|
|
358
|
+
path = File.join(@target_path, "#{file_prefix}_issues")
|
|
359
|
+
return [] unless @file_system.dir_exist?(path)
|
|
360
|
+
|
|
361
|
+
keys = []
|
|
362
|
+
@file_system.foreach(path) do |filename|
|
|
363
|
+
keys << filename.split('-').first if filename.match?(/^[A-Z][A-Z_0-9]+-\d+-\d+\.json$/)
|
|
364
|
+
end
|
|
365
|
+
keys.uniq
|
|
366
|
+
end
|
|
367
|
+
|
|
322
368
|
def file_prefix
|
|
323
369
|
@download_config.project_config.get_file_prefix
|
|
324
370
|
end
|