diff --git a/crates/uv-publish/src/trusted_publishing.rs b/crates/uv-publish/src/trusted_publishing.rs index 8230e60079e6..18f59b0cb795 100644 --- a/crates/uv-publish/src/trusted_publishing.rs +++ b/crates/uv-publish/src/trusted_publishing.rs @@ -5,6 +5,7 @@ use reqwest_middleware::ClientWithMiddleware; use serde::{Deserialize, Serialize}; use std::env; use std::env::VarError; +use std::ffi::OsString; use std::fmt::Display; use thiserror::Error; use tracing::{debug, trace}; @@ -13,8 +14,10 @@ use uv_static::EnvVars; #[derive(Debug, Error)] pub enum TrustedPublishingError { - #[error(transparent)] - Var(#[from] VarError), + #[error("Environment variable {0} not set, is the `id-token: write` permission missing?")] + MissingEnvVar(&'static str), + #[error("Environment variable {0} is not valid UTF-8: `{1:?}`")] + InvalidEnvVar(&'static str, OsString), #[error(transparent)] Url(#[from] url::ParseError), #[error("Failed to fetch: `{0}`")] @@ -29,6 +32,15 @@ pub enum TrustedPublishingError { Pypi(StatusCode, String), } +impl TrustedPublishingError { + fn from_var_err(env_var: &'static str, err: VarError) -> Self { + match err { + VarError::NotPresent => Self::MissingEnvVar(env_var), + VarError::NotUnicode(os_string) => Self::InvalidEnvVar(env_var, os_string), + } + } +} + #[derive(Deserialize)] #[serde(transparent)] pub struct TrustedPublishingToken(String); @@ -75,7 +87,10 @@ pub(crate) async fn get_token( client: &ClientWithMiddleware, ) -> Result { // If this fails, we can skip the audience request. - let oidc_token_request_token = env::var(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN)?; + let oidc_token_request_token = + env::var(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN).map_err(|err| { + TrustedPublishingError::from_var_err(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN, err) + })?; // Request 1: Get the audience let audience = get_audience(registry, client).await?; @@ -125,7 +140,10 @@ async fn get_oidc_token( oidc_token_request_token: &str, client: &ClientWithMiddleware, ) -> Result { - let mut oidc_token_url = Url::parse(&env::var(EnvVars::ACTIONS_ID_TOKEN_REQUEST_URL)?)?; + let oidc_token_url = env::var(EnvVars::ACTIONS_ID_TOKEN_REQUEST_URL).map_err(|err| { + TrustedPublishingError::from_var_err(EnvVars::ACTIONS_ID_TOKEN_REQUEST_URL, err) + })?; + let mut oidc_token_url = Url::parse(&oidc_token_url)?; oidc_token_url .query_pairs_mut() .append_pair("audience", audience); diff --git a/crates/uv/tests/it/publish.rs b/crates/uv/tests/it/publish.rs index 94039ba75188..5cf993871d48 100644 --- a/crates/uv/tests/it/publish.rs +++ b/crates/uv/tests/it/publish.rs @@ -1,4 +1,5 @@ use crate::common::{uv_snapshot, TestContext}; +use uv_static::EnvVars; #[test] fn username_password_no_longer_supported() { @@ -51,3 +52,34 @@ fn invalid_token() { "### ); } + +#[test] +fn missing_trusted_publishing_permission() { + let context = TestContext::new("3.12"); + + uv_snapshot!(context.filters(), context.publish() + .arg("-u") + .arg("__token__") + .arg("-p") + .arg("dummy") + .arg("--publish-url") + .arg("https://test.pypi.org/legacy/") + .arg("--trusted-publishing") + .arg("always") + .arg("../../scripts/links/ok-1.0.0-py3-none-any.whl") + // Emulate CI + .env(EnvVars::GITHUB_ACTIONS, "true") + // Just to make sure + .env_remove(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv publish` is experimental and may change without warning + Publishing 1 file to https://test.pypi.org/legacy/ + error: Failed to obtain token for trusted publishing + Caused by: Environment variable ACTIONS_ID_TOKEN_REQUEST_TOKEN not set, is the `id-token: write` permission missing? + "### + ); +}