jirametrics 2.13 → 2.22

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aging_work_bar_chart.rb +176 -134
  3. data/lib/jirametrics/anonymizer.rb +8 -6
  4. data/lib/jirametrics/atlassian_document_format.rb +8 -4
  5. data/lib/jirametrics/bar_chart_range.rb +17 -0
  6. data/lib/jirametrics/board.rb +4 -0
  7. data/lib/jirametrics/board_config.rb +4 -1
  8. data/lib/jirametrics/change_item.rb +11 -4
  9. data/lib/jirametrics/chart_base.rb +36 -2
  10. data/lib/jirametrics/cycletime_config.rb +22 -4
  11. data/lib/jirametrics/cycletime_histogram.rb +3 -1
  12. data/lib/jirametrics/cycletime_scatterplot.rb +36 -17
  13. data/lib/jirametrics/daily_view.rb +49 -42
  14. data/lib/jirametrics/daily_wip_by_age_chart.rb +3 -4
  15. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +13 -3
  16. data/lib/jirametrics/daily_wip_chart.rb +1 -1
  17. data/lib/jirametrics/data_quality_report.rb +8 -3
  18. data/lib/jirametrics/dependency_chart.rb +4 -1
  19. data/lib/jirametrics/downloader.rb +34 -99
  20. data/lib/jirametrics/downloader_for_cloud.rb +202 -0
  21. data/lib/jirametrics/downloader_for_data_center.rb +94 -0
  22. data/lib/jirametrics/examples/standard_project.rb +9 -9
  23. data/lib/jirametrics/expedited_chart.rb +1 -1
  24. data/lib/jirametrics/exporter.rb +12 -5
  25. data/lib/jirametrics/file_system.rb +24 -1
  26. data/lib/jirametrics/fix_version.rb +13 -0
  27. data/lib/jirametrics/flow_efficiency_scatterplot.rb +1 -1
  28. data/lib/jirametrics/groupable_issue_chart.rb +7 -1
  29. data/lib/jirametrics/html/aging_work_bar_chart.erb +2 -1
  30. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +3 -1
  31. data/lib/jirametrics/html/aging_work_table.erb +2 -0
  32. data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
  33. data/lib/jirametrics/html/cycletime_histogram.erb +4 -2
  34. data/lib/jirametrics/html/cycletime_scatterplot.erb +6 -6
  35. data/lib/jirametrics/html/daily_wip_chart.erb +2 -0
  36. data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -0
  37. data/lib/jirametrics/html/expedited_chart.erb +3 -1
  38. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -0
  39. data/lib/jirametrics/html/index.css +21 -9
  40. data/lib/jirametrics/html/index.erb +3 -35
  41. data/lib/jirametrics/html/index.js +114 -0
  42. data/lib/jirametrics/html/sprint_burndown.erb +11 -3
  43. data/lib/jirametrics/html/throughput_chart.erb +2 -2
  44. data/lib/jirametrics/html_generator.rb +31 -0
  45. data/lib/jirametrics/html_report_config.rb +8 -25
  46. data/lib/jirametrics/issue.rb +127 -22
  47. data/lib/jirametrics/jira_gateway.rb +55 -17
  48. data/lib/jirametrics/project_config.rb +42 -5
  49. data/lib/jirametrics/raw_javascript.rb +13 -0
  50. data/lib/jirametrics/settings.json +3 -1
  51. data/lib/jirametrics/sprint.rb +12 -0
  52. data/lib/jirametrics/sprint_burndown.rb +6 -2
  53. data/lib/jirametrics/stitcher.rb +75 -0
  54. data/lib/jirametrics.rb +26 -70
  55. metadata +10 -3
@@ -3,21 +3,61 @@
3
3
  require 'cgi'
4
4
  require 'json'
5
5
  require 'English'
6
+ require 'open3'
6
7
 
7
8
  class JiraGateway
8
- attr_accessor :ignore_ssl_errors, :jira_url
9
+ attr_accessor :ignore_ssl_errors
10
+ attr_reader :jira_url, :settings, :file_system
9
11
 
10
- def initialize file_system:
12
+ def initialize file_system:, jira_config:, settings:
11
13
  @file_system = file_system
