jirametrics 2.5 → 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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/bin/jirametrics-mcp +5 -0
  3. data/lib/jirametrics/aggregate_config.rb +16 -3
  4. data/lib/jirametrics/aging_work_bar_chart.rb +193 -133
  5. data/lib/jirametrics/aging_work_in_progress_chart.rb +138 -42
  6. data/lib/jirametrics/aging_work_table.rb +63 -19
  7. data/lib/jirametrics/anonymizer.rb +81 -6
  8. data/lib/jirametrics/atlassian_document_format.rb +160 -0
  9. data/lib/jirametrics/bar_chart_range.rb +17 -0
  10. data/lib/jirametrics/blocked_stalled_change.rb +6 -4
  11. data/lib/jirametrics/board.rb +73 -20
  12. data/lib/jirametrics/board_config.rb +10 -2
  13. data/lib/jirametrics/board_feature.rb +14 -0
  14. data/lib/jirametrics/board_movement_calculator.rb +155 -0
  15. data/lib/jirametrics/cfd_data_builder.rb +108 -0
  16. data/lib/jirametrics/change_item.rb +54 -18
  17. data/lib/jirametrics/chart_base.rb +203 -30
  18. data/lib/jirametrics/css_variable.rb +2 -2
  19. data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
  20. data/lib/jirametrics/cycle_time_config.rb +137 -0
  21. data/lib/jirametrics/cycletime_histogram.rb +17 -38
  22. data/lib/jirametrics/cycletime_scatterplot.rb +18 -87
  23. data/lib/jirametrics/daily_view.rb +306 -0
  24. data/lib/jirametrics/daily_wip_by_age_chart.rb +5 -8
  25. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +15 -5
  26. data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -6
  27. data/lib/jirametrics/daily_wip_chart.rb +36 -16
  28. data/lib/jirametrics/data_quality_report.rb +251 -42
  29. data/lib/jirametrics/dependency_chart.rb +8 -6
  30. data/lib/jirametrics/download_config.rb +17 -2
  31. data/lib/jirametrics/downloader.rb +177 -108
  32. data/lib/jirametrics/downloader_for_cloud.rb +287 -0
  33. data/lib/jirametrics/downloader_for_data_center.rb +95 -0
  34. data/lib/jirametrics/estimate_accuracy_chart.rb +75 -14
  35. data/lib/jirametrics/estimation_configuration.rb +25 -0
  36. data/lib/jirametrics/examples/aggregated_project.rb +5 -8
  37. data/lib/jirametrics/examples/standard_project.rb +54 -38
  38. data/lib/jirametrics/expedited_chart.rb +10 -9
  39. data/lib/jirametrics/exporter.rb +51 -16
  40. data/lib/jirametrics/file_config.rb +21 -6
  41. data/lib/jirametrics/file_system.rb +96 -4
  42. data/lib/jirametrics/fix_version.rb +13 -0
  43. data/lib/jirametrics/flow_efficiency_scatterplot.rb +115 -0
  44. data/lib/jirametrics/github_gateway.rb +115 -0
  45. data/lib/jirametrics/groupable_issue_chart.rb +12 -4
  46. data/lib/jirametrics/grouping_rules.rb +26 -4
  47. data/lib/jirametrics/html/aging_work_bar_chart.erb +8 -17
  48. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +24 -5
  49. data/lib/jirametrics/html/aging_work_table.erb +13 -4
  50. data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
  51. data/lib/jirametrics/html/cumulative_flow_diagram.erb +503 -0
  52. data/lib/jirametrics/html/daily_wip_chart.erb +41 -15
  53. data/lib/jirametrics/html/estimate_accuracy_chart.erb +4 -12
  54. data/lib/jirametrics/html/expedited_chart.erb +7 -24
  55. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +81 -0
  56. data/lib/jirametrics/html/hierarchy_table.erb +1 -1
  57. data/lib/jirametrics/html/index.css +336 -62
  58. data/lib/jirametrics/html/index.erb +16 -21
  59. data/lib/jirametrics/html/index.js +164 -0
  60. data/lib/jirametrics/html/legacy_colors.css +174 -0
  61. data/lib/jirametrics/html/sprint_burndown.erb +18 -25
  62. data/lib/jirametrics/html/throughput_chart.erb +43 -21
  63. data/lib/jirametrics/html/time_based_histogram.erb +123 -0
  64. data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +16 -21
  65. data/lib/jirametrics/html/wip_by_column_chart.erb +250 -0
  66. data/lib/jirametrics/html_generator.rb +32 -0
  67. data/lib/jirametrics/html_report_config.rb +83 -76
  68. data/lib/jirametrics/issue.rb +481 -97
  69. data/lib/jirametrics/issue_collection.rb +33 -0
  70. data/lib/jirametrics/issue_printer.rb +97 -0
  71. data/lib/jirametrics/jira_gateway.rb +96 -16
  72. data/lib/jirametrics/mcp_server.rb +531 -0
  73. data/lib/jirametrics/project_config.rb +374 -130
  74. data/lib/jirametrics/pull_request.rb +30 -0
  75. data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
  76. data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
  77. data/lib/jirametrics/pull_request_review.rb +13 -0
  78. data/lib/jirametrics/raw_javascript.rb +17 -0
  79. data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
  80. data/lib/jirametrics/settings.json +7 -1
  81. data/lib/jirametrics/sprint.rb +13 -0
  82. data/lib/jirametrics/sprint_burndown.rb +47 -39
  83. data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
  84. data/lib/jirametrics/status.rb +84 -19
  85. data/lib/jirametrics/status_collection.rb +83 -38
  86. data/lib/jirametrics/stitcher.rb +81 -0
  87. data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
  88. data/lib/jirametrics/throughput_chart.rb +73 -23
  89. data/lib/jirametrics/time_based_histogram.rb +139 -0
  90. data/lib/jirametrics/time_based_scatterplot.rb +107 -0
  91. data/lib/jirametrics/user.rb +12 -0
  92. data/lib/jirametrics/value_equality.rb +2 -2
  93. data/lib/jirametrics/wip_by_column_chart.rb +236 -0
  94. data/lib/jirametrics.rb +101 -66
  95. metadata +72 -16
  96. data/lib/jirametrics/cycletime_config.rb +0 -69
  97. data/lib/jirametrics/discard_changes_before.rb +0 -37
  98. data/lib/jirametrics/html/cycletime_histogram.erb +0 -47
  99. data/lib/jirametrics/html/data_quality_report.erb +0 -126
