jirametrics 2.14 → 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 +96 -96
- 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 +3 -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 +139 -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 +42 -31
- 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 +244 -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 +302 -98
- 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 +108 -9
- 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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
class GithubGateway
|
|
7
|
+
attr_reader :repo
|
|
8
|
+
|
|
9
|
+
def initialize repo:, project_keys:, file_system:, raw_pr_cache: {}
|
|
10
|
+
@repo = repo
|
|
11
|
+
@project_keys = project_keys
|
|
12
|
+
@file_system = file_system
|
|
13
|
+
@raw_pr_cache = raw_pr_cache
|
|
14
|
+
@issue_key_pattern = build_issue_key_pattern
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def fetch_pull_requests since: nil
|
|
18
|
+
raw_prs = @raw_pr_cache[[@repo, since]] ||= fetch_raw_pull_requests(since: since)
|
|
19
|
+
raw_prs.filter_map { |pr| build_pr_data(pr) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def fetch_raw_pull_requests since: nil
|
|
23
|
+
# Note: 'commits' is intentionally excluded — including it triggers GitHub's GraphQL node
|
|
24
|
+
# limit (authors sub-connection × PRs × commits exceeds 500,000 nodes). Branch name,
|
|
25
|
+
# title, and body are sufficient for issue key extraction in the vast majority of cases.
|
|
26
|
+
json_fields = %w[number title body headRefName createdAt closedAt mergedAt
|
|
27
|
+
url state reviews additions deletions changedFiles].join(',')
|
|
28
|
+
args = ['pr', 'list', '--state', 'all', '--limit', '5000', '--json', json_fields]
|
|
29
|
+
args += ['--repo', @repo]
|
|
30
|
+
args += ['--search', "updated:>=#{since}"] if since
|
|
31
|
+
|
|
32
|
+
@file_system.log " Downloading pull requests from #{@repo}", also_write_to_stderr: true
|
|
33
|
+
run_command(args)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_pr_data raw_pr
|
|
37
|
+
issue_keys = extract_issue_keys(raw_pr)
|
|
38
|
+
return nil if issue_keys.empty?
|
|
39
|
+
|
|
40
|
+
PullRequest.new(raw: {
|
|
41
|
+
'number' => raw_pr['number'],
|
|
42
|
+
'repo' => @repo,
|
|
43
|
+
'url' => raw_pr['url'],
|
|
44
|
+
'title' => raw_pr['title'],
|
|
45
|
+
'branch' => raw_pr['headRefName'],
|
|
46
|
+
'opened_at' => raw_pr['createdAt'],
|
|
47
|
+
'closed_at' => raw_pr['closedAt'],
|
|
48
|
+
'merged_at' => raw_pr['mergedAt'],
|
|
49
|
+
'state' => raw_pr['state'],
|
|
50
|
+
'issue_keys' => issue_keys,
|
|
51
|
+
'reviews' => extract_reviews(raw_pr['reviews'] || []),
|
|
52
|
+
'additions' => raw_pr['additions'],
|
|
53
|
+
'deletions' => raw_pr['deletions'],
|
|
54
|
+
'changed_files' => raw_pr['changedFiles']
|
|
55
|
+
})
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def extract_issue_keys raw_pr
|
|
59
|
+
return [] if @issue_key_pattern.nil?
|
|
60
|
+
|
|
61
|
+
sources = [
|
|
62
|
+
raw_pr['headRefName'],
|
|
63
|
+
raw_pr['title'],
|
|
64
|
+
raw_pr['body']
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
keys = sources.compact.flat_map { |s| s.scan(@issue_key_pattern) }.uniq
|
|
68
|
+
return keys unless keys.empty?
|
|
69
|
+
|
|
70
|
+
commit_messages_for(raw_pr['number']).flat_map { |msg| msg.scan(@issue_key_pattern) }.uniq
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def extract_reviews raw_reviews
|
|
74
|
+
raw_reviews
|
|
75
|
+
.select { |r| %w[APPROVED CHANGES_REQUESTED].include?(r['state']) }
|
|
76
|
+
.map do |r|
|
|
77
|
+
{
|
|
78
|
+
'author' => r.dig('author', 'login'),
|
|
79
|
+
'submitted_at' => r['submittedAt'],
|
|
80
|
+
'state' => r['state']
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def commit_messages_for pr_number
|
|
88
|
+
args = ['pr', 'view', pr_number.to_s, '--json', 'commits', '--repo', @repo]
|
|
89
|
+
result = run_command(args)
|
|
90
|
+
(result['commits'] || []).flat_map do |commit|
|
|
91
|
+
[commit['messageHeadline'], commit['messageBody']].compact
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def build_issue_key_pattern
|
|
96
|
+
return nil if @project_keys.empty?
|
|
97
|
+
|
|
98
|
+
keys_pattern = @project_keys.map { |k| Regexp.escape(k) }.join('|')
|
|
99
|
+
Regexp.new("\\b(?:#{keys_pattern})-\\d+(?![A-Za-z0-9])")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def run_command args
|
|
103
|
+
stdout, stderr, status = Open3.capture3('gh', *args)
|
|
104
|
+
|
|
105
|
+
# This extra check seems to only matter on Windows. On the mac, auth failures don't pass status.success?
|
|
106
|
+
if stderr.include?('SAML enforcement')
|
|
107
|
+
raise "GitHub CLI is not authorized to access #{@repo}. " \
|
|
108
|
+
'Run: gh auth refresh -h github.com -s read:org'
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
raise "GitHub CLI command failed for #{@repo}: #{stderr}" unless status.success?
|
|
112
|
+
|
|
113
|
+
JSON.parse(stdout)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -15,14 +15,24 @@ module GroupableIssueChart
|
|
|
15
15
|
|
|
16
16
|
def group_issues completed_issues
|
|
17
17
|
result = {}
|
|
18
|
+
ignored_issues = []
|
|
19
|
+
@issue_hints = {}
|
|
20
|
+
@issue_periods = {}
|
|
18
21
|
completed_issues.each do |issue|
|
|
19
22
|
rules = GroupingRules.new
|
|
20
23
|
@group_by_block.call(issue, rules)
|
|
21
|
-
|
|
24
|
+
if rules.ignored?
|
|
25
|
+
ignored_issues << issue
|
|
26
|
+
next
|
|
27
|
+
end
|
|
22
28
|
|
|
29
|
+
@issue_hints[issue] = rules.issue_hint
|
|
30
|
+
@issue_periods[issue] = rules.last_day_of_period
|
|
23
31
|
(result[rules] ||= []) << issue
|
|
24
32
|
end
|
|
25
33
|
|
|
34
|
+
completed_issues.reject! { |issue| ignored_issues.include? issue }
|
|
35
|
+
|
|
26
36
|
result.each_key do |rules|
|
|
27
37
|
rules.color = random_color if rules.color.nil?
|
|
28
38
|
end
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class GroupingRules < Rules
|
|
4
|
-
attr_accessor :label
|
|
5
|
-
attr_reader :color
|
|
4
|
+
attr_accessor :label, :issue_hint, :label_hint
|
|
5
|
+
attr_reader :color, :last_day_of_period
|
|
6
|
+
|
|
7
|
+
def last_day_of_period= value
|
|
8
|
+
@last_day_of_period = value.is_a?(String) ? Date.parse(value) : value
|
|
9
|
+
end
|
|
6
10
|
|
|
7
11
|
def eql? other
|
|
8
12
|
other.label == @label && other.color == @color
|
|
@@ -13,7 +17,25 @@ class GroupingRules < Rules
|
|
|
13
17
|
end
|
|
14
18
|
|
|
15
19
|
def color= color
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
if color.is_a?(Array)
|
|
21
|
+
raise ArgumentError, 'Color pair must have exactly two elements: [light_color, dark_color]' unless color.size == 2
|
|
22
|
+
raise ArgumentError, 'Color pair elements must be strings' unless color.all?(String)
|
|
23
|
+
|
|
24
|
+
if color.any? { |c| c.start_with?('--') }
|
|
25
|
+
raise ArgumentError,
|
|
26
|
+
'CSS variable references are not supported as color pair elements; use a literal color value instead'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
light, dark = color
|
|
30
|
+
@color = RawJavascript.new(
|
|
31
|
+
"(document.documentElement.dataset.theme === 'dark' || " \
|
|
32
|
+
'(!document.documentElement.dataset.theme && ' \
|
|
33
|
+
"window.matchMedia('(prefers-color-scheme: dark)').matches)) " \
|
|
34
|
+
"? #{dark.to_json} : #{light.to_json}"
|
|
35
|
+
)
|
|
36
|
+
else
|
|
37
|
+
color = CssVariable[color] unless color.is_a?(CssVariable)
|
|
38
|
+
@color = color
|
|
39
|
+
end
|
|
18
40
|
end
|
|
19
41
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%= seam_start %>
|
|
1
2
|
<div class="chart">
|
|
2
3
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
3
4
|
</div>
|
|
@@ -15,11 +16,9 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
15
16
|
x: {
|
|
16
17
|
type: 'time',
|
|
17
18
|
min: '<%= @date_range.begin.to_s %>',
|
|
18
|
-
max: '<%= (@date_range.end ).to_s %>',
|
|
19
|
+
max: '<%= (@date_range.end + 1).to_s %>',
|
|
19
20
|
stacked: false,
|
|
20
|
-
|
|
21
|
-
display: false
|
|
22
|
-
},
|
|
21
|
+
<%= render_axis_title :x %>
|
|
23
22
|
grid: {
|
|
24
23
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
25
24
|
},
|
|
@@ -30,6 +29,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
30
29
|
ticks: {
|
|
31
30
|
display: true
|
|
32
31
|
},
|
|
32
|
+
<%= render_axis_title :y %>
|
|
33
33
|
grid: {
|
|
34
34
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
35
35
|
},
|
|
@@ -66,4 +66,4 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
</script>
|
|
69
|
-
|
|
69
|
+
<%= seam_end %>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%= seam_start %>
|
|
1
2
|
<div class="chart">
|
|
2
3
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
3
4
|
</div>
|
|
@@ -40,7 +41,7 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
|
|
|
40
41
|
color: <%= CssVariable['--grid-line-color'].to_json %>,
|
|
41
42
|
z: 1 // draw the grid lines on top of the bars
|
|
42
43
|
},
|
|
43
|
-
stacked:
|
|
44
|
+
stacked: false,
|
|
44
45
|
max: <%= (@max_age * 1.1).to_i %>
|
|
45
46
|
}
|
|
46
47
|
},
|
|
@@ -73,3 +74,4 @@ new Chart(document.getElementById(<%= chart_id.inspect %>).getContext('2d'),
|
|
|
73
74
|
}
|
|
74
75
|
});
|
|
75
76
|
</script>
|
|
77
|
+
<%= seam_end %>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<%= seam_start %>
|
|
1
2
|
<table class='standard'>
|
|
2
3
|
<thead>
|
|
3
4
|
<tr>
|
|
@@ -40,6 +41,9 @@
|
|
|
40
41
|
<%= link_to_issue parent, style: "color: #{color}" %>
|
|
41
42
|
</span>
|
|
42
43
|
<i><%= parent.summary.strip.inspect %></i>
|
|
44
|
+
<% if parent == issue && (text = not_visible_text(issue)) %>
|
|
45
|
+
<br /><%= text %>
|
|
46
|
+
<% end %>
|
|
43
47
|
</div>
|
|
44
48
|
<% end %>
|
|
45
49
|
</td>
|
|
@@ -54,3 +58,4 @@
|
|
|
54
58
|
<% end %>
|
|
55
59
|
</tbody>
|
|
56
60
|
</table>
|
|
61
|
+
<%= seam_end %>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
<table class='standard' id='<%= issues_id %>'
|
|
1
|
+
<div class='foldable startFolded'>Show details</div>
|
|
2
|
+
<table class='standard' id='<%= issues_id %>'>
|
|
3
3
|
<thead>
|
|
4
4
|
<tr>
|
|
5
5
|
<th>Issue</th>
|