14
+ load_jira_config(jira_config)
15
+ @settings = settings
16
+ @ignore_ssl_errors = settings['ignore_ssl_errors']
17
+ end
18
+
19
+ def post_request relative_url:, payload:
20
+ command = make_curl_command url: "#{@jira_url}#{relative_url}", method: 'POST'
21
+ exec_and_parse_response command: command, stdin_data: payload
22
+ end
23
+
24
+ def exec_and_parse_response command:, stdin_data:
25
+ log_entry = " #{command.gsub(/\s+/, ' ')}"
26
+ log_entry = sanitize_message log_entry
27
+ @file_system.log log_entry
28
+
29
+ stdout, stderr, status = capture3(command, stdin_data: stdin_data)
30
+ unless status.success?
31
+ @file_system.log "Failed call with exit status #{status.exitstatus}!"
32
+ @file_system.log "Returned (stdout): #{stdout.inspect}"
33
+ @file_system.log "Returned (stderr): #{stderr.inspect}"
34
+ raise "Failed call with exit status #{status.exitstatus}. " \
35
+ "See #{@file_system.logfile_name} for details"
36
+ end
37
+
38
+ @file_system.log "Returned (stderr): #{stderr.inspect}" unless stderr == ''
39
+ raise 'no response from curl on stdout' if stdout == ''
40
+
41
+ parse_response(command: command, result: stdout)
42
+ end
43
+
44
+ def capture3 command, stdin_data:
45
+ # In it's own method so we can mock it out in tests
46
+ Open3.capture3(command, stdin_data: stdin_data)
12
47
  end
13
48
 
14
49
  def call_url relative_url:
15
50
  command = make_curl_command url: "#{@jira_url}#{relative_url}"
16
- result = call_command command
51
+ exec_and_parse_response command: command, stdin_data: nil
52
+ end
53
+
54
+ def parse_response command:, result:
17
55
  begin
18
56
  json = JSON.parse(result)
19
57
  rescue # rubocop:disable Style/RescueStandardError
20
- raise "Error when parsing result: #{result.inspect}"
58
+ message = "Unable to parse results from #{sanitize_message(command)}"
59
+ @file_system.error message, more: result
60
+ raise message
21
61
  end
22
62
 
23
63
  raise "Download failed with: #{JSON.pretty_generate(json)}" unless json_successful?(json)
@@ -25,18 +65,11 @@ class JiraGateway
25
65
  json
26
66
  end
27
67
 
28
- def call_command command
29
- log_entry = " #{command.gsub(/\s+/, ' ')}"
30
- log_entry = log_entry.gsub(@jira_api_token, '[API_TOKEN]') if @jira_api_token
31
- @file_system.log log_entry
68
+ def sanitize_message message
69
+ token = @jira_api_token || @jira_personal_access_token
70
+ return message unless token # cookie based authentication
32
71
 
33
- result = `#{command}`
34
- @file_system.log result unless $CHILD_STATUS.success?
35
- return result if $CHILD_STATUS.success?
36
-
37
- @file_system.log "Failed call with exit status #{$CHILD_STATUS.exitstatus}."
38
- raise "Failed call with exit status #{$CHILD_STATUS.exitstatus}. " \
39
- "See #{@file_system.logfile_name} for details"
72
+ message.gsub(token, '[API_TOKEN]')
40
73
  end
41
74
 
42
75
  def load_jira_config jira_config
@@ -56,7 +89,7 @@ class JiraGateway
56
89
  @cookies = (jira_config['cookies'] || []).collect { |key, value| "#{key}=#{value}" }.join(';')
57
90
  end
58
91
 
59
- def make_curl_command url:
92
+ def make_curl_command url:, method: 'GET'
60
93
  command = +''
61
94
  command << 'curl'
62
95
  command << ' -L' # follow redirects
@@ -65,8 +98,13 @@ class JiraGateway
65
98
  command << " --cookie #{@cookies.inspect}" unless @cookies.empty?
66
99
  command << " --user #{@jira_email}:#{@jira_api_token}" if @jira_api_token
67
100
  command << " -H \"Authorization: Bearer #{@jira_personal_access_token}\"" if @jira_personal_access_token
68
- command << ' --request GET'
101
+ command << " --request #{method}"
102
+ if method == 'POST'
103
+ command << ' --data @-'
104
+ command << ' --header "Content-Type: application/json"'
105
+ end
69
106
  command << ' --header "Accept: application/json"'
107
+ command << ' --show-error --fail' # Better diagnostics when the server returns an error
70
108
  command << " --url \"#{url}\""
71
109
  command
72
110
  end
@@ -58,7 +58,16 @@ class ProjectConfig
58
58
 
59
59
  def load_settings
60
60
  # This is the weird exception that we don't ever want mocked out so we skip FileSystem entirely.
