@@ -3,10 +3,12 @@ mod api;
33use crate :: team_api:: TeamApi ;
44use std:: cmp:: Ordering ;
55
6- use crate :: crates_io:: api:: { CratesIoApi , CratesIoCrate , TrustedPublishingGitHubConfig , UserId } ;
6+ use crate :: crates_io:: api:: {
7+ CratesIoApi , CratesIoCrate , CratesIoOwner , OwnerKind , TrustedPublishingGitHubConfig , UserId ,
8+ } ;
79use anyhow:: Context ;
810use secrecy:: SecretString ;
9- use std:: collections:: BTreeMap ;
11+ use std:: collections:: { BTreeMap , HashMap , HashSet } ;
1012use std:: fmt:: { Display , Formatter } ;
1113
1214#[ derive( Clone , PartialEq , Eq , PartialOrd , Ord , Hash , Debug ) ]
@@ -18,6 +20,12 @@ impl Display for CrateName {
1820 }
1921}
2022
23+ #[ derive( Clone , PartialEq , Eq , PartialOrd , Ord , Debug ) ]
24+ struct TeamOwner {
25+ org : String ,
26+ name : String ,
27+ }
28+
2129#[ derive( Clone , PartialEq , Eq , PartialOrd , Ord , Debug ) ]
2230struct CrateConfig {
2331 krate : CrateName ,
@@ -26,6 +34,7 @@ struct CrateConfig {
2634 workflow_file : String ,
2735 environment : String ,
2836 trusted_publishing_only : bool ,
37+ teams : Vec < TeamOwner > ,
2938}
3039
3140pub ( crate ) struct SyncCratesIo {
@@ -55,6 +64,7 @@ impl SyncCratesIo {
5564 let Some ( publishing) = & krate. crates_io_publishing else {
5665 return None ;
5766 } ;
67+
5868 Some ( (
5969 CrateName ( krate. name . clone ( ) ) ,
6070 CrateConfig {
@@ -64,6 +74,15 @@ impl SyncCratesIo {
6474 workflow_file : publishing. workflow_file . clone ( ) ,
6575 environment : publishing. environment . clone ( ) ,
6676 trusted_publishing_only : krate. trusted_publishing_only ,
77+ teams : krate
78+ . teams
79+ . clone ( )
80+ . into_iter ( )
81+ . map ( |owner| TeamOwner {
82+ org : owner. org ,
83+ name : owner. name ,
84+ } )
85+ . collect ( ) ,
6786 } ,
6887 ) )
6988 } )
@@ -117,6 +136,7 @@ impl SyncCratesIo {
117136 // to enable doing a crates.io dry-run without a privileged token.
118137 // Because crates.io does not currently support read-only token
119138 if !is_ci_dry_run {
139+ // Sync trusted publishing configs
120140 let mut empty_vec = vec ! [ ] ;
121141 let configs = tp_configs. get_mut ( & krate. 0 ) . unwrap_or ( & mut empty_vec) ;
122142
@@ -153,6 +173,7 @@ impl SyncCratesIo {
153173 config_diffs. extend ( configs. iter_mut ( ) . map ( |c| ConfigDiff :: Delete ( c. clone ( ) ) ) ) ;
154174 }
155175
176+ // Sync "trusted publishing only" crate option
156177 let Some ( crates_io_crate) = crates. get ( & krate. 0 ) else {
157178 return Err ( anyhow:: anyhow!(
158179 "Crate `{krate}` is not owned by user `{0}`. Please invite `{0}` to be its owner." ,
@@ -165,6 +186,48 @@ impl SyncCratesIo {
165186 value : desired. trusted_publishing_only ,
166187 } ) ;
167188 }
189+
190+ // Sync crate owners
191+ let owners = self
192+ . crates_io_api
193+ . list_crate_owners ( & krate. 0 )
194+ . with_context ( || anyhow:: anyhow!( "Cannot list crate owners of {krate}" ) ) ?;
195+
196+ // Sync team owners
197+ let existing_teams: HashSet < CratesIoOwner > = owners
198+ . iter ( )
199+ . filter ( |owner| match owner. kind ( ) {
200+ OwnerKind :: User => false ,
201+ OwnerKind :: Team => true ,
202+ } )
203+ . cloned ( )
204+ . collect ( ) ;
205+ let target_teams: HashSet < CratesIoOwner > = desired
206+ . teams
207+ . iter ( )
208+ . map ( |team| CratesIoOwner :: team ( team. org . clone ( ) , team. name . clone ( ) ) )
209+ . collect ( ) ;
210+ let teams_to_add = target_teams
211+ . difference ( & existing_teams)
212+ . cloned ( )
213+ . collect :: < Vec < _ > > ( ) ;
214+ if !teams_to_add. is_empty ( ) {
215+ crate_diffs. push ( CrateDiff :: AddOwners {
216+ krate : krate. to_string ( ) ,
217+ owners : teams_to_add,
218+ } ) ;
219+ }
220+
221+ let teams_to_remove = existing_teams
222+ . difference ( & target_teams)
223+ . cloned ( )
224+ . collect :: < Vec < _ > > ( ) ;
225+ if !teams_to_remove. is_empty ( ) {
226+ crate_diffs. push ( CrateDiff :: RemoveOwners {
227+ krate : krate. to_string ( ) ,
228+ owners : teams_to_remove,
229+ } ) ;
230+ }
168231 }
169232
170233 // If any trusted publishing configs remained in the hashmap, they are leftover and should
@@ -174,8 +237,7 @@ impl SyncCratesIo {
174237 }
175238
176239 // We want to apply deletions first, and only then create new configs, to ensure that we
177- // don't try to create a duplicate config where e.g. only the environment differs, which
178- // would be an error in crates.io.
240+ // don't try to create a duplicate config where e.g. only the environment differs.
179241 config_diffs. sort_by ( |a, b| match & ( a, b) {
180242 ( ConfigDiff :: Delete ( _) , ConfigDiff :: Create ( _) ) => Ordering :: Less ,
181243 ( ConfigDiff :: Create ( _) , ConfigDiff :: Delete ( _) ) => Ordering :: Greater ,
@@ -302,7 +364,18 @@ impl std::fmt::Display for ConfigDiff {
302364}
303365
304366enum CrateDiff {
305- SetTrustedPublishingOnly { krate : String , value : bool } ,
367+ SetTrustedPublishingOnly {
368+ krate : String ,
369+ value : bool ,
370+ } ,
371+ AddOwners {
372+ krate : String ,
373+ owners : Vec < CratesIoOwner > ,
374+ } ,
375+ RemoveOwners {
376+ krate : String ,
377+ owners : Vec < CratesIoOwner > ,
378+ } ,
306379}
307380
308381impl CrateDiff {
@@ -311,6 +384,12 @@ impl CrateDiff {
311384 Self :: SetTrustedPublishingOnly { krate, value } => sync
312385 . crates_io_api
313386 . set_trusted_publishing_only ( krate, * value) ,
387+ CrateDiff :: AddOwners { krate, owners } => {
388+ sync. crates_io_api . invite_crate_owners ( krate, owners)
389+ }
390+ CrateDiff :: RemoveOwners { krate, owners } => {
391+ sync. crates_io_api . delete_crate_owners ( krate, owners)
392+ }
314393 }
315394 }
316395}
@@ -324,6 +403,32 @@ impl std::fmt::Display for CrateDiff {
324403 " Setting trusted publishing only option for krate `{krate}` to `{value}`" ,
325404 ) ?;
326405 }
406+ CrateDiff :: AddOwners { krate, owners } => {
407+ for owner in owners {
408+ let kind = match owner. kind ( ) {
409+ OwnerKind :: User => "user" ,
410+ OwnerKind :: Team => "team" ,
411+ } ;
412+ writeln ! (
413+ f,
414+ " Adding `{kind}` owner `{}` to krate `{krate}`" ,
415+ owner. login( )
416+ ) ?;
417+ }
418+ }
419+ CrateDiff :: RemoveOwners { krate, owners } => {
420+ for owner in owners {
421+ let kind = match owner. kind ( ) {
422+ OwnerKind :: User => "user" ,
423+ OwnerKind :: Team => "team" ,
424+ } ;
425+ writeln ! (
426+ f,
427+ " Removing `{kind}` owner `{}` from krate `{krate}`" ,
428+ owner. login( )
429+ ) ?;
430+ }
431+ }
327432 }
328433 Ok ( ( ) )
329434 }
0 commit comments