Skip to content

Commit a8889c6

Browse files
authored
Merge pull request #17 from krukow/specify-team-maintainers
Support: role maintainer in team membership
2 parents efa2dc0 + f3956a3 commit a8889c6

File tree

2 files changed

+138
-7
lines changed

2 files changed

+138
-7
lines changed

lib/entitlements/backend/github_team/service.rb

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require_relative "../../service/github"
55

66
require "base64"
7+
require "set"
78

89
module Entitlements
910
class Backend
@@ -196,14 +197,56 @@ def sync_team(desired_state, current_state)
196197
end
197198
end
198199

199-
added_members = desired_state.member_strings.map { |u| u.downcase } - current_state.member_strings.map { |u| u.downcase }
200-
removed_members = current_state.member_strings.map { |u| u.downcase } - desired_state.member_strings.map { |u| u.downcase }
200+
desired_team_members = Set.new(desired_state.member_strings.map { |u| u.downcase })
201+
current_team_members = Set.new(current_state.member_strings.map { |u| u.downcase })
202+
203+
added_members = desired_team_members - current_team_members
204+
removed_members = current_team_members - desired_team_members
201205

202206
added_members.select! { |username| add_user_to_team(user: username, team: current_state) }
203207
removed_members.select! { |username| remove_user_from_team(user: username, team: current_state) }
204208

209+
added_maintainers = Set.new
210+
removed_maintainers = Set.new
211+
unless desired_metadata["team_maintainers"] == current_metadata["team_maintainers"]
212+
if desired_metadata["team_maintainers"].nil?
213+
# We will not delete ALL maintainers from a team and leave it without maintainers.
214+
Entitlements.logger.debug "sync_team(#{current_state.team_name}=#{current_state.team_id}): IGNORING GitHub Team Maintainer DELETE"
215+
else
216+
desired_maintainers_str = desired_metadata["team_maintainers"] # not nil, we tested that above
217+
desired_maintainers = Set.new(desired_maintainers_str.split(",").map { |u| u.strip.downcase })
218+
unless desired_maintainers.subset?(desired_team_members)
219+
maintainer_not_member = desired_maintainers - desired_team_members
220+
Entitlements.logger.warn "sync_team(#{current_state.team_name}=#{current_state.team_id}): Maintainers must be a subset of team members. Desired maintainers: #{maintainer_not_member.to_a} are not members. Ignoring."
221+
desired_maintainers = desired_maintainers.intersection(desired_team_members)
222+
end
223+
224+
current_maintainers_str = current_metadata["team_maintainers"]
225+
current_maintainers = Set.new(
226+
current_maintainers_str.nil? ? [] : current_maintainers_str.split(",").map { |u| u.strip.downcase }
227+
)
228+
# We ignore any current maintainer who is not a member of the team according to the team spec
229+
# This avoids messing with teams who have been manually modified to add a maintainer
230+
current_maintainers = current_maintainers.intersection(desired_team_members)
231+
added_maintainers = desired_maintainers - current_maintainers
232+
removed_maintainers = current_maintainers - desired_maintainers
233+
if added_maintainers.empty? && removed_maintainers.empty?
234+
Entitlements.logger.debug "sync_team(#{current_state.team_name}=#{current_state.team_id}): Textual change but no semantic change in maintainers. It is remains: #{current_maintainers.to_a}."
235+
else
236+
Entitlements.logger.debug "sync_team(#{current_state.team_name}=#{current_state.team_id}): Maintainer members change found - From #{current_maintainers.to_a} to #{desired_maintainers.to_a}"
237+
added_maintainers.select! { |username| add_user_to_team(user: username, team: current_state, role: "maintainer") }
238+
239+
## We only touch previous maintainers who are actually still going to be members of the team
240+
removed_maintainers = removed_maintainers.intersection(desired_team_members)
241+
## Downgrade membership to default (role: "member")
242+
removed_maintainers.select! { |username| add_user_to_team(user: username, team: current_state, role: "member") }
243+
end
244+
end
245+
end
246+
247+
205248
Entitlements.logger.debug "sync_team(#{current_state.team_name}=#{current_state.team_id}): Added #{added_members.count}, removed #{removed_members.count}"
206-
added_members.any? || removed_members.any? || changed_parent_team
249+
added_members.any? || removed_members.any? || added_maintainers.any? || removed_maintainers.any? || changed_parent_team
207250
end
208251

