jirametrics 2.31 → 3.0

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +42 -29
  3. data/lib/jirametrics/aging_work_bar_chart.rb +52 -40
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +65 -60
  5. data/lib/jirametrics/aging_work_table.rb +36 -33
  6. data/lib/jirametrics/anonymizer.rb +31 -86
  7. data/lib/jirametrics/atlassian_document_format.rb +11 -4
  8. data/lib/jirametrics/blocked_stalled_by_date_builder.rb +64 -0
  9. data/lib/jirametrics/blocked_stalled_change.rb +5 -1
  10. data/lib/jirametrics/blocked_stalled_change_stream_builder.rb +194 -0
  11. data/lib/jirametrics/board.rb +23 -9
  12. data/lib/jirametrics/board_config.rb +0 -7
  13. data/lib/jirametrics/board_movement_calculator.rb +24 -21
  14. data/lib/jirametrics/cfd_data_builder.rb +46 -44
  15. data/lib/jirametrics/change_item.rb +19 -7
  16. data/lib/jirametrics/chart_base.rb +50 -64
  17. data/lib/jirametrics/cumulative_flow_diagram.rb +59 -46
  18. data/lib/jirametrics/cycle_time_config.rb +35 -46
  19. data/lib/jirametrics/cycletime_histogram.rb +7 -0
  20. data/lib/jirametrics/cycletime_scatterplot.rb +7 -0
  21. data/lib/jirametrics/daily_view.rb +83 -61
  22. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +24 -9
  23. data/lib/jirametrics/daily_wip_chart.rb +72 -45
  24. data/lib/jirametrics/data_quality_report.rb +61 -63
  25. data/lib/jirametrics/dependency_chart.rb +40 -31
  26. data/lib/jirametrics/download_config.rb +0 -3
  27. data/lib/jirametrics/downloader.rb +8 -7
  28. data/lib/jirametrics/downloader_for_cloud.rb +108 -72
  29. data/lib/jirametrics/estimate_accuracy_chart.rb +13 -13
  30. data/lib/jirametrics/examples/standard_project.rb +2 -2
  31. data/lib/jirametrics/expedited_chart.rb +47 -37
  32. data/lib/jirametrics/exporter.rb +19 -10
  33. data/lib/jirametrics/file_config.rb +19 -12
  34. data/lib/jirametrics/file_system.rb +16 -10
  35. data/lib/jirametrics/flow_efficiency_calculator.rb +62 -0
  36. data/lib/jirametrics/flow_efficiency_scatterplot.rb +4 -2
  37. data/lib/jirametrics/github_gateway.rb +23 -9
  38. data/lib/jirametrics/groupable_issue_chart.rb +3 -0
  39. data/lib/jirametrics/html/index.css +6 -0
  40. data/lib/jirametrics/html_report_config.rb +9 -29
  41. data/lib/jirametrics/issue.rb +290 -389
  42. data/lib/jirametrics/issue_collection.rb +1 -0
  43. data/lib/jirametrics/issue_printer.rb +60 -23
  44. data/lib/jirametrics/jira_gateway.rb +29 -18
  45. data/lib/jirametrics/mcp_server.rb +225 -209
  46. data/lib/jirametrics/project_config.rb +158 -134
  47. data/lib/jirametrics/pull_request_cycle_time_histogram.rb +1 -20
  48. data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +11 -33
  49. data/lib/jirametrics/rules.rb +1 -0
  50. data/lib/jirametrics/self_or_issue_dispatcher.rb +1 -3
  51. data/lib/jirametrics/sprint_burndown.rb +142 -242
  52. data/lib/jirametrics/sprint_count_measure.rb +42 -0
  53. data/lib/jirametrics/sprint_issue_change_data.rb +1 -0
  54. data/lib/jirametrics/sprint_points_measure.rb +62 -0
  55. data/lib/jirametrics/sprint_summary_stats.rb +16 -0
  56. data/lib/jirametrics/status_collection.rb +2 -2
  57. data/lib/jirametrics/stitcher.rb +21 -16
  58. data/lib/jirametrics/throughput_chart.rb +38 -24
  59. data/lib/jirametrics/time_based_chart.rb +65 -0
  60. data/lib/jirametrics/time_based_histogram.rb +30 -23
  61. data/lib/jirametrics/time_based_scatterplot.rb +11 -2
  62. data/lib/jirametrics/trend_line_calculator.rb +2 -2
  63. data/lib/jirametrics/wip_by_column_chart.rb +73 -32
  64. data/lib/jirametrics.rb +6 -2
  65. metadata +13 -20