@@ -20,15 +20,30 @@ class DownloadConfig
20
20
  @rolling_date_count
21
21
  end
22
22
 
23
- def no_earlier_than date = nil
24
- @no_earlier_than = Date.parse(date) unless date.nil?
23
+ def no_earlier_than date = :not_set
24
+ @no_earlier_than = Date.parse(date) unless date == :not_set
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,21 +3,53 @@
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 = 4
28
+ CURRENT_METADATA_VERSION = 5
8
29
 
9
- attr_accessor :metadata, :quiet_mode
30
+ attr_accessor :metadata
10
31
  attr_reader :file_system
11
32
 
12
33
  # For testing only
13
34
  attr_reader :start_date_in_query, :board_id_to_filter_id
14
35
 
15
- def initialize download_config:, file_system:, jira_gateway:
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']
@@ -39,94 +70,49 @@ class Downloader
39
70
  # board_ids = @download_config.board_ids
40
71
 
41
72
  remove_old_files
73
+ update_status_history_file
42
74
  download_statuses
43
75
  find_board_ids.each do |id|
44
- download_board_configuration board_id: id
45
- download_issues board_id: id
76
+ board = download_board_configuration board_id: id
77
+ board.project_config = @download_config.project_config
78
+ download_issues board: board
46
79
  end
80
+ download_users
47
81
 
48
82
  save_metadata
49
- end
50
-
51
- def init_gateway
52
- @jira_gateway.load_jira_config(@download_config.project_config.jira_config)
53
- @jira_gateway.ignore_ssl_errors = @download_config.project_config.settings['ignore_ssl_errors']
83
+ download_github_prs if @download_config.github_repos.any?
54
84
  end
55
85
 
56
86
  def log text, both: false
57
- @file_system.log text
58
- puts text if both && !@quiet_mode
87
+ @file_system.log text, also_write_to_stderr: both
59
88
  end
60
89
 
61
- def find_board_ids
62
- ids = @download_config.project_config.board_configs.collect(&:id)
63
- raise 'Board ids must be specified' if ids.empty?
64
-
65
- ids
90
+ def log_start text
91
+ @file_system.log_start text
66
92
  end
67
93
 
68
- def download_issues board_id:
69
- log " Downloading primary issues for board #{board_id}", both: true
70
- path = "#{@target_path}#{@download_config.project_config.file_prefix}_issues/"
71
- unless Dir.exist?(path)
72
- log " Creating path #{path}"
73
- Dir.mkdir(path)
74
- end
75
-
76
- filter_id = @board_id_to_filter_id[board_id]
77
- jql = make_jql(filter_id: filter_id)
78
- jira_search_by_jql(jql: jql, initial_query: true, board_id: board_id, path: path)
79
-
80
- log " Downloading linked issues for board #{board_id}", both: true
81
- loop do
82
- @issue_keys_pending_download.reject! { |key| @issue_keys_downloaded_in_current_run.include? key }
83
- break if @issue_keys_pending_download.empty?
84
-
85
- keys_to_request = @issue_keys_pending_download[0..99]
86
- @issue_keys_pending_download.reject! { |key| keys_to_request.include? key }
87
- jql = "key in (#{keys_to_request.join(', ')})"
88
- jira_search_by_jql(jql: jql, initial_query: false, board_id: board_id, path: path)
89
- end
94
+ def start_progress
95
+ @file_system.start_progress
90
96
  end
91
97
 
92
- def jira_search_by_jql jql:, initial_query:, board_id:, path:
93
- intercept_jql = @download_config.project_config.settings['intercept_jql']
94
- jql = intercept_jql.call jql if intercept_jql
95
-
96
- log " JQL: #{jql}"
97
- escaped_jql = CGI.escape jql
98
-
99
- max_results = 100
100
- start_at = 0
101
- total = 1
102
- while start_at < total
103
- json = @jira_gateway.call_url relative_url: '/rest/api/2/search' \
104
- "?jql=#{escaped_jql}&maxResults=#{max_results}&startAt=#{start_at}&expand=changelog&fields=*all"
105
-
106
- exit_if_call_failed json
107
-
108
- json['issues'].each do |issue_json|
109
- issue_json['exporter'] = {
110
- 'in_initial_query' => initial_query
111
- }
112
- identify_other_issues_to_be_downloaded issue_json
113
- file = "#{issue_json['key']}-#{board_id}.json"
114
-
115
- @file_system.save_json(json: issue_json, filename: File.join(path, file))
116
- end
98
+ def progress_dot message = nil
99
+ @file_system.log message if message
100
+ @file_system.progress_dot
101
+ end
117
102
 
118
- total = json['total'].to_i
119
- max_results = json['maxResults']
103
+ def end_progress
104
+ @file_system.end_progress
105
+ end
120
106
 
121
- message = " Downloaded #{start_at + 1}-#{[start_at + max_results, total].min} of #{total} issues to #{path} "
122
- log message, both: true
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?
123
110
 
124
- start_at += json['issues'].size
125
- end
111
+ ids
126
112
  end
127
113
 
128
- def identify_other_issues_to_be_downloaded raw_issue
129
- issue = Issue.new raw: raw_issue, board: nil
114
+ def identify_other_issues_to_be_downloaded raw_issue:, board:
115
+ issue = Issue.new raw: raw_issue, board: board
130
116
  @issue_keys_downloaded_in_current_run << issue.key
131
117
 
132
118
  # Parent
@@ -137,22 +123,6 @@ class Downloader
137
123
  issue.raw['fields']['subtasks']&.each do |raw_subtask|
138
124
  @issue_keys_pending_download << raw_subtask['key']
139
125
  end