61
- JSON.parse(File.read(File.join(__dir__, 'settings.json'), encoding: 'UTF-8'))
61
+ settings = JSON.parse(File.read(File.join(__dir__, 'settings.json'), encoding: 'UTF-8'))
62
+
63
+ if settings['blocked_color']
64
+ file_system.deprecated message: 'blocked color should be set via css now', date: '2024-05-03'
65
+ end
66
+ if settings['stalled_color']
67
+ file_system.deprecated message: 'stalled color should be set via css now', date: '2024-05-03'
68
+ end
69
+
70
+ settings
62
71
  end
63
72
 
64
73
  def guess_project_id
@@ -114,10 +123,14 @@ class ProjectConfig
114
123
  def file_prefix prefix
115
124
  # The file_prefix has to be set before almost everything else. It really should have been an attribute
116
125
  # on the project declaration itself. Hindsight is 20/20.
126
+
127
+ # There can only be one of these
117
128
  if @file_prefix
118
- raise "file_prefix should only be set once. Was #{@file_prefix.inspect} and now changed to #{prefix.inspect}."
129
+ raise "file_prefix can only be set once. Was #{@file_prefix.inspect} and now changed to #{prefix.inspect}."
119
130
  end
120
131
 
132
+ raise_if_prefix_already_used(prefix)
133
+
121
134
  @file_prefix = prefix
122
135
 
123
136
  # Yes, this is a wierd place to be initializing this. Unfortunately, it has to happen after the file_prefix
@@ -130,8 +143,21 @@ class ProjectConfig
130
143
  @file_prefix
131
144
  end
132
145
 
133
- def get_file_prefix # rubocop:disable Naming/AccessorMethodName
134
- raise 'file_prefix has not been set yet. Move it to the top of the project declaration.' unless @file_prefix
146
+ def raise_if_prefix_already_used prefix
147
+ @exporter.project_configs.each do |project|
148
+ next unless project.get_file_prefix(raise_if_not_set: false) == prefix && project.target_path == target_path
149
+
150
+ raise "Project #{name.inspect} specifies file prefix #{prefix.inspect}, " \
151
+ "but that is already used by project #{project.name.inspect} in the same target path #{target_path.inspect}. " \
152
+ 'This is almost guaranteed to be too much copy and paste in your configuration. ' \
153
+ 'File prefixes must be unique within a directory.'
154
+ end
155
+ end
156
+
157
+ def get_file_prefix raise_if_not_set: true
158
+ if @file_prefix.nil? && raise_if_not_set
159
+ raise 'file_prefix has not been set yet. Move it to the top of the project declaration.'
160
+ end
135
161
 
136
162
  @file_prefix
137
163
  end
@@ -278,8 +304,9 @@ class ProjectConfig
278
304
  file_system.foreach(@target_path) do |file|
279
305
  next unless file =~ /^#{get_file_prefix}_board_(\d+)_sprints_\d+.json$/
280
306
 
307
+ board_id = $1.to_i
281
308
  file_path = File.join(@target_path, file)
282
- board = @all_boards[$1.to_i]
309
+ board = @all_boards[board_id]
283
310
  unless board
284
311
  @exporter.file_system.log(
285
312
  'Found sprint data but can\'t find a matching board in config. ' \
@@ -335,6 +362,12 @@ class ProjectConfig
335
362
  json.each { |user_data| @users << User.new(raw: user_data) }
336
363
  end
337
364
 
365
+ def atlassian_document_format
366
+ @atlassian_document_format ||= AtlassianDocumentFormat.new(
367
+ users: @users, timezone_offset: exporter.timezone_offset
368
+ )
369
+ end
370
+
338
371
  def to_time string, end_of_day: false
339
372
  time = end_of_day ? '23:59:59' : '00:00:00'
340
373
  string = "#{string}T#{time}#{exporter.timezone_offset}" if string.match?(/^\d{4}-\d{2}-\d{2}$/)
@@ -526,6 +559,7 @@ class ProjectConfig
526
559
  end
527
560
 
528
561
  def discard_changes_before status_becomes: nil, &block
562
+ cycletimes_touched = Set.new
529
563
  if status_becomes
530
564
  status_becomes = [status_becomes] unless status_becomes.is_a? Array
531
565
 
@@ -558,6 +592,7 @@ class ProjectConfig
558
592
  next if original_start_time.nil?
559
593
 
560
594
  issue.discard_changes_before cutoff_time
595
+ cycletimes_touched << issue.board.cycletime
561
596
 
562
597
  next unless cutoff_time
563
598
  next if original_start_time > cutoff_time # ie the cutoff would have made no difference.
@@ -568,5 +603,7 @@ class ProjectConfig
568
603
  issue: issue
569
604
  }
