jirametrics 2.13 → 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 +101 -97
- 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 +14 -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 +85 -53
- 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 -100
- 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 +9 -35
- 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 +304 -101
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/jira_gateway.rb +77 -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/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 -69
- metadata +60 -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,114 +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
|
-
if @jira_gateway.cloud?
|
|
101
|
-
max_results = 5_000 # The maximum allowed by Jira
|
|
102
|
-
next_page_token = nil
|
|
103
|
-
issue_count = 0
|
|
104
|
-
|
|
105
|
-
loop do
|
|
106
|
-
json = @jira_gateway.call_url relative_url: '/rest/api/3/search/jql' \
|
|
107
|
-
"?jql=#{escaped_jql}&maxResults=#{max_results}&" \
|
|
108
|
-
"nextPageToken=#{next_page_token}&expand=changelog&fields=*all"
|
|
109
|
-
next_page_token = json['nextPageToken']
|
|
110
|
-
|
|
111
|
-
json['issues'].each do |issue_json|
|
|
112
|
-
issue_json['exporter'] = {
|
|
113
|
-
'in_initial_query' => initial_query
|
|
114
|
-
}
|
|
115
|
-
identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
|
|
116
|
-
file = "#{issue_json['key']}-#{board.id}.json"
|
|
98
|
+
def progress_dot message = nil
|
|
99
|
+
@file_system.log message if message
|
|
100
|
+
@file_system.progress_dot
|
|
101
|
+
end
|
|
117
102
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
103
|
+
def end_progress
|
|
104
|
+
@file_system.end_progress
|
|
105
|
+
end
|
|
121
106
|
|
|
122
|
-
|
|
123
|
-
|
|
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?
|
|
124
110
|
|
|
125
|
-
|
|
126
|
-
end
|
|
127
|
-
else
|
|
128
|
-
max_results = 100
|
|
129
|
-
start_at = 0
|
|
130
|
-
total = 1
|
|
131
|
-
while start_at < total
|
|
132
|
-
json = @jira_gateway.call_url relative_url: '/rest/api/2/search' \
|
|
133
|
-
"?jql=#{escaped_jql}&maxResults=#{max_results}&startAt=#{start_at}&expand=changelog&fields=*all"
|
|
134
|
-
|
|
135
|
-
json['issues'].each do |issue_json|
|
|
136
|
-
issue_json['exporter'] = {
|
|
137
|
-
'in_initial_query' => initial_query
|
|
138
|
-
}
|
|
139
|
-
identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
|
|
140
|
-
file = "#{issue_json['key']}-#{board.id}.json"
|
|
141
|
-
|
|
142
|
-
@file_system.save_json(json: issue_json, filename: File.join(path, file))
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
total = json['total'].to_i
|
|
146
|
-
max_results = json['maxResults']
|
|
147
|
-
|
|
148
|
-
message = " Downloaded #{start_at + 1}-#{[start_at + max_results, total].min} of #{total} issues to #{path} "
|
|
149
|
-
log message, both: true
|
|
150
|
-
|
|
151
|
-
start_at += json['issues'].size
|
|
152
|
-
end
|
|
153
|
-
end
|
|
111
|
+
ids
|
|
154
112
|
end
|
|
155
113
|
|
|
156
114
|
def identify_other_issues_to_be_downloaded raw_issue:, board:
|
|
@@ -178,6 +136,8 @@ class Downloader
|
|
|
178
136
|
end
|
|
179
137
|
|
|
180
138
|
def download_users
|
|
139
|
+
return unless @jira_gateway.cloud?
|
|
140
|
+
|
|
181
141
|
log ' Downloading all users', both: true
|
|
182
142
|
json = @jira_gateway.call_url relative_url: '/rest/api/2/users'
|
|
183
143
|
|
|
@@ -226,11 +186,28 @@ class Downloader
|
|
|
226
186
|
# actually look at the returned json.
|
|
227
187
|
@board_id_to_filter_id[board_id] = json['filter']['id'].to_i
|
|
228
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
|
|
229
195
|
download_sprints board_id: board_id if json['type'] == 'scrum'
|
|
230
196
|
# TODO: Should be passing actual statuses, not empty list
|
|
231
197
|
Board.new raw: json, possible_statuses: StatusCollection.new
|
|
232
198
|
end
|
|
233
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
|
+
|
|
234
211
|
def download_sprints board_id:
|
|
235
212
|
log " Downloading sprints for board #{board_id}", both: true
|
|
236
213
|
max_results = 100
|
|
@@ -272,19 +249,29 @@ class Downloader
|
|
|
272
249
|
value = Date.parse(value) if value.is_a?(String) && value =~ /^\d{4}-\d{2}-\d{2}$/
|
|
273
250
|
@metadata[key] = value
|
|
274
251
|
end
|
|
252
|
+
|
|
275
253
|
end
|
|
276
254
|
|
|
277
255
|
# Even if this is the old format, we want to obey this one tag
|
|
278
256
|
@metadata['no-download'] = hash['no-download'] if hash['no-download']
|
|
279
257
|
end
|
|
280
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
|
+
|
|
281
267
|
def save_metadata
|
|
282
268
|
@metadata['version'] = CURRENT_METADATA_VERSION
|
|
269
|
+
@metadata['rolling_date_count'] = @download_config.rolling_date_count
|
|
283
270
|
@metadata['date_start_from_last_query'] = @start_date_in_query if @start_date_in_query
|
|
284
271
|
|
|
285
272
|
if @download_date_range.nil?
|
|
286
273
|
log "Making up a date range in meta since one wasn't specified. You'll want to change that.", both: true
|
|
287
|
-
today =
|
|
274
|
+
today = today_in_project_timezone
|
|
288
275
|
@download_date_range = (today - 7)..today
|
|
289
276
|
end
|
|
290
277
|
|
|
@@ -319,7 +306,8 @@ class Downloader
|
|
|
319
306
|
end
|
|
320
307
|
end
|
|
321
308
|
|
|
322
|
-
def make_jql filter_id:, today:
|
|
309
|
+
def make_jql filter_id:, today: nil
|
|
310
|
+
today ||= today_in_project_timezone
|
|
323
311
|
segments = []
|
|
324
312
|
segments << "filter=#{filter_id}"
|
|
325
313
|
|
|
@@ -327,11 +315,7 @@ class Downloader
|
|
|
327
315
|
|
|
328
316
|
if start_date
|
|
329
317
|
@download_date_range = start_date..today.to_date
|
|
330
|
-
|
|
331
|
-
# For an incremental download, we want to query from the end of the previous one, not from the
|
|
332
|
-
# beginning of the full range.
|
|
333
|
-
@start_date_in_query = metadata['date_end'] || @download_date_range.begin
|
|
334
|
-
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
|
|
335
319
|
|
|
336
320
|
# Catch-all to pick up anything that's been around since before the range started but hasn't
|
|
337
321
|
# had an update during the range.
|
|
@@ -348,6 +332,39 @@ class Downloader
|
|
|
348
332
|
segments.join ' AND '
|
|
349
333
|
end
|
|
350
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
|
+
|
|
351
368
|
def file_prefix
|
|
352
369
|
@download_config.project_config.get_file_prefix
|
|
353
370
|
end
|