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

Commit b657699

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

File tree

6 files changed

+290
-2
lines changed

6 files changed

+290
-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

+121-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ mod tests;
44

55
use self::api::{BranchProtectionOp, TeamPrivacy, TeamRole};
66
use crate::github::api::{GithubRead, Login, PushAllowanceActor, RepoPermission, RepoSettings};
7+
use anyhow::anyhow;
78
use log::debug;
8-
use rust_team_data::v1::{Bot, BranchProtectionMode, MergeBot};
9+
use rust_team_data::v1::{Bot, BranchProtectionMode, GitHubTeam, MergeBot};
910
use std::collections::{HashMap, HashSet};
1011
use std::fmt::{Display, Formatter, Write};
1112

@@ -73,6 +74,7 @@ struct SyncGitHub {
7374
repos: Vec<rust_team_data::v1::Repo>,
7475
usernames_cache: HashMap<u64, String>,
7576
org_owners: HashMap<OrgName, HashSet<u64>>,
77+
org_members: HashMap<OrgName, HashSet<u64>>,
7678
org_apps: HashMap<OrgName, Vec<OrgAppInstallation>>,
7779
}
7880

@@ -103,10 +105,12 @@ impl SyncGitHub {
103105
.collect::<HashSet<_>>();
104106

105107
let mut org_owners = HashMap::new();
108+
let mut org_members = HashMap::new();
106109
let mut org_apps = HashMap::new();
107110

108111
for org in &orgs {
109112
org_owners.insert((*org).to_string(), github.org_owners(org)?);
113+
org_members.insert((*org).to_string(), github.org_members(org)?);
110114

111115
let mut installations: Vec<OrgAppInstallation> = vec![];
112116

@@ -134,17 +138,21 @@ impl SyncGitHub {
134138
repos,
135139
usernames_cache,
136140
org_owners,
141+
org_members,
137142
org_apps,
138143
})
139144
}
140145

141146
pub(crate) fn diff_all(&self) -> anyhow::Result<Diff> {
142147
let team_diffs = self.diff_teams()?;
143148
let repo_diffs = self.diff_repos()?;
149+
let org_team_members = self.map_teams_to_org()?;
150+
let toml_github_diffs = self.diff_teams_gh_org(org_team_members)?;
144151

145152
Ok(Diff {
146153
team_diffs,
147154
repo_diffs,
155+
toml_github_diffs,
148156
})
149157
}
150158

@@ -195,6 +203,66 @@ impl SyncGitHub {
195203
Ok(diffs)
196204
}
197205

206+
// collect all org and respective teams members in a HashMap
207+
fn map_teams_to_org(&self) -> anyhow::Result<HashMap<String, HashSet<u64>>> {
208+
let mut org_team_members: HashMap<String, HashSet<u64>> = HashMap::new();
209+
210+
for team in &self.teams {
211+
let team_org;
212+
// get the team github org through the corresponding github team
213+
if let Some(gh) = &team.github {
214+
let github_teams = &gh.teams;
215+
let github_team: &GitHubTeam = github_teams
216+
.iter()
217+
.find(|&gt| gt.name == team.name)
218+
.expect("Team Not Found");
219+
team_org = github_team.org.clone();
220+
} else {
221+
return Err(anyhow!(
222+
"TeamGitHub object not found, got {:?}",
223+
&team.github
224+
));
225+
}
226+
227+
let team_members_github_id: HashSet<u64> =
228+
team.members.iter().map(|member| member.github_id).collect();
229+
230+
org_team_members
231+
.entry(team_org)
232+
.or_default()
233+
.extend(team_members_github_id);
234+
}
235+
Ok(org_team_members)
236+
}
237+
238+
// create diff against github org members against toml team members
239+
fn diff_teams_gh_org(
240+
&self,
241+
org_team_members: HashMap<String, HashSet<u64>>,
242+
) -> anyhow::Result<OrgMembershipDiff> {
243+
let mut org_with_members_to_be_removed: HashMap<String, HashSet<String>> = HashMap::new();
244+
245+
for (gh_org, toml_members_across_teams) in org_team_members.into_iter() {
246+
let gh_org_members = self.org_members.get(&gh_org).unwrap();
247+
248+
let mut members_to_be_removed = HashSet::new();
249+
250+
for toml_member in toml_members_across_teams {
251+
if !gh_org_members.contains(&toml_member.clone()) {
252+
members_to_be_removed.insert(self.usernames_cache[&toml_member].clone());
253+
}
254+
}
255+
org_with_members_to_be_removed
256+
.entry(gh_org)
257+
.or_default()
258+
.extend(members_to_be_removed);
259+
}
260+
261+
Ok(OrgMembershipDiff::Delete(DeleteOrgMembershipDiff {
262+
org_with_members: org_with_members_to_be_removed,
263+
}))
264+
}
265+
198266
fn diff_team(&self, github_team: &rust_team_data::v1::GitHubTeam) -> anyhow::Result<TeamDiff> {
199267
// Ensure the team exists and is consistent
200268
let team = match self.github.team(&github_team.org, &github_team.name)? {
@@ -667,6 +735,7 @@ const BOTS_TEAMS: &[&str] = &["bors", "highfive", "rfcbot", "bots"];
667735
pub(crate) struct Diff {
668736
team_diffs: Vec<TeamDiff>,
669737
repo_diffs: Vec<RepoDiff>,
738+
toml_github_diffs: OrgMembershipDiff,
670739
}
671740

672741
impl Diff {
@@ -679,6 +748,8 @@ impl Diff {
679748
repo_diff.apply(sync)?;
680749
}
681750

751+
self.toml_github_diffs.apply(sync)?;
752+
682753
Ok(())
683754
}
684755
}
@@ -720,6 +791,55 @@ impl std::fmt::Display for RepoDiff {
720791
}
721792
}
722793

