mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-26 13:52:51 +00:00
Hint at wrong endpoint in publish (#7872)
Improve hints when using the simple index URL instead of the upload URL in `uv publish`. This is the most common confusion when publishing, so we give it some extra care and put it more centrally in the CLI help. Fixes #7860 --------- Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
parent
9e98055a9e
commit
282fab5f70
3 changed files with 34 additions and 14 deletions
|
|
@ -4449,7 +4449,8 @@ pub struct DisplayTreeArgs {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub no_dedupe: bool,
|
pub no_dedupe: bool,
|
||||||
|
|
||||||
/// Show the reverse dependencies for the given package. This flag will invert the tree and display the packages that depend on the given package.
|
/// Show the reverse dependencies for the given package. This flag will invert the tree and
|
||||||
|
/// display the packages that depend on the given package.
|
||||||
#[arg(long, alias = "reverse")]
|
#[arg(long, alias = "reverse")]
|
||||||
pub invert: bool,
|
pub invert: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -4463,9 +4464,10 @@ pub struct PublishArgs {
|
||||||
#[arg(default_value = "dist/*")]
|
#[arg(default_value = "dist/*")]
|
||||||
pub files: Vec<String>,
|
pub files: Vec<String>,
|
||||||
|
|
||||||
/// The URL of the upload endpoint.
|
/// The URL of the upload endpoint (not the index URL).
|
||||||
///
|
///
|
||||||
/// Note that this typically differs from the index URL.
|
/// Note that there are typically different URLs for index access (e.g., `https:://.../simple`)
|
||||||
|
/// and index upload.
|
||||||
///
|
///
|
||||||
/// Defaults to PyPI's publish URL (<https://upload.pypi.org/legacy/>).
|
/// Defaults to PyPI's publish URL (<https://upload.pypi.org/legacy/>).
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -81,8 +81,12 @@ pub enum PublishSendError {
|
||||||
ReqwestMiddleware(#[from] reqwest_middleware::Error),
|
ReqwestMiddleware(#[from] reqwest_middleware::Error),
|
||||||
#[error("Upload failed with status {0}")]
|
#[error("Upload failed with status {0}")]
|
||||||
StatusNoBody(StatusCode, #[source] reqwest::Error),
|
StatusNoBody(StatusCode, #[source] reqwest::Error),
|
||||||
#[error("Upload failed with status code {0}: {1}")]
|
#[error("Upload failed with status code {0}. Server says: {1}")]
|
||||||
Status(StatusCode, String),
|
Status(StatusCode, String),
|
||||||
|
#[error("POST requests are not supported by the endpoint, are you using the simple index URL instead of the upload URL?")]
|
||||||
|
MethodNotAllowedNoBody,
|
||||||
|
#[error("POST requests are not supported by the endpoint, are you using the simple index URL instead of the upload URL? Server says: {0}")]
|
||||||
|
MethodNotAllowed(String),
|
||||||
/// The registry returned a "403 Forbidden".
|
/// The registry returned a "403 Forbidden".
|
||||||
#[error("Permission denied (status code {0}): {1}")]
|
#[error("Permission denied (status code {0}): {1}")]
|
||||||
PermissionDenied(StatusCode, String),
|
PermissionDenied(StatusCode, String),
|
||||||
|
|
@ -577,18 +581,32 @@ async fn handle_response(registry: &Url, response: Response) -> Result<bool, Pub
|
||||||
.get(reqwest::header::CONTENT_TYPE)
|
.get(reqwest::header::CONTENT_TYPE)
|
||||||
.and_then(|content_type| content_type.to_str().ok())
|
.and_then(|content_type| content_type.to_str().ok())
|
||||||
.map(ToString::to_string);
|
.map(ToString::to_string);
|
||||||
let upload_error = response
|
let upload_error = response.bytes().await.map_err(|err| {
|
||||||
.bytes()
|
if status_code == StatusCode::METHOD_NOT_ALLOWED {
|
||||||
.await
|
PublishSendError::MethodNotAllowedNoBody
|
||||||
.map_err(|err| PublishSendError::StatusNoBody(status_code, err))?;
|
} else {
|
||||||
|
PublishSendError::StatusNoBody(status_code, err)
|
||||||
|
}
|
||||||
|
})?;
|
||||||
let upload_error = String::from_utf8_lossy(&upload_error);
|
let upload_error = String::from_utf8_lossy(&upload_error);
|
||||||
|
|
||||||
trace!("Response content for non-200 for {registry}: {upload_error}");
|
trace!("Response content for non-200 response for {registry}: {upload_error}");
|
||||||
|
|
||||||
debug!("Upload error response: {upload_error}");
|
debug!("Upload error response: {upload_error}");
|
||||||
|
|
||||||
|
// That's most likely the simple index URL, not the upload URL.
|
||||||
|
if status_code == StatusCode::METHOD_NOT_ALLOWED {
|
||||||
|
return Err(PublishSendError::MethodNotAllowed(
|
||||||
|
PublishSendError::extract_error_message(
|
||||||
|
upload_error.to_string(),
|
||||||
|
content_type.as_deref(),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// Detect existing file errors the way twine does.
|
// Detect existing file errors the way twine does.
|
||||||
// https://github.com/pypa/twine/blob/c512bbf166ac38239e58545a39155285f8747a7b/twine/commands/upload.py#L34-L72
|
// https://github.com/pypa/twine/blob/c512bbf166ac38239e58545a39155285f8747a7b/twine/commands/upload.py#L34-L72
|
||||||
if status_code == 403 {
|
if status_code == StatusCode::FORBIDDEN {
|
||||||
if upload_error.contains("overwrite artifact") {
|
if upload_error.contains("overwrite artifact") {
|
||||||
// Artifactory (https://jfrog.com/artifactory/)
|
// Artifactory (https://jfrog.com/artifactory/)
|
||||||
Ok(false)
|
Ok(false)
|
||||||
|
|
@ -601,10 +619,10 @@ async fn handle_response(registry: &Url, response: Response) -> Result<bool, Pub
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
} else if status_code == 409 {
|
} else if status_code == StatusCode::CONFLICT {
|
||||||
// conflict, pypiserver (https://pypi.org/project/pypiserver)
|
// conflict, pypiserver (https://pypi.org/project/pypiserver)
|
||||||
Ok(false)
|
Ok(false)
|
||||||
} else if status_code == 400
|
} else if status_code == StatusCode::BAD_REQUEST
|
||||||
&& (upload_error.contains("updating asset") || upload_error.contains("already been taken"))
|
&& (upload_error.contains("updating asset") || upload_error.contains("already been taken"))
|
||||||
{
|
{
|
||||||
// Nexus Repository OSS (https://www.sonatype.com/nexus-repository-oss)
|
// Nexus Repository OSS (https://www.sonatype.com/nexus-repository-oss)
|
||||||
|
|
|
||||||
|
|
@ -7214,9 +7214,9 @@ uv publish [OPTIONS] [FILES]...
|
||||||
|
|
||||||
<p>This setting has no effect when used in the <code>uv pip</code> interface.</p>
|
<p>This setting has no effect when used in the <code>uv pip</code> interface.</p>
|
||||||
|
|
||||||
</dd><dt><code>--publish-url</code> <i>publish-url</i></dt><dd><p>The URL of the upload endpoint.</p>
|
</dd><dt><code>--publish-url</code> <i>publish-url</i></dt><dd><p>The URL of the upload endpoint (not the index URL).</p>
|
||||||
|
|
||||||
<p>Note that this typically differs from the index URL.</p>
|
<p>Note that there are typically different URLs for index access (e.g., <code>https:://.../simple</code>) and index upload.</p>
|
||||||
|
|
||||||
<p>Defaults to PyPI’s publish URL (<https://upload.pypi.org/legacy/>).</p>
|
<p>Defaults to PyPI’s publish URL (<https://upload.pypi.org/legacy/>).</p>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue