Skip to content

Commit 6472bf3

Browse files
authored
Merge pull request #143 from github/github-service-improvements
GitHub Service Improvements
2 parents 9f785f4 + 761b861 commit 6472bf3

File tree

13 files changed

+145
-19
lines changed

13 files changed

+145
-19
lines changed

Gemfile.lock

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
PATH
22
remote: .
33
specs:
4-
entitlements-github-plugin (1.1.0)
4+
entitlements-github-plugin (1.2.0)
55
contracts (~> 0.17.0)
66
faraday (~> 2.0)
77
faraday-retry (~> 2.0)
88
octokit (~> 4.25)
9+
retryable (~> 3.0, >= 3.0.5)
910

1011
GEM
1112
remote: https://rubygems.org/
@@ -79,6 +80,7 @@ GEM
7980
rbs (3.6.1)
8081
logger
8182
regexp_parser (2.9.2)
83+
retryable (3.0.5)
8284
rexml (3.3.9)
8385
rspec (3.13.0)
8486
rspec-core (~> 3.13.0)

entitlements-github-plugin.gemspec

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
2020
s.add_dependency "faraday", "~> 2.0"
2121
s.add_dependency "faraday-retry", "~> 2.0"
2222
s.add_dependency "octokit", "~> 4.25"
23+
s.add_dependency "retryable", "~> 3.0", ">= 3.0.5"
2324

2425
s.add_development_dependency "entitlements-app", "~> 1.0"
2526
s.add_development_dependency "rake", "~> 13.2", ">= 13.2.1"

lib/entitlements/backend/github_org.rb

+2
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ class DuplicateUserError < RuntimeError; end
2424
require_relative "github_org/controller"
2525
require_relative "github_org/provider"
2626
require_relative "github_org/service"
27+
require_relative "../config/retry"
28+
Retry.setup!

lib/entitlements/backend/github_org/service.rb

+7-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ def add_user_to_organization(user, role)
4646
Entitlements.logger.debug "#{identifier} add_user_to_organization(user=#{user}, org=#{org}, role=#{role})"
4747

4848
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
5052
rescue Octokit::NotFound => e
5153
raise e unless ignore_not_found
5254

@@ -78,7 +80,10 @@ def add_user_to_organization(user, role)
7880
Contract String => C::Bool
7981
def remove_user_from_organization(user)
8082
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
8287

8388
# If we removed the user, remove them from the cache of members, so that any GitHub team
8489
# operations in this organization will ignore this user.

lib/entitlements/backend/github_team.rb

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44
require_relative "github_team/models/team"
55
require_relative "github_team/provider"
66
require_relative "github_team/service"
7+
require_relative "../config/retry"
8+
Retry.setup!

lib/entitlements/backend/github_team/service.rb

+30-10
Original file line numberDiff line numberDiff line change
@@ -279,10 +279,12 @@ def create_team(entitlement_group:)
279279
team_options[:parent_team_id] = parent_team_data[:team_id]
280280
rescue TeamNotFound
281281
# 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
286288

287289
Entitlements.logger.debug "created parent team #{entitlement_metadata["parent_team_name"]} with id #{result[:id]}"
288290

@@ -296,7 +298,11 @@ def create_team(entitlement_group:)
296298
end
297299

298300
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+
300306
Entitlements.logger.debug "created team #{team_name} with id #{result[:id]}"
301307
true
302308
rescue Octokit::UnprocessableEntity => e
@@ -317,7 +323,10 @@ def update_team(team:, metadata: {})
317323
Entitlements.logger.debug "update_team(team=#{team.team_name})"
318324
options = { name: team.team_name, repo_names: [], privacy: "closed",
319325
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+
321330
true
322331
rescue Octokit::UnprocessableEntity => e
323332
Entitlements.logger.debug "update_team(team=#{team.team_name}) ERROR - #{e.message}"
@@ -334,7 +343,9 @@ def update_team(team:, metadata: {})
334343
team_name: String
335344
] => Sawyer::Resource
336345
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
338349
end
339350

340351
private
@@ -426,7 +437,10 @@ def validate_team_id_and_slug!(team_id, team_slug)
426437
@validation_cache ||= {}
427438
@validation_cache[team_id] ||= begin
428439
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+
430444
team_data[:slug]
431445
end
432446
return if @validation_cache[team_id] == team_slug
@@ -457,7 +471,10 @@ def add_user_to_team(user:, team:, role: "member")
457471
validate_team_id_and_slug!(team.team_id, team.team_name)
458472

459473
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+
461478
result[:state] == "active" || result[:state] == "pending"
462479
rescue Octokit::UnprocessableEntity => e
463480
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 @@ def remove_user_from_team(user:, team:)
487504

488505
Entitlements.logger.debug "#{identifier} remove_user_from_team(user=#{user}, org=#{org}, team_id=#{team.team_id})"
489506
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
491511
end
492512
end
493513
end

lib/entitlements/config/retry.rb

+21
Original file line numberDiff line numberDiff line change
@@ -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

lib/entitlements/service/github.rb

+24-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require_relative "../config/retry"
4+
35
require "net/http"
46
require "octokit"
57
require "uri"
@@ -36,6 +38,9 @@ class GitHub
3638
ignore_not_found: C::Maybe[C::Bool],
3739
] => C::Any
3840
def initialize(addr: nil, org:, token:, ou:, ignore_not_found: false)
41+
# init the retry module
42+
Retry.setup!
43+
3944
# Save some parameters for the connection but don't actually connect yet.
4045
@addr = addr
4146
@org = org
@@ -94,7 +99,10 @@ def org_members
9499
# Returns true if the github instance is an enterprise server instance
95100
Contract C::None => C::Bool
96101
def enterprise?
97-
meta = octokit.github_meta
102+
meta = Retryable.with_context(:default) do
103+
octokit.github_meta
104+
end
105+
98106
meta.key? :installed_version
99107
end
100108

