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.
@@ -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
- class GitHub
19
- TOKEN_EXPIRATION_TIME = 2700 # 45 minutes
20
- JWT_EXPIRATION_TIME = 600 # 10 minutes
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
- def initialize(log: nil, app_id: nil, installation_id: nil, app_key: nil, app_algo: nil)
23
- @log = log || create_default_logger
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
- # app ids are found on the App's settings page
26
- @app_id = app_id || fetch_env_var("GH_APP_ID").to_i
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
- # installation ids look like this:
29
- # https://github.com/organizations/<org>/settings/installations/<8_digit_id>
30
- @installation_id = installation_id || fetch_env_var("GH_APP_INSTALLATION_ID").to_i
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
- # app keys are found on the App's settings page and can be downloaded
33
- # format: "-----BEGIN...key\n...END-----\n"
34
- # make sure this key in your env is a single line string with newlines as "\n"
35
- @app_key = resolve_app_key(app_key)
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
- @app_algo = app_algo || ENV.fetch("GH_APP_ALGO", "RS256")
39
+ @app_algo = app_algo || ENV.fetch("GH_APP_ALGO", "RS256")
38
40
 
39
- @client = nil
40
- @token_refresh_time = nil
41
- @rate_limit_all = nil
41
+ @client = nil
42
+ @token_refresh_time = nil
43
+ @rate_limit_all = nil
42
44
 
43
- setup_retry_config!
44
- end
45
+ setup_retry_config!
46
+ end
45
47
 
46
- # A helper method to check the client's current rate limit status before making a request
47
- # NOTE: This method will sleep for the remaining time until the rate limit resets if the rate limit is hit
48
- # :param: type [Symbol] the type of rate limit to check (core, search, graphql, etc) - default: :core
49
- # :return: nil (nothing) - this method will block until the rate limit is reset for the given type
50
- def wait_for_rate_limit!(type = :core)
51
- @log.debug("checking rate limit status for type: #{type}")
52
- # make a request to get the comprehensive rate limit status
53
- # note: checking the rate limit status does not count against the rate limit in any way
54
- fetch_rate_limit if @rate_limit_all.nil?
55
-
56
- details = rate_limit_details(type)
57
- rate_limit = details[:rate_limit]
58
- resets_at = details[:resets_at]
59
-
60
- @log.debug(
61
- "rate_limit remaining: #{rate_limit[:remaining]} - " \
62
- "used: #{rate_limit[:used]} - " \
63
- "resets_at: #{resets_at} - " \
64
- "current time: #{Time.now}"
65
- )
66
-
67
- # exit early if the rate limit is not hit (we have remaining requests)
68
- unless rate_limit[:remaining].zero?
69
- update_rate_limit(type)
70
- return
71
- end
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
- # if we make it here, we (probably) have hit the rate limit
74
- # fetch the rate limit again if we are at zero or if the rate limit reset time is in the past
75
- fetch_rate_limit if rate_limit[:remaining].zero? || rate_limit[:remaining] < 0 || resets_at < Time.now
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
- details = rate_limit_details(type)
78
- rate_limit = details[:rate_limit]
79
- resets_at = details[:resets_at]
79
+ details = rate_limit_details(type)
80
+ rate_limit = details[:rate_limit]
81
+ resets_at = details[:resets_at]
80
82
 
81
- # exit early if the rate limit is not actually hit (we have remaining requests)
82
- unless rate_limit[:remaining].zero?
83
- @log.debug("rate_limit not hit - remaining: #{rate_limit[:remaining]}")
84
- update_rate_limit(type)
85
- return
86
- end
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
- # calculate the sleep duration - ex: reset time - current time
89
- sleep_duration = resets_at - Time.now
90
- @log.debug("sleep_duration: #{sleep_duration}")
91
- sleep_duration = [sleep_duration, 0].max # ensure sleep duration is not negative
92
- sleep_duration_and_a_little_more = sleep_duration.ceil + 2 # sleep a little more than the rate limit reset time
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
- # log the sleep duration and begin the blocking sleep call
95
- @log.info("github rate_limit hit: sleeping for: #{sleep_duration_and_a_little_more} seconds")
96
- sleep(sleep_duration_and_a_little_more)
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
- @log.info("github rate_limit sleep complete - Time.now: #{Time.now}")
99
- end
100
+ @log.info("github rate_limit sleep complete - Time.now: #{Time.now}")
101
+ end
100
102
 
101
- private
103
+ private
102
104
 
103
- # Creates a default logger if none is provided
104
- # @return [RedactingLogger] A new logger instance
105
- def create_default_logger
106
- RedactingLogger.new($stdout, level: ENV.fetch("GH_APP_LOG_LEVEL", "INFO").upcase)
107
- end
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
- # Sets up retry configuration for handling API errors
110
- # Should the number of retries be reached without success, the last exception will be raised
111
- def setup_retry_config!
112
- @retry_sleep = ENV.fetch("GH_APP_SLEEP", 3).to_i
113
- @retry_tries = ENV.fetch("GH_APP_RETRIES", 10).to_i
114
- @retry_exponential_backoff = ENV.fetch("GH_APP_EXPONENTIAL_BACKOFF", "false").downcase == "true"
115
- end
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
- # Custom retry logic with optional exponential backoff and logging
118
- # @param retries [Integer] Number of retries to attempt
119
- # @param sleep_time [Integer] Base sleep time between retries
120
- # @param block [Proc] The block to execute with retry logic
121
- # @return [Object] The result of the block execution
122
- # When exponential backoff is enabled (default is disabled):
123
- # 1st retry: 3 seconds
124
- # 2nd retry: 6 seconds
125
- # 3rd retry: 12 seconds
126
- # 4th retry: 24 seconds
127
- # When exponential backoff is disabled:
128
- # All retries: 3 seconds (fixed rate)
129
- def retry_request(retries: @retry_tries, sleep_time: @retry_sleep, &block)
130
- attempt = 0
131
- begin
132
- attempt += 1
133
- yield
134
- rescue StandardError => e
135
- if attempt < retries
136
- if @retry_exponential_backoff
137
- backoff_time = sleep_time * (2**(attempt - 1)) # Exponential backoff
138
- else
139
- backoff_time = sleep_time # Fixed rate
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
- # calculate the time the rate limit will reset
168
- resets_at = Time.at(rate_limit[:reset]).utc
153
+ def fetch_rate_limit
154
+ @rate_limit_all = retry_request do
155
+ client.get("rate_limit")
156
+ end
157
+ end
169
158
 
170
- return {
171
- rate_limit: rate_limit,
172
- resets_at: resets_at,
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
- private
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
- # 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
169
+ # calculate the time the rate limit will reset
170
+ resets_at = Time.at(rate_limit[:reset]).utc
184
171
 
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
172
+ return {
173
+ rate_limit: rate_limit,
174
+ resets_at: resets_at,
175
+ }
176
+ end
196
177
 
197
- @log.debug("Loading app key from file: #{app_key}")
198
- key_content = File.read(app_key)
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
- if key_content.strip.empty?
201
- raise "App key file is empty: #{app_key}"
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
- @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)
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
- # 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
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
- @client
237
- end
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
- # 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)
236
+ @client
237
+ end
243
238
 
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
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
- JWT.encode(payload, private_key, @app_algo)
251
- end
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
- # 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
250
+ JWT.encode(payload, private_key, @app_algo)
251
+ end
264
252
 
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
271
- end
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
- # 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
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
- begin
308
- if disable_retry
309
- request_proc.call
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
- 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)
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 disable_retry
329
- request_proc.call
330
- else
331
- retry_request(&request_proc)
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