Skip to content

Commit 162e7ad

Browse files
authored
Merge pull request #2160 from Kobzol/remove-unused-trusted-publishing
Batch load trusted publishing configs and remove unused ones
2 parents 376e58f + fccd667 commit 162e7ad

File tree

4 files changed

+170
-37
lines changed

4 files changed

+170
-37
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ jobs:
104104
ZULIP_API_TOKEN: ${{ secrets.ZULIP_API_TOKEN }}
105105
ZULIP_USERNAME: ${{ secrets.ZULIP_USERNAME }}
106106
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
107+
CRATES_IO_USERNAME: "rust-lang-owner"
107108
run: |
108109
cargo run sync apply --src build
109110

sync-team/src/crates_io/api.rs

Lines changed: 114 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use reqwest::header;
77
use reqwest::header::{HeaderMap, HeaderValue};
88
use secrecy::{ExposeSecret, SecretString};
99
use serde::Serialize;
10+
use serde::de::DeserializeOwned;
11+
use std::collections::HashMap;
1012
use std::fmt::{Display, Formatter};
1113

1214
// OpenAPI spec: https://crates.io/api/openapi.json
@@ -42,26 +44,50 @@ impl CratesIoApi {
4244
self.dry_run
4345
}
4446

45-
/// List existing trusted publishing configurations for a given crate.
46-
pub(crate) fn list_trusted_publishing_github_configs(
47-
&self,
48-
krate: &str,
49-
) -> anyhow::Result<Vec<TrustedPublishingGitHubConfig>> {
47+
/// Return the user ID based on the username.
48+
pub(crate) fn get_user_id(&self, username: &str) -> anyhow::Result<UserId> {
5049
#[derive(serde::Deserialize)]
51-
struct GetTrustedPublishing {
52-
github_configs: Vec<TrustedPublishingGitHubConfig>,
50+
struct User {
51+
id: u32,
5352
}
5453

55-
let response: GetTrustedPublishing = self
54+
#[derive(serde::Deserialize)]
55+
struct UserResponse {
56+
user: User,
57+
}
58+
59+
let response: UserResponse = self
5660
.req::<()>(
5761
reqwest::Method::GET,
58-
&format!("/trusted_publishing/github_configs?crate={krate}"),
62+
&format!("/users/{username}"),
63+
HashMap::new(),
5964
None,
6065
)?
6166
.error_for_status()?
6267
.json_annotated()?;
6368

64-
Ok(response.github_configs)
69+
Ok(UserId(response.user.id))
70+
}
71+
72+
/// List existing trusted publishing configurations for a given crate.
73+
pub(crate) fn list_trusted_publishing_github_configs(
74+
&self,
75+
user_id: UserId,
76+
) -> anyhow::Result<Vec<TrustedPublishingGitHubConfig>> {
77+
#[derive(serde::Deserialize)]
78+
struct GetTrustedPublishing {
79+
github_configs: Vec<TrustedPublishingGitHubConfig>,
80+
}
81+
82+
let mut configs = vec![];
83+
self.req_paged::<(), GetTrustedPublishing, _>(
84+
"/trusted_publishing/github_configs",
85+
HashMap::from([("user_id".to_string(), user_id.0.to_string())]),
86+
None,
87+
|resp| configs.extend(resp.github_configs),
88+
)?;
89+
90+
Ok(configs)
6591
}
6692

6793
/// Create a new trusted publishing configuration for a given crate.
@@ -110,6 +136,7 @@ impl CratesIoApi {
110136
self.req(
111137
reqwest::Method::POST,
112138
"/trusted_publishing/github_configs",
139+
HashMap::new(),
113140
Some(&request),
114141
)?
115142
.error_for_status()
@@ -129,6 +156,7 @@ impl CratesIoApi {
129156
self.req::<()>(
130157
reqwest::Method::DELETE,
131158
&format!("/trusted_publishing/github_configs/{}", id.0),
159+
HashMap::new(),
132160
None,
133161
)?
134162
.error_for_status()
@@ -138,20 +166,24 @@ impl CratesIoApi {
138166
Ok(())
139167
}
140168

141-
/// Get information about a crate.
142-
pub(crate) fn get_crate(&self, krate: &str) -> anyhow::Result<CratesIoCrate> {
169+
/// Return all crates owned by the given user.
170+
pub(crate) fn get_crates_owned_by(&self, user: UserId) -> anyhow::Result<Vec<CratesIoCrate>> {
143171
#[derive(serde::Deserialize)]
144-
struct CrateResponse {
145-
#[serde(rename = "crate")]
146-
krate: CratesIoCrate,
172+
struct CratesResponse {
173+
crates: Vec<CratesIoCrate>,
147174
}
148175

149-
let response: CrateResponse = self
150-
.req::<()>(reqwest::Method::GET, &format!("/crates/{krate}"), None)?
151-
.error_for_status()?
152-
.json_annotated()?;
176+
let mut crates = vec![];
177+
self.req_paged::<(), CratesResponse, _>(
178+
"/crates",
179+
HashMap::from([("user_id".to_string(), user.0.to_string())]),
180+
None,
181+
|res| {
182+
crates.extend(res.crates);
183+
},
184+
)?;
153185

154-
Ok(response.krate)
186+
Ok(crates)
155187
}
156188

157189
/// Enable or disable the `trustpub_only` crate option, which specifies whether a crate
@@ -176,6 +208,7 @@ impl CratesIoApi {
176208
self.req(
177209
reqwest::Method::PATCH,
178210
&format!("/crates/{krate}"),
211+
HashMap::new(),
179212
Some(&PatchCrateRequest {
180213
krate: Crate {
181214
trustpub_only: value,
@@ -194,20 +227,76 @@ impl CratesIoApi {
194227
&self,
195228
method: reqwest::Method,
196229
path: &str,
230+
query: HashMap<String, String>,
197231
data: Option<&T>,
198232
) -> anyhow::Result<reqwest::blocking::Response> {
199233
let mut req = self
200234
.client
201235
.request(method, format!("{CRATES_IO_BASE_URL}{path}"))
202-
.bearer_auth(self.token.expose_secret());
236+
.bearer_auth(self.token.expose_secret())
237+
.query(&query);
203238
if let Some(data) = data {
204239
req = req.json(data);
205240
}
206241

207242
Ok(req.send()?)
208243
}
244+
245+
/// Fetch a resource that is paged.
246+
fn req_paged<T: Serialize, R: DeserializeOwned, F>(
247+
&self,
248+
path: &str,
249+
mut query: HashMap<String, String>,
250+
data: Option<&T>,
251+
mut handle_response: F,
252+
) -> anyhow::Result<()>
253+
where
254+
F: FnMut(R),
255+
{
256+
#[derive(serde::Deserialize, Debug)]
257+
struct Response<R> {
258+
meta: MetaResponse,
259+
#[serde(flatten)]
260+
data: R,
261+
}
262+
263+
#[derive(serde::Deserialize, Debug)]
264+
struct MetaResponse {
265+
next_page: Option<String>,
266+
}
267+
268+
if !query.contains_key("per_page") {
269+
query.insert("per_page".to_string(), "100".to_string());
270+
}
271+
272+
let mut query = query;
273+
let mut path_extra: Option<String> = None;
274+
loop {
275+
let path = match path_extra {
276+
Some(p) => format!("{path}{p}"),
277+
None => path.to_owned(),
278+
};
279+
280+
let response: Response<R> = self
281+
.req(reqwest::Method::GET, &path, query, data)?
282+
.error_for_status()?
283+
.json_annotated()?;
284+
handle_response(response.data);
285+
match response.meta.next_page {
286+
Some(next) => {
287+
path_extra = Some(next);
288+
query = HashMap::new();
289+
}
290+
None => break,
291+
}
292+
}
293+
Ok(())
294+
}
209295
}
210296

297+
#[derive(Copy, Clone, Debug)]
298+
pub struct UserId(pub u32);
299+
211300
#[derive(serde::Deserialize, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
212301
pub struct TrustedPublishingId(u64);
213302

@@ -217,8 +306,10 @@ impl Display for TrustedPublishingId {
217306
}
218307
}
219308

220-
#[derive(serde::Deserialize, Debug)]
309+
#[derive(serde::Deserialize, Debug, Clone)]
221310
pub(crate) struct TrustedPublishingGitHubConfig {
311+
#[serde(rename = "crate")]
312+
pub(crate) krate: String,
222313
pub(crate) id: TrustedPublishingId,
223314
pub(crate) repository_owner: String,
224315
pub(crate) repository_name: String,
@@ -228,6 +319,7 @@ pub(crate) struct TrustedPublishingGitHubConfig {
228319

229320
#[derive(serde::Deserialize, Debug)]
230321
pub(crate) struct CratesIoCrate {
322+
pub(crate) name: String,
231323
#[serde(rename = "trustpub_only")]
232324
pub(crate) trusted_publishing_only: bool,
233325
}

sync-team/src/crates_io/mod.rs

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ mod api;
33
use crate::team_api::TeamApi;
44
use std::cmp::Ordering;
55

6-
use crate::crates_io::api::{CratesIoApi, TrustedPublishingGitHubConfig};
6+
use crate::crates_io::api::{CratesIoApi, CratesIoCrate, TrustedPublishingGitHubConfig, UserId};
77
use anyhow::Context;
88
use secrecy::SecretString;
99
use std::collections::HashMap;
@@ -31,15 +31,20 @@ struct CrateConfig {
3131
pub(crate) struct SyncCratesIo {
3232
crates_io_api: CratesIoApi,
3333
crates: HashMap<CrateName, CrateConfig>,
34+
user_id: UserId,
35+
username: String,
3436
}
3537

3638
impl SyncCratesIo {
3739
pub(crate) fn new(
3840
token: SecretString,
41+
username: String,
3942
team_api: &TeamApi,
4043
dry_run: bool,
4144
) -> anyhow::Result<Self> {
4245
let crates_io_api = CratesIoApi::new(token, dry_run);
46+
let user_id = crates_io_api.get_user_id(&username)?;
47+
4348
let crates: HashMap<CrateName, CrateConfig> = team_api
4449
.get_repos()?
4550
.into_iter()
@@ -69,6 +74,8 @@ impl SyncCratesIo {
6974
Ok(Self {
7075
crates_io_api,
7176
crates,
77+
user_id,
78+
username,
7279
})
7380
}
7481

@@ -77,6 +84,31 @@ impl SyncCratesIo {
7784
let mut crate_diffs: Vec<CrateDiff> = vec![];
7885

7986
let is_ci_dry_run = std::env::var("CI").is_ok() && self.crates_io_api.is_dry_run();
87+
let mut tp_configs = if is_ci_dry_run {
88+
HashMap::new()
89+
} else {
90+
let tp_configs = self
91+
.crates_io_api
92+
.list_trusted_publishing_github_configs(self.user_id)
93+
.with_context(|| {
94+
format!("Failed to list configs for user_id `{:?}`", self.user_id)
95+
})?;
96+
let tp_configs: HashMap<String, Vec<TrustedPublishingGitHubConfig>> = tp_configs
97+
.into_iter()
98+
.fold(HashMap::new(), |mut map, config| {
99+
map.entry(config.krate.clone()).or_default().push(config);
100+
map
101+
});
102+
tp_configs
103+
};
104+
105+
// Batch load all crates owned by the current user
106+
let crates: HashMap<String, CratesIoCrate> = self
107+
.crates_io_api
108+
.get_crates_owned_by(self.user_id)?
109+
.into_iter()
110+
.map(|krate| (krate.name.clone(), krate))
111+
.collect();
80112

81113
// Note: we currently only support one trusted publishing configuration per crate
82114
for (krate, desired) in &self.crates {
@@ -85,15 +117,15 @@ impl SyncCratesIo {
85117
// to enable doing a crates.io dry-run without a privileged token.
86118
// Because crates.io does not currently support read-only token
87119
if !is_ci_dry_run {
88-
let mut configs = self
89-
.crates_io_api
90-
.list_trusted_publishing_github_configs(&krate.0)
91-
.with_context(|| format!("Failed to list configs for crate '{}'", krate.0))?;
120+
let mut empty_vec = vec![];
121+
let configs = tp_configs.get_mut(&krate.0).unwrap_or(&mut empty_vec);
92122

93-
// Find if there are config(s) that match what we need
123+
// Find if there are config(s) that match what we need and remove them from the list
124+
// of found configs.
94125
let matching_configs = configs
95126
.extract_if(.., |config| {
96127
let TrustedPublishingGitHubConfig {
128+
krate: _,
97129
id: _,
98130
repository_owner,
99131
repository_name,
@@ -118,22 +150,29 @@ impl SyncCratesIo {
118150
}
119151

120152
// Non-matching configs should be deleted
121-
config_diffs.extend(configs.into_iter().map(ConfigDiff::Delete));
153+
config_diffs.extend(configs.iter_mut().map(|c| ConfigDiff::Delete(c.clone())));
122154
}
123155

124-
let trusted_publish_only_expected = desired.trusted_publishing_only;
125-
let crates_io_crate = self
126-
.crates_io_api
127-
.get_crate(&krate.0)
128-
.with_context(|| anyhow::anyhow!("Cannot load crate {krate}"))?;
129-
if crates_io_crate.trusted_publishing_only != trusted_publish_only_expected {
156+
let Some(crates_io_crate) = crates.get(&krate.0) else {
157+
return Err(anyhow::anyhow!(
158+
"Crate `{krate}` is not owned by user `{0}`. Please invite `{0}` to be its owner.",
159+
self.username
160+
));
161+
};
162+
if crates_io_crate.trusted_publishing_only != desired.trusted_publishing_only {
130163
crate_diffs.push(CrateDiff::SetTrustedPublishingOnly {
131164
krate: krate.to_string(),
132-
value: trusted_publish_only_expected,
165+
value: desired.trusted_publishing_only,
133166
});
134167
}
135168
}
136169

170+
// If any trusted publishing configs remained in the hashmap, they are leftover and should
171+
// be removed.
172+
for config in tp_configs.into_values().flatten() {
173+
config_diffs.push(ConfigDiff::Delete(config));
174+
}
175+
137176
// We want to apply deletions first, and only then create new configs, to ensure that we
138177
// don't try to create a duplicate config where e.g. only the environment differs, which
139178
// would be an error in crates.io.

sync-team/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ pub fn run_sync_team(
7070
}
7171
"crates-io" => {
7272
let token = SecretString::from(get_env("CRATES_IO_TOKEN")?);
73-
let sync = SyncCratesIo::new(token, &team_api, dry_run)?;
73+
let username = get_env("CRATES_IO_USERNAME")?;
74+
let sync = SyncCratesIo::new(token, username, &team_api, dry_run)?;
7475
let diff = sync.diff_all()?;
7576
if !diff.is_empty() {
7677
info!("{diff}");

0 commit comments

Comments
 (0)