entitlements-github-plugin 1.1.0 → 1.2.1

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: 684563b348978cba23282a6ea80a590b5a7335ec3ebf0278abb6a7a7b891c8ce
4
- data.tar.gz: 360ddce764d1e3077216e066a3bdaba44a37e8df61c817f9df0eb887fcb3beca
3
+ metadata.gz: 63c03f0243b0e1b51bcf58b214a4cf70bc856522209b07f721d4e6b5c219dbe1
4
+ data.tar.gz: 1fc7c61f4ca66cc75a4c212f4f0a40a21b6e5c7e4011dabef46bc6293d4220e0
5
5
  SHA512:
6
- metadata.gz: c5d0ed6468aff36d7ca1047218273dc86173063896eaf4cbf88d47189c6daeb73e3eac8f84c6ecabc82b4293079bc06b0f6edb146a6168043cfe0629ecb5c014
7
- data.tar.gz: a79d07768d11a62362b6dfe55a897805d763c4b493475b8e1607a211b0c82066aa284effb191554cabaaaf8b7ba2c94fc1d6712d706afba4bbbab98ff5cc9ddb
6
+ metadata.gz: b4344c74a359dc828711ceaea05cf083fc699218a43af046a8fddcd98fc8fe7651dc71e108c2d6f9abc9b5f1aa9f9b7a7b2f70d525927f1b3a179fca2f4e5d5c
7
+ data.tar.gz: ca57d0790b548d6039b48163d38753cacbfd80d86fef7888835f0c70426273188d7533c2e04ce9ecd370ec5553996beb7613aba5fa2788d9ce6ffbde623b5285
@@ -46,7 +46,9 @@ module Entitlements
46
46
  Entitlements.logger.debug "#{identifier} add_user_to_organization(user=#{user}, org=#{org}, role=#{role})"
47
47
 
48
48
  begin
49
- new_membership = octokit.update_organization_membership(org, user:, role:)
49
+ new_membership = Retryable.with_context(:default, not: [Octokit::NotFound]) do
50
+ octokit.update_organization_membership(org, user:, role:)
51
+ end
50
52
  rescue Octokit::NotFound => e
51
53
  raise e unless ignore_not_found
52
54
 
@@ -78,7 +80,10 @@ module Entitlements
78
80
  Contract String => C::Bool
79
81
  def remove_user_from_organization(user)
80
82
  Entitlements.logger.debug "#{identifier} remove_user_from_organization(user=#{user}, org=#{org})"
81
- result = octokit.remove_organization_membership(org, user:)
83
+
84
+ result = Retryable.with_context(:default) do
85
+ octokit.remove_organization_membership(org, user:)
86
+ end
82
87
 
83
88
  # If we removed the user, remove them from the cache of members, so that any GitHub team
84
89
  # operations in this organization will ignore this user.
@@ -24,3 +24,5 @@ end
24
24
  require_relative "github_org/controller"
25
25
  require_relative "github_org/provider"
26
26
  require_relative "github_org/service"
27
+ require_relative "../config/retry"
28
+ Retry.setup!
@@ -162,7 +162,7 @@ module Entitlements
162
162
  metadata = entitlement_group.metadata
163
163
  metadata["team_id"] = -999
164
164
  rescue Entitlements::Models::Group::NoMetadata
165
- metadata = {"team_id" => -999}
165
+ metadata = { "team_id" => -999 }
166
166
  end
167
167
  Entitlements::Backend::GitHubTeam::Models::Team.new(
168
168
  team_id: -999,
@@ -279,10 +279,12 @@ module Entitlements
279
279
  team_options[:parent_team_id] = parent_team_data[:team_id]
280
280
  rescue TeamNotFound
281
281
  # if the parent team does not exist, create it (think `mkdir -p` logic here)
282
- result = octokit.create_team(
283
- org,
284
- { name: entitlement_metadata["parent_team_name"], repo_names: [], privacy: "closed" }
285
- )
282
+ result = Retryable.with_context(:default, not: [Octokit::UnprocessableEntity]) do
283
+ octokit.create_team(
284
+ org,
285
+ { name: entitlement_metadata["parent_team_name"], repo_names: [], privacy: "closed" }
286
+ )
287
+ end
286
288
 
287
289
  Entitlements.logger.debug "created parent team #{entitlement_metadata["parent_team_name"]} with id #{result[:id]}"
288
290
 
@@ -296,7 +298,11 @@ module Entitlements
296
298
  end
297
299
 
298
300
  Entitlements.logger.debug "create_team(team=#{team_name})"
299
- result = octokit.create_team(org, team_options)
301
+
302
+ result = Retryable.with_context(:default, not: [Octokit::UnprocessableEntity]) do
303
+ octokit.create_team(org, team_options)
304
+ end
305
+
300
306
  Entitlements.logger.debug "created team #{team_name} with id #{result[:id]}"
301
307
  true
302
308
  rescue Octokit::UnprocessableEntity => e
@@ -317,7 +323,10 @@ module Entitlements
317
323
  Entitlements.logger.debug "update_team(team=#{team.team_name})"
318
324
  options = { name: team.team_name, repo_names: [], privacy: "closed",
319
325
  parent_team_id: metadata[:parent_team_id] }
320
- octokit.update_team(team.team_id, options)
326
+ Retryable.with_context(:default, not: [Octokit::UnprocessableEntity]) do
327
+ octokit.update_team(team.team_id, options)
328
+ end
329
+
321
330
  true
322
331
  rescue Octokit::UnprocessableEntity => e
323
332
  Entitlements.logger.debug "update_team(team=#{team.team_name}) ERROR - #{e.message}"
@@ -334,7 +343,9 @@ module Entitlements
334
343
  team_name: String
335
344
  ] => Sawyer::Resource
