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: c1b70c2648c844b678a4b20ae4f9ed570e2a471ff306b4b682d1d0566ba9c334
4
- data.tar.gz: ce6972d092d1f5b1bd7425f9d19a8dbbb1d2a7f584e0fe48b9f140faaca96e57
3
+ metadata.gz: 4d72da7d4b92e9ea83de819c57a477926fc5957abd19bde090846e36e7c73f0b
4
+ data.tar.gz: 7e953c0c3af6184a65ec820e23c0e53026eec6381818617ad8bf738179fc22b8
5
5
  SHA512:
6
- metadata.gz: 98e35757d64618ff0ee87854a88b98196ccf87cb47761c4a065ead4aae84cb6653cbb37c6746f06a61418fee4c75f31089172028b485b276939f3c07d99b045d
7
- data.tar.gz: 1c509febc055eec7ca7d8d9a89a9136e6b6ded8ff8b0585c9b1432b0e2aeb8a8d7301808070eb52b8919e6340928b9aecf137e98a2acd79e6c271fd36a1bc83a
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.map { |r| normalize_github_repo(r) })
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: true)
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
- unless in_related_phase
236
- in_related_phase = true
237
- log " Identifying related issues (parents, subtasks, links) for board #{board.id}", both: true
238
- log_start ' Downloading more issues '
239
- end
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: File.join(path, file))
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
- # Note: 'commits' is intentionally excluded — including it triggers GitHub's GraphQL node
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| %w[APPROVED CHANGES_REQUESTED].include?(r['state']) }
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
- 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
+ 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
- raise "GitHub CLI command failed for #{@repo}: #{stderr}" unless status.success?
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
- JSON.parse(stdout)
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.1pre1
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.10
213
+ rubygems_version: 4.0.13
214
214
  specification_version: 4
215
215
  summary: Extract Jira metrics
216
216
  test_files: []