Skip to content

Commit 69382de

Browse files
committed
Update CODEOWNERS and load users based on Data
1 parent c042dc2 commit 69382de

File tree

3 files changed

+133
-52
lines changed

3 files changed

+133
-52
lines changed

.github/CODEOWNERS

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
# This is an automatically generated file
22
# Run `cargo run ci generate-codeowners` to regenerate it.
3+
# Note that the file is scanned bottom-to-top and the first match wins.
34

4-
/.github/ @Mark-Simulacrum @pietroalbini @jdno @marcoieni
5-
/src/ @Mark-Simulacrum @pietroalbini @jdno @marcoieni
6-
/rust_team_data/ @Mark-Simulacrum @pietroalbini @jdno @marcoieni
5+
# If none of the rules below match, we apply this catch-all rule
6+
# and require admin approval for such a change.
7+
* @Mark-Simulacrum @pietroalbini @jdno @marcoieni
8+
9+
# Data files can be approved by users with write access.
10+
# We don't list these users explicitly to avoid notifying all of them
11+
# on every change to the data files.
12+
people/**/*.toml
13+
repos/**/*.toml
14+
teams/**/*.toml
15+
16+
# Modifying these files requires admin approval.
717
/repos/rust-lang/team.toml @Mark-Simulacrum @pietroalbini @jdno @marcoieni
818
/repos/rust-lang/sync-team.toml @Mark-Simulacrum @pietroalbini @jdno @marcoieni
919
/teams/infra-admins.toml @Mark-Simulacrum @pietroalbini @jdno @marcoieni
1020
/teams/team-repo-admins.toml @Mark-Simulacrum @pietroalbini @jdno @marcoieni
11-
.cargo @Mark-Simulacrum @pietroalbini @jdno @marcoieni
12-
target @Mark-Simulacrum @pietroalbini @jdno @marcoieni
13-
Cargo.lock @Mark-Simulacrum @pietroalbini @jdno @marcoieni
14-
Cargo.toml @Mark-Simulacrum @pietroalbini @jdno @marcoieni
15-
config.toml @Mark-Simulacrum @pietroalbini @jdno @marcoieni
1621
/people/Mark-Simulacrum.toml @Mark-Simulacrum @pietroalbini @jdno @marcoieni
17-
/people/pietroalbini.toml @Mark-Simulacrum @pietroalbini @jdno @marcoieni
22+
/people/jackh726.toml @Mark-Simulacrum @pietroalbini @jdno @marcoieni
1823
/people/jdno.toml @Mark-Simulacrum @pietroalbini @jdno @marcoieni
1924
/people/marcoieni.toml @Mark-Simulacrum @pietroalbini @jdno @marcoieni
25+
/people/oli-obk.toml @Mark-Simulacrum @pietroalbini @jdno @marcoieni
26+
/people/pietroalbini.toml @Mark-Simulacrum @pietroalbini @jdno @marcoieni
27+
/people/rylev.toml @Mark-Simulacrum @pietroalbini @jdno @marcoieni
28+
/people/technetos.toml @Mark-Simulacrum @pietroalbini @jdno @marcoieni

src/ci.rs

Lines changed: 113 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
use crate::schema::Team;
1+
use crate::data::Data;
2+
use crate::schema::RepoPermission;
23
use anyhow::Context;
4+
use std::collections::BTreeSet;
35
use std::path::{Path, PathBuf};
46