570
605
  end
606
+
607
+ cycletimes_touched.each { |c| c.flush_cache }
571
608
  end
572
609
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # When strings are serialized into JSON, they're converted to actual strings. The purpose
4
+ # of this class is to allow raw javascript to be passed through.
5
+ class RawJavascript
6
+ def initialize content
7
+ @content = content
8
+ end
9
+
10
+ def to_json(*_args)
11
+ @content
12
+ end
13
+ end
@@ -7,5 +7,7 @@
7
7
  "flagged_means_blocked": true,
8
8
 
9
9
  "expedited_priority_names": ["Critical", "Highest"],
10
- "priority_order": ["Lowest", "Low", "Medium", "High", "Highest"]
10
+ "priority_order": ["Lowest", "Low", "Medium", "High", "Highest"],
11
+
12
+ "cache_cycletime_calculations": true
11
13
  }
@@ -13,6 +13,7 @@ class Sprint
13
13
  def id = @raw['id']
14
14
  def active? = (@raw['state'] == 'active')
15
15
  def closed? = (@raw['state'] == 'closed')
16
+ def future? = (@raw['state'] == 'future')
16
17
 
17
18
  def completed_at? time
18
19
  completed_at = completed_time
@@ -36,6 +37,17 @@ class Sprint
36
37
  def goal = @raw['goal']
37
38
  def name = @raw['name']
38
39
 
40
+ def day_count
41
+ return '' if future?
42
+
43
+ if closed?
44
+ days = (completed_time.to_date - start_time.to_date).to_i + 1
45
+ else
46
+ days = (end_time.to_date - start_time.to_date).to_i + 1
47
+ end
48
+ "#{days} days"
49
+ end
50
+
39
51
  private
40
52
 
41
53
  def parse_time time_string
@@ -48,8 +48,9 @@ class SprintBurndown < ChartBase
48
48
  end
49
49
 
50
50
  def run
51
- sprints = sprints_in_time_range all_boards[board_id]
52
- return nil if sprints.empty?
51
+ return nil unless current_board.scrum?
52
+
53
+ sprints = sprints_in_time_range current_board
53
54
 
54
55
  change_data_by_sprint = {}
55
56
  sprints.each do |sprint|
@@ -110,6 +111,9 @@ class SprintBurndown < ChartBase
110
111
 
111
112
  def sprints_in_time_range board
112
113
  board.sprints.select do |sprint|
114
+ # If it's never been started then it's just a holding area. Ignore it.
115
+ next if sprint.future?
116
+
113
117
  sprint_end_time = sprint.completed_time || sprint.end_time
114
118
  sprint_start_time = sprint.start_time
115
119
  next false if sprint_start_time.nil?
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Stitcher < HtmlGenerator
4
+ class StitchContent
5
+ include ValueEquality
6
+
7
+ attr_reader :file, :title, :content, :type
8
+
9
+ def initialize file:, title:, type:, content:
10
+ @file = file
11
+ @title = title
12
+ @content = content
13
+ @type = type
14
+ end
15
+ end
16
+
17
+ attr_reader :loaded_files, :all_stitches
18
+
19
+ def initialize file_system:
20
+ super()
21
+ self.file_system = file_system
22
+ @all_stitches = []
23
+ @loaded_files = []
24
+ end
25
+
26
+ def run stitch_file:
27
+ output_filename = make_output_filename stitch_file
28
+ file_system.log "Creating file #{output_filename.inspect}", also_write_to_stderr: true
29
+ erb = ERB.new file_system.load(stitch_file)
30
+ @sections = [[erb.result(binding), :body]]
31
+ create_html output_filename: output_filename, settings: {}
32
+ end
33
+
34
+ def make_output_filename input_filename
35
+ if /^(.+)\.erb$/ =~ input_filename
36
+ "#{$1}.html"
37
+ else
38
+ "#{input_filename}.html"
39
+ end
40
+ end
41
+
42
+ def grab_by_title title, from_file:, type: 'chart'
43
+ parse_file from_file
44
+ stitch_content = @all_stitches.find { |s| s.file == from_file && s.title == title && s.type == type }
45
+ return stitch_content.content if stitch_content
46
+
47
+ raise "Unable to find content in file #{from_file.inspect} matching title: #{title.inspect}"
48
+ end
49
+
50
+ def parse_file filename
51
+ return false if @loaded_files.include? filename
52
+
53
+ # To match: <!-- seam-start | chart78 | GithubPrScatterplot | PR Scatterplot | chart -->
54
+ regex = /^<!-- seam-(?<seam>start|end) \| (?<id>[^|]+) \| (?<clazz>[^|]+) \| (?<title>[^|]+) \| (?<type>[^|]+) -->$/
55
+ content = nil
56
+ file_system.load(filename).lines do |line|
57
+ matches = line.match(regex)
58
+ if matches
59
+ if matches[:seam] == 'start'
60
+ content = +''
61
+ else
62
+ @all_stitches << Stitcher::StitchContent.new(
63
+ file: filename, title: matches[:title], type: matches[:type], content: content
64
+ )
65
+ content = nil
66
+ end
67
+ elsif content
68
+ content << line
69
+ end
70
+ end
71
+
72
+ @loaded_files << filename
73
+ true
74
+ end
75
+ end
data/lib/jirametrics.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
+ require 'require_all'
5
+
6
+ # This one does need to be loaded early. The rest will be loaded later.
7
+ require 'jirametrics/file_system'
4
8
 