209252
# Create a team
@@ -366,17 +409,23 @@ def validate_team_id_and_slug!(team_id, team_slug)
366409
#
367410
# user - String with the GitHub username
368411
# team - Entitlements::Backend::GitHubTeam::Models::Team object for the team.
412+
# role - optional (default: "member") String with the role to assign to the user: either "member" or "maintainer"
369413
#
370-
# Returns true if the user was added to the team, false if user was already on team.
414+
# Returns true if the user was added to the team or role changed; false if user was already on team with same role
371415
Contract C::KeywordArgs[
372416
user: String,
373417
team: Entitlements::Backend::GitHubTeam::Models::Team,
418+
role: C::Optional[String]
374419
] => C::Bool
375-
def add_user_to_team(user:, team:)
420+
def add_user_to_team(user:, team:, role: "member")
376421
return false unless org_members.include?(user.downcase)
377-
Entitlements.logger.debug "#{identifier} add_user_to_team(user=#{user}, org=#{org}, team_id=#{team.team_id})"
422+
unless role == "member" || role == "maintainer"
423+
# :nocov:
424+
raise "add_user_to_team role mismatch: team_id=#{team.team_id} user=#{user} expected role=maintainer/member got=#{role}"
425+
end
426+
Entitlements.logger.debug "#{identifier} add_user_to_team(user=#{user}, org=#{org}, team_id=#{team.team_id}, role=#{role})"
378427
validate_team_id_and_slug!(team.team_id, team.team_name)
379-
result = octokit.add_team_membership(team.team_id, user)
428+
result = octokit.add_team_membership(team.team_id, user, role: role)
380429
result[:state] == "active" || result[:state] == "pending"
381430
end
382431

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,53 @@
340340
)
341341
end
342342

343+
let(:team_metadata_add_maintainer) do
344+
Entitlements::Backend::GitHubTeam::Models::Team.new(
345+
team_id: 1001,
346+
team_name: "russian-blues",
347+
members: Set.new(%w[blackmanx ragamuffin MAINECOON]),
348+
ou: "ou=kittensinc,dc=github,dc=com",
349+
metadata: {
350+
"parent_team_name" => "cuddly-kittens",
351+
"team_maintainers" => "blackmanx,ragamuffin"
352+
}
353+
)
354+
end
355+
let(:team_metadata_maintainer_old) do
356+
Entitlements::Backend::GitHubTeam::Models::Team.new(
357+
team_id: 1001,
358+
team_name: "russian-blues",
359+
members: Set.new(%w[blackmanx ragamuffin MAINECOON]),
360+
ou: "ou=kittensinc,dc=github,dc=com",
361+
metadata: {
362+
"parent_team_name" => "cuddly-kittens",
363+
"team_maintainers" => "ragamuffin"
364+
}
365+
)
366+
end
367+
let(:team_metadata_remove_all_maintainers) do
368+
Entitlements::Backend::GitHubTeam::Models::Team.new(
369+
team_id: 1001,
370+
team_name: "russian-blues",
371+
members: Set.new(%w[blackmanx ragamuffin MAINECOON]),
372+
ou: "ou=kittensinc,dc=github,dc=com",
373+
metadata: {
374+
"parent_team_name" => "cuddly-kittens",
375+
}
376+
)
377+
end
378+
let(:team_metadata_add_non_member_maintainer) do
379+
Entitlements::Backend::GitHubTeam::Models::Team.new(
380+
team_id: 1001,
381+
team_name: "russian-blues",
382+
members: Set.new(%w[blackmanx ragamuffin MAINECOON]),
383+
ou: "ou=kittensinc,dc=github,dc=com",
384+
metadata: {
385+
"parent_team_name" => "cuddly-kittens",
386+
"team_maintainers" => "krukow,ragamuffin"
387+
}
388+
)
389+
end
343390
let(:team_metadata_remove) do
344391
Entitlements::Backend::GitHubTeam::Models::Team.new(
345392
team_id: 1001,
@@ -401,6 +448,41 @@
401448
expect(result).to eq(true)
402449
end
403450

451+
it "returns true when there were metadata changes to add maintainer" do
452+
allow(subject).to receive(:read_team).with(team_metadata_add_maintainer).and_return(team_metadata_maintainer_old)
453+
expect(logger).to receive(:debug).with(/sync_team\(russian-blues=1001\): Maintainer members change found - From \["ragamuffin"\] to \["blackmanx", \"ragamuffin\"\]/)
454+
expect(subject).to receive(:add_user_to_team).with(user: "blackmanx", team: team_metadata_maintainer_old, role: "maintainer").and_return(true)
455+
expect(logger).to receive(:debug).with(/sync_team\(russian-blues=1001\): Added 0, removed 0/)
456+
result = subject.sync_team(team_metadata_add_maintainer, team_metadata_maintainer_old)
457+
expect(result).to eq(true)
458+
end
459+
460+
it "returns false when there were metadata changes to remove ALL maintainers" do
461+
allow(subject).to receive(:read_team).with(team_metadata_remove_all_maintainers).and_return(team_metadata_maintainer_old)
462+
expect(logger).to receive(:debug).with(/sync_team\(russian-blues=1001\): IGNORING GitHub Team Maintainer DELETE/)
463+
expect(logger).to receive(:debug).with(/sync_team\(russian-blues=1001\): Added 0, removed 0/)
464+
result = subject.sync_team(team_metadata_remove_all_maintainers, team_metadata_maintainer_old)
465+
expect(result).to eq(false)
466+
end
467+
468+
it "returns true when there were metadata changes to remove a maintainer" do
469+
allow(subject).to receive(:read_team).with(team_metadata_maintainer_old).and_return(team_metadata_add_maintainer)
470+
expect(logger).to receive(:debug).with(/sync_team\(russian-blues=1001\): Maintainer members change found - From \["blackmanx", "ragamuffin"\] to \["ragamuffin"\]/)
471+
expect(subject).to receive(:add_user_to_team).with(user: "blackmanx", team: team_metadata_maintainer_old, role: "member").and_return(true)
472+
expect(logger).to receive(:debug).with(/sync_team\(russian-blues=1001\): Added 0, removed 0/)
473+
result = subject.sync_team(team_metadata_maintainer_old, team_metadata_add_maintainer)
474+
expect(result).to eq(true)
475+
end
476+
477+
it "returns false when there were metadata changes to add maintainer who is NOT in the team" do
478+
allow(subject).to receive(:read_team).with(team_metadata_add_non_member_maintainer).and_return(team_metadata_maintainer_old)
479+
expect(logger).to receive(:warn).with(/sync_team\(russian-blues=1001\): Maintainers must be a subset of team members. Desired maintainers: \["krukow"\] are not members. Ignoring./)
480+
expect(logger).to receive(:debug).with(/sync_team\(russian-blues=1001\): Textual change but no semantic change in maintainers. It is remains: \["ragamuffin\"\]./)
481+
expect(logger).to receive(:debug).with(/sync_team\(russian-blues=1001\): Added 0, removed 0/)
482+
result = subject.sync_team(team_metadata_add_non_member_maintainer, team_metadata_maintainer_old)
483+
expect(result).to eq(false)
484+
end
485+
404486
# TODO: I'm hard-coding a block for deletes, for now. I'm doing that by making sure we dont set the desired parent_team_id to nil for teams where it is already set
405487
it "returns false while deletes are prevented" do
406488
allow(subject).to receive(:read_team).with(team_metadata_add).and_return(team_metadata_remove)

0 commit comments

Comments
 (0)