140
-
141
- # Links
142
- # We shouldn't blindly follow links as some, like cloners, aren't valuable and are just wasting time/effort
143
- # to download
144
- # issue.raw['fields']['issuelinks'].each do |raw_link|
145
- # @issue_keys_pending_download << IssueLink(raw: raw_link).other_issue.key
146
- # end
147
- end
148
-
149
- def exit_if_call_failed json
150
- # Sometimes Jira returns the singular form of errorMessage and sometimes the plural. Consistency FTW.
151
- return unless json['error'] || json['errorMessages'] || json['errorMessage']
152
-
153
- log "Download failed. See #{@file_system.logfile_name} for details.", both: true
154
- log " #{JSON.pretty_generate(json)}"
155
- exit 1
156
126
  end
157
127
 
158
128
  def download_statuses
@@ -161,29 +131,85 @@ class Downloader
161
131
 
162
132
  @file_system.save_json(
163
133
  json: json,
164
- filename: "#{@target_path}#{@download_config.project_config.file_prefix}_statuses.json"
134
+ filename: File.join(@target_path, "#{file_prefix}_statuses.json")
165
135
  )
166
136
  end
167
137
 
138
+ def download_users
139
+ return unless @jira_gateway.cloud?
140
+
141
+ log ' Downloading all users', both: true
142
+ json = @jira_gateway.call_url relative_url: '/rest/api/2/users'
143
+
144
+ @file_system.save_json(
145
+ json: json,
146
+ filename: File.join(@target_path, "#{file_prefix}_users.json")
147
+ )
148
+ end
149
+
150
+ def update_status_history_file
151
+ status_filename = File.join(@target_path, "#{file_prefix}_statuses.json")
152
+ return unless file_system.file_exist? status_filename
153
+
154
+ status_json = file_system.load_json(status_filename)
155
+
156
+ history_filename = File.join(@target_path, "#{file_prefix}_status_history.json")
157
+ history_json = file_system.load_json(history_filename) if file_system.file_exist? history_filename
158
+
159
+ if history_json
160
+ file_system.log ' Updating status history file', also_write_to_stderr: true
161
+ else
162
+ file_system.log ' Creating status history file', also_write_to_stderr: true
163
+ history_json = []
164
+ end
165
+
166
+ status_json.each do |status_item|
167
+ id = status_item['id']
168
+ history_item = history_json.find { |s| s['id'] == id }
169
+ history_json.delete(history_item) if history_item
170
+ history_json << status_item
171
+ end
172
+
173
+ file_system.save_json(filename: history_filename, json: history_json)
174
+ end
175
+
168
176
  def download_board_configuration board_id:
169
177
  log " Downloading board configuration for board #{board_id}", both: true
170
178
  json = @jira_gateway.call_url relative_url: "/rest/agile/1.0/board/#{board_id}/configuration"
171
179
 
172
- exit_if_call_failed json
173
-
174
- file_prefix = @download_config.project_config.file_prefix
175
- @file_system.save_json json: json, filename: "#{@target_path}#{file_prefix}_board_#{board_id}_configuration.json"
180
+ @file_system.save_json(
181
+ json: json,
182
+ filename: File.join(@target_path, "#{file_prefix}_board_#{board_id}_configuration.json")
183
+ )
176
184
 
177
185
  # We have a reported bug that blew up on this line. Moved it after the save so we can
178
186
  # actually look at the returned json.
179
187
  @board_id_to_filter_id[board_id] = json['filter']['id'].to_i
180
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
181
195
  download_sprints board_id: board_id if json['type'] == 'scrum'
196
+ # TODO: Should be passing actual statuses, not empty list
197
+ Board.new raw: json, possible_statuses: StatusCollection.new
198
+ end
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
182
209
  end
183
210
 
184
211
  def download_sprints board_id:
185
212
  log " Downloading sprints for board #{board_id}", both: true
186
- file_prefix = @download_config.project_config.file_prefix
187
213
  max_results = 100
188
214
  start_at = 0
189
215
  is_last = false
@@ -191,11 +217,10 @@ class Downloader
191
217
  while is_last == false