@@ -27,6 +27,7 @@ class IssueCollection < Array
27
27
  issue = hidden.find(&block) if issue.nil? && include_hidden
28
28
  issue
29
29
  end
30
+
30
31
  def clone
31
32
  raise 'baboom'
32
33
  end
@@ -6,45 +6,82 @@ class IssuePrinter
6
6
  end
7
7
 
8
8
  def to_s
9
- issue = @issue
10
9
  result = +''
11
- result << "#{issue.key} (#{issue.type}): #{issue.compact_text issue.summary, max: 200}\n"
10
+ result << header_line
11
+ result << assignee_line
12
+ result << links_section
13
+ result << history_section
14
+ result
15
+ end
16
+
17
+ def header_line
18
+ "#{@issue.key} (#{@issue.type}): #{@issue.compact_text @issue.summary, max: 200}\n"
19
+ end
12
20
 
13
- assignee = issue.raw['fields']['assignee']
14
- result << " [assignee] #{assignee['name'].inspect} <#{assignee['emailAddress']}>\n" unless assignee.nil?
21
+ def assignee_line
22
+ assignee = @issue.raw['fields']['assignee']
23
+ return '' if assignee.nil?
15
24
 
16
- issue.raw['fields']['issuelinks']&.each do |link|
25
+ " [assignee] #{assignee['name'].inspect} <#{assignee['emailAddress']}>\n"
26
+ end
27
+
28
+ def links_section
29
+ result = +''
30
+ @issue.raw['fields']['issuelinks']&.each do |link|
17
31
  result << " [link] #{link['type']['outward']} #{link['outwardIssue']['key']}\n" if link['outwardIssue']
18
32
  result << " [link] #{link['type']['inward']} #{link['inwardIssue']['key']}\n" if link['inwardIssue']
19
33
  end
34
+ result
35
+ end
20
36
 
21
- history = [] # time, type, detail
37
+ def history_section
38
+ result = +''
39
+ result << cycletime_warning
40
+ result << " History:\n"
41
+ result << render_history(build_history)
42
+ result
43
+ end
22
44
 
23
- if issue.board.cycletime
24
- started_at, stopped_at = issue.started_stopped_times
25
- history << [started_at, nil, 'vvvv Started here vvvv', true] if started_at
26
- history << [stopped_at, nil, '^^^^ Finished here ^^^^', true] if stopped_at
27
- else
28
- result << " Unable to determine start/end times as board #{issue.board.id} has no cycletime specified\n"
29
- end
45
+ def cycletime_warning
46
+ return '' if @issue.board.cycletime
47
+
48
+ " Unable to determine start/end times as board #{@issue.board.id} has no cycletime specified\n"
49
+ end
50
+
51
+ # Each history entry is [time, type, detail, artificial?].
52
+ def build_history
53
+ start_stop_entries + discarded_change_entries + change_entries
54
+ end
55
+
56
+ def start_stop_entries
57
+ return [] unless @issue.board.cycletime
58
+
59
+ started_at, stopped_at = @issue.started_stopped_times
60
+ entries = []
61
+ entries << [started_at, nil, 'vvvv Started here vvvv', true] if started_at
62
+ entries << [stopped_at, nil, '^^^^ Finished here ^^^^', true] if stopped_at
63
+ entries
64
+ end
30
65
 
31
- issue.discarded_change_times&.each do |time|
32
- history << [time, nil, '^^^^ Changes discarded ^^^^', true]
66
+ def discarded_change_entries
67
+ (@issue.discarded_change_times || []).map do |time|
68
+ [time, nil, '^^^^ Changes discarded ^^^^', true]
33
69
  end
70
+ end
34
71
 
35
- (issue.changes + (issue.discarded_changes || [])).each do |change|
36
- history << [change.time, change.field, create_change_message(change: change, issue: issue), change.artificial?]
72
+ def change_entries
73
+ (@issue.changes + (@issue.discarded_changes || [])).map do |change|
74
+ [change.time, change.field, create_change_message(change: change, issue: @issue), change.artificial?]
37
75
  end
76
+ end
38
77
 
39
- result << " History:\n"
78
+ def render_history history
40
79
  type_width = history.collect { |_time, type, _detail, _artificial| type&.length || 0 }.max
41
80
  sort_history!(history)
42
- history.each do |time, type, detail, _artificial|
81
+ history.map do |time, type, detail, _artificial|
43
82
  type = type.nil? ? '-' * type_width : type.rjust(type_width)
