issue-db 1.1.0 → 1.2.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/README.md +34 -6
- data/issue-db.gemspec +1 -1
- data/lib/issue_db/authentication.rb +36 -28
- data/lib/issue_db/cache.rb +27 -25
- data/lib/issue_db/database.rb +191 -189
- data/lib/issue_db/models/record.rb +25 -23
- data/lib/issue_db/models/repository.rb +15 -13
- data/lib/issue_db/utils/generate.rb +38 -36
- data/lib/issue_db/utils/github.rb +282 -280
- data/lib/issue_db/utils/init.rb +19 -17
- data/lib/issue_db/utils/parse.rb +33 -31
- data/lib/issue_db.rb +59 -47
- data/lib/version.rb +4 -2
- metadata +1 -1
@@ -7,7 +7,7 @@
|
|
7
7
|
# Why? In some cases, you may not want to have a static long lived token like a GitHub PAT when authenticating...
|
8
8
|
# with octokit.rb.
|
9
9
|
# Most importantly, this class will handle automatic token refreshing, retries, and rate limiting for you out-of-the-box.
|
10
|
-
# Simply provide the correct environment variables, call `GitHub.new`, and then use the returned object as you would an Octokit client.
|
10
|
+
# Simply provide the correct environment variables, call `IssueDB::Utils::GitHub.new`, and then use the returned object as you would an Octokit client.
|
11
11
|
|
12
12
|
# Note: Environment variables have the `GH_` prefix because in GitHub Actions, you cannot use `GITHUB_` for secrets
|
13
13
|
|
@@ -15,330 +15,332 @@ require "octokit"
|
|
15
15
|
require "jwt"
|
16
16
|
require "redacting_logger"
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
module IssueDB
|
19
|
+
module Utils
|
20
|
+
class GitHub
|
21
|
+
TOKEN_EXPIRATION_TIME = 2700 # 45 minutes
|
22
|
+
JWT_EXPIRATION_TIME = 600 # 10 minutes
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
+
def initialize(log: nil, app_id: nil, installation_id: nil, app_key: nil, app_algo: nil)
|
25
|
+
@log = log || create_default_logger
|
24
26
|
|
25
|
-
|
26
|
-
|
27
|
+
# app ids are found on the App's settings page
|
28
|
+
@app_id = app_id || fetch_env_var("GH_APP_ID").to_i
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
-
|
30
|
+
# installation ids look like this:
|
31
|
+
# https://github.com/organizations/<org>/settings/installations/<8_digit_id>
|
32
|
+
@installation_id = installation_id || fetch_env_var("GH_APP_INSTALLATION_ID").to_i
|
31
33
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
34
|
+
# app keys are found on the App's settings page and can be downloaded
|
35
|
+
# format: "-----BEGIN...key\n...END-----\n"
|
36
|
+
# make sure this key in your env is a single line string with newlines as "\n"
|
37
|
+
@app_key = resolve_app_key(app_key)
|
36
38
|
|
37
|
-
|
39
|
+
@app_algo = app_algo || ENV.fetch("GH_APP_ALGO", "RS256")
|
38
40
|
|
39
|
-
|
40
|
-
|
41
|
-
|
41
|
+
@client = nil
|
42
|
+
@token_refresh_time = nil
|
43
|
+
@rate_limit_all = nil
|
42
44
|
|
43
|
-
|
44
|
-
|
45
|
+
setup_retry_config!
|
46
|
+
end
|
45
47
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
48
|
+
# A helper method to check the client's current rate limit status before making a request
|
49
|
+
# NOTE: This method will sleep for the remaining time until the rate limit resets if the rate limit is hit
|
50
|
+
# :param: type [Symbol] the type of rate limit to check (core, search, graphql, etc) - default: :core
|
51
|
+
# :return: nil (nothing) - this method will block until the rate limit is reset for the given type
|
52
|
+
def wait_for_rate_limit!(type = :core)
|
53
|
+
@log.debug("checking rate limit status for type: #{type}")
|
54
|
+
# make a request to get the comprehensive rate limit status
|
55
|
+
# note: checking the rate limit status does not count against the rate limit in any way
|
56
|
+
fetch_rate_limit if @rate_limit_all.nil?
|
57
|
+
|
58
|
+
details = rate_limit_details(type)
|
59
|
+
rate_limit = details[:rate_limit]
|
60
|
+
resets_at = details[:resets_at]
|
61
|
+
|
62
|
+
@log.debug(
|
63
|
+
"rate_limit remaining: #{rate_limit[:remaining]} - " \
|
64
|
+
"used: #{rate_limit[:used]} - " \
|
65
|
+
"resets_at: #{resets_at} - " \
|
66
|
+
"current time: #{Time.now}"
|
67
|
+
)
|
68
|
+
|
69
|
+
# exit early if the rate limit is not hit (we have remaining requests)
|
70
|
+
unless rate_limit[:remaining].zero?
|
71
|
+
update_rate_limit(type)
|
72
|
+
return
|
73
|
+
end
|
72
74
|
|
73
|
-
|
74
|
-
|
75
|
-
|
75
|
+
# if we make it here, we (probably) have hit the rate limit
|
76
|
+
# fetch the rate limit again if we are at zero or if the rate limit reset time is in the past
|
77
|
+
fetch_rate_limit if rate_limit[:remaining].zero? || rate_limit[:remaining] < 0 || resets_at < Time.now
|
76
78
|
|
77
|
-
|
78
|
-
|
79
|
-
|
79
|
+
details = rate_limit_details(type)
|
80
|
+
rate_limit = details[:rate_limit]
|
81
|
+
resets_at = details[:resets_at]
|
80
82
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
83
|
+
# exit early if the rate limit is not actually hit (we have remaining requests)
|
84
|
+
unless rate_limit[:remaining].zero?
|
85
|
+
@log.debug("rate_limit not hit - remaining: #{rate_limit[:remaining]}")
|
86
|
+
update_rate_limit(type)
|
87
|
+
return
|
88
|
+
end
|
87
89
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
90
|
+
# calculate the sleep duration - ex: reset time - current time
|
91
|
+
sleep_duration = resets_at - Time.now
|
92
|
+
@log.debug("sleep_duration: #{sleep_duration}")
|
93
|
+
sleep_duration = [sleep_duration, 0].max # ensure sleep duration is not negative
|
94
|
+
sleep_duration_and_a_little_more = sleep_duration.ceil + 2 # sleep a little more than the rate limit reset time
|
93
95
|
|
94
|
-
|
95
|
-
|
96
|
-
|
96
|
+
# log the sleep duration and begin the blocking sleep call
|
97
|
+
@log.info("github rate_limit hit: sleeping for: #{sleep_duration_and_a_little_more} seconds")
|
98
|
+
sleep(sleep_duration_and_a_little_more)
|
97
99
|
|
98
|
-
|
99
|
-
|
100
|
+
@log.info("github rate_limit sleep complete - Time.now: #{Time.now}")
|
101
|
+
end
|
100
102
|
|
101
|
-
|
103
|
+
private
|
102
104
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
105
|
+
# Creates a default logger if none is provided
|
106
|
+
# @return [RedactingLogger] A new logger instance
|
107
|
+
def create_default_logger
|
108
|
+
RedactingLogger.new($stdout, level: ENV.fetch("GH_APP_LOG_LEVEL", "INFO").upcase)
|
109
|
+
end
|
108
110
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
111
|
+
# Sets up retry configuration for handling API errors
|
112
|
+
# Should the number of retries be reached without success, the last exception will be raised
|
113
|
+
def setup_retry_config!
|
114
|
+
@retry_sleep = ENV.fetch("GH_APP_SLEEP", 3).to_i
|
115
|
+
@retry_tries = ENV.fetch("GH_APP_RETRIES", 10).to_i
|
116
|
+
@retry_exponential_backoff = ENV.fetch("GH_APP_EXPONENTIAL_BACKOFF", "false").downcase == "true"
|
117
|
+
end
|
116
118
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
119
|
+
# Custom retry logic with optional exponential backoff and logging
|
120
|
+
# @param retries [Integer] Number of retries to attempt
|
121
|
+
# @param sleep_time [Integer] Base sleep time between retries
|
122
|
+
# @param block [Proc] The block to execute with retry logic
|
123
|
+
# @return [Object] The result of the block execution
|
124
|
+
# When exponential backoff is enabled (default is disabled):
|
125
|
+
# 1st retry: 3 seconds
|
126
|
+
# 2nd retry: 6 seconds
|
127
|
+
# 3rd retry: 12 seconds
|
128
|
+
# 4th retry: 24 seconds
|
129
|
+
# When exponential backoff is disabled:
|
130
|
+
# All retries: 3 seconds (fixed rate)
|
131
|
+
def retry_request(retries: @retry_tries, sleep_time: @retry_sleep, &block)
|
132
|
+
attempt = 0
|
133
|
+
begin
|
134
|
+
attempt += 1
|
135
|
+
yield
|
136
|
+
rescue StandardError => e
|
137
|
+
if attempt < retries
|
138
|
+
if @retry_exponential_backoff
|
139
|
+
backoff_time = sleep_time * (2**(attempt - 1)) # Exponential backoff
|
140
|
+
else
|
141
|
+
backoff_time = sleep_time # Fixed rate
|
142
|
+
end
|
143
|
+
@log.debug("[retry ##{attempt}] #{e.class}: #{e.message} - sleeping #{backoff_time}s before retry")
|
144
|
+
sleep(backoff_time)
|
145
|
+
retry
|
146
|
+
else
|
147
|
+
@log.debug("[retry ##{attempt}] #{e.class}: #{e.message} - max retries exceeded")
|
148
|
+
raise e
|
149
|
+
end
|
140
150
|
end
|
141
|
-
@log.debug("[retry ##{attempt}] #{e.class}: #{e.message} - sleeping #{backoff_time}s before retry")
|
142
|
-
sleep(backoff_time)
|
143
|
-
retry
|
144
|
-
else
|
145
|
-
@log.debug("[retry ##{attempt}] #{e.class}: #{e.message} - max retries exceeded")
|
146
|
-
raise e
|
147
151
|
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
def fetch_rate_limit
|
152
|
-
@rate_limit_all = retry_request do
|
153
|
-
client.get("rate_limit")
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
# Update the in-memory "cached" rate limit value for the given rate limit type
|
158
|
-
def update_rate_limit(type)
|
159
|
-
@rate_limit_all[:resources][type][:remaining] -= 1
|
160
|
-
end
|
161
|
-
|
162
|
-
def rate_limit_details(type)
|
163
|
-
# fetch the provided rate limit type
|
164
|
-
# rate_limit resulting structure: {:limit=>5000, :used=>15, :remaining=>4985, :reset=>1713897293}
|
165
|
-
rate_limit = @rate_limit_all[:resources][type]
|
166
152
|
|
167
|
-
|
168
|
-
|
153
|
+
def fetch_rate_limit
|
154
|
+
@rate_limit_all = retry_request do
|
155
|
+
client.get("rate_limit")
|
156
|
+
end
|
157
|
+
end
|
169
158
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
end
|
159
|
+
# Update the in-memory "cached" rate limit value for the given rate limit type
|
160
|
+
def update_rate_limit(type)
|
161
|
+
@rate_limit_all[:resources][type][:remaining] -= 1
|
162
|
+
end
|
175
163
|
|
176
|
-
|
164
|
+
def rate_limit_details(type)
|
165
|
+
# fetch the provided rate limit type
|
166
|
+
# rate_limit resulting structure: {:limit=>5000, :used=>15, :remaining=>4985, :reset=>1713897293}
|
167
|
+
rate_limit = @rate_limit_all[:resources][type]
|
177
168
|
|
178
|
-
|
179
|
-
|
180
|
-
# @return [String] The value of the environment variable.
|
181
|
-
def fetch_env_var(key)
|
182
|
-
ENV.fetch(key) { raise "environment variable #{key} is not set" }
|
183
|
-
end
|
169
|
+
# calculate the time the rate limit will reset
|
170
|
+
resets_at = Time.at(rate_limit[:reset]).utc
|
184
171
|
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
if app_key
|
191
|
-
# Check if it's a file path (ends with .pem)
|
192
|
-
if app_key.end_with?(".pem")
|
193
|
-
unless File.exist?(app_key)
|
194
|
-
raise "App key file not found: #{app_key}"
|
195
|
-
end
|
172
|
+
return {
|
173
|
+
rate_limit: rate_limit,
|
174
|
+
resets_at: resets_at,
|
175
|
+
}
|
176
|
+
end
|
196
177
|
|
197
|
-
|
198
|
-
|
178
|
+
# Fetches the value of an environment variable and raises an error if it is not set.
|
179
|
+
# @param key [String] The name of the environment variable.
|
180
|
+
# @return [String] The value of the environment variable.
|
181
|
+
def fetch_env_var(key)
|
182
|
+
ENV.fetch(key) { raise "environment variable #{key} is not set" }
|
183
|
+
end
|
199
184
|
|
200
|
-
|
201
|
-
|
185
|
+
# Resolves the app key from various sources
|
186
|
+
# @param app_key [String, nil] The app key parameter
|
187
|
+
# @return [String] The resolved app key content
|
188
|
+
def resolve_app_key(app_key)
|
189
|
+
# If app_key is provided as a parameter
|
190
|
+
if app_key
|
191
|
+
# Check if it's a file path (ends with .pem)
|
192
|
+
if app_key.end_with?(".pem")
|
193
|
+
unless File.exist?(app_key)
|
194
|
+
raise "App key file not found: #{app_key}"
|
195
|
+
end
|
196
|
+
|
197
|
+
@log.debug("Loading app key from file: #{app_key}")
|
198
|
+
key_content = File.read(app_key)
|
199
|
+
|
200
|
+
if key_content.strip.empty?
|
201
|
+
raise "App key file is empty: #{app_key}"
|
202
|
+
end
|
203
|
+
|
204
|
+
@log.debug("Successfully loaded app key from file (#{key_content.length} characters)")
|
205
|
+
return key_content
|
206
|
+
else
|
207
|
+
# It's a key string, process escape sequences
|
208
|
+
@log.debug("Using provided app key string")
|
209
|
+
return normalize_key_string(app_key)
|
210
|
+
end
|
202
211
|
end
|
203
212
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
@log.debug("Using provided app key string")
|
209
|
-
return normalize_key_string(app_key)
|
213
|
+
# Fall back to environment variable
|
214
|
+
@log.debug("Loading app key from environment variable")
|
215
|
+
env_key = fetch_env_var("GH_APP_KEY")
|
216
|
+
normalize_key_string(env_key)
|
210
217
|
end
|
211
|
-
end
|
212
|
-
|
213
|
-
# Fall back to environment variable
|
214
|
-
@log.debug("Loading app key from environment variable")
|
215
|
-
env_key = fetch_env_var("GH_APP_KEY")
|
216
|
-
normalize_key_string(env_key)
|
217
|
-
end
|
218
218
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
# Caches the octokit client if it is not nil and the token has not expired
|
229
|
-
# If it is nil or the token has expired, it creates a new client
|
230
|
-
# @return [Octokit::Client] The octokit client
|
231
|
-
def client
|
232
|
-
if @client.nil? || token_expired?
|
233
|
-
@client = create_client
|
234
|
-
end
|
219
|
+
# Normalizes escape sequences in key strings safely
|
220
|
+
# @param key_string [String] The key string to normalize
|
221
|
+
# @return [String] The normalized key string
|
222
|
+
def normalize_key_string(key_string)
|
223
|
+
# Use simple string replacement to avoid ReDoS vulnerability
|
224
|
+
# This handles both single \n and multiple consecutive \\n sequences
|
225
|
+
key_string.gsub('\\n', "\n")
|
226
|
+
end
|
235
227
|
|
236
|
-
|
237
|
-
|
228
|
+
# Caches the octokit client if it is not nil and the token has not expired
|
229
|
+
# If it is nil or the token has expired, it creates a new client
|
230
|
+
# @return [Octokit::Client] The octokit client
|
231
|
+
def client
|
232
|
+
if @client.nil? || token_expired?
|
233
|
+
@client = create_client
|
234
|
+
end
|
238
235
|
|
239
|
-
|
240
|
-
|
241
|
-
def jwt_token
|
242
|
-
private_key = OpenSSL::PKey::RSA.new(@app_key)
|
236
|
+
@client
|
237
|
+
end
|
243
238
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
end
|
239
|
+
# A helper method for generating a JWT token for the GitHub App
|
240
|
+
# @return [String] The JWT token
|
241
|
+
def jwt_token
|
242
|
+
private_key = OpenSSL::PKey::RSA.new(@app_key)
|
249
243
|
|
250
|
-
|
251
|
-
|
244
|
+
payload = {}.tap do |opts|
|
245
|
+
opts[:iat] = Time.now.to_i - 60 # issued at time, 60 seconds in the past to allow for clock drift
|
246
|
+
opts[:exp] = opts[:iat] + JWT_EXPIRATION_TIME # JWT expiration time (10 minute maximum)
|
247
|
+
opts[:iss] = @app_id # GitHub App ID
|
248
|
+
end
|
252
249
|
|
253
|
-
|
254
|
-
|
255
|
-
def create_client
|
256
|
-
client = ::Octokit::Client.new(bearer_token: jwt_token)
|
257
|
-
access_token = client.create_app_installation_access_token(@installation_id)[:token]
|
258
|
-
client = ::Octokit::Client.new(access_token:)
|
259
|
-
client.auto_paginate = true
|
260
|
-
client.per_page = 100
|
261
|
-
@token_refresh_time = Time.now
|
262
|
-
client
|
263
|
-
end
|
250
|
+
JWT.encode(payload, private_key, @app_algo)
|
251
|
+
end
|
264
252
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
253
|
+
# Creates a new octokit client and fetches a new installation access token
|
254
|
+
# @return [Octokit::Client] The octokit client
|
255
|
+
def create_client
|
256
|
+
client = ::Octokit::Client.new(bearer_token: jwt_token)
|
257
|
+
access_token = client.create_app_installation_access_token(@installation_id)[:token]
|
258
|
+
client = ::Octokit::Client.new(access_token:)
|
259
|
+
client.auto_paginate = true
|
260
|
+
client.per_page = 100
|
261
|
+
@token_refresh_time = Time.now
|
262
|
+
client
|
263
|
+
end
|
272
264
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
def method_missing(method, *args, **kwargs, &block)
|
280
|
-
# Check if retry is explicitly disabled for this call
|
281
|
-
disable_retry = kwargs.delete(:disable_retry) || false
|
282
|
-
|
283
|
-
# Determine the rate limit type based on the method name and arguments
|
284
|
-
rate_limit_type = case method.to_s
|
285
|
-
when /search_/
|
286
|
-
:search
|
287
|
-
when /graphql/
|
288
|
-
# :nocov:
|
289
|
-
:graphql # I don't actually know of any endpoints that match this method sig yet
|
290
|
-
# :nocov:
|
291
|
-
else
|
292
|
-
# Check if this is a GraphQL call via POST
|
293
|
-
if method.to_s == "post" && args.first&.include?("/graphql")
|
294
|
-
:graphql
|
295
|
-
else
|
296
|
-
:core
|
297
|
-
end
|
298
|
-
end
|
299
|
-
|
300
|
-
# Handle special case for search_issues which can hit secondary rate limits
|
301
|
-
if method.to_s == "search_issues"
|
302
|
-
request_proc = proc do
|
303
|
-
wait_for_rate_limit!(rate_limit_type)
|
304
|
-
client.send(method, *args, **kwargs, &block) # rubocop:disable GitHub/AvoidObjectSendWithDynamicMethod
|
265
|
+
# GitHub App installation access tokens expire after 1h
|
266
|
+
# This method checks if the token has expired and returns true if it has
|
267
|
+
# It is very cautious and expires tokens at 45 minutes to account for clock drift
|
268
|
+
# @return [Boolean] True if the token has expired, false otherwise
|
269
|
+
def token_expired?
|
270
|
+
@token_refresh_time.nil? || (Time.now - @token_refresh_time) > TOKEN_EXPIRATION_TIME
|
305
271
|
end
|
306
272
|
|
307
|
-
|
308
|
-
|
309
|
-
|
273
|
+
# This method is called when a method is called on the GitHub class that does not exist.
|
274
|
+
# It delegates the method call to the Octokit client with built-in retry logic and rate limiting.
|
275
|
+
# @param method [Symbol] The name of the method being called.
|
276
|
+
# @param args [Array] The arguments passed to the method.
|
277
|
+
# @param block [Proc] An optional block passed to the method.
|
278
|
+
# @return [Object] The result of the method call on the Octokit client.
|
279
|
+
def method_missing(method, *args, **kwargs, &block)
|
280
|
+
# Check if retry is explicitly disabled for this call
|
281
|
+
disable_retry = kwargs.delete(:disable_retry) || false
|
282
|
+
|
283
|
+
# Determine the rate limit type based on the method name and arguments
|
284
|
+
rate_limit_type = case method.to_s
|
285
|
+
when /search_/
|
286
|
+
:search
|
287
|
+
when /graphql/
|
288
|
+
# :nocov:
|
289
|
+
:graphql # I don't actually know of any endpoints that match this method sig yet
|
290
|
+
# :nocov:
|
291
|
+
else
|
292
|
+
# Check if this is a GraphQL call via POST
|
293
|
+
if method.to_s == "post" && args.first&.include?("/graphql")
|
294
|
+
:graphql
|
295
|
+
else
|
296
|
+
:core
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# Handle special case for search_issues which can hit secondary rate limits
|
301
|
+
if method.to_s == "search_issues"
|
302
|
+
request_proc = proc do
|
303
|
+
wait_for_rate_limit!(rate_limit_type)
|
304
|
+
client.send(method, *args, **kwargs, &block) # rubocop:disable GitHub/AvoidObjectSendWithDynamicMethod
|
305
|
+
end
|
306
|
+
|
307
|
+
begin
|
308
|
+
if disable_retry
|
309
|
+
request_proc.call
|
310
|
+
else
|
311
|
+
retry_request(&request_proc)
|
312
|
+
end
|
313
|
+
rescue StandardError => e
|
314
|
+
# re-raise the error but if its a secondary rate limit error, just sleep for a minute
|
315
|
+
if e.message.include?("exceeded a secondary rate limit")
|
316
|
+
@log.warn("GitHub secondary rate limit hit, sleeping for 60 seconds")
|
317
|
+
sleep(60)
|
318
|
+
end
|
319
|
+
raise e
|
320
|
+
end
|
310
321
|
else
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
322
|
+
# For all other methods, use standard retry and rate limiting
|
323
|
+
request_proc = proc do
|
324
|
+
wait_for_rate_limit!(rate_limit_type)
|
325
|
+
client.send(method, *args, **kwargs, &block) # rubocop:disable GitHub/AvoidObjectSendWithDynamicMethod
|
326
|
+
end
|
327
|
+
|
328
|
+
if disable_retry
|
329
|
+
request_proc.call
|
330
|
+
else
|
331
|
+
retry_request(&request_proc)
|
332
|
+
end
|
318
333
|
end
|
319
|
-
raise e
|
320
|
-
end
|
321
|
-
else
|
322
|
-
# For all other methods, use standard retry and rate limiting
|
323
|
-
request_proc = proc do
|
324
|
-
wait_for_rate_limit!(rate_limit_type)
|
325
|
-
client.send(method, *args, **kwargs, &block) # rubocop:disable GitHub/AvoidObjectSendWithDynamicMethod
|
326
334
|
end
|
327
335
|
|
328
|
-
if
|
329
|
-
|
330
|
-
|
331
|
-
|
336
|
+
# This method is called to check if the GitHub class responds to a method.
|
337
|
+
# It checks if the Octokit client responds to the method.
|
338
|
+
# @param method [Symbol] The name of the method being checked.
|
339
|
+
# @param include_private [Boolean] Whether to include private methods in the check.
|
340
|
+
# @return [Boolean] True if the Octokit client responds to the method, false otherwise.
|
341
|
+
def respond_to_missing?(method, include_private = false)
|
342
|
+
client.respond_to?(method, include_private) || super
|
332
343
|
end
|
333
344
|
end
|
334
345
|
end
|
335
|
-
|
336
|
-
# This method is called to check if the GitHub class responds to a method.
|
337
|
-
# It checks if the Octokit client responds to the method.
|
338
|
-
# @param method [Symbol] The name of the method being checked.
|
339
|
-
# @param include_private [Boolean] Whether to include private methods in the check.
|
340
|
-
# @return [Boolean] True if the Octokit client responds to the method, false otherwise.
|
341
|
-
def respond_to_missing?(method, include_private = false)
|
342
|
-
client.respond_to?(method, include_private) || super
|
343
|
-
end
|
344
346
|
end
|