@@ -163,6 +171,7 @@ def octokit
163171
client = Octokit::Client.new(access_token: token)
164172
client.api_endpoint = addr if addr
165173
client.auto_paginate = true
174+
client.per_page = 100
166175
Entitlements.logger.debug "Setting up GitHub API connection to #{client.api_endpoint}"
167176
client
168177
end
@@ -246,11 +255,22 @@ def members_and_roles_from_graphql
246255
def members_and_roles_from_rest
247256
Entitlements.logger.debug "Loading organization members and roles for #{org}"
248257
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|
251270
result[member[:login].downcase] = "ADMIN"
252271
end
253-
octokit.organization_members(org, { role: "member" }).each do |member|
272+
273+
regular_members.each do |member|
254274
result[member[:login].downcase] = "MEMBER"
255275
end
256276

lib/version.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
module Entitlements
44
module Version
5-
VERSION = "1.1.0"
5+
VERSION = "1.2.0"
66
end
77
end

spec/unit/entitlements/backend/github_team/service_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,7 @@
845845
it "does not handle octokit error" do
846846
exc = StandardError.new("Whoops!")
847847
allow(subject).to receive(:octokit).and_return(octokit)
848-
expect(octokit).to receive(:team).with(1234).and_raise(exc)
848+
expect(octokit).to receive(:team).with(1234).and_raise(exc).exactly(3).times
849849

850850
expect do
851851
subject.send(:validate_team_id_and_slug!, 1234, "my-slug")

spec/unit/entitlements/service/github_spec.rb

+48
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,54 @@
223223
expect(result).to eq({"ocicat"=>"MEMBER", "blackmanx"=>"MEMBER", "toyger"=>"MEMBER", "highlander"=>"MEMBER", "russianblue"=>"MEMBER", "ragamuffin"=>"MEMBER", "monalisa"=>"ADMIN", "peterbald"=>"MEMBER", "mainecoon"=>"MEMBER", "laperm"=>"MEMBER"})
224224
end
225225
end
226+
227+
context "when there are no admins" do
228+
let(:members) { %w[ocicat blackmanx] }
229+
let(:octokit) { instance_double(Octokit::Client) }
230+
it "returns only members" do
231+
expect(subject).to receive(:octokit).and_return(octokit).twice
232+
expect(octokit).to receive(:organization_members).with("kittensinc", { role: "admin" }).and_return([])
233+
expect(octokit).to receive(:organization_members).with("kittensinc", { role: "member" }).and_return(members.map { |login| { login: } })
234+
result = subject.send(:members_and_roles_from_rest)
235+
expect(result).to eq({"ocicat"=>"MEMBER", "blackmanx"=>"MEMBER"})
236+
end
237+
end
238+
239+
context "when there are no members" do
240+
let(:admins) { %w[monalisa] }
241+
let(:octokit) { instance_double(Octokit::Client) }
242+
it "returns only admins" do
243+
expect(subject).to receive(:octokit).and_return(octokit).twice
244+
expect(octokit).to receive(:organization_members).with("kittensinc", { role: "admin" }).and_return(admins.map { |login| { login: } })
245+
expect(octokit).to receive(:organization_members).with("kittensinc", { role: "member" }).and_return([])
246+
result = subject.send(:members_and_roles_from_rest)
247+
expect(result).to eq({"monalisa"=>"ADMIN"})
248+
end
249+
end
250+
251+
context "when usernames have different cases" do
252+
let(:admins) { ["Monalisa"] }
253+
let(:members) { ["OCICAT", "BlackManx"] }
254+
let(:octokit) { instance_double(Octokit::Client) }
255+
it "downcases all usernames" do
256+
expect(subject).to receive(:octokit).and_return(octokit).twice
257+
expect(octokit).to receive(:organization_members).with("kittensinc", { role: "admin" }).and_return(admins.map { |login| { login: } })
258+
expect(octokit).to receive(:organization_members).with("kittensinc", { role: "member" }).and_return(members.map { |login| { login: } })
259+
result = subject.send(:members_and_roles_from_rest)
260+
expect(result).to eq({"monalisa"=>"ADMIN", "ocicat"=>"MEMBER", "blackmanx"=>"MEMBER"})
261+
end
262+
end
263+
264+
context "when organization is empty" do
265+
let(:octokit) { instance_double(Octokit::Client) }
266+
it "returns an empty hash" do
267+
expect(subject).to receive(:octokit).and_return(octokit).twice
268+
expect(octokit).to receive(:organization_members).with("kittensinc", { role: "admin" }).and_return([])
269+
expect(octokit).to receive(:organization_members).with("kittensinc", { role: "member" }).and_return([])
270+
result = subject.send(:members_and_roles_from_rest)
271+
expect(result).to eq({})
272+
end
273+
end
226274
end
227275

228276
describe "#pending_members_from_graphql" do

spec/unit/spec_helper.rb

+5
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ def instance_double(klass, *args)
184184

185185
config.before :each do
186186
allow(Time).to receive(:now).and_return(Time.utc(2018, 4, 1, 12, 0, 0))
187+
188+
allow(Kernel).to receive(:sleep)
189+
allow_any_instance_of(Kernel).to receive(:sleep)
190+
allow_any_instance_of(Object).to receive(:sleep)
191+
187192
allow(Entitlements).to receive(:cache).and_return(cache)
188193
if entitlements_config_hash
189194
Entitlements.config = entitlements_config_hash

vendor/cache/retryable-3.0.5.gem

13.5 KB
Binary file not shown.

0 commit comments

Comments
 (0)