5
9
  class JiraMetrics < Thor
6
10
  def self.exit_on_failure?
@@ -43,81 +47,33 @@ class JiraMetrics < Thor
43
47
 
44
48
  option :config
45
49
  desc 'info', 'Dump information about one issue'
46
- def info keys
50
+ def info key
47
51
  load_config options[:config]
48
- Exporter.instance.info(keys, name_filter: options[:name] || '*')
52
+ Exporter.instance.info(key, name_filter: options[:name] || '*')
49
53
  end
50
54
 
51
- private
52
-
53
- def load_config config_file
54
- config_file = './config.rb' if config_file.nil?
55
+ option :config
56
+ desc 'stitch', 'Dump information about one issue'
57
+ def stitch stitch_file = 'stitcher.erb'
58
+ load_config options[:config]
59
+ Exporter.instance.stitch stitch_file
60
+ end
55
61
 
56
- if File.exist? config_file
57
- # The fact that File.exist can see the file does not mean that require will be
58
- # able to load it. Convert this to an absolute pathname now for require.
59
- config_file = File.absolute_path(config_file).to_s
60
- else
61
- puts "Cannot find configuration file #{config_file.inspect}"
62
- exit 1
63
- end
62
+ no_commands do
63
+ def load_config config_file, file_system: FileSystem.new
64
+ config_file = './config.rb' if config_file.nil?
64
65
 
65
- require 'jirametrics/value_equality'
66
- require 'jirametrics/chart_base'
67
- require 'jirametrics/rules'
68
- require 'jirametrics/grouping_rules'
69
- require 'jirametrics/daily_wip_chart'
70
- require 'jirametrics/groupable_issue_chart'
71
- require 'jirametrics/css_variable'
72
- require 'jirametrics/issue_collection'
66
+ if file_system.file_exist? config_file
67
+ # The fact that File.exist can see the file does not mean that require will be
68
+ # able to load it. Convert this to an absolute pathname now for require.
69
+ config_file = File.absolute_path(config_file).to_s
70
+ else
71
+ file_system.error "Cannot find configuration file #{config_file.inspect}"
72
+ exit 1
73
+ end
73
74
 