44
- result << " #{time.strftime '%Y-%m-%d %H:%M:%S %z'} [#{type}] #{detail}\n"
45
- end
46
-
47
- result
83
+ " #{time.strftime '%Y-%m-%d %H:%M:%S %z'} [#{type}] #{detail}\n"
84
+ end.join
48
85
  end
49
86
 
50
87
  def create_change_message change:, issue:
@@ -43,7 +43,7 @@ class JiraGateway
43
43
  return parse_response(command: command, result: stdout)
44
44
  end
45
45
 
46
- if RETRYABLE_EXIT_CODES.include?(status.exitstatus) && retries < MAX_RETRIES && !stderr.include?('503')
46
+ if retryable?(status: status, stderr: stderr, retries: retries)
47
47
  retries += 1
48
48
  @file_system.log "Transient network error (exit #{status.exitstatus}), retrying in " \
49
49
  "#{RETRY_DELAY_SECONDS}s (attempt #{retries}/#{MAX_RETRIES})..."
@@ -51,22 +51,30 @@ class JiraGateway
51
51
  next
52
52
  end
53
53
 
54
- @file_system.error "Failed call with exit status #{status.exitstatus}!"
55
- @file_system.error "Returned (stdout): #{stdout.inspect}"
56
- @file_system.error "Returned (stderr): #{stderr.inspect}"
57
- if stderr.include?('401')
58
- raise 'The request was not authorized. Verify that your authentication token hasn\'t expired'
59
- end
60
- if stderr.include?('503')
61
- raise 'Jira returned 503 (Service Unavailable). This may be a temporary outage, or your ' \
62
- 'Jira account may have been deactivated due to inactivity. Check your Jira subscription ' \
63
- 'and try again later.'
64
- end
65
- raise "Failed call with exit status #{status.exitstatus}. " \
66
- "See #{@file_system.logfile_name} for details"
54
+ raise_for_failure status: status, stdout: stdout, stderr: stderr
67
55
  end
68
56
  end
69
57
 
58
+ def retryable? status:, stderr:, retries:
59
+ RETRYABLE_EXIT_CODES.include?(status.exitstatus) && retries < MAX_RETRIES && !stderr.include?('503')
60
+ end
61
+
62
+ def raise_for_failure status:, stdout:, stderr:
63
+ @file_system.error "Failed call with exit status #{status.exitstatus}!"
64
+ @file_system.error "Returned (stdout): #{stdout.inspect}"
65
+ @file_system.error "Returned (stderr): #{stderr.inspect}"
66
+ if stderr.include?('401')
67
+ raise 'The request was not authorized. Verify that your authentication token hasn\'t expired'
68
+ end
69
+ if stderr.include?('503')
70
+ raise 'Jira returned 503 (Service Unavailable). This may be a temporary outage, or your ' \
71
+ 'Jira account may have been deactivated due to inactivity. Check your Jira subscription ' \
72
+ 'and try again later.'
73
+ end
74
+ raise "Failed call with exit status #{status.exitstatus}. " \
75
+ "See #{@file_system.logfile_name} for details"
76
+ end
77
+
70
78
  def capture3 command, stdin_data:
71
79
  # In it's own method so we can mock it out in tests
72
80
  Open3.capture3(command, stdin_data: stdin_data)
@@ -115,14 +123,17 @@ class JiraGateway
115
123
  @jira_email = jira_config['email']
116
124
  @jira_api_token = jira_config['api_token']
117
125
  @jira_personal_access_token = jira_config['personal_access_token']
126
+ validate_jira_auth
127
+
128
+ @cookies = (jira_config['cookies'] || []).collect { |key, value| "#{key}=#{value}" }.join(';')
129
+ end
118
130
 
131
+ def validate_jira_auth
119
132
  raise 'When specifying an api-token, you must also specify email' if @jira_api_token && !@jira_email
120
133
 
121
- if @jira_api_token && @jira_personal_access_token
122
- raise "You can't specify both an api-token and a personal-access-token. They don't work together."
123
- end
134
+ return unless @jira_api_token && @jira_personal_access_token
124
135
 
125
- @cookies = (jira_config['cookies'] || []).collect { |key, value| "#{key}=#{value}" }.join(';')
136
+ raise "You can't specify both an api-token and a personal-access-token. They don't work together."
126
137
  end
127
138
 
128
139
  def make_curl_command url:, method: 'GET'