1
- use crate :: schema:: Team ;
1
+ use crate :: data:: Data ;
2
+ use crate :: schema:: RepoPermission ;
2
3
use anyhow:: Context ;
4
+ use std:: collections:: BTreeSet ;
3
5
use std:: path:: { Path , PathBuf } ;
4
6
5
7
/// Generates the contents of `.github/CODEOWNERS`, based on
6
8
/// 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) ;
10
11
std:: fs:: write ( codeowners_path ( ) , codeowners_content) . context ( "cannot write CODEOWNERS" ) ?;
11
12
Ok ( ( ) )
12
13
}
13
14
14
15
/// Check if `.github/CODEOWNERS` are up-to-date, based on the
15
16
/// `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) ;
19
19
let actual_codeowners =
20
20
std:: fs:: read_to_string ( codeowners_path ( ) ) . context ( "cannot read CODEOWNERS" ) ?;
21
21
if expected_codeowners != actual_codeowners {
@@ -25,67 +25,139 @@ pub fn check_codeowners() -> anyhow::Result<()> {
25
25
Ok ( ( ) )
26
26
}
27
27
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 {
29
34
use std:: fmt:: Write ;
30
35
31
- let mut output = String :: new ( ) ;
36
+ let mut codeowners = String :: new ( ) ;
32
37
writeln ! (
33
- output ,
38
+ codeowners ,
34
39
r#"# This is an automatically generated file
35
40
# Run `cargo run ci generate-codeowners` to regenerate it.
41
+ # Note that the file is scanned bottom-to-top and the first match wins.
36
42
"#
37
43
)
38
44
. unwrap ( ) ;
39
45
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
+
40
90
let admin_list = admins
41
91
. iter ( )
42
92
. map ( |admin| format ! ( "@{admin}" ) )
43
93
. collect :: < Vec < _ > > ( )
44
94
. join ( " " ) ;
45
95
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 ! [
51
138
"/repos/rust-lang/team.toml" . to_string( ) ,
52
139
"/repos/rust-lang/sync-team.toml" . to_string( ) ,
53
140
"/teams/infra-admins.toml" . to_string( ) ,
54
141
"/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( ) ,
60
142
] ;
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" ) ) ;
63
151
}
64
152
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 ( ) ;
67
155
}
68
- output
156
+ codeowners
69
157
}
70
158
71
159
fn codeowners_path ( ) -> PathBuf {
72
160
Path :: new ( & env ! ( "CARGO_MANIFEST_DIR" ) )
73
161
. join ( ".github" )
74
162
. join ( "CODEOWNERS" )
75
163
}
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
- }
0 commit comments