jirametrics 2.30.1pre1 → 2.30.1pre5
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d72da7d4b92e9ea83de819c57a477926fc5957abd19bde090846e36e7c73f0b
|
|
4
|
+
data.tar.gz: 7e953c0c3af6184a65ec820e23c0e53026eec6381818617ad8bf738179fc22b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 25c2c1beee567c1ce1c43493c36d178d87eb6991716f0318691ed2933b9f38ffa86d07857bcdffde8cf6b6e1acc70dba95192ea45319c06675f99f65197ee267
|
|
7
|
+
data.tar.gz: 1873e39f4af4cb0f1db2c28192c8a8bbf635f0a8d57d8df810f368dcdf3b3b2a3b73192d90099c7e918585439310d4f48e0a2e58486a7358fe1b26b42c868ab2
|
|
@@ -30,7 +30,7 @@ class DownloadConfig
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def github_repo *repos
|
|
33
|
-
github_repos.concat(repos
|
|
33
|
+
github_repos.concat(repos)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def start_date today:
|
|
@@ -42,8 +42,4 @@ class DownloadConfig
|
|
|
42
42
|
|
|
43
43
|
private
|
|
44
44
|
|
|
45
|
-
def normalize_github_repo repo
|
|
46
|
-
match = repo.match(%r{github\.com/([^/]+/[^/]+?)/?$})
|
|
47
|
-
match ? match[1] : repo
|
|
48
|
-
end
|
|
49
45
|
end
|
|
@@ -101,6 +101,7 @@ class DownloaderForCloud < Downloader
|
|
|
101
101
|
)
|
|
102
102
|
|
|
103
103
|
attach_changelog_to_issues issue_datas: issue_datas, issue_jsons: response['issues']
|
|
104
|
+
attach_worklogs_to_issues issue_datas: issue_datas, issue_jsons: response['issues']
|
|
104
105
|
|
|
105
106
|
response['issues'].each do |issue_json|
|
|
106
107
|
issue_json['exporter'] = {
|
|
@@ -129,6 +130,45 @@ class DownloaderForCloud < Downloader
|
|
|
129
130
|
issue_datas
|
|
130
131
|
end
|
|
131
132
|
|
|
133
|
+
def attach_worklogs_to_issues issue_datas:, issue_jsons:, max_results: 100 # rubocop:disable Lint/UnusedMethodArgument
|
|
134
|
+
issue_jsons.each do |issue_json|
|
|
135
|
+
worklog = issue_json['fields']['worklog']
|
|
136
|
+
next unless worklog
|
|
137
|
+
|
|
138
|
+
total = worklog['total'].to_i
|
|
139
|
+
all_worklogs = worklog['worklogs'] || []
|
|
140
|
+
next if all_worklogs.size >= total
|
|
141
|
+
|
|
142
|
+
key = issue_json['key']
|
|
143
|
+
start_at = all_worklogs.size
|
|
144
|
+
|
|
145
|
+
loop do
|
|
146
|
+
response = @jira_gateway.call_url(
|
|
147
|
+
relative_url: "/rest/api/3/issue/#{CGI.escape(key)}/worklog?startAt=#{start_at}&maxResults=#{max_results}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
worklogs = response['worklogs'] || []
|
|
151
|
+
all_worklogs.concat(worklogs)
|
|
152
|
+
|
|
153
|
+
total = response['total'].to_i
|
|
154
|
+
log " #{key} worklogs: page startAt=#{start_at}, " \
|
|
155
|
+
"received=#{worklogs.size}, fetched=#{all_worklogs.size}/#{total}"
|
|
156
|
+
break if all_worklogs.size >= total
|
|
157
|
+
|
|
158
|
+
start_at += worklogs.size
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
issue_json['fields']['worklog'] = {
|
|
162
|
+
'startAt' => 0,
|
|
163
|
+
'maxResults' => all_worklogs.size,
|
|
164
|
+
'total' => all_worklogs.size,
|
|
165
|
+
'worklogs' => all_worklogs
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
log " Enhanced #{key} with #{all_worklogs.size} worklogs"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
132
172
|
def attach_changelog_to_issues issue_datas:, issue_jsons:
|
|
133
173
|
max_results = 10_000 # The max jira accepts is 10K
|
|
134
174
|
payload = {
|
|
@@ -190,7 +230,7 @@ class DownloaderForCloud < Downloader
|
|
|
190
230
|
unless stale.empty?
|
|
191
231
|
log_start ' Downloading more issues ' unless in_related_phase
|
|
192
232
|
stale.each_slice(100) do |slice|
|
|
193
|
-
slice = bulk_fetch_issues(issue_datas: slice, board: board, in_initial_query:
|
|
233
|
+
slice = bulk_fetch_issues(issue_datas: slice, board: board, in_initial_query: !in_related_phase)
|
|
194
234
|
progress_dot
|
|
195
235
|
slice.each do |data|
|
|
196
236
|
next unless data.issue
|
|
@@ -232,11 +272,11 @@ class DownloaderForCloud < Downloader
|
|
|
232
272
|
end
|
|
233
273
|
break if related_issue_keys.empty?
|
|
234
274
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
275
|
+
next if in_related_phase
|
|
276
|
+
|
|
277
|
+
in_related_phase = true
|
|
278
|
+
log " Identifying related issues (parents, subtasks, links) for board #{board.id}", both: true
|
|
279
|
+
log_start ' Downloading more issues '
|
|
240
280
|
end
|
|
241
281
|
|
|
242
282
|
end_progress if in_related_phase
|
|
@@ -49,8 +49,12 @@ class DownloaderForDataCenter < Downloader
|
|
|
49
49
|
}
|
|
50
50
|
identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
|
|
51
51
|
file = "#{issue_json['key']}-#{board.id}.json"
|
|
52
|
+
issue_path = File.join(path, file)
|
|
52
53
|
|
|
53
|
-
@file_system.save_json(json: issue_json, filename:
|
|
54
|
+
@file_system.save_json(json: issue_json, filename: issue_path)
|
|
55
|
+
|
|
56
|
+
# Fetch complete worklog data for this issue
|
|
57
|
+
enhance_issue_with_worklogs(issue_key: issue_json['key'], issue_path: issue_path)
|
|
54
58
|
end
|
|
55
59
|
|
|
56
60
|
total = json['total'].to_i
|
|
@@ -63,6 +67,36 @@ class DownloaderForDataCenter < Downloader
|
|
|
63
67
|
end
|
|
64
68
|
end
|
|
65
69
|
|
|
70
|
+
def enhance_issue_with_worklogs issue_key:, issue_path:, max_results: 100
|
|
71
|
+
all_worklogs = []
|
|
72
|
+
start_at = 0
|
|
73
|
+
|
|
74
|
+
loop do
|
|
75
|
+
url = "/rest/api/2/issue/#{CGI.escape(issue_key)}/worklog?startAt=#{start_at}&maxResults=#{max_results}"
|
|
76
|
+
response = @jira_gateway.call_url(relative_url: url)
|
|
77
|
+
|
|
78
|
+
worklogs = response['worklogs'] || []
|
|
79
|
+
all_worklogs.concat(worklogs)
|
|
80
|
+
|
|
81
|
+
total = response['total'].to_i
|
|
82
|
+
break if start_at + worklogs.size >= total
|
|
83
|
+
|
|
84
|
+
start_at += worklogs.size
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
issue_json = @file_system.load_json(issue_path)
|
|
88
|
+
issue_json['fields'] ||= {}
|
|
89
|
+
issue_json['fields']['worklog'] = {
|
|
90
|
+
'startAt' => 0,
|
|
91
|
+
'maxResults' => all_worklogs.size,
|
|
92
|
+
'total' => all_worklogs.size,
|
|
93
|
+
'worklogs' => all_worklogs
|
|
94
|
+
}
|
|
95
|
+
@file_system.save_json(json: issue_json, filename: issue_path)
|
|
96
|
+
|
|
97
|
+
log " Enhanced #{issue_key} with #{all_worklogs.size} worklogs" if all_worklogs.any?
|
|
98
|
+
end
|
|
99
|
+
|
|
66
100
|
def make_jql filter_id:, today: nil
|
|
67
101
|
today ||= today_in_project_timezone
|
|
68
102
|
segments = []
|
|
@@ -6,6 +6,13 @@ require 'json'
|
|
|
6
6
|
class GithubGateway
|
|
7
7
|
attr_reader :repo
|
|
8
8
|
|
|
9
|
+
TRANSIENT_ERROR_PATTERNS = (
|
|
10
|
+
[429, 500, 502, 503, 504].map { |code| "HTTP #{code}" } +
|
|
11
|
+
['stream error:']
|
|
12
|
+
).freeze
|
|
13
|
+
MAX_RETRIES = 3
|
|
14
|
+
REVIEW_STATES = %w[APPROVED CHANGES_REQUESTED].freeze
|
|
15
|
+
|
|
9
16
|
def initialize repo:, project_keys:, file_system:, raw_pr_cache: {}
|
|
10
17
|
@repo = repo
|
|
11
18
|
@project_keys = project_keys
|
|
@@ -20,7 +27,7 @@ class GithubGateway
|
|
|
20
27
|
end
|
|
21
28
|
|
|
22
29
|
def fetch_raw_pull_requests since: nil
|
|
23
|
-
#
|
|
30
|
+
# NOTE: 'commits' is intentionally excluded — including it triggers GitHub's GraphQL node
|
|
24
31
|
# limit (authors sub-connection × PRs × commits exceeds 500,000 nodes). Branch name,
|
|
25
32
|
# title, and body are sufficient for issue key extraction in the vast majority of cases.
|
|
26
33
|
json_fields = %w[number title body headRefName createdAt closedAt mergedAt
|
|
@@ -72,7 +79,7 @@ class GithubGateway
|
|
|
72
79
|
|
|
73
80
|
def extract_reviews raw_reviews
|
|
74
81
|
raw_reviews
|
|
75
|
-
.select { |r|
|
|
82
|
+
.select { |r| REVIEW_STATES.include?(r['state']) }
|
|
76
83
|
.map do |r|
|
|
77
84
|
{
|
|
78
85
|
'author' => r.dig('author', 'login'),
|
|
@@ -100,16 +107,36 @@ class GithubGateway
|
|
|
100
107
|
end
|
|
101
108
|
|
|
102
109
|
def run_command args
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
attempts = 0
|
|
111
|
+
loop do
|
|
112
|
+
attempts += 1
|
|
113
|
+
stdout, stderr, status = Open3.capture3('gh', *args)
|
|
114
|
+
|
|
115
|
+
# This extra check seems to only matter on Windows. On the mac, auth failures don't pass status.success?
|
|
116
|
+
if stderr.include?('SAML enforcement')
|
|
117
|
+
raise "GitHub CLI is not authorized to access #{@repo}. " \
|
|
118
|
+
'Run: gh auth refresh -h github.com -s read:org'
|
|
119
|
+
end
|
|
110
120
|
|
|
111
|
-
|
|
121
|
+
unless status.success?
|
|
122
|
+
error_message = " GitHub CLI command failed for #{@repo} " \
|
|
123
|
+
"(attempt #{attempts}/#{MAX_RETRIES}): #{stderr.strip}"
|
|
124
|
+
if attempts < MAX_RETRIES && TRANSIENT_ERROR_PATTERNS.any? { |pattern| stderr.include?(pattern) }
|
|
125
|
+
delay = 2**attempts
|
|
126
|
+
@file_system.log error_message
|
|
127
|
+
@file_system.log " Transient error detected. Retrying in #{delay}s..."
|
|
128
|
+
sleep delay
|
|
129
|
+
next
|
|
130
|
+
end
|
|
131
|
+
@file_system.warning error_message
|
|
132
|
+
raise "GitHub CLI command failed for #{@repo}: #{stderr}"
|
|
133
|
+
end
|
|
112
134
|
|
|
113
|
-
|
|
135
|
+
result = JSON.parse(stdout)
|
|
136
|
+
if result.nil? || (result.is_a?(Array) && result.empty?)
|
|
137
|
+
@file_system.warning "No data was found in GitHub for #{@repo}. Is that what you expected?"
|
|
138
|
+
end
|
|
139
|
+
return result
|
|
140
|
+
end
|
|
114
141
|
end
|
|
115
142
|
end
|
|
@@ -40,7 +40,7 @@ class JiraGateway
|
|
|
40
40
|
return parse_response(command: command, result: stdout)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
if RETRYABLE_EXIT_CODES.include?(status.exitstatus) && retries < MAX_RETRIES
|
|
43
|
+
if RETRYABLE_EXIT_CODES.include?(status.exitstatus) && retries < MAX_RETRIES && !stderr.include?('503')
|
|
44
44
|
retries += 1
|
|
45
45
|
@file_system.log "Transient network error (exit #{status.exitstatus}), retrying in #{RETRY_DELAY_SECONDS}s (attempt #{retries}/#{MAX_RETRIES})..."
|
|
46
46
|
sleep_between_retries
|
|
@@ -53,6 +53,11 @@ class JiraGateway
|
|
|
53
53
|
if stderr.include?('401')
|
|
54
54
|
raise 'The request was not authorized. Verify that your authentication token hasn\'t expired'
|
|
55
55
|
end
|
|
56
|
+
if stderr.include?('503')
|
|
57
|
+
raise 'Jira returned 503 (Service Unavailable). This may be a temporary outage, or your ' \
|
|
58
|
+
'Jira account may have been deactivated due to inactivity. Check your Jira subscription ' \
|
|
59
|
+
'and try again later.'
|
|
60
|
+
end
|
|
56
61
|
raise "Failed call with exit status #{status.exitstatus}. " \
|
|
57
62
|
"See #{@file_system.logfile_name} for details"
|
|
58
63
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jirametrics
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.30.
|
|
4
|
+
version: 2.30.1pre5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Bowler
|
|
@@ -210,7 +210,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
210
210
|
- !ruby/object:Gem::Version
|
|
211
211
|
version: '0'
|
|
212
212
|
requirements: []
|
|
213
|
-
rubygems_version: 4.0.
|
|
213
|
+
rubygems_version: 4.0.13
|
|
214
214
|
specification_version: 4
|
|
215
215
|
summary: Extract Jira metrics
|
|
216
216
|
test_files: []
|