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.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +42 -29
- data/lib/jirametrics/aging_work_bar_chart.rb +52 -40
- data/lib/jirametrics/aging_work_in_progress_chart.rb +65 -60
- data/lib/jirametrics/aging_work_table.rb +36 -33
- data/lib/jirametrics/anonymizer.rb +31 -86
- data/lib/jirametrics/atlassian_document_format.rb +11 -4
- data/lib/jirametrics/blocked_stalled_by_date_builder.rb +64 -0
- data/lib/jirametrics/blocked_stalled_change.rb +5 -1
- data/lib/jirametrics/blocked_stalled_change_stream_builder.rb +194 -0
- data/lib/jirametrics/board.rb +23 -9
- data/lib/jirametrics/board_config.rb +0 -7
- data/lib/jirametrics/board_movement_calculator.rb +24 -21
- data/lib/jirametrics/cfd_data_builder.rb +46 -44
- data/lib/jirametrics/change_item.rb +19 -7
- data/lib/jirametrics/chart_base.rb +50 -64
- data/lib/jirametrics/cumulative_flow_diagram.rb +59 -46
- data/lib/jirametrics/cycle_time_config.rb +35 -46
- data/lib/jirametrics/cycletime_histogram.rb +7 -0
- data/lib/jirametrics/cycletime_scatterplot.rb +7 -0
- data/lib/jirametrics/daily_view.rb +83 -61
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +24 -9
- data/lib/jirametrics/daily_wip_chart.rb +72 -45
- data/lib/jirametrics/data_quality_report.rb +61 -63
- data/lib/jirametrics/dependency_chart.rb +40 -31
- data/lib/jirametrics/download_config.rb +0 -3
- data/lib/jirametrics/downloader.rb +8 -7
- data/lib/jirametrics/downloader_for_cloud.rb +108 -72
- data/lib/jirametrics/estimate_accuracy_chart.rb +13 -13
- data/lib/jirametrics/examples/standard_project.rb +2 -2
- data/lib/jirametrics/expedited_chart.rb +47 -37
- data/lib/jirametrics/exporter.rb +19 -10
- data/lib/jirametrics/file_config.rb +19 -12
- data/lib/jirametrics/file_system.rb +16 -10
- data/lib/jirametrics/flow_efficiency_calculator.rb +62 -0
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +4 -2
- data/lib/jirametrics/github_gateway.rb +23 -9
- data/lib/jirametrics/groupable_issue_chart.rb +3 -0
- data/lib/jirametrics/html/index.css +6 -0
- data/lib/jirametrics/html_report_config.rb +9 -29
- data/lib/jirametrics/issue.rb +290 -389
- data/lib/jirametrics/issue_collection.rb +1 -0
- data/lib/jirametrics/issue_printer.rb +60 -23
- data/lib/jirametrics/jira_gateway.rb +29 -18
- data/lib/jirametrics/mcp_server.rb +225 -209
- data/lib/jirametrics/project_config.rb +158 -134
- data/lib/jirametrics/pull_request_cycle_time_histogram.rb +1 -20
- data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +11 -33
- data/lib/jirametrics/rules.rb +1 -0
- data/lib/jirametrics/self_or_issue_dispatcher.rb +1 -3
- data/lib/jirametrics/sprint_burndown.rb +142 -242
- data/lib/jirametrics/sprint_count_measure.rb +42 -0
- data/lib/jirametrics/sprint_issue_change_data.rb +1 -0
- data/lib/jirametrics/sprint_points_measure.rb +62 -0
- data/lib/jirametrics/sprint_summary_stats.rb +16 -0
- data/lib/jirametrics/status_collection.rb +2 -2
- data/lib/jirametrics/stitcher.rb +21 -16
- data/lib/jirametrics/throughput_chart.rb +38 -24
- data/lib/jirametrics/time_based_chart.rb +65 -0
- data/lib/jirametrics/time_based_histogram.rb +30 -23
- data/lib/jirametrics/time_based_scatterplot.rb +11 -2
- data/lib/jirametrics/trend_line_calculator.rb +2 -2
- data/lib/jirametrics/wip_by_column_chart.rb +73 -32
- data/lib/jirametrics.rb +6 -2
- metadata +13 -20
|
@@ -6,45 +6,82 @@ class IssuePrinter
|
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
def to_s
|
|
9
|
-
issue = @issue
|
|
10
9
|
result = +''
|
|
11
|
-
result <<
|
|
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
|
-
|
|
14
|
-
|
|
21
|
+
def assignee_line
|
|
22
|
+
assignee = @issue.raw['fields']['assignee']
|
|
23
|
+
return '' if assignee.nil?
|
|
15
24
|
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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.
|
|
81
|
+
history.map do |time, type, detail, _artificial|
|
|
43
82
|
type = type.nil? ? '-' * type_width : type.rjust(type_width)
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'
|