diff --git a/docs/toml-schema.md b/docs/toml-schema.md index 6985d2e81..8d75161e6 100644 --- a/docs/toml-schema.md +++ b/docs/toml-schema.md @@ -456,6 +456,9 @@ crates = ["regex"] workflow-filename = "ci.yml" # GitHub Actions environment that has to be used for the publishing (required) environment = "deploy" +# Disable other mechanisms for publishing this set of crates (optional, default is false) +# If set to `true`, the crates will only be publishable through trusted publishing +disable-other-publish-methods = false ``` > [!TIP] diff --git a/rust_team_data/src/v1.rs b/rust_team_data/src/v1.rs index 3318e5fac..b50eeeb52 100644 --- a/rust_team_data/src/v1.rs +++ b/rust_team_data/src/v1.rs @@ -188,6 +188,7 @@ pub struct Repo { pub struct Crate { pub name: String, pub crates_io_publishing: Option, + pub trusted_publishing_only: bool, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] diff --git a/src/schema.rs b/src/schema.rs index 76457443b..85d08a920 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -880,6 +880,8 @@ pub(crate) struct CratesIoPublishing { pub crates: Vec, pub workflow_filename: String, pub environment: String, + #[serde(default)] + pub disable_other_publish_methods: bool, } #[derive(serde_derive::Deserialize, Debug, Clone)] diff --git a/src/static_api.rs b/src/static_api.rs index e4bdadd1e..53e896ec1 100644 --- a/src/static_api.rs +++ b/src/static_api.rs @@ -157,6 +157,7 @@ impl<'a> Generator<'a> { workflow_file: p.workflow_filename.clone(), environment: p.environment.clone(), }), + trusted_publishing_only: p.disable_other_publish_methods, }) }) .collect(), diff --git a/sync-team/src/crates_io/api.rs b/sync-team/src/crates_io/api.rs index f30eb0f95..e3db2dc67 100644 --- a/sync-team/src/crates_io/api.rs +++ b/sync-team/src/crates_io/api.rs @@ -1,4 +1,4 @@ -use crate::crates_io::CratesIoPublishingConfig; +use crate::crates_io::CrateConfig; use crate::utils::ResponseExt; use anyhow::{Context, anyhow}; use log::debug; @@ -63,7 +63,7 @@ impl CratesIoApi { /// Create a new trusted publishing configuration for a given crate. pub(crate) fn create_trusted_publishing_github_config( &self, - config: &CratesIoPublishingConfig, + config: &CrateConfig, ) -> anyhow::Result<()> { debug!( "Creating trusted publishing config for '{}' in repo '{}/{}', workflow file '{}' and environment '{}'", @@ -134,6 +134,57 @@ impl CratesIoApi { Ok(()) } + /// Get information about a crate. + pub(crate) fn get_crate(&self, krate: &str) -> anyhow::Result { + #[derive(serde::Deserialize)] + struct CrateResponse { + #[serde(rename = "crate")] + krate: CratesIoCrate, + } + + let response: CrateResponse = self + .req::<()>(reqwest::Method::GET, &format!("/crates/{krate}"), None)? + .error_for_status()? + .json_annotated()?; + + Ok(response.krate) + } + + /// Enable or disable the `trustpub_only` crate option, which specifies whether a crate + /// has to be published **only** through trusted publishing. + pub(crate) fn set_trusted_publishing_only( + &self, + krate: &str, + value: bool, + ) -> anyhow::Result<()> { + #[derive(serde::Serialize)] + struct PatchCrateRequest { + #[serde(rename = "crate")] + krate: Crate, + } + + #[derive(serde::Serialize)] + struct Crate { + trustpub_only: bool, + } + + if !self.dry_run { + self.req( + reqwest::Method::PATCH, + &format!("/crates/{krate}"), + Some(&PatchCrateRequest { + krate: Crate { + trustpub_only: value, + }, + }), + )? + .error_for_status() + .with_context(|| anyhow::anyhow!("Cannot patch crate {krate}"))?; + } + + Ok(()) + } + /// Perform a request against the crates.io API fn req( &self, @@ -170,3 +221,9 @@ pub(crate) struct TrustedPublishingGitHubConfig { pub(crate) workflow_filename: String, pub(crate) environment: Option, } + +#[derive(serde::Deserialize, Debug)] +pub(crate) struct CratesIoCrate { + #[serde(rename = "trustpub_only")] + pub(crate) trusted_publishing_only: bool, +} diff --git a/sync-team/src/crates_io/mod.rs b/sync-team/src/crates_io/mod.rs index b740ee264..7aa3278a1 100644 --- a/sync-team/src/crates_io/mod.rs +++ b/sync-team/src/crates_io/mod.rs @@ -7,22 +7,30 @@ use crate::crates_io::api::{CratesIoApi, TrustedPublishingGitHubConfig}; use anyhow::Context; use secrecy::SecretString; use std::collections::HashMap; +use std::fmt::{Display, Formatter}; #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] struct CrateName(String); +impl Display for CrateName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] -struct CratesIoPublishingConfig { +struct CrateConfig { krate: CrateName, repo_org: String, repo_name: String, workflow_file: String, environment: String, + trusted_publishing_only: bool, } pub(crate) struct SyncCratesIo { crates_io_api: CratesIoApi, - crates: HashMap, + crates: HashMap, } impl SyncCratesIo { @@ -32,7 +40,7 @@ impl SyncCratesIo { dry_run: bool, ) -> anyhow::Result { let crates_io_api = CratesIoApi::new(token, dry_run); - let crates: HashMap = team_api + let crates: HashMap = team_api .get_repos()? .into_iter() .flat_map(|repo| { @@ -44,12 +52,13 @@ impl SyncCratesIo { }; Some(( CrateName(krate.name.clone()), - CratesIoPublishingConfig { + CrateConfig { krate: CrateName(krate.name.clone()), repo_org: repo.org.clone(), repo_name: repo.name.clone(), workflow_file: publishing.workflow_file.clone(), environment: publishing.environment.clone(), + trusted_publishing_only: krate.trusted_publishing_only, }, )) }) @@ -65,6 +74,7 @@ impl SyncCratesIo { pub(crate) fn diff_all(&self) -> anyhow::Result { let mut config_diffs: Vec = vec![]; + let mut crate_diffs: Vec = vec![]; // Note: we currently only support one trusted publishing configuration per crate for (krate, desired) in &self.crates { @@ -102,6 +112,18 @@ impl SyncCratesIo { // Non-matching configs should be deleted config_diffs.extend(configs.into_iter().map(ConfigDiff::Delete)); + + let trusted_publish_only_expected = desired.trusted_publishing_only; + let crates_io_crate = self + .crates_io_api + .get_crate(&krate.0) + .with_context(|| anyhow::anyhow!("Cannot load crate {krate}"))?; + if crates_io_crate.trusted_publishing_only != trusted_publish_only_expected { + crate_diffs.push(CrateDiff::SetTrustedPublishingOnly { + krate: krate.to_string(), + value: trusted_publish_only_expected, + }); + } } // We want to apply deletions first, and only then create new configs, to ensure that we @@ -114,17 +136,29 @@ impl SyncCratesIo { (ConfigDiff::Create(a), ConfigDiff::Create(b)) => a.cmp(b), }); - Ok(Diff { config_diffs }) + Ok(Diff { + config_diffs, + crate_diffs, + }) } } pub(crate) struct Diff { config_diffs: Vec, + crate_diffs: Vec, } impl Diff { pub(crate) fn apply(&self, sync: &SyncCratesIo) -> anyhow::Result<()> { - for diff in &self.config_diffs { + let Diff { + config_diffs, + crate_diffs, + } = self; + + for diff in config_diffs { + diff.apply(sync)?; + } + for diff in crate_diffs { diff.apply(sync)?; } Ok(()) @@ -149,9 +183,10 @@ impl std::fmt::Display for Diff { } enum ConfigDiff { - Create(CratesIoPublishingConfig), + Create(CrateConfig), Delete(TrustedPublishingGitHubConfig), } + impl ConfigDiff { fn apply(&self, sync: &SyncCratesIo) -> anyhow::Result<()> { match self { @@ -200,3 +235,31 @@ impl std::fmt::Display for ConfigDiff { Ok(()) } } + +enum CrateDiff { + SetTrustedPublishingOnly { krate: String, value: bool }, +} + +impl CrateDiff { + fn apply(&self, sync: &SyncCratesIo) -> anyhow::Result<()> { + match self { + Self::SetTrustedPublishingOnly { krate, value } => sync + .crates_io_api + .set_trusted_publishing_only(krate, *value), + } + } +} + +impl std::fmt::Display for CrateDiff { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::SetTrustedPublishingOnly { krate, value } => { + writeln!( + f, + " Setting trusted publishing only option for krate `{krate}` to `{value}`", + )?; + } + } + Ok(()) + } +} diff --git a/tests/static-api/_expected/v1/repos.json b/tests/static-api/_expected/v1/repos.json index 8667c8c93..7694c0680 100644 --- a/tests/static-api/_expected/v1/repos.json +++ b/tests/static-api/_expected/v1/repos.json @@ -71,14 +71,16 @@ "crates_io_publishing": { "workflow_file": "ci.yml", "environment": "deploy" - } + }, + "trusted_publishing_only": false }, { "name": "my-crate-2", "crates_io_publishing": { "workflow_file": "ci.yml", "environment": "deploy" - } + }, + "trusted_publishing_only": false } ], "environments": [], diff --git a/tests/static-api/_expected/v1/repos/some_repo.json b/tests/static-api/_expected/v1/repos/some_repo.json index 8c11f1336..de739cd88 100644 --- a/tests/static-api/_expected/v1/repos/some_repo.json +++ b/tests/static-api/_expected/v1/repos/some_repo.json @@ -39,14 +39,16 @@ "crates_io_publishing": { "workflow_file": "ci.yml", "environment": "deploy" - } + }, + "trusted_publishing_only": false }, { "name": "my-crate-2", "crates_io_publishing": { "workflow_file": "ci.yml", "environment": "deploy" - } + }, + "trusted_publishing_only": false } ], "environments": [],