57
/// Generates the contents of `.github/CODEOWNERS`, based on
68
/// the infra admins in `infra-admins.toml`.
7-
pub fn generate_codeowners_file() -> anyhow::Result<()> {
8-
let admins = load_infra_admins()?;
9-
let codeowners_content = generate_codeowners_content(admins);
9+
pub fn generate_codeowners_file(data: Data) -> anyhow::Result<()> {
10+
let codeowners_content = generate_codeowners_content(data);
1011
std::fs::write(codeowners_path(), codeowners_content).context("cannot write CODEOWNERS")?;
1112
Ok(())
1213
}
1314

1415
/// Check if `.github/CODEOWNERS` are up-to-date, based on the
1516
/// `infra-admins.toml` file.
16-
pub fn check_codeowners() -> anyhow::Result<()> {
17-
let admins = load_infra_admins()?;
18-
let expected_codeowners = generate_codeowners_content(admins);
17+
pub fn check_codeowners(data: Data) -> anyhow::Result<()> {
18+
let expected_codeowners = generate_codeowners_content(data);
1919
let actual_codeowners =
2020
std::fs::read_to_string(codeowners_path()).context("cannot read CODEOWNERS")?;
2121
if expected_codeowners != actual_codeowners {
@@ -25,67 +25,139 @@ pub fn check_codeowners() -> anyhow::Result<()> {
2525
Ok(())
2626
}
2727

28-
fn generate_codeowners_content(admins: Vec<String>) -> String {
28+
/// We want to allow access to the data files to `team-repo-admins`
29+
/// (maintainers), while requiring a review from `infra-admins` (admins)
30+
/// for any other changes.
31+
///
32+
/// We also want to explicitly protect special data files.
33+
fn generate_codeowners_content(data: Data) -> String {
2934
use std::fmt::Write;
3035

31-
let mut output = String::new();
36+
let mut codeowners = String::new();
3237
writeln!(
33-
output,
38+
codeowners,
3439
r#"# This is an automatically generated file
3540
# Run `cargo run ci generate-codeowners` to regenerate it.
41+
# Note that the file is scanned bottom-to-top and the first match wins.
3642
"#
3743
)
3844
.unwrap();
3945

46+
// For the admins, we use just the people directly listed
47+
// in the infra-admins.toml file, without resolving
48+
// other included members, just to be extra sure that no one else is included.
49+
let admins = data
50+
.team("infra-admins")
51+
.expect("infra-admins team not found")
52+
.raw_people()
53+
.members
54+
.iter()
55+
.map(|m| m.github.as_str())
56+
.collect::<Vec<&str>>();
57+
58+
let team_repo = data
59+
.repos()
60+
.find(|r| r.org == "rust-lang" && r.name == "team")
61+
.expect("team repository not found");
62+
let mut maintainers = team_repo
63+
.access
64+
.individuals
65+
.iter()
66+
.filter_map(|(user, permission)| match permission {
67+
RepoPermission::Triage => None,
68+
RepoPermission::Write | RepoPermission::Maintain | RepoPermission::Admin => {
69+
Some(user.as_str())
70+
}
71+
})
72+
.collect::<Vec<&str>>();
73+
maintainers.extend(
74+
team_repo
75+
.access
76+
.teams
77+
.iter()
78+
.filter(|(_, permission)| match permission {
79+
RepoPermission::Triage => false,
80+
RepoPermission::Write | RepoPermission::Maintain | RepoPermission::Admin => true,
81+
})
82+
.flat_map(|(team, _)| {
83+
data.team(team)
84+
.expect(&format!("team {team} not found"))
85+
.members(&data)
86+
.expect(&format!("team {team} members couldn't be loaded"))
87+
}),
88+
);
89+
4090
let admin_list = admins
4191
.iter()
4292
.map(|admin| format!("@{admin}"))
4393
.collect::<Vec<_>>()
4494
.join(" ");
4595

46-
// Set of paths that should only be modifiable by infra-admins
47-
let mut secure_paths = vec![
48-
"/.github/".to_string(),
49-
"/src/".to_string(),
50-
"/rust_team_data/".to_string(),
96+
// The codeowners content is parsed bottom-to-top, and the first
97+
// rule that is matched will be applied. We thus write the most
98+
// general rules first, and then include specific exceptions.
99+
100+
// Any changes in the repo not matched by rules below need to have admin
101+
// approval
102+
writeln!(
103+
codeowners,
104+
r#"# If none of the rules below match, we apply this catch-all rule
105+
# and require admin approval for such a change.
106+
* {admin_list}"#
107+
)
108+
.unwrap();
109+
110+
// Data files have no owner. This means that they can be approved by
111+
// maintainers (which we want), but at the same time all maintainers will
112+
// not be pinged if a PR modified these files (which we also want).
113+
writeln!(
114+
codeowners,
115+
r#"
116+
# Data files can be approved by users with write access.
117+
# We don't list these users explicitly to avoid notifying all of them
118+
# on every change to the data files.
119+
people/**/*.toml
120+
repos/**/*.toml
121+
teams/**/*.toml
122+
"#
123+
)
124+
.unwrap();
125+
126+
// There are several data files that we want to be protected more
127+
// Notably, the properties of the team and sync-team repositories,
128+
// the infra-admins and team-repo-admins teams and also the
129+
// accounts of the infra-admins and team-repo-admins members.
130+
131+
writeln!(
132+
codeowners,
133+
"# Modifying these files requires admin approval."
134+
)
135+
.unwrap();
136+
137+
let mut protected_paths = vec![
51138
"/repos/rust-lang/team.toml".to_string(),
52139
"/repos/rust-lang/sync-team.toml".to_string(),
53140
"/teams/infra-admins.toml".to_string(),
54141
"/teams/team-repo-admins.toml".to_string(),
55-
".cargo".to_string(),
56-
"target".to_string(),
57-
"Cargo.lock".to_string(),
58-
"Cargo.toml".to_string(),
59-
"config.toml".to_string(),
60142
];
61-
for admin in admins {
62-
secure_paths.push(format!("/people/{admin}.toml"));
143+
144+
// Some users can be both admins and maintainers.
145+
let all_users = admins
146+
.iter()
147+
.chain(maintainers.iter())
148+
.collect::<BTreeSet<_>>();
149+
for user in all_users {
150+
protected_paths.push(format!("/people/{user}.toml"));
63151
}
64152

65-
for path in secure_paths {
66-
writeln!(output, "{path} {admin_list}").unwrap();
153+
for path in protected_paths {
154+
writeln!(codeowners, "{path} {admin_list}").unwrap();
67155
}
68-
output
156+
codeowners
69157
}
70158

71159
fn codeowners_path() -> PathBuf {
72160
Path::new(&env!("CARGO_MANIFEST_DIR"))
73161
.join(".github")
74162
.join("CODEOWNERS")
75163
}
76-
77-
fn load_infra_admins() -> anyhow::Result<Vec<String>> {
78-
let admins = std::fs::read_to_string(
79-
Path::new(&env!("CARGO_MANIFEST_DIR"))
80-
.join("teams")
81-
.join("infra-admins.toml"),
82-
)
83-
.context("cannot load infra-admins.toml")?;
84-
let team: Team = toml::from_str(&admins).context("cannot deserialize infra-admins")?;
85-
Ok(team
86-
.raw_people()
87-
.members
88-
.iter()
89-
.map(|member| member.github.clone())
90-
.collect())
91-
}

src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,8 +435,8 @@ fn run() -> Result<(), Error> {
435435
);
436436
}
437437
Cli::Ci(opts) => match opts {
438-
CiOpts::GenerateCodeowners => generate_codeowners_file()?,
439-
CiOpts::CheckCodeowners => check_codeowners()?,
438+
CiOpts::GenerateCodeowners => generate_codeowners_file(data)?,
439+
CiOpts::CheckCodeowners => check_codeowners(data)?,
440440
},
441441
}
442442

0 commit comments

Comments
 (0)