mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-03 05:03:46 +00:00
Infer check URL from publish URL when known (#15886)
Some checks are pending
CI / typos (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / ecosystem test | pydantic/pydantic-core (push) Blocked by required conditions
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | aarch64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / build binary | linux libc (push) Blocked by required conditions
CI / build binary | linux aarch64 (push) Blocked by required conditions
CI / build binary | linux musl (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows x86_64 (push) Blocked by required conditions
CI / build binary | windows aarch64 (push) Blocked by required conditions
CI / build binary | msrv (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / smoke test | linux aarch64 (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / smoke test | macos (push) Blocked by required conditions
CI / smoke test | windows x86_64 (push) Blocked by required conditions
CI / smoke test | windows aarch64 (push) Blocked by required conditions
CI / integration test | activate nushell venv (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | deadsnakes python3.9 on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | aarch64 windows implicit (push) Blocked by required conditions
CI / integration test | aarch64 windows explicit (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | pyodide on ubuntu (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | graalpy on ubuntu (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / smoke test | linux (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | free-threaded python on github actions (push) Blocked by required conditions
CI / integration test | pyenv on wsl x86-64 (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | registries (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / integration test | uv_build (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on linux x86-64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | x86-64 python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows x86-64 (push) Blocked by required conditions
CI / check system | x86-64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | aarch64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | windows registry (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.8 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on windows x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on windows x86-64 (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows x86-64 (push) Blocked by required conditions
CI / benchmarks | walltime aarch64 linux (push) Blocked by required conditions
CI / benchmarks | instrumented (push) Blocked by required conditions
zizmor / Run zizmor (push) Waiting to run
Some checks are pending
CI / typos (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / ecosystem test | pydantic/pydantic-core (push) Blocked by required conditions
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | aarch64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / build binary | linux libc (push) Blocked by required conditions
CI / build binary | linux aarch64 (push) Blocked by required conditions
CI / build binary | linux musl (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows x86_64 (push) Blocked by required conditions
CI / build binary | windows aarch64 (push) Blocked by required conditions
CI / build binary | msrv (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / smoke test | linux aarch64 (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / smoke test | macos (push) Blocked by required conditions
CI / smoke test | windows x86_64 (push) Blocked by required conditions
CI / smoke test | windows aarch64 (push) Blocked by required conditions
CI / integration test | activate nushell venv (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | deadsnakes python3.9 on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | aarch64 windows implicit (push) Blocked by required conditions
CI / integration test | aarch64 windows explicit (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | pyodide on ubuntu (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | graalpy on ubuntu (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / smoke test | linux (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | free-threaded python on github actions (push) Blocked by required conditions
CI / integration test | pyenv on wsl x86-64 (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | registries (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / integration test | uv_build (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on linux x86-64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | x86-64 python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows x86-64 (push) Blocked by required conditions
CI / check system | x86-64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | aarch64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | windows registry (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.8 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on windows x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on windows x86-64 (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows x86-64 (push) Blocked by required conditions
CI / benchmarks | walltime aarch64 linux (push) Blocked by required conditions
CI / benchmarks | instrumented (push) Blocked by required conditions
zizmor / Run zizmor (push) Waiting to run
## Summary If we know the publish URL-to-check URL mapping, we can just infer it.
This commit is contained in:
parent
ac52201626
commit
422863ffde
4 changed files with 128 additions and 44 deletions
|
|
@ -6677,14 +6677,15 @@ pub struct PublishArgs {
|
|||
/// Check an index URL for existing files to skip duplicate uploads.
|
||||
///
|
||||
/// This option allows retrying publishing that failed after only some, but not all files have
|
||||
/// been uploaded, and handles error due to parallel uploads of the same file.
|
||||
/// been uploaded, and handles errors due to parallel uploads of the same file.
|
||||
///
|
||||
/// Before uploading, the index is checked. If the exact same file already exists in the index,
|
||||
/// the file will not be uploaded. If an error occurred during the upload, the index is checked
|
||||
/// again, to handle cases where the identical file was uploaded twice in parallel.
|
||||
///
|
||||
/// The exact behavior will vary based on the index. When uploading to PyPI, uploading the same
|
||||
/// file succeeds even without `--check-url`, while most other indexes error.
|
||||
/// file succeeds even without `--check-url`, while most other indexes error. When uploading to
|
||||
/// pyx, the index URL can be inferred automatically from the publish URL.
|
||||
///
|
||||
/// The index must provide one of the supported hashes (SHA-256, SHA-384, or SHA-512).
|
||||
#[arg(long, env = EnvVars::UV_PUBLISH_CHECK_URL)]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use uv_cache::Cache;
|
|||
use uv_client::{AuthIntegration, BaseClient, BaseClientBuilder, RegistryClientBuilder};
|
||||
use uv_configuration::{KeyringProviderType, TrustedPublishing};
|
||||
use uv_distribution_types::{IndexCapabilities, IndexLocations, IndexUrl};
|
||||
use uv_pep508::VerbatimUrl;
|
||||
use uv_publish::{
|
||||
CheckUrlClient, FormMetadata, PublishError, TrustedPublishResult, check_trusted_publishing,
|
||||
files_for_publishing, upload,
|
||||
|
|
@ -32,6 +33,7 @@ pub(crate) async fn publish(
|
|||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
check_url: Option<IndexUrl>,
|
||||
index: Option<String>,
|
||||
index_locations: IndexLocations,
|
||||
dry_run: bool,
|
||||
cache: &Cache,
|
||||
|
|
@ -41,6 +43,51 @@ pub(crate) async fn publish(
|
|||
bail!("Unable to publish files in offline mode");
|
||||
}
|
||||
|
||||
let token_store = PyxTokenStore::from_settings()?;
|
||||
|
||||
let (publish_url, check_url) = if let Some(index_name) = index {
|
||||
// If the user provided an index by name, look it up.
|
||||
debug!("Publishing with index {index_name}");
|
||||
let index = index_locations
|
||||
.simple_indexes()
|
||||
.find(|index| {
|
||||
index
|
||||
.name
|
||||
.as_ref()
|
||||
.is_some_and(|name| name.as_ref() == index_name)
|
||||
})
|
||||
.with_context(|| {
|
||||
let mut index_names: Vec<String> = index_locations
|
||||
.simple_indexes()
|
||||
.filter_map(|index| index.name.as_ref())
|
||||
.map(ToString::to_string)
|
||||
.collect();
|
||||
index_names.sort();
|
||||
if index_names.is_empty() {
|
||||
format!("No indexes were found, can't use index: `{index_name}`")
|
||||
} else {
|
||||
let index_names = index_names.join("`, `");
|
||||
format!("Index not found: `{index_name}`. Found indexes: `{index_names}`")
|
||||
}
|
||||
})?;
|
||||
let publish_url = index
|
||||
.publish_url
|
||||
.clone()
|
||||
.with_context(|| format!("Index is missing a publish URL: `{index_name}`"))?;
|
||||
let check_url = index.url.clone();
|
||||
(publish_url, Some(check_url))
|
||||
} else if token_store.is_known_url(&publish_url) {
|
||||
// If the user is publishing to a known index, construct the check URL from the publish
|
||||
// URL.
|
||||
let check_url = check_url.or_else(|| {
|
||||
infer_check_url(&publish_url)
|
||||
.inspect(|check_url| debug!("Inferred check URL: {check_url}"))
|
||||
});
|
||||
(publish_url, check_url)
|
||||
} else {
|
||||
(publish_url, check_url)
|
||||
};
|
||||
|
||||
let files = files_for_publishing(paths)?;
|
||||
match files.len() {
|
||||
0 => bail!("No files found to publish"),
|
||||
|
|
@ -88,9 +135,7 @@ pub(crate) async fn publish(
|
|||
// We're only checking a single URL and one at a time, so 1 permit is sufficient
|
||||
let download_concurrency = Arc::new(Semaphore::new(1));
|
||||
|
||||
// Load credentials from the token store.
|
||||
let token_store = PyxTokenStore::from_settings()?;
|
||||
|
||||
// Load credentials.
|
||||
let (publish_url, credentials) = gather_credentials(
|
||||
publish_url,
|
||||
username,
|
||||
|
|
@ -395,6 +440,50 @@ fn prompt_username_and_password() -> Result<(Option<String>, Option<String>)> {
|
|||
Ok((Some(username), Some(password)))
|
||||
}
|
||||
|
||||
/// Construct a Simple Index URL from a publish URL, if possible.
|
||||
///
|
||||
/// Matches against a publish URL of the form `/v1/upload/{workspace}/{registry}` and returns
|
||||
/// `/simple/{workspace}/{registry}`.
|
||||
fn infer_check_url(publish_url: &DisplaySafeUrl) -> Option<IndexUrl> {
|
||||
let mut segments = publish_url.path_segments()?;
|
||||
|
||||
let v1 = segments.next()?;
|
||||
if v1 != "v1" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let upload = segments.next()?;
|
||||
if upload != "upload" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let workspace = segments.next()?;
|
||||
if workspace.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let registry = segments.next()?;
|
||||
if registry.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Skip any empty segments (trailing slash handling)
|
||||
for remaining in segments {
|
||||
if !remaining.is_empty() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstruct the URL with `/simple/{workspace}/{registry}`.
|
||||
let mut check_url = publish_url.clone();
|
||||
{
|
||||
let mut segments = check_url.path_segments_mut().ok()?;
|
||||
segments.clear();
|
||||
segments.push("simple").push(workspace).push(registry);
|
||||
}
|
||||
Some(IndexUrl::from(VerbatimUrl::from(check_url)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -507,4 +596,33 @@ mod tests {
|
|||
@"The password can't be set both in the publish URL and in the CLI"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_infer_check_url() {
|
||||
let url =
|
||||
DisplaySafeUrl::from_str("https://example.com/v1/upload/workspace/registry").unwrap();
|
||||
let check_url = infer_check_url(&url);
|
||||
assert_eq!(
|
||||
check_url,
|
||||
Some(IndexUrl::from_str("https://example.com/simple/workspace/registry").unwrap())
|
||||
);
|
||||
|
||||
let url =
|
||||
DisplaySafeUrl::from_str("https://example.com/v1/upload/workspace/registry/").unwrap();
|
||||
let check_url = infer_check_url(&url);
|
||||
assert_eq!(
|
||||
check_url,
|
||||
Some(IndexUrl::from_str("https://example.com/simple/workspace/registry").unwrap())
|
||||
);
|
||||
|
||||
let url =
|
||||
DisplaySafeUrl::from_str("https://example.com/upload/workspace/registry").unwrap();
|
||||
let check_url = infer_check_url(&url);
|
||||
assert_eq!(check_url, None);
|
||||
|
||||
let url = DisplaySafeUrl::from_str("https://example.com/upload/workspace/registry/package")
|
||||
.unwrap();
|
||||
let check_url = infer_check_url(&url);
|
||||
assert_eq!(check_url, None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use std::str::FromStr;
|
|||
use std::sync::atomic::Ordering;
|
||||
|
||||
use anstream::eprintln;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use anyhow::{Result, bail};
|
||||
use clap::error::{ContextKind, ContextValue};
|
||||
use clap::{CommandFactory, Parser};
|
||||
use futures::FutureExt;
|
||||
|
|
@ -1663,42 +1663,6 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
|||
index_locations,
|
||||
} = PublishSettings::resolve(args, filesystem);
|
||||
|
||||
let (publish_url, check_url) = if let Some(index_name) = index {
|
||||
debug!("Publishing with index {index_name}");
|
||||
let index = index_locations
|
||||
.simple_indexes()
|
||||
.find(|index| {
|
||||
index
|
||||
.name
|
||||
.as_ref()
|
||||
.is_some_and(|name| name.as_ref() == index_name)
|
||||
})
|
||||
.with_context(|| {
|
||||
let mut index_names: Vec<String> = index_locations
|
||||
.simple_indexes()
|
||||
.filter_map(|index| index.name.as_ref())
|
||||
.map(ToString::to_string)
|
||||
.collect();
|
||||
index_names.sort();
|
||||
if index_names.is_empty() {
|
||||
format!("No indexes were found, can't use index: `{index_name}`")
|
||||
} else {
|
||||
let index_names = index_names.join("`, `");
|
||||
format!(
|
||||
"Index not found: `{index_name}`. Found indexes: `{index_names}`"
|
||||
)
|
||||
}
|
||||
})?;
|
||||
let publish_url = index
|
||||
.publish_url
|
||||
.clone()
|
||||
.with_context(|| format!("Index is missing a publish URL: `{index_name}`"))?;
|
||||
let check_url = index.url.clone();
|
||||
(publish_url, Some(check_url))
|
||||
} else {
|
||||
(publish_url, check_url)
|
||||
};
|
||||
|
||||
commands::publish(
|
||||
files,
|
||||
publish_url,
|
||||
|
|
@ -1708,6 +1672,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
|||
username,
|
||||
password,
|
||||
check_url,
|
||||
index,
|
||||
index_locations,
|
||||
dry_run,
|
||||
&cache,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue