Skip to content

Commit ef09805

Browse files
authored
Merge pull request #1868 from rust-lang/sync-org-members
Sync organization members
2 parents 36dcbbe + 78a61b8 commit ef09805

File tree

16 files changed

+406
-12
lines changed

16 files changed

+406
-12
lines changed

config.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ allowed-github-orgs = [
1515
"rust-analyzer",
1616
]
1717

18+
# GitHub organizations where member management is independent
19+
# and should not be automatically synchronized
20+
independent-github-orgs = [
21+
"rust-embedded",
22+
]
23+
1824
permissions-bors-repos = [
1925
"bors-kindergarten",
2026
"rust",
@@ -27,3 +33,25 @@ permissions-bools = [
2733
"dev-desktop",
2834
"sync-team-confirmation",
2935
]
36+
37+
# GitHub accounts that are allowed to stay in the GitHub organizations,
38+
# even if they may not be members of any team.
39+
# Note: Infra admins are automatically included from the infra-admins team.
40+
special-org-members = [
41+
# Bots.
42+
"bors",
43+
"craterbot",
44+
"rust-embedded-bot",
45+
"rust-highfive",
46+
"rust-lang-core-team-agenda-bot",
47+
"rust-lang-glacier-bot",
48+
"rust-lang-owner",
49+
"rust-log-analyzer",
50+
"rust-timer",
51+
"rustbot",
52+
53+
# Ferrous systems employees who manage the github sponsors
54+
# for rust-analyzer.
55+
"MissHolmes",
56+
"skade",
57+
]

src/data.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,26 @@ impl Data {
217217
.flatten()
218218
.any(|github_team| github_team.name == team_name && github_team.org == org)
219219
}
220+
221+
pub(crate) fn get_sync_team_config(&self) -> anyhow::Result<sync_team::Config> {
222+
let mut special_org_members = self.config.special_org_members().clone();
223+
224+
// Add infra admins from the infra-admins team
225+
let infra_admins_team = self
226+
.team("infra-admins")
227+
.context("cannot find infra-admins team")?;
228+
let infra_admins_usernames = infra_admins_team
229+
.raw_people()
230+
.members
231+
.iter()
232+
.map(|m| m.github.clone());
233+
special_org_members.extend(infra_admins_usernames);
234+
235+
Ok(sync_team::Config {
236+
special_org_members,
237+
independent_github_orgs: self.config.independent_github_orgs().clone(),
238+
})
239+
}
220240
}
221241

222242
fn load_file<T: DeserializeOwned>(path: &Path) -> Result<T, Error> {

src/main.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,5 +590,11 @@ fn perform_sync(opts: SyncOpts, data: Data) -> anyhow::Result<()> {
590590
let subcmd = opts.command.unwrap_or(SyncCommand::DryRun);
591591
let only_print_plan = matches!(subcmd, SyncCommand::PrintPlan);
592592
let dry_run = only_print_plan || matches!(subcmd, SyncCommand::DryRun);
593-
run_sync_team(team_api, &services, dry_run, only_print_plan)
593+
run_sync_team(
594+
team_api,
595+
&services,
596+
dry_run,
597+
only_print_plan,
598+
data.get_sync_team_config()?,
599+
)
594600
}

src/schema.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ pub(crate) use crate::permissions::Permissions;
33
use anyhow::{bail, format_err, Error};
44
use serde::de::{Deserialize, Deserializer};
55
use serde_untagged::UntaggedEnumVisitor;
6-
use std::collections::{HashMap, HashSet};
6+
use std::collections::{BTreeSet, HashMap, HashSet};
77

88
#[derive(serde_derive::Deserialize, Debug)]
99
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
1010
pub(crate) struct Config {
1111
allowed_mailing_lists_domains: HashSet<String>,
1212
allowed_github_orgs: HashSet<String>,
13+
independent_github_orgs: BTreeSet<String>,
1314
permissions_bors_repos: HashSet<String>,
1415
permissions_bools: HashSet<String>,
16+
// Use a BTreeSet for consistent ordering in tests
17+
special_org_members: BTreeSet<String>,
1518
}
1619

1720
impl Config {
@@ -30,6 +33,14 @@ impl Config {
3033
pub(crate) fn permissions_bools(&self) -> &HashSet<String> {
3134
&self.permissions_bools
3235
}
36+
37+
pub(crate) fn independent_github_orgs(&self) -> &BTreeSet<String> {
38+
&self.independent_github_orgs
39+
}
40+
41+
pub(crate) fn special_org_members(&self) -> &BTreeSet<String> {
42+
&self.special_org_members
43+
}
3344
}
3445

3546
// This is an enum to allow two kinds of values for the email field:

sync-team/src/github/api/read.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ pub(crate) trait GithubRead {
1515
/// Get the owners of an org
1616
fn org_owners(&self, org: &str) -> anyhow::Result<HashSet<u64>>;
1717

18+
/// Get the members of an org
19+
fn org_members(&self, org: &str) -> anyhow::Result<HashMap<u64, String>>;
20+
1821
/// Get all teams associated with a org
1922
///
2023
/// Returns a list of tuples of team name and slug
@@ -119,6 +122,26 @@ impl GithubRead for GitHubApiRead {
119122
Ok(owners)
120123
}
121124

125+
fn org_members(&self, org: &str) -> anyhow::Result<HashMap<u64, String>> {
126+
#[derive(serde::Deserialize, Eq, PartialEq, Hash)]
127+
struct User {
128+
id: u64,
129+
login: String,
130+
}
131+
let mut members = HashMap::new();
132+
self.client.rest_paginated(
133+
&Method::GET,
134+
&GitHubUrl::orgs(org, "members")?,
135+
|resp: Vec<User>| {
136+
for user in resp {
137+
members.insert(user.id, user.login);
138+
}
139+
Ok(())
140+
},
141+
)?;
142+
Ok(members)
143+
}
144+
122145
fn org_teams(&self, org: &str) -> anyhow::Result<Vec<(String, String)>> {
123146
let mut teams = Vec::new();
124147

sync-team/src/github/api/write.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,18 @@ impl GitHubWrite {
344344
Ok(())
345345
}
346346

347+
/// Remove a member from an org
348+
pub(crate) fn remove_gh_member_from_org(&self, org: &str, user: &str) -> anyhow::Result<()> {
349+
debug!("Removing user {user} from org {org}");
350+
if !self.dry_run {
351+
let method = Method::DELETE;
352+
let url = GitHubUrl::orgs(org, &format!("members/{user}"))?;
353+
let resp = self.client.req(method.clone(), &url)?.send()?;
354+
allow_not_found(resp, method, url.url())?;
355+
}
356+
Ok(())
357+
}
358+
347359
/// Remove a collaborator from a repo
348360
pub(crate) fn remove_collaborator_from_repo(
349361
&self,

0 commit comments

Comments
 (0)