-
Notifications
You must be signed in to change notification settings - Fork 299
/
Copy pathci.rs
172 lines (153 loc) · 5.57 KB
/
ci.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
use crate::data::Data;
use crate::schema::RepoPermission;
use anyhow::Context;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
/// Generates the contents of `.github/CODEOWNERS`, based on
/// the infra admins in `infra-admins.toml`.
pub fn generate_codeowners_file(data: Data) -> anyhow::Result<()> {
let codeowners_content = generate_codeowners_content(data);
std::fs::write(codeowners_path(), codeowners_content).context("cannot write CODEOWNERS")?;
Ok(())
}
/// Check if `.github/CODEOWNERS` are up-to-date, based on the
/// `infra-admins.toml` file.
pub fn check_codeowners(data: Data) -> anyhow::Result<()> {
let expected_codeowners = generate_codeowners_content(data);
let actual_codeowners =
std::fs::read_to_string(codeowners_path()).context("cannot read CODEOWNERS")?;
if expected_codeowners != actual_codeowners {
return Err(anyhow::anyhow!("CODEOWNERS content is not up-to-date. Regenerate it using `cargo run ci generate-codeowners`."));
}
Ok(())
}
/// Sensitive TOML data files.
/// PRs that modify them need to be approved by an infra-admin.
const PROTECTED_PATHS: &[&str] = &[
"/repos/rust-lang/team.toml",
"/repos/rust-lang/sync-team.toml",
"/repos/rust-lang/rust.toml",
"/teams/infra-admins.toml",
"/teams/team-repo-admins.toml",
];
/// We want to allow access to the data files to `team-repo-admins`
/// (maintainers), while requiring a review from `infra-admins` (admins)
/// for any other changes.
///
/// We also want to explicitly protect special data files.
fn generate_codeowners_content(data: Data) -> String {
use std::fmt::Write;
let mut codeowners = String::new();
writeln!(
codeowners,
r#"# This is an automatically generated file
# Run `cargo run ci generate-codeowners` to regenerate it.
# Note that the file is scanned bottom-to-top and the first match wins.
"#
)
.unwrap();
// For the admins, we use just the people directly listed
// in the infra-admins.toml file, without resolving
// other included members, just to be extra sure that no one else is included.
let admins = data
.team("infra-admins")
.expect("infra-admins team not found")
.raw_people()
.members
.iter()
.map(|m| m.github.as_str())
.collect::<Vec<&str>>();
let team_repo = data
.repos()
.find(|r| r.org == "rust-lang" && r.name == "team")
.expect("team repository not found");
let mut maintainers = team_repo
.access
.individuals
.iter()
.filter_map(|(user, permission)| match permission {
RepoPermission::Triage => None,
RepoPermission::Write | RepoPermission::Maintain | RepoPermission::Admin => {
Some(user.as_str())
}
})
.collect::<Vec<&str>>();
maintainers.extend(
team_repo
.access
.teams
.iter()
.filter(|(_, permission)| match permission {
RepoPermission::Triage => false,
RepoPermission::Write | RepoPermission::Maintain | RepoPermission::Admin => true,
})
.flat_map(|(team, _)| {
data.team(team)
.expect(&format!("team {team} not found"))
.members(&data)
.expect(&format!("team {team} members couldn't be loaded"))
}),
);
let admin_list = admins
.iter()
.map(|admin| format!("@{admin}"))
.collect::<Vec<_>>()
.join(" ");
// The codeowners content is parsed bottom-to-top, and the first
// rule that is matched will be applied. We thus write the most
// general rules first, and then include specific exceptions.
// Any changes in the repo not matched by rules below need to have admin
// approval
writeln!(
codeowners,
r#"# If none of the rules below match, we apply this catch-all rule
# and require admin approval for such a change.
* {admin_list}"#
)
.unwrap();
// Data files have no owner. This means that they can be approved by
// maintainers (which we want), but at the same time all maintainers will
// not be pinged if a PR modified these files (which we also want).
writeln!(
codeowners,
r#"
# Data files can be approved by users with write access.
# We don't list these users explicitly to avoid notifying all of them
# on every change to the data files.
/people/**/*.toml
/repos/**/*.toml
/teams/**/*.toml
# Do not require admin approvals for Markdown file modifications.
*.md
"#
)
.unwrap();
// There are several data files that we want to be protected more
// Notably, the properties of the team and sync-team repositories,
// the infra-admins and team-repo-admins teams and also the
// accounts of the infra-admins and team-repo-admins members.
writeln!(
codeowners,
"# Modifying these files requires admin approval."
)
.unwrap();
let mut protected_paths: Vec<String> =
PROTECTED_PATHS.iter().map(|&p| String::from(p)).collect();
// Some users can be both admins and maintainers.
let all_users = admins
.iter()
.chain(maintainers.iter())
.collect::<BTreeSet<_>>();
for user in all_users {
protected_paths.push(format!("/people/{user}.toml"));
}
for path in protected_paths {
writeln!(codeowners, "{path} {admin_list}").unwrap();
}
codeowners
}
fn codeowners_path() -> PathBuf {
Path::new(&env!("CARGO_MANIFEST_DIR"))
.join(".github")
.join("CODEOWNERS")
}