Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/toml-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions rust_team_data/src/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ pub struct Repo {
pub struct Crate {
pub name: String,
pub crates_io_publishing: Option<CratesIoPublishing>,
pub trusted_publishing_only: bool,
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
Expand Down
2 changes: 2 additions & 0 deletions src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,8 @@ pub(crate) struct CratesIoPublishing {
pub crates: Vec<String>,
pub workflow_filename: String,
pub environment: String,
#[serde(default)]
pub disable_other_publish_methods: bool,
}

#[derive(serde_derive::Deserialize, Debug, Clone)]
Expand Down
1 change: 1 addition & 0 deletions src/static_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
61 changes: 59 additions & 2 deletions sync-team/src/crates_io/api.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 '{}'",
Expand Down Expand Up @@ -134,6 +134,57 @@ impl CratesIoApi {
Ok(())
}

/// Get information about a crate.
pub(crate) fn get_crate(&self, krate: &str) -> anyhow::Result<CratesIoCrate> {
#[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<T: Serialize>(
&self,
Expand Down Expand Up @@ -170,3 +221,9 @@ pub(crate) struct TrustedPublishingGitHubConfig {
pub(crate) workflow_filename: String,
pub(crate) environment: Option<String>,
}

#[derive(serde::Deserialize, Debug)]
pub(crate) struct CratesIoCrate {
#[serde(rename = "trustpub_only")]
pub(crate) trusted_publishing_only: bool,
}
77 changes: 70 additions & 7 deletions sync-team/src/crates_io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CrateName, CratesIoPublishingConfig>,
crates: HashMap<CrateName, CrateConfig>,
}

impl SyncCratesIo {
Expand All @@ -32,7 +40,7 @@ impl SyncCratesIo {
dry_run: bool,
) -> anyhow::Result<Self> {
let crates_io_api = CratesIoApi::new(token, dry_run);
let crates: HashMap<CrateName, CratesIoPublishingConfig> = team_api
let crates: HashMap<CrateName, CrateConfig> = team_api
.get_repos()?
.into_iter()
.flat_map(|repo| {
Expand All @@ -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,
},
))
})
Expand All @@ -65,6 +74,7 @@ impl SyncCratesIo {

pub(crate) fn diff_all(&self) -> anyhow::Result<Diff> {
let mut config_diffs: Vec<ConfigDiff> = vec![];
let mut crate_diffs: Vec<CrateDiff> = vec![];

// Note: we currently only support one trusted publishing configuration per crate
for (krate, desired) in &self.crates {
Expand Down Expand Up @@ -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
Expand All @@ -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<ConfigDiff>,
crate_diffs: Vec<CrateDiff>,
}

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(())
Expand All @@ -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 {
Expand Down Expand Up @@ -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(())
}
}
6 changes: 4 additions & 2 deletions tests/static-api/_expected/v1/repos.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
6 changes: 4 additions & 2 deletions tests/static-api/_expected/v1/repos/some_repo.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down