336
345
  def team_by_name(org_name:, team_name:)
337
- octokit.team_by_name(org_name, team_name)
346
+ Retryable.with_context(:default) do
347
+ octokit.team_by_name(org_name, team_name)
348
+ end
338
349
  end
339
350
 
340
351
  private
@@ -426,7 +437,10 @@ module Entitlements
426
437
  @validation_cache ||= {}
427
438
  @validation_cache[team_id] ||= begin
428
439
  Entitlements.logger.debug "validate_team_id_and_slug!(#{team_id}, #{team_slug.inspect})"
429
- team_data = octokit.team(team_id)
440
+ team_data = Retryable.with_context(:default) do
441
+ octokit.team(team_id)
442
+ end
443
+
430
444
  team_data[:slug]
431
445
  end
432
446
  return if @validation_cache[team_id] == team_slug
@@ -457,7 +471,10 @@ module Entitlements
457
471
  validate_team_id_and_slug!(team.team_id, team.team_name)
458
472
 
459
473
  begin
460
- result = octokit.add_team_membership(team.team_id, user, role:)
474
+ result = Retryable.with_context(:default, not: [Octokit::UnprocessableEntity, Octokit::NotFound]) do
475
+ octokit.add_team_membership(team.team_id, user, role:)
476
+ end
477
+
461
478
  result[:state] == "active" || result[:state] == "pending"
462
479
  rescue Octokit::UnprocessableEntity => e
463
480
  raise e unless ignore_not_found && e.message =~ /Enterprise Managed Users must be part of the organization to be assigned to the team/
@@ -487,7 +504,10 @@ module Entitlements
487
504
 
488
505
  Entitlements.logger.debug "#{identifier} remove_user_from_team(user=#{user}, org=#{org}, team_id=#{team.team_id})"
489
506
  validate_team_id_and_slug!(team.team_id, team.team_name)
490
- octokit.remove_team_membership(team.team_id, user)
507
+
508
+ Retryable.with_context(:default) do
509
+ octokit.remove_team_membership(team.team_id, user)
510
+ end
491
511
  end
492
512
  end
493
513
  end
@@ -4,3 +4,5 @@ require_relative "github_team/controller"
4
4
  require_relative "github_team/models/team"
5
5
  require_relative "github_team/provider"
6
6
  require_relative "github_team/service"
7
+ require_relative "../config/retry"
8
+ Retry.setup!
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "retryable"
4
+
5
+ module Retry
6
+ # This method should be called as early as possible in the startup of your application
7
+ # It sets up the Retryable gem with custom contexts and passes through a few options
8
+ # Should the number of retries be reached without success, the last exception will be raised
9
+ def self.setup!
10
+ ######## Retryable Configuration ########
11
+ # All defaults available here:
12
+ # https://github.com/nfedyashev/retryable/blob/6a04027e61607de559e15e48f281f3ccaa9750e8/lib/retryable/configuration.rb#L22-L33
13
+ Retryable.configure do |config|
14
+ config.contexts[:default] = {
15
+ on: [StandardError],
16
+ sleep: 1,
17
+ tries: 3
18
+ }
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../config/retry"
4
+
3
5
  require "net/http"
4
6
  require "octokit"
5
7
  require "uri"
@@ -36,6 +38,9 @@ module Entitlements
36
38
  ignore_not_found: C::Maybe[C::Bool],
37
39
  ] => C::Any
38
40
  def initialize(addr: nil, org:, token:, ou:, ignore_not_found: false)
41
+ # init the retry module
42
+ Retry.setup!
43
+
39
44
  # Save some parameters for the connection but don't actually connect yet.
