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
This commit is contained in:
konsti 2025-08-27 18:19:10 +02:00 committed by GitHub
parent bce30be3a5
commit 0bde9e4b8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 133 additions and 9 deletions

1
Cargo.lock generated
View File

@ -5026,6 +5026,7 @@ dependencies = [
"self-replace", "self-replace",
"serde", "serde",
"serde_json", "serde_json",
"sha2",
"similar", "similar",
"tar", "tar",
"tempfile", "tempfile",

View File

@ -8,6 +8,7 @@ use std::sync::{Arc, LazyLock, RwLock};
use itertools::Either; use itertools::Either;
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use thiserror::Error; use thiserror::Error;
use tracing::trace;
use url::{ParseError, Url}; use url::{ParseError, Url};
use uv_pep508::{Scheme, VerbatimUrl, VerbatimUrlError, split_scheme}; use uv_pep508::{Scheme, VerbatimUrl, VerbatimUrlError, split_scheme};
@ -457,6 +458,14 @@ impl<'a> IndexLocations {
pub fn cache_index_credentials(&self) { pub fn cache_index_credentials(&self) {
for index in self.known_indexes() { for index in self.known_indexes() {
if let Some(credentials) = index.credentials() { 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); let credentials = Arc::new(credentials);
uv_auth::store_credentials(index.raw_url(), credentials.clone()); uv_auth::store_credentials(index.raw_url(), credentials.clone());
if let Some(root_url) = index.root_url() { if let Some(root_url) = index.root_url() {

View File

@ -406,7 +406,9 @@ doc_comment::doctest!("../README.md", readme);
/// Instead, it contains generics that each keystore invokes in their tests, /// Instead, it contains generics that each keystore invokes in their tests,
/// passing their store-specific parameters for the generic ones. /// passing their store-specific parameters for the generic ones.
mod tests { 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; use std::collections::HashMap;
/// Create a platform-specific credential given the constructor, service, and user /// Create a platform-specific credential given the constructor, service, and user

View File

@ -131,6 +131,7 @@ insta = { workspace = true }
predicates = { workspace = true } predicates = { workspace = true }
regex = { workspace = true } regex = { workspace = true }
reqwest = { workspace = true, features = ["blocking"], default-features = false } reqwest = { workspace = true, features = ["blocking"], default-features = false }
sha2 = { workspace = true }
similar = { workspace = true } similar = { workspace = true }
tar = { workspace = true } tar = { workspace = true }
tempfile = { workspace = true } tempfile = { workspace = true }

View File

@ -11,7 +11,7 @@ use uv_auth::Credentials;
use uv_cache::Cache; use uv_cache::Cache;
use uv_client::{AuthIntegration, BaseClient, BaseClientBuilder, RegistryClientBuilder}; use uv_client::{AuthIntegration, BaseClient, BaseClientBuilder, RegistryClientBuilder};
use uv_configuration::{KeyringProviderType, TrustedPublishing}; use uv_configuration::{KeyringProviderType, TrustedPublishing};
use uv_distribution_types::{Index, IndexCapabilities, IndexLocations, IndexUrl}; use uv_distribution_types::{IndexCapabilities, IndexLocations, IndexUrl};
use uv_publish::{ use uv_publish::{
CheckUrlClient, TrustedPublishResult, check_trusted_publishing, files_for_publishing, upload, CheckUrlClient, TrustedPublishResult, check_trusted_publishing, files_for_publishing, upload,
}; };
@ -32,6 +32,7 @@ pub(crate) async fn publish(
username: Option<String>, username: Option<String>,
password: Option<String>, password: Option<String>,
check_url: Option<IndexUrl>, check_url: Option<IndexUrl>,
index_locations: IndexLocations,
cache: &Cache, cache: &Cache,
printer: Printer, printer: Printer,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
@ -88,11 +89,7 @@ pub(crate) async fn publish(
// Initialize the registry client. // Initialize the registry client.
let check_url_client = if let Some(index_url) = &check_url { let check_url_client = if let Some(index_url) = &check_url {
let index_locations = IndexLocations::new( index_locations.cache_index_credentials();
vec![Index::from_index_url(index_url.clone())],
Vec::new(),
false,
);
let registry_client_builder = RegistryClientBuilder::new(cache.clone()) let registry_client_builder = RegistryClientBuilder::new(cache.clone())
.retries_from_env()? .retries_from_env()?
.native_tls(network_settings.native_tls) .native_tls(network_settings.native_tls)

View File

@ -1626,6 +1626,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
username, username,
password, password,
check_url, check_url,
index_locations,
&cache, &cache,
printer, printer,
) )

View File

@ -18,8 +18,8 @@ use indoc::formatdoc;
use itertools::Itertools; use itertools::Itertools;
use predicates::prelude::predicate; use predicates::prelude::predicate;
use regex::Regex; use regex::Regex;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use uv_cache::{Cache, CacheBucket}; use uv_cache::{Cache, CacheBucket};
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_preview::Preview; use uv_preview::Preview;

View File

@ -1,10 +1,16 @@
use crate::common::{TestContext, uv_snapshot, venv_bin_path}; use crate::common::{TestContext, uv_snapshot, venv_bin_path};
use assert_cmd::assert::OutputAssertExt; use assert_cmd::assert::OutputAssertExt;
use assert_fs::fixture::{FileTouch, FileWriteStr, PathChild}; 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;
use std::env::current_dir; use std::env::current_dir;
use std::io::Write;
use uv_static::EnvVars; use uv_static::EnvVars;
use wiremock::matchers::{basic_auth, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[test] #[test]
fn username_password_no_longer_supported() { fn username_password_no_longer_supported() {
@ -387,3 +393,110 @@ fn invalid_index() {
"### "###
); );
} }
/// Ensure that we read index credentials from the environment when publishing.
///
/// <https://github.com/astral-sh/uv/issues/11836#issuecomment-3022735011>
#[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
"
);
}