74
- require 'jirametrics/aggregate_config'
75
- require 'jirametrics/expedited_chart'
76
- require 'jirametrics/board_config'
77
- require 'jirametrics/file_config'
78
- require 'jirametrics/jira_gateway'
79
- require 'jirametrics/trend_line_calculator'
80
- require 'jirametrics/status'
81
- require 'jirametrics/issue_link'
82
- require 'jirametrics/estimate_accuracy_chart'
83
- require 'jirametrics/status_collection'
84
- require 'jirametrics/sprint'
85
- require 'jirametrics/issue'
86
- require 'jirametrics/daily_wip_by_age_chart'
87
- require 'jirametrics/daily_wip_by_parent_chart'
88
- require 'jirametrics/aging_work_in_progress_chart'
89
- require 'jirametrics/cycletime_scatterplot'
90
- require 'jirametrics/flow_efficiency_scatterplot'
91
- require 'jirametrics/sprint_issue_change_data'
92
- require 'jirametrics/cycletime_histogram'
93
- require 'jirametrics/daily_wip_by_blocked_stalled_chart'
94
- require 'jirametrics/html_report_config'
95
- require 'jirametrics/data_quality_report'
96
- require 'jirametrics/aging_work_bar_chart'
97
- require 'jirametrics/change_item'
98
- require 'jirametrics/project_config'
99
- require 'jirametrics/dependency_chart'
100
- require 'jirametrics/cycletime_config'
101
- require 'jirametrics/tree_organizer'
102
- require 'jirametrics/aging_work_table'
103
- require 'jirametrics/sprint_burndown'
104
- require 'jirametrics/self_or_issue_dispatcher'
105
- require 'jirametrics/throughput_chart'
106
- require 'jirametrics/exporter'
107
- require 'jirametrics/file_system'
108
- require 'jirametrics/blocked_stalled_change'
109
- require 'jirametrics/board_column'
110
- require 'jirametrics/anonymizer'
111
- require 'jirametrics/downloader'
112
- require 'jirametrics/fix_version'
113
- require 'jirametrics/download_config'
114
- require 'jirametrics/columns_config'
115
- require 'jirametrics/hierarchy_table'
116
- require 'jirametrics/estimation_configuration'
117
- require 'jirametrics/board'
118
- require 'jirametrics/daily_view'
119
- require 'jirametrics/user'
120
- require 'jirametrics/atlassian_document_format'
121
- load config_file
75
+ require_rel 'jirametrics'
76
+ load config_file
77
+ end
122
78
  end
123
79
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.13'
4
+ version: '2.22'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-07-25 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: random-word
@@ -66,6 +66,7 @@ files:
66
66
  - lib/jirametrics/aging_work_table.rb
67
67
  - lib/jirametrics/anonymizer.rb
68
68
  - lib/jirametrics/atlassian_document_format.rb
69
+ - lib/jirametrics/bar_chart_range.rb
69
70
  - lib/jirametrics/blocked_stalled_change.rb
70
71
  - lib/jirametrics/board.rb
71
72
  - lib/jirametrics/board_column.rb
@@ -87,6 +88,8 @@ files:
87
88
  - lib/jirametrics/dependency_chart.rb
88
89
  - lib/jirametrics/download_config.rb
89
90
  - lib/jirametrics/downloader.rb
91
+ - lib/jirametrics/downloader_for_cloud.rb
92
+ - lib/jirametrics/downloader_for_data_center.rb
90
93
  - lib/jirametrics/estimate_accuracy_chart.rb
91
94
  - lib/jirametrics/estimation_configuration.rb
92
95
  - lib/jirametrics/examples/aggregated_project.rb
@@ -113,14 +116,17 @@ files:
113
116
  - lib/jirametrics/html/hierarchy_table.erb
114
117
  - lib/jirametrics/html/index.css
115
118
  - lib/jirametrics/html/index.erb
119
+ - lib/jirametrics/html/index.js
116
120
  - lib/jirametrics/html/sprint_burndown.erb
117
121
  - lib/jirametrics/html/throughput_chart.erb
122
+ - lib/jirametrics/html_generator.rb
118
123
  - lib/jirametrics/html_report_config.rb
119
124
  - lib/jirametrics/issue.rb
120
125
  - lib/jirametrics/issue_collection.rb
121
126
  - lib/jirametrics/issue_link.rb
122
127
  - lib/jirametrics/jira_gateway.rb
123
128
  - lib/jirametrics/project_config.rb
129
+ - lib/jirametrics/raw_javascript.rb
124
130
  - lib/jirametrics/rules.rb
125
131
  - lib/jirametrics/self_or_issue_dispatcher.rb
126
132
  - lib/jirametrics/settings.json
@@ -129,6 +135,7 @@ files:
129
135
  - lib/jirametrics/sprint_issue_change_data.rb
130
136
  - lib/jirametrics/status.rb
131
137
  - lib/jirametrics/status_collection.rb
138
+ - lib/jirametrics/stitcher.rb
132
139
  - lib/jirametrics/throughput_chart.rb
133
140
  - lib/jirametrics/tree_organizer.rb
134
141
  - lib/jirametrics/trend_line_calculator.rb
@@ -156,7 +163,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
156
163
  - !ruby/object:Gem::Version
157
164
  version: '0'
158
165
  requirements: []
159
- rubygems_version: 3.6.2
166
+ rubygems_version: 3.6.9
160
167
  specification_version: 4
161
168
  summary: Extract Jira metrics
162
169
  test_files: []