40
45
  @addr = addr
41
46
  @org = org
@@ -94,7 +99,10 @@ module Entitlements
94
99
  # Returns true if the github instance is an enterprise server instance
95
100
  Contract C::None => C::Bool
96
101
  def enterprise?
97
- meta = octokit.github_meta
102
+ meta = Retryable.with_context(:default) do
103
+ octokit.github_meta
104
+ end
105
+
98
106
  meta.key? :installed_version
99
107
  end
100
108
 
@@ -163,6 +171,7 @@ module Entitlements
163
171
  client = Octokit::Client.new(access_token: token)
164
172
  client.api_endpoint = addr if addr
165
173
  client.auto_paginate = true
174
+ client.per_page = 100
166
175
  Entitlements.logger.debug "Setting up GitHub API connection to #{client.api_endpoint}"
167
176
  client
168
177
  end
@@ -246,11 +255,22 @@ module Entitlements
246
255
  def members_and_roles_from_rest
247
256
  Entitlements.logger.debug "Loading organization members and roles for #{org}"
248
257
  result = {}
249
- members = octokit.organization_members(org, { role: "admin" })
250
- members.each do |member|
258
+
259
+ # fetch all the admin members from the org
260
+ admin_members = Retryable.with_context(:default) do
261
+ octokit.organization_members(org, { role: "admin" })
262
+ end
263
+
264
+ # fetch all the regular members from the org
265
+ regular_members = Retryable.with_context(:default) do
266
+ octokit.organization_members(org, { role: "member" })
267
+ end
268
+
269
+ admin_members.each do |member|
251
270
  result[member[:login].downcase] = "ADMIN"
252
271
  end
253
- octokit.organization_members(org, { role: "member" }).each do |member|
272
+
273
+ regular_members.each do |member|
254
274
  result[member[:login].downcase] = "MEMBER"
255
275
  end
256
276
 
@@ -318,18 +338,30 @@ module Entitlements
318
338
  def graphql_http_post(query)
319
339
  1.upto(MAX_GRAPHQL_RETRIES) do |try_number|
320
340
  result = graphql_http_post_real(query)
321
- if result[:code] < 500
341
+ if !graphql_result_retryable?(result)
322
342
  return result
323
343
  elsif try_number >= MAX_GRAPHQL_RETRIES
324
- Entitlements.logger.error "Query still failing after #{MAX_GRAPHQL_RETRIES} tries. Giving up."
344
+ Entitlements.logger.error "Query still failing after #{MAX_GRAPHQL_RETRIES} tries (last code: #{result[:code]}). Giving up."
325
345
  return result
326
346
  else
327
347
  Entitlements.logger.warn "GraphQL failed on try #{try_number} of #{MAX_GRAPHQL_RETRIES}. Will retry."
328
- sleep WAIT_BETWEEN_GRAPHQL_RETRIES * (2 ** (try_number - 1))
348
+ sleep WAIT_BETWEEN_GRAPHQL_RETRIES * (2**(try_number - 1))
329
349
  end
330
350
  end
331
351
  end
332
352
 
353
+ # Helper: determine whether a result hash from `graphql_http_post_real` represents
354
+ # a transient failure that the retry wrapper will retry. Used by the wrapper itself
355
+ # and to decide log severity inside `graphql_http_post_real`.
356
+ #
357
+ # result - Hash returned by `graphql_http_post_real`.
358
+ #
359
+ # Returns true if the result is retryable (HTTP 5xx or synthetic 5xx), false otherwise.
360
+ Contract ({ code: Integer, data: C::Or[nil, Hash] }) => C::Bool
361
+ def graphql_result_retryable?(result)
362
+ result[:code] >= 500
363
+ end
364
+
333
365
  # Helper method: Do the HTTP POST to the GitHub API for GraphQL.
334
366
  #
335
367
  # query - String with the data to be posted.
@@ -350,23 +382,35 @@ module Entitlements
350
382
  response = http.request(request)
351
383
 
352
384
  if response.code != "200"
353
- Entitlements.logger.error "Got HTTP #{response.code} POSTing to #{uri}"
354
- Entitlements.logger.error response.body
385
+ # The retry wrapper retries on 5xx, so log those at WARN to avoid misleading
386
+ # the operator with an ERROR for a transient failure that we recover from.
387
+ # Terminal non-2xx responses (4xx) stay at ERROR.
388
+ msg = "POST to #{uri} returned HTTP Code #{response.code} and Body: #{response.body}"
389
+ response.code.start_with?("5") ? Entitlements.logger.warn(msg) : Entitlements.logger.error(msg)
355
390
  return { code: response.code.to_i, data: { "body" => response.body } }
356
391
  end
357
392
 
358
393
  begin
359
394
  data = JSON.parse(response.body)
360
395
  if data.key?("errors")
361
- Entitlements.logger.error "Errors reported: #{data['errors'].inspect}"
396
+ # Synthesized 500 below triggers a retry, so log at WARN. Note: some GraphQL
397
+ # `errors` are permanent (bad query, auth, schema). The retry wrapper's final
398
+ # "Giving up" ERROR will surface persistent cases.
399
+ Entitlements.logger.warn "Errors reported: #{data['errors'].inspect}"
362
400
  return { code: 500, data: }
363
401
  end
364
402
  { code: response.code.to_i, data: }
365
403
  rescue JSON::ParserError => e
404
+ # JSON parse errors mean the API returned something we can't interpret. The
405
+ # synthesized 500 below triggers a retry, but the cause is more likely a real
406
+ # protocol/server problem than a transient network blip, so log at ERROR.
366
407
  Entitlements.logger.error "#{e.class} #{e.message}: #{response.body.inspect}"
367
408
  { code: 500, data: { "body" => response.body } }
368
409
  end
369
410
  rescue => e
411
+ # Catch-all for any unexpected exception (network blip OR local code bug).
412
+ # We retry below via the synthesized 500, but log at ERROR because this
413
+ # branch can mask programming errors that operators must see.
370
414
  Entitlements.logger.error "Caught #{e.class} POSTing to #{uri}: #{e.message}"
371
415
  { code: 500, data: nil }
372
416
  end
data/lib/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Entitlements
4
4
  module Version
5
- VERSION = "1.1.0"
5
+ VERSION = "1.2.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: entitlements-github-plugin
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub, Inc. Security Ops
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-14 00:00:00.000000000 Z
11
+ date: 2026-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: contracts
@@ -66,6 +66,26 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '4.25'
69
+ - !ruby/object:Gem::Dependency
70
+ name: retryable
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: 3.0.5
79
+ type: :runtime
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - "~>"
84
+ - !ruby/object:Gem::Version
85
+ version: '3.0'
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 3.0.5
69
89
  - !ruby/object:Gem::Dependency
70
90
  name: entitlements-app
71
91
  requirement: !ruby/object:Gem::Requirement
@@ -106,14 +126,14 @@ dependencies:
106
126
  requirements:
107
127
  - - '='
108
128
  - !ruby/object:Gem::Version
109
- version: 3.13.0
129
+ version: 3.13.2
110
130
  type: :development
111
131
  prerelease: false
112
132
  version_requirements: !ruby/object:Gem::Requirement
113
133
  requirements:
114
134
  - - '='
115
135
  - !ruby/object:Gem::Version
116
- version: 3.13.0
136
+ version: 3.13.2
117
137
  - !ruby/object:Gem::Dependency
118
138
  name: rubocop
119
139
  requirement: !ruby/object:Gem::Requirement
@@ -162,14 +182,14 @@ dependencies:
162
182
  requirements:
163
183
  - - "~>"
164
184
  - !ruby/object:Gem::Version
165
- version: 0.19.1
185
+ version: 0.26.1
166
186
  type: :development
167
187
  prerelease: false
168
188
  version_requirements: !ruby/object:Gem::Requirement
169
189
  requirements:
170
190
  - - "~>"
171
191
  - !ruby/object:Gem::Version
172
- version: 0.19.1
192
+ version: 0.26.1
173
193
  - !ruby/object:Gem::Dependency
174
194
  name: rugged
175
195
  requirement: !ruby/object:Gem::Requirement
@@ -273,13 +293,14 @@ files:
273
293
  - lib/entitlements/backend/github_team/models/team.rb
274
294
  - lib/entitlements/backend/github_team/provider.rb
275
295
  - lib/entitlements/backend/github_team/service.rb
296
+ - lib/entitlements/config/retry.rb
276
297
  - lib/entitlements/service/github.rb
277
298
  - lib/version.rb
278
299
  homepage: https://github.com/github/entitlements-github-plugin
279
300
  licenses:
280
301
  - MIT
281
302
  metadata: {}
282
- post_install_message:
303
+ post_install_message:
283
304
  rdoc_options: []
284
305
  require_paths:
285
306
  - lib
@@ -294,8 +315,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
294
315
  - !ruby/object:Gem::Version
295
316
  version: '0'
296
317
  requirements: []
297
- rubygems_version: 3.5.9
298
- signing_key:
318
+ rubygems_version: 3.5.22
319
+ signing_key:
299
320
  specification_version: 4
300
321
  summary: GitHub dotcom provider for entitlements-app
301
322
  test_files: []