mirror of https://github.com/astral-sh/uv
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:
parent
bce30be3a5
commit
0bde9e4b8f
|
|
@ -5026,6 +5026,7 @@ dependencies = [
|
||||||
"self-replace",
|
"self-replace",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"similar",
|
"similar",
|
||||||
"tar",
|
"tar",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue