Skip to content
This repository was archived by the owner on Mar 31, 2025. It is now read-only.

Commit f2d7303

Browse files
committed
Add functionality to sync org members on GitHub
Signed-off-by: Rohit Dandamudi <[email protected]>
1 parent 1480ace commit f2d7303

File tree

5 files changed

+199
-2
lines changed

5 files changed

+199
-2
lines changed

src/github/api/read.rs

+20
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ pub(crate) trait GithubRead {
1313
/// Get the owners of an org
1414
fn org_owners(&self, org: &str) -> anyhow::Result<HashSet<u64>>;
1515

16+
/// Get the members of an org
17+
fn org_members(&self, org: &str) -> anyhow::Result<HashSet<u64>>;
18+
1619
/// Get the app installations of an org
1720
fn org_app_installations(&self, org: &str) -> anyhow::Result<Vec<OrgAppInstallation>>;
1821

@@ -120,6 +123,23 @@ impl GithubRead for GitHubApiRead {
120123
Ok(owners)
121124
}
122125

126+
fn org_members(&self, org: &str) -> anyhow::Result<HashSet<u64>> {
127+
#[derive(serde::Deserialize, Eq, PartialEq, Hash)]
128+
struct User {
129+
id: u64,
130+
}
131+
let mut members = HashSet::new();
132+
self.client.rest_paginated(
133+
&Method::GET,
134+
format!("orgs/{org}/members"),
135+
|resp: Vec<User>| {
136+
members.extend(resp.into_iter().map(|u| u.id));
137+
Ok(())
138+
},
139+
)?;
140+
Ok(members)
141+
}
142+
123143
fn org_app_installations(&self, org: &str) -> anyhow::Result<Vec<OrgAppInstallation>> {
124144
#[derive(serde::Deserialize, Debug)]
125145
struct InstallationPage {

src/github/api/write.rs

+12
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,18 @@ impl GitHubWrite {
375375
Ok(())
376376
}
377377

378+
/// Remove a member from an org
379+
pub(crate) fn remove_gh_member_from_org(&self, org: &str, user: &str) -> anyhow::Result<()> {
380+
debug!("Removing user {user} from org {org}");
381+
if !self.dry_run {
382+
let method = Method::DELETE;
383+
let url = &format!("orgs/{org}/members/{user}");
384+
let resp = self.client.req(method.clone(), url)?.send()?;
385+
allow_not_found(resp, method, url)?;
386+
}
387+
Ok(())
388+
}
389+
378390
/// Remove a collaborator from a repo
379391
pub(crate) fn remove_collaborator_from_repo(
380392
&self,

src/github/mod.rs

+108
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ struct SyncGitHub {
7373
repos: Vec<rust_team_data::v1::Repo>,
7474
usernames_cache: HashMap<u64, String>,
7575
org_owners: HashMap<OrgName, HashSet<u64>>,
76+
org_members: HashMap<OrgName, HashSet<u64>>,
7677
org_apps: HashMap<OrgName, Vec<OrgAppInstallation>>,
7778
}
7879

@@ -103,10 +104,12 @@ impl SyncGitHub {
103104
.collect::<HashSet<_>>();
104105

105106
let mut org_owners = HashMap::new();
107+
let mut org_members = HashMap::new();
106108
let mut org_apps = HashMap::new();
107109

108110
for org in &orgs {
109111
org_owners.insert((*org).to_string(), github.org_owners(org)?);
112+
org_members.insert((*org).to_string(), github.org_members(org)?);
110113

111114
let mut installations: Vec<OrgAppInstallation> = vec![];
112115

@@ -134,17 +137,21 @@ impl SyncGitHub {
134137
repos,
135138
usernames_cache,
136139
org_owners,
140+
org_members,
137141
org_apps,
138142
})
139143
}
140144

141145
pub(crate) fn diff_all(&self) -> anyhow::Result<Diff> {
142146
let team_diffs = self.diff_teams()?;
143147
let repo_diffs = self.diff_repos()?;
148+
let org_team_members = self.map_teams_to_org()?;
149+
let toml_github_diffs = self.diff_teams_gh_org(org_team_members)?;
144150

145151
Ok(Diff {
146152
team_diffs,
147153
repo_diffs,
154+
toml_github_diffs,
148155
})
149156
}
150157

@@ -195,6 +202,55 @@ impl SyncGitHub {
195202
Ok(diffs)
196203
}
197204

205+
// collect all org and respective teams members in a HashMap
206+
fn map_teams_to_org(&self) -> anyhow::Result<HashMap<String, HashSet<u64>>> {
207+
let mut org_team_members: HashMap<String, HashSet<u64>> = HashMap::new();
208+
209+
for team in &self.teams {
210+
let mut team_org;
211+
212+
if let Some(gh) = &team.github {
213+
for toml_gh_team in &gh.teams {
214+
team_org = toml_gh_team.org.clone();
215+
let toml_team_mems_gh_id: HashSet<u64> =
216+
toml_gh_team.members.iter().copied().collect();
217+
218+
org_team_members
219+
.entry(team_org)
220+
.or_default()
221+
.extend(toml_team_mems_gh_id);
222+
}
223+
}
224+
}
225+
Ok(org_team_members)
226+
}
227+
228+
// create diff against github org members against toml team members
229+
fn diff_teams_gh_org(
230+
&self,
231+
org_team_members: HashMap<String, HashSet<u64>>,
232+
) -> anyhow::Result<OrgMembershipDiff> {
233+
let mut org_with_members_to_be_removed: HashMap<String, HashSet<String>> = HashMap::new();
234+
235+
for (gh_org, toml_members_across_teams) in org_team_members.into_iter() {
236+
let gh_org_members = self.org_members.get(&gh_org).unwrap();
237+
238+
let members_to_be_removed = (&toml_members_across_teams - gh_org_members)
239+
.into_iter()
240+
.map(|user| self.usernames_cache[&user].clone())
241+
.collect::<HashSet<String>>();
242+
243+
org_with_members_to_be_removed
244+
.entry(gh_org)
245+
.or_default()
246+
.extend(members_to_be_removed);
247+
}
248+
249+
Ok(OrgMembershipDiff::Delete(DeleteOrgMembershipDiff {
250+
org_with_members: org_with_members_to_be_removed,
251+
}))
252+
}
253+
198254
fn diff_team(&self, github_team: &rust_team_data::v1::GitHubTeam) -> anyhow::Result<TeamDiff> {
199255
// Ensure the team exists and is consistent
200256
let team = match self.github.team(&github_team.org, &github_team.name)? {
@@ -667,6 +723,7 @@ const BOTS_TEAMS: &[&str] = &["bors", "highfive", "rfcbot", "bots"];
667723
pub(crate) struct Diff {
668724
team_diffs: Vec<TeamDiff>,
669725
repo_diffs: Vec<RepoDiff>,
726+
toml_github_diffs: OrgMembershipDiff,
670727
}
671728

672729
impl Diff {
@@ -679,6 +736,8 @@ impl Diff {
679736
repo_diff.apply(sync)?;
680737
}
681738

739+
self.toml_github_diffs.apply(sync)?;
740+
682741
Ok(())
683742
}
684743
}
@@ -720,6 +779,55 @@ impl std::fmt::Display for RepoDiff {
720779
}
721780
}
722781

782+
#[derive(Debug)]
783+
784+
enum OrgMembershipDiff {
785+
Delete(DeleteOrgMembershipDiff),
786+
}
787+
788+
impl OrgMembershipDiff {
789+
fn apply(self, sync: &GitHubWrite) -> anyhow::Result<()> {
790+
match self {
791+
OrgMembershipDiff::Delete(d) => d.apply(sync)?,
792+
}
793+
794+
Ok(())
795+
}
796+
}
797+
798+
impl std::fmt::Display for OrgMembershipDiff {
799+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
800+
match self {
801+
OrgMembershipDiff::Delete(d) => write!(f, "{d}"),
802+
}
803+
}
804+
}
805+
806+
#[derive(Debug)]
807+
808+
struct DeleteOrgMembershipDiff {
809+
org_with_members: HashMap<String, HashSet<String>>,
810+
}
811+
812+
impl DeleteOrgMembershipDiff {
813+
fn apply(self, sync: &GitHubWrite) -> anyhow::Result<()> {
814+
for (gh_org, members) in self.org_with_members.iter() {
815+
for member in members {
816+
sync.remove_gh_member_from_org(gh_org, member)?;
817+
}
818+
}
819+
820+
Ok(())
821+
}
822+
}
823+
824+
impl std::fmt::Display for DeleteOrgMembershipDiff {
825+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
826+
writeln!(f, "❌ Deleting members '{:?}'", self.org_with_members)?;
827+
Ok(())
828+
}
829+
}
830+
723831
struct CreateRepoDiff {
724832
org: String,
725833
name: String,

src/github/tests/mod.rs

+27
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,33 @@ fn team_dont_add_member_if_invitation_is_pending() {
116116
"###);
117117
}
118118

119+
#[test]
120+
fn org_member_not_sync() {
121+
let mut model = DataModel::default();
122+
let user = model.create_user("sakura");
123+
let user2 = model.create_user("hitori");
124+
model.create_team(TeamData::new("team-1").gh_team("members-gh", &[user, user2]));
125+
let gh = model.gh_model();
126+
127+
model
128+
.get_team("team-1")
129+
.remove_gh_member("members-gh", user);
130+
131+
let gh_org_diff = model.diff_toml_gh_org_teams(gh);
132+
133+
insta::assert_debug_snapshot!(gh_org_diff, @r###"
134+
Delete(
135+
DeleteOrgMembershipDiff {
136+
org_with_members: {
137+
"rust-lang": {
138+
"hitori",
139+
},
140+
},
141+
},
142+
)
143+
"###);
144+
}
145+
119146
#[test]
120147
fn team_remove_member() {
121148
let mut model = DataModel::default();

src/github/tests/test_utils.rs

+32-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
use std::collections::{HashMap, HashSet};
22

33
use derive_builder::Builder;
4-
use rust_team_data::v1::{GitHubTeam, Person, TeamGitHub, TeamKind};
4+
use rust_team_data::v1::{GitHubTeam, Person, Team as TomlTeam, TeamGitHub, TeamKind};
55

66
use crate::github::api::{
77
BranchProtection, GithubRead, OrgAppInstallation, Repo, RepoAppInstallation, RepoTeam,
88
RepoUser, Team, TeamMember, TeamPrivacy, TeamRole,
99
};
10-
use crate::github::{api, SyncGitHub, TeamDiff};
10+
use crate::github::{api, OrgMembershipDiff, SyncGitHub, TeamDiff};
1111

1212
const DEFAULT_ORG: &str = "rust-lang";
1313

@@ -92,12 +92,29 @@ impl DataModel {
9292
GithubMock {
9393
users,
9494
owners: Default::default(),
95+
members: Default::default(),
9596
teams,
9697
team_memberships,
9798
team_invitations: Default::default(),
9899
}
99100
}
100101

102+
pub fn diff_toml_gh_org_teams(
103+
&self,
104+
github: GithubMock,
105+
// members: HashSet<u64>,
106+
) -> OrgMembershipDiff {
107+
let teams: Vec<TomlTeam> = self.teams.iter().map(|r| r.to_data()).collect();
108+
let repos = vec![];
109+
let read = Box::new(github);
110+
let sync = SyncGitHub::new(read, teams, repos).expect("Cannot create SyncGitHub");
111+
112+
let org_team_members = sync.map_teams_to_org().unwrap();
113+
114+
sync.diff_teams_gh_org(org_team_members)
115+
.expect("Cannot diff toml teams")
116+
}
117+
101118
pub fn diff_teams(&self, github: GithubMock) -> Vec<TeamDiff> {
102119
let teams = self.teams.iter().map(|r| r.to_data()).collect();
103120
let repos = vec![];
@@ -184,6 +201,9 @@ pub struct GithubMock {
184201
// org name -> user ID
185202
owners: HashMap<String, Vec<UserId>>,
186203
teams: Vec<Team>,
204+
205+
// org name -> user ID (members)
206+
members: HashMap<String, Vec<UserId>>,
187207
// Team name -> members
188208
team_memberships: HashMap<String, HashMap<UserId, TeamMember>>,
189209
// Team name -> list of invited users
@@ -219,6 +239,16 @@ impl GithubRead for GithubMock {
219239
.collect())
220240
}
221241

242+
fn org_members(&self, org: &str) -> anyhow::Result<HashSet<u64>> {
243+
Ok(self
244+
.members
245+
.get(org)
246+
.cloned()
247+
.unwrap_or_default()
248+
.into_iter()
249+
.collect())
250+
}
251+
222252
fn org_app_installations(&self, _org: &str) -> anyhow::Result<Vec<OrgAppInstallation>> {
223253
Ok(vec![])
224254
}

0 commit comments

Comments
 (0)