Add more context to publish-failed error message (#15879)

## What does this PR try to resolve?

This PR fixes issue #15754 where workspace publish failures provided
non-actionable error messages. Previously, when publishing a workspace
failed (due to rate limiting or other errors), users only saw generic
errors like `[ERROR] failed to publish to registry` without knowing
which package failed or what packages remained to be published.

## How to test and review this PR?

**Testing**: Run `cargo test workspace_publish_rate_limit_error` to see
the improved behavior, then `cargo test publish` to ensure all tests
pass.

**Review**:
- **Commit 1**: Adds test demonstrating current problematic behavior
- **Commit 2**: Implements fix and updates test to expect improved
output

The fix transforms error messages from generic `failed to publish to
registry` to actionable `failed to publish 'package_a' v0.1.0; the
following crates have not been published yet: package_b v0.1.0,
package_c v0.1.0`. This improvement applies consistently across all
publish error scenarios, giving users clear information about what went
wrong and what remains to be done.
This commit is contained in:
Ed Page 2025-08-26 16:39:05 +00:00 committed by GitHub
commit 0bfd820f83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 139 additions and 10 deletions

View File

@ -210,7 +210,8 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
// `b`, and we uploaded `a` and `b` but only confirmed `a`, then on
// the following pass through the outer loop nothing will be ready for
// upload.
for pkg_id in plan.take_ready() {
let mut ready = plan.take_ready();
while let Some(pkg_id) = ready.pop_first() {
let (pkg, (_features, tarball)) = &pkg_dep_graph.packages[&pkg_id];
opts.gctx.shell().status("Uploading", pkg.package_id())?;
@ -236,6 +237,19 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
)?));
}
let workspace_context = || {
let mut remaining = ready.clone();
remaining.extend(plan.iter());
if !remaining.is_empty() {
format!(
"\n\nnote: the following crates have not been published yet:\n {}",
remaining.into_iter().join("\n ")
)
} else {
String::new()
}
};
transmit(
opts.gctx,
ws,
@ -244,6 +258,7 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
&mut registry,
source_ids.original,
opts.dry_run,
workspace_context,
)?;
to_confirm.insert(pkg_id);
@ -632,6 +647,7 @@ fn transmit(
registry: &mut Registry,
registry_id: SourceId,
dry_run: bool,
workspace_context: impl Fn() -> String,
) -> CargoResult<()> {
let new_crate = prepare_transmit(gctx, ws, pkg, registry_id)?;
@ -641,9 +657,15 @@ fn transmit(
return Ok(());
}
let warnings = registry
.publish(&new_crate, tarball)
.with_context(|| format!("failed to publish to registry at {}", registry.host()))?;
let warnings = registry.publish(&new_crate, tarball).with_context(|| {
format!(
"failed to publish {} v{} to registry at {}{}",
pkg.name(),
pkg.version(),
registry.host(),
workspace_context()
)
})?;
if !warnings.invalid_categories.is_empty() {
let msg = format!(

View File

@ -2260,7 +2260,7 @@ fn api_error_json() {
[PACKAGING] foo v0.0.1 ([ROOT]/foo)
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
[UPLOADING] foo v0.0.1 ([ROOT]/foo)
[ERROR] failed to publish to registry at http://127.0.0.1:[..]/
[ERROR] failed to publish foo v0.0.1 to registry at http://127.0.0.1:[..]/
Caused by:
the remote server responded with an error (status 403 Forbidden): you must be logged in
@ -2308,7 +2308,7 @@ fn api_error_200() {
[PACKAGING] foo v0.0.1 ([ROOT]/foo)
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
[UPLOADING] foo v0.0.1 ([ROOT]/foo)
[ERROR] failed to publish to registry at http://127.0.0.1:[..]/
[ERROR] failed to publish foo v0.0.1 to registry at http://127.0.0.1:[..]/
Caused by:
the remote server responded with an [ERROR] max upload size is 123
@ -2356,7 +2356,7 @@ fn api_error_code() {
[PACKAGING] foo v0.0.1 ([ROOT]/foo)
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
[UPLOADING] foo v0.0.1 ([ROOT]/foo)
[ERROR] failed to publish to registry at http://127.0.0.1:[..]/
[ERROR] failed to publish foo v0.0.1 to registry at http://127.0.0.1:[..]/
Caused by:
failed to get a 200 OK response, got 400
@ -2413,7 +2413,7 @@ fn api_curl_error() {
[PACKAGING] foo v0.0.1 ([ROOT]/foo)
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
[UPLOADING] foo v0.0.1 ([ROOT]/foo)
[ERROR] failed to publish to registry at http://127.0.0.1:[..]/
[ERROR] failed to publish foo v0.0.1 to registry at http://127.0.0.1:[..]/
Caused by:
[52] Server returned nothing (no headers, no data) (Empty reply from server)
@ -2461,7 +2461,7 @@ fn api_other_error() {
[PACKAGING] foo v0.0.1 ([ROOT]/foo)
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
[UPLOADING] foo v0.0.1 ([ROOT]/foo)
[ERROR] failed to publish to registry at http://127.0.0.1:[..]/
[ERROR] failed to publish foo v0.0.1 to registry at http://127.0.0.1:[..]/
Caused by:
invalid response body from server
@ -3608,7 +3608,7 @@ fn invalid_token() {
[PACKAGING] foo v0.0.1 ([ROOT]/foo)
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
[UPLOADING] foo v0.0.1 ([ROOT]/foo)
[ERROR] failed to publish to registry at http://127.0.0.1:[..]/
[ERROR] failed to publish foo v0.0.1 to registry at http://127.0.0.1:[..]/
Caused by:
token contains invalid characters.
@ -4402,3 +4402,110 @@ See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for
"#]])
.run();
}
#[cargo_test]
fn workspace_publish_rate_limit_error() {
let registry = registry::RegistryBuilder::new()
.http_api()
.http_index()
.add_responder("/api/v1/crates/new", |_req, _| {
// For simplicity, let's just return rate limit error for all requests
// This simulates hitting rate limit during workspace publish
Response {
code: 429,
headers: vec!["Retry-After: 3600".to_string()],
body: format!(
"You have published too many new crates in a short period of time. Please try again after Fri, 18 Jul 2025 20:00:34 GMT or email help@crates.io to have your limit increased."
).into_bytes(),
}
})
.build();
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
members = ["package_a", "package_b", "package_c"]
"#,
)
.file("src/lib.rs", "")
.file(
"package_a/Cargo.toml",
r#"
[package]
name = "package_a"
version = "0.1.0"
edition = "2015"
license = "MIT"
description = "package a"
repository = "https://github.com/test/package_a"
"#,
)
.file("package_a/src/lib.rs", "")
.file(
"package_b/Cargo.toml",
r#"
[package]
name = "package_b"
version = "0.1.0"
edition = "2015"
license = "MIT"
description = "package b"
repository = "https://github.com/test/package_b"
"#,
)
.file("package_b/src/lib.rs", "")
.file(
"package_c/Cargo.toml",
r#"
[package]
name = "package_c"
version = "0.1.0"
edition = "2015"
license = "MIT"
description = "package c"
repository = "https://github.com/test/package_c"
[dependencies]
package_a = { version = "0.1.0", path = "../package_a" }
"#,
)
.file("package_c/src/lib.rs", "")
.build();
// This demonstrates the improved error message after the fix
// The user now knows which package failed and what packages remain to be published
p.cargo("publish --workspace --no-verify")
.replace_crates_io(registry.index_url())
.with_status(101)
.with_stderr_data(str![[r#"
[UPDATING] crates.io index
[PACKAGING] package_a v0.1.0 ([ROOT]/foo/package_a)
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
[PACKAGING] package_b v0.1.0 ([ROOT]/foo/package_b)
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
[PACKAGING] package_c v0.1.0 ([ROOT]/foo/package_c)
[UPDATING] crates.io index
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
[UPLOADING] package_a v0.1.0 ([ROOT]/foo/package_a)
[ERROR] failed to publish package_a v0.1.0 to registry at http://127.0.0.1:[..]/
[NOTE] the following crates have not been published yet:
package_b v0.1.0 ([ROOT]/foo/package_b)
package_c v0.1.0 ([ROOT]/foo/package_c)
Caused by:
failed to get a 200 OK response, got 429
headers:
HTTP/1.1 429
Content-Length: 172
Connection: close
Retry-After: 3600
body:
You have published too many new crates in a short period of time. Please try again after Fri, 18 Jul 2025 20:00:34 GMT or email help@crates.io to have your limit increased.
"#]])
.run();
}