From 0bde9e4b8f45e3b3859b8be69ac988a847d89a5f Mon Sep 17 00:00:00 2001 From: konsti Date: Wed, 27 Aug 2025 18:19:10 +0200 Subject: [PATCH] Read index credentials from env for `uv publish` (#15545) We were previously missing the `index_locations.cache_index_credentials()` call in `uv publish` to load index credentials from the env. See https://github.com/astral-sh/uv/issues/11836#issuecomment-3022735011 Fixes #11836 --- Cargo.lock | 1 + crates/uv-distribution-types/src/index_url.rs | 9 ++ crates/uv-keyring/src/lib.rs | 4 +- crates/uv/Cargo.toml | 1 + crates/uv/src/commands/publish.rs | 9 +- crates/uv/src/lib.rs | 1 + crates/uv/tests/it/common/mod.rs | 2 +- crates/uv/tests/it/publish.rs | 115 +++++++++++++++++- 8 files changed, 133 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e72ced7eb..2b3f407fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5026,6 +5026,7 @@ dependencies = [ "self-replace", "serde", "serde_json", + "sha2", "similar", "tar", "tempfile", diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index 5ccd559fb..73188289a 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -8,6 +8,7 @@ use std::sync::{Arc, LazyLock, RwLock}; use itertools::Either; use rustc_hash::{FxHashMap, FxHashSet}; use thiserror::Error; +use tracing::trace; use url::{ParseError, Url}; use uv_pep508::{Scheme, VerbatimUrl, VerbatimUrlError, split_scheme}; @@ -457,6 +458,14 @@ impl<'a> IndexLocations { pub fn cache_index_credentials(&self) { for index in self.known_indexes() { if let Some(credentials) = index.credentials() { + trace!( + "Read credentials for index {}", + index + .name + .as_ref() + .map(ToString::to_string) + .unwrap_or_else(|| index.url.to_string()) + ); let credentials = Arc::new(credentials); uv_auth::store_credentials(index.raw_url(), credentials.clone()); if let Some(root_url) = index.root_url() { diff --git a/crates/uv-keyring/src/lib.rs b/crates/uv-keyring/src/lib.rs index 6675164c7..9502c91dd 100644 --- a/crates/uv-keyring/src/lib.rs +++ b/crates/uv-keyring/src/lib.rs @@ -406,7 +406,9 @@ doc_comment::doctest!("../README.md", readme); /// Instead, it contains generics that each keystore invokes in their tests, /// passing their store-specific parameters for the generic ones. mod tests { - use super::{Entry, Error, Result, credential::CredentialApi}; + use super::{Entry, Error}; + #[cfg(feature = "keyring-tests")] + use super::{Result, credential::CredentialApi}; use std::collections::HashMap; /// Create a platform-specific credential given the constructor, service, and user diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index e10a84e1f..0b6ae8ea9 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -131,6 +131,7 @@ insta = { workspace = true } predicates = { workspace = true } regex = { workspace = true } reqwest = { workspace = true, features = ["blocking"], default-features = false } +sha2 = { workspace = true } similar = { workspace = true } tar = { workspace = true } tempfile = { workspace = true } diff --git a/crates/uv/src/commands/publish.rs b/crates/uv/src/commands/publish.rs index da61b7ca5..18767d902 100644 --- a/crates/uv/src/commands/publish.rs +++ b/crates/uv/src/commands/publish.rs @@ -11,7 +11,7 @@ use uv_auth::Credentials; use uv_cache::Cache; use uv_client::{AuthIntegration, BaseClient, BaseClientBuilder, RegistryClientBuilder}; use uv_configuration::{KeyringProviderType, TrustedPublishing}; -use uv_distribution_types::{Index, IndexCapabilities, IndexLocations, IndexUrl}; +use uv_distribution_types::{IndexCapabilities, IndexLocations, IndexUrl}; use uv_publish::{ CheckUrlClient, TrustedPublishResult, check_trusted_publishing, files_for_publishing, upload, }; @@ -32,6 +32,7 @@ pub(crate) async fn publish( username: Option, password: Option, check_url: Option, + index_locations: IndexLocations, cache: &Cache, printer: Printer, ) -> Result { @@ -88,11 +89,7 @@ pub(crate) async fn publish( // Initialize the registry client. let check_url_client = if let Some(index_url) = &check_url { - let index_locations = IndexLocations::new( - vec![Index::from_index_url(index_url.clone())], - Vec::new(), - false, - ); + index_locations.cache_index_credentials(); let registry_client_builder = RegistryClientBuilder::new(cache.clone()) .retries_from_env()? .native_tls(network_settings.native_tls) diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index ac9308166..6175ee2c1 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1626,6 +1626,7 @@ async fn run(mut cli: Cli) -> Result { username, password, check_url, + index_locations, &cache, printer, ) diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 011d53a1a..2ba07c209 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -18,8 +18,8 @@ use indoc::formatdoc; use itertools::Itertools; use predicates::prelude::predicate; use regex::Regex; - use tokio::io::AsyncWriteExt; + use uv_cache::{Cache, CacheBucket}; use uv_fs::Simplified; use uv_preview::Preview; diff --git a/crates/uv/tests/it/publish.rs b/crates/uv/tests/it/publish.rs index ba86cf7e9..9f3a4e91e 100644 --- a/crates/uv/tests/it/publish.rs +++ b/crates/uv/tests/it/publish.rs @@ -1,10 +1,16 @@ use crate::common::{TestContext, uv_snapshot, venv_bin_path}; use assert_cmd::assert::OutputAssertExt; use assert_fs::fixture::{FileTouch, FileWriteStr, PathChild}; -use indoc::indoc; +use fs_err::OpenOptions; +use indoc::{formatdoc, indoc}; +use serde_json::json; +use sha2::{Digest, Sha256}; use std::env; use std::env::current_dir; +use std::io::Write; use uv_static::EnvVars; +use wiremock::matchers::{basic_auth, method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; #[test] fn username_password_no_longer_supported() { @@ -387,3 +393,110 @@ fn invalid_index() { "### ); } + +/// Ensure that we read index credentials from the environment when publishing. +/// +/// +#[tokio::test] +async fn read_index_credential_env_vars_for_check_url() { + let context = TestContext::new("3.12"); + + let server = MockServer::start().await; + + context + .init() + .arg("--name") + .arg("astral-test-private") + .arg(".") + .assert() + .success(); + + context.build().arg("--wheel").assert().success(); + + let mut file = OpenOptions::new() + .write(true) + .append(true) + .create(false) + .open(context.temp_dir.join("pyproject.toml")) + .unwrap(); + file.write_all( + formatdoc! { + r#" + [[tool.uv.index]] + name = "private-index" + url = "{index_uri}/simple/" + publish-url = "{index_uri}/upload" + "#, + index_uri = server.uri() + } + .as_bytes(), + ) + .unwrap(); + + let filename = "astral_test_private-0.1.0-py3-none-any.whl"; + let wheel = context.temp_dir.join("dist").join(filename); + let sha256 = format!("{:x}", Sha256::digest(fs_err::read(&wheel).unwrap())); + + let simple_index = json! ({ + "files": [ + { + "filename": filename, + "hashes": { + "sha256": sha256 + }, + "url": format!("{}/{}", server.uri(), filename), + } + ] + }); + Mock::given(method("GET")) + .and(path("/simple/astral-test-private/")) + .and(basic_auth("username", "secret")) + .respond_with(ResponseTemplate::new(200).set_body_raw( + simple_index.to_string().into_bytes(), + "application/vnd.pypi.simple.v1+json", + )) + .mount(&server) + .await; + + // Test that we fail without credentials + uv_snapshot!(context.filters(), context.publish() + .current_dir(&context.temp_dir) + .arg(&wheel) + .arg("--index") + .arg("private-index") + .arg("--trusted-publishing") + .arg("never"), + @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Publishing 1 file to http://[LOCALHOST]/upload + Uploading astral_test_private-0.1.0-py3-none-any.whl ([SIZE]) + error: Failed to publish `dist/astral_test_private-0.1.0-py3-none-any.whl` to http://[LOCALHOST]/upload + Caused by: Failed to send POST request + Caused by: Missing credentials for http://[LOCALHOST]/upload + " + ); + // Test that it works with credentials + uv_snapshot!(context.filters(), context.publish() + .current_dir(&context.temp_dir) + .arg(&wheel) + .arg("--index") + .arg("private-index") + .env("UV_INDEX_PRIVATE_INDEX_USERNAME", "username") + .env("UV_INDEX_PRIVATE_INDEX_PASSWORD", "secret") + .arg("--trusted-publishing") + .arg("never"), + @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Publishing 1 file to http://[LOCALHOST]/upload + File astral_test_private-0.1.0-py3-none-any.whl already exists, skipping + " + ); +}