192
218
  json = @jira_gateway.call_url relative_url: "/rest/agile/1.0/board/#{board_id}/sprint?" \
193
219
  "maxResults=#{max_results}&startAt=#{start_at}"
194
- exit_if_call_failed json
195
220
 
196
221
  @file_system.save_json(
197
222
  json: json,
198
- filename: "#{@target_path}#{file_prefix}_board_#{board_id}_sprints_#{start_at}.json"
223
+ filename: File.join(@target_path, "#{file_prefix}_board_#{board_id}_sprints_#{start_at}.json")
199
224
  )
200
225
  is_last = json['isLast']
201
226
  max_results = json['maxResults']
@@ -208,7 +233,7 @@ class Downloader
208
233
  end
209
234
 
210
235
  def metadata_pathname
211
- "#{@target_path}#{@download_config.project_config.file_prefix}_meta.json"
236
+ File.join(@target_path, "#{file_prefix}_meta.json")
212
237
  end
213
238
 
214
239
  def load_metadata
@@ -224,19 +249,29 @@ class Downloader
224
249
  value = Date.parse(value) if value.is_a?(String) && value =~ /^\d{4}-\d{2}-\d{2}$/
225
250
  @metadata[key] = value
226
251
  end
252
+
227
253
  end
228
254
 
229
255
  # Even if this is the old format, we want to obey this one tag
230
256
  @metadata['no-download'] = hash['no-download'] if hash['no-download']
231
257
  end
232
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
+
233
267
  def save_metadata
234
268
  @metadata['version'] = CURRENT_METADATA_VERSION
269
+ @metadata['rolling_date_count'] = @download_config.rolling_date_count
235
270
  @metadata['date_start_from_last_query'] = @start_date_in_query if @start_date_in_query
236
271
 
237
272
  if @download_date_range.nil?
238
273
  log "Making up a date range in meta since one wasn't specified. You'll want to change that.", both: true
239
- today = Date.today
274
+ today = today_in_project_timezone
240
275
  @download_date_range = (today - 7)..today
241
276
  end
242
277
 
@@ -251,17 +286,17 @@ class Downloader
251
286
  end
252
287
 
253
288
  def remove_old_files
254
- file_prefix = @download_config.project_config.file_prefix
255
289
  Dir.foreach @target_path do |file|
256
290
  next unless file.match?(/^#{file_prefix}_\d+\.json$/)
291
+ next if file == "#{file_prefix}_status_history.json"
257
292
 
258
- File.unlink "#{@target_path}#{file}"
293
+ File.unlink File.join(@target_path, file)
259
294
  end
260
295
 
261
296
  return if @cached_data_format_is_current
262
297
 
263
298
  # Also throw away all the previously downloaded issues.
264
- path = File.join @target_path, "#{file_prefix}_issues"
299
+ path = File.join(@target_path, "#{file_prefix}_issues")
265
300
  return unless File.exist? path
266
301
 
267
302
  Dir.foreach path do |file|
@@ -271,7 +306,8 @@ class Downloader
271
306
  end
272
307
  end
273
308
 
274
- def make_jql filter_id:, today: Date.today
309
+ def make_jql filter_id:, today: nil
310
+ today ||= today_in_project_timezone
275
311
  segments = []
276
312
  segments << "filter=#{filter_id}"
277
313
 
@@ -279,11 +315,7 @@ class Downloader
279
315
 
280
316
  if start_date
281
317
  @download_date_range = start_date..today.to_date
282
-
283
- # For an incremental download, we want to query from the end of the previous one, not from the
284
- # beginning of the full range.
285
- @start_date_in_query = metadata['date_end'] || @download_date_range.begin
286
- 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
287
319
 
288
320
  # Catch-all to pick up anything that's been around since before the range started but hasn't
289
321
  # had an update during the range.
@@ -299,4 +331,41 @@ class Downloader
299
331
 
300
332
  segments.join ' AND '
301
333
  end
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
+
368
+ def file_prefix
369
+ @download_config.project_config.get_file_prefix
370
+ end
302
371
  end