794+
#[derive(Debug)]
795+
796+
enum OrgMembershipDiff {
797+
Delete(DeleteOrgMembershipDiff),
798+
}
799+
800+
impl OrgMembershipDiff {
801+
fn apply(self, sync: &GitHubWrite) -> anyhow::Result<()> {
802+
match self {
803+
OrgMembershipDiff::Delete(d) => d.apply(sync)?,
804+
}
805+
806+
Ok(())
807+
}
808+
}
809+
810+
impl std::fmt::Display for OrgMembershipDiff {
811+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
812+
match self {
813+
OrgMembershipDiff::Delete(d) => write!(f, "{d}"),
814+
}
815+
}
816+
}
817+
818+
#[derive(Debug)]
819+
820+
struct DeleteOrgMembershipDiff {
821+
org_with_members: HashMap<String, HashSet<String>>,
822+
}
823+
824+
impl DeleteOrgMembershipDiff {
825+
fn apply(self, sync: &GitHubWrite) -> anyhow::Result<()> {
826+
for (gh_org, members) in self.org_with_members.iter() {
827+
for member in members {
828+
sync.remove_gh_member_from_org(gh_org, member)?;
829+
}
830+
}
831+
832+
Ok(())
833+
}
834+
}
835+
836+
impl std::fmt::Display for DeleteOrgMembershipDiff {
837+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
838+
writeln!(f, "❌ Deleting members '{:?}'", self.org_with_members)?;
839+
Ok(())
840+
}
841+
}
842+
723843
struct CreateRepoDiff {
724844
org: String,
725845
name: String,

src/github/tests/.mod.rs.pending-snap

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{"run_id":"1730274919-200165096","line":135,"new":{"module_name":"sync_team__github__tests","snapshot_name":"org_member_not_sync","metadata":{"source":"src/github/tests/mod.rs","assertion_line":135,"expression":"team_diff"},"snapshot":"[\n Edit(\n EditTeamDiff {\n org: \"rust-lang\",\n name: \"members-gh\",\n name_diff: None,\n description_diff: None,\n privacy_diff: None,\n member_diffs: [\n (\n \"hitori\",\n Noop,\n ),\n (\n \"sakura\",\n Delete,\n ),\n ],\n },\n ),\n]"},"old":{"module_name":"sync_team__github__tests","metadata":{},"snapshot":"[\n Edit(\n EditTeamDiff {\n org: \"rust-lang\",\n name: \"admins-gh\",\n name_diff: None,\n description_diff: None,\n privacy_diff: None,\n member_diffs: [\n (\n \"mark\",\n Noop,\n ),\n (\n \"jan\",\n Delete,\n ),\n ],\n },\n ),\n]"}}
2+
{"run_id":"1732431099-593466759","line":96,"new":null,"old":null}
3+
{"run_id":"1732431099-593466759","line":57,"new":null,"old":null}
4+
{"run_id":"1732431099-593466759","line":23,"new":null,"old":null}
5+
{"run_id":"1732431099-593466759","line":211,"new":null,"old":null}
6+
{"run_id":"1732431099-593466759","line":169,"new":null,"old":null}
7+
{"run_id":"1732431099-593466759","line":136,"new":{"module_name":"sync_team__github__tests","snapshot_name":"org_member_not_sync","metadata":{"source":"src/github/tests/mod.rs","assertion_line":136,"expression":"gh_org_diff","snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\": {},\n },\n },\n)"},"old":{"module_name":"sync_team__github__tests","metadata":{"snapshot_kind":"text"},"snapshot":"[\n Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\" : [\n \"hitori\",\n \"sakura\"\n ]\n }\n\n }\n\n )\n]"}}
8+
{"run_id":"1732431332-201294834","line":96,"new":null,"old":null}
9+
{"run_id":"1732431332-201294834","line":169,"new":null,"old":null}
10+
{"run_id":"1732431332-201294834","line":23,"new":null,"old":null}
11+
{"run_id":"1732431332-201294834","line":57,"new":null,"old":null}
12+
{"run_id":"1732431332-201294834","line":211,"new":null,"old":null}
13+
{"run_id":"1732431332-201294834","line":136,"new":{"module_name":"sync_team__github__tests","snapshot_name":"org_member_not_sync","metadata":{"source":"src/github/tests/mod.rs","assertion_line":136,"expression":"gh_org_diff","snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\": {},\n },\n },\n)"},"old":{"module_name":"sync_team__github__tests","metadata":{"snapshot_kind":"text"},"snapshot":"[\n Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\" : {\n \"hitori\",\n \"sakura\"\n }\n }\n\n }\n\n )\n]"}}
14+
{"run_id":"1732431401-588201486","line":23,"new":null,"old":null}
15+
{"run_id":"1732431401-588201486","line":96,"new":null,"old":null}
16+
{"run_id":"1732431401-588201486","line":57,"new":null,"old":null}
17+
{"run_id":"1732431401-588201486","line":207,"new":null,"old":null}
18+
{"run_id":"1732431401-588201486","line":165,"new":null,"old":null}
19+
{"run_id":"1732431401-588201486","line":136,"new":{"module_name":"sync_team__github__tests","snapshot_name":"org_member_not_sync","metadata":{"source":"src/github/tests/mod.rs","assertion_line":136,"expression":"gh_org_diff","snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\": {},\n },\n },\n)"},"old":{"module_name":"sync_team__github__tests","metadata":{"snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\" : {\n \"hitori\",\n \"sakura\"\n }\n }\n\n }\n\n)"}}
20+
{"run_id":"1732431789-963770036","line":25,"new":null,"old":null}
21+
{"run_id":"1732431789-963770036","line":98,"new":null,"old":null}
22+
{"run_id":"1732431789-963770036","line":171,"new":null,"old":null}
23+
{"run_id":"1732431789-963770036","line":59,"new":null,"old":null}
24+
{"run_id":"1732431789-963770036","line":213,"new":null,"old":null}
25+
{"run_id":"1732431837-581539242","line":96,"new":null,"old":null}
26+
{"run_id":"1732431837-581539242","line":211,"new":null,"old":null}
27+
{"run_id":"1732431837-581539242","line":57,"new":null,"old":null}
28+
{"run_id":"1732431837-581539242","line":169,"new":null,"old":null}
29+
{"run_id":"1732431837-581539242","line":23,"new":null,"old":null}
30+
{"run_id":"1732431854-502008095","line":23,"new":null,"old":null}
31+
{"run_id":"1732431854-502008095","line":211,"new":null,"old":null}
32+
{"run_id":"1732431854-502008095","line":96,"new":null,"old":null}
33+
{"run_id":"1732431854-502008095","line":57,"new":null,"old":null}
34+
{"run_id":"1732431854-502008095","line":169,"new":null,"old":null}
35+
{"run_id":"1732431897-574204682","line":23,"new":null,"old":null}
36+
{"run_id":"1732431897-574204682","line":96,"new":null,"old":null}
37+
{"run_id":"1732431897-574204682","line":169,"new":null,"old":null}
38+
{"run_id":"1732431897-574204682","line":57,"new":null,"old":null}
39+
{"run_id":"1732431897-574204682","line":211,"new":null,"old":null}
40+
{"run_id":"1732431897-574204682","line":140,"new":{"module_name":"sync_team__github__tests","snapshot_name":"org_member_not_sync","metadata":{"source":"src/github/tests/mod.rs","assertion_line":140,"expression":"gh_org_diff","snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\": {\n \"hitori\",\n },\n },\n },\n)"},"old":{"module_name":"sync_team__github__tests","metadata":{"snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\" : {\n \"hitori\",\n \"sakura\"\n }\n }\n\n }\n\n)"}}
41+
{"run_id":"1732431916-949458174","line":23,"new":null,"old":null}
42+
{"run_id":"1732431916-949458174","line":96,"new":null,"old":null}
43+
{"run_id":"1732431916-949458174","line":168,"new":null,"old":null}
44+
{"run_id":"1732431916-949458174","line":210,"new":null,"old":null}
45+
{"run_id":"1732431916-949458174","line":57,"new":null,"old":null}
46+
{"run_id":"1732431916-949458174","line":140,"new":{"module_name":"sync_team__github__tests","snapshot_name":"org_member_not_sync","metadata":{"source":"src/github/tests/mod.rs","assertion_line":140,"expression":"gh_org_diff","snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\": {\n \"hitori\",\n },\n },\n },\n)"},"old":{"module_name":"sync_team__github__tests","metadata":{"snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\" : {\n \"sakura\"\n }\n }\n\n }\n\n)"}}
47+
{"run_id":"1732431927-155044648","line":210,"new":null,"old":null}
48+
{"run_id":"1732431927-155044648","line":57,"new":null,"old":null}
49+
{"run_id":"1732431927-155044648","line":23,"new":null,"old":null}
50+
{"run_id":"1732431927-155044648","line":96,"new":null,"old":null}
51+
{"run_id":"1732431927-155044648","line":168,"new":null,"old":null}
52+
{"run_id":"1732431927-155044648","line":140,"new":{"module_name":"sync_team__github__tests","snapshot_name":"org_member_not_sync","metadata":{"source":"src/github/tests/mod.rs","assertion_line":140,"expression":"gh_org_diff","snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\": {\n \"hitori\",\n },\n },\n },\n)"},"old":{"module_name":"sync_team__github__tests","metadata":{"snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\" : {\n \"hitori\",\n }\n }\n\n }\n\n)"}}
53+
{"run_id":"1732431939-75302299","line":210,"new":null,"old":null}
54+
{"run_id":"1732431939-75302299","line":96,"new":null,"old":null}
55+
{"run_id":"1732431939-75302299","line":23,"new":null,"old":null}
56+
{"run_id":"1732431939-75302299","line":168,"new":null,"old":null}
57+
{"run_id":"1732431939-75302299","line":57,"new":null,"old":null}
58+
{"run_id":"1732431939-75302299","line":140,"new":{"module_name":"sync_team__github__tests","snapshot_name":"org_member_not_sync","metadata":{"source":"src/github/tests/mod.rs","assertion_line":140,"expression":"gh_org_diff","snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\": {\n \"hitori\",\n },\n },\n },\n)"},"old":{"module_name":"sync_team__github__tests","metadata":{"snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\": {\n \"hitori\",\n }\n }\n\n }\n\n)"}}
59+
{"run_id":"1732431953-661969600","line":57,"new":null,"old":null}
60+
{"run_id":"1732431953-661969600","line":213,"new":null,"old":null}
61+
{"run_id":"1732431953-661969600","line":171,"new":null,"old":null}
62+
{"run_id":"1732431953-661969600","line":96,"new":null,"old":null}
63+
{"run_id":"1732431953-661969600","line":23,"new":null,"old":null}
64+
{"run_id":"1732431953-661969600","line":140,"new":{"module_name":"sync_team__github__tests","snapshot_name":"org_member_not_sync","metadata":{"source":"src/github/tests/mod.rs","assertion_line":140,"expression":"gh_org_diff","snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\": {\n \"hitori\",\n },\n },\n },\n)"},"old":{"module_name":"sync_team__github__tests","metadata":{"snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\": {\n \"hitori\",\n\n\n \n }\n }\n\n }\n\n)"}}
65+
{"run_id":"1732431971-87005085","line":23,"new":null,"old":null}
66+
{"run_id":"1732431971-87005085","line":210,"new":null,"old":null}
67+
{"run_id":"1732431971-87005085","line":168,"new":null,"old":null}
68+
{"run_id":"1732431971-87005085","line":57,"new":null,"old":null}
69+
{"run_id":"1732431971-87005085","line":96,"new":null,"old":null}
70+
{"run_id":"1732431971-87005085","line":140,"new":{"module_name":"sync_team__github__tests","snapshot_name":"org_member_not_sync","metadata":{"source":"src/github/tests/mod.rs","assertion_line":140,"expression":"gh_org_diff","snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\": {\n \"hitori\",\n },\n },\n },\n)"},"old":{"module_name":"sync_team__github__tests","metadata":{"snapshot_kind":"text"},"snapshot":"Delete(\n DeleteOrgMembershipDiff {\n org_with_members: {\n \"rust-lang\": {\n \"hitori\",\n },\n },\n\n },\n\n)"}}
71+
{"run_id":"1732431979-150062574","line":96,"new":null,"old":null}
72+
{"run_id":"1732431979-150062574","line":208,"new":null,"old":null}
73+
{"run_id":"1732431979-150062574","line":57,"new":null,"old":null}
74+
{"run_id":"1732431979-150062574","line":140,"new":null,"old":null}
75+
{"run_id":"1732431979-150062574","line":23,"new":null,"old":null}
76+
{"run_id":"1732431979-150062574","line":166,"new":null,"old":null}

0 commit comments

Comments
 (0)