Skip to content

Commit 8ccc0e2

Browse files
committed
publish: report failing crate and remaining unpublished on workspace failure
- Keep single-crate error format unchanged - Add test for workspace failure messaging
1 parent 93c3513 commit 8ccc0e2

File tree

2 files changed

+140
-42
lines changed

2 files changed

+140
-42
lines changed

src/cargo/ops/registry/publish.rs

Lines changed: 59 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
197197
)?;
198198

199199
let mut plan = PublishPlan::new(&pkg_dep_graph.graph);
200+
// Store the original list of packages to be published for error reporting
201+
let original_packages: BTreeSet<_> = plan.iter().collect();
200202
// May contains packages from previous rounds as `wait_for_any_publish_confirmation` returns
201203
// after it confirms any packages, not all packages, requiring us to handle the rest in the next
202204
// iteration.
@@ -236,47 +238,67 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
236238
)?));
237239
}
238240

239-
// Always wrap transmit with context for enhanced error reporting
240-
let transmit_result = transmit(
241-
opts.gctx,
242-
ws,
243-
pkg,
244-
tarball.file(),
245-
&mut registry,
246-
source_ids.original,
247-
opts.dry_run,
248-
).map_err(|e| {
249-
// Collect remaining packages that have not been published yet
250-
let mut remaining: Vec<_> = plan
251-
.iter()
252-
.filter(|id| *id != pkg_id)
253-
.map(|id| {
241+
let transmit_result = if original_packages.len() > 1 {
242+
// For workspace publishes, wrap transmit with enhanced error reporting
243+
transmit(
244+
opts.gctx,
245+
ws,
246+
pkg,
247+
tarball.file(),
248+
&mut registry,
249+
source_ids.original,
250+
opts.dry_run,
251+
).map_err(|e| {
252+
// Collect remaining packages that have not been published yet
253+
let mut remaining: Vec<_> = original_packages
254+
.iter()
255+
.filter(|id| **id != pkg_id)
256+
.map(|id| {
257+
let pkg = &pkg_dep_graph.packages[&id].0;
258+
format!("{} v{}", pkg.name(), pkg.version())
259+
})
260+
.collect();
261+
// Also include any packages that are still waiting for confirmation
262+
for id in to_confirm.iter().filter(|id| **id != pkg_id) {
254263
let pkg = &pkg_dep_graph.packages[&id].0;
255-
format!("{} v{}", pkg.name(), pkg.version())
256-
})
257-
.collect();
258-
// Also include any packages that are still waiting for confirmation
259-
for id in to_confirm.iter().filter(|id| **id != pkg_id) {
260-
let pkg = &pkg_dep_graph.packages[&id].0;
261-
let entry = format!("{} v{}", pkg.name(), pkg.version());
262-
if !remaining.contains(&entry) {
263-
remaining.push(entry);
264+
let entry = format!("{} v{}", pkg.name(), pkg.version());
265+
if !remaining.contains(&entry) {
266+
remaining.push(entry);
267+
}
264268
}
265-
}
266269

267-
let message = if !remaining.is_empty() {
268-
format!(
269-
"failed to publish `{}` v{}; the following crates have not been published yet: {}",
270+
let message = if !remaining.is_empty() {
271+
format!(
272+
"failed to publish `{}` v{}; the following crates have not been published yet: {}",
273+
pkg.name(),
274+
pkg.version(),
275+
remaining.join(", ")
276+
)
277+
} else {
278+
format!("failed to publish `{}` v{}", pkg.name(), pkg.version())
279+
};
280+
281+
e.context(message)
282+
})
283+
} else {
284+
// For single package publishes, preserve original top-level error message with package name
285+
transmit(
286+
opts.gctx,
287+
ws,
288+
pkg,
289+
tarball.file(),
290+
&mut registry,
291+
source_ids.original,
292+
opts.dry_run,
293+
)
294+
.map_err(|e| {
295+
e.context(format!(
296+
"failed to publish `{}` v{}",
270297
pkg.name(),
271-
pkg.version(),
272-
remaining.join(", ")
273-
)
274-
} else {
275-
format!("failed to publish `{}` v{}", pkg.name(), pkg.version())
276-
};
277-
278-
e.context(message)
279-
});
298+
pkg.version()
299+
))
300+
})
301+
};
280302

281303
if let Err(e) = transmit_result {
282304
return Err(e);

tests/testsuite/publish.rs

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4351,6 +4351,38 @@ fn all_unpublishable_packages() {
43514351
#[cargo_test]
43524352
fn workspace_publish_failure_reports_remaining_packages() {
43534353
use cargo_test_support::project;
4354+
use cargo_test_support::registry::RegistryBuilder;
4355+
4356+
// Create a registry that will fail for package 'a' but succeed for others
4357+
let _registry = RegistryBuilder::new()
4358+
.alternative()
4359+
.http_api()
4360+
.add_responder("/api/v1/crates/new", |req, _| {
4361+
if let Some(body) = &req.body {
4362+
let body_str = String::from_utf8_lossy(body);
4363+
if body_str.contains("\"name\":\"a\"") {
4364+
Response {
4365+
body: b"{\"errors\":[{\"detail\":\"crate [email protected] already exists on crates.io index\"}]}".to_vec(),
4366+
code: 400,
4367+
headers: vec!["Content-Type: application/json".to_string()],
4368+
}
4369+
} else {
4370+
Response {
4371+
body: b"{\"ok\":true}".to_vec(),
4372+
code: 200,
4373+
headers: vec!["Content-Type: application/json".to_string()],
4374+
}
4375+
}
4376+
} else {
4377+
Response {
4378+
body: b"{\"ok\":true}".to_vec(),
4379+
code: 200,
4380+
headers: vec!["Content-Type: application/json".to_string()],
4381+
}
4382+
}
4383+
})
4384+
.build();
4385+
43544386
let p = project()
43554387
.file(
43564388
"Cargo.toml",
@@ -4366,6 +4398,9 @@ fn workspace_publish_failure_reports_remaining_packages() {
43664398
name = "a"
43674399
version = "0.1.0"
43684400
edition = "2021"
4401+
authors = []
4402+
license = "MIT"
4403+
description = "Package A"
43694404
"#,
43704405
)
43714406
.file("a/src/lib.rs", "")
@@ -4376,6 +4411,9 @@ fn workspace_publish_failure_reports_remaining_packages() {
43764411
name = "b"
43774412
version = "0.1.0"
43784413
edition = "2021"
4414+
authors = []
4415+
license = "MIT"
4416+
description = "Package B"
43794417
"#,
43804418
)
43814419
.file("b/src/lib.rs", "")
@@ -4386,17 +4424,55 @@ fn workspace_publish_failure_reports_remaining_packages() {
43864424
name = "c"
43874425
version = "0.1.0"
43884426
edition = "2021"
4427+
authors = []
4428+
license = "MIT"
4429+
description = "Package C"
43894430
"#,
43904431
)
43914432
.file("c/src/lib.rs", "")
43924433
.build();
43934434

4394-
// Simulate a publish failure for package 'a' (e.g., already published)
4395-
// The error message should match the actual output from the verify step
4396-
p.cargo("publish --workspace")
4397-
.env("CARGO_REGISTRY_TOKEN", "dummy-token")
4435+
// Test that when package 'a' fails to publish, the error message includes
4436+
// information about which packages remain unpublished
4437+
p.cargo("publish --workspace --registry alternative")
43984438
.with_status(101)
4399-
.with_stderr_contains("[ERROR] crate [email protected] already exists on crates.io index")
4439+
.with_stderr_data(str![[r#"
4440+
[WARNING] virtual workspace defaulting to `resolver = "1"` despite one or more workspace members being on edition 2021 which implies `resolver = "2"`
4441+
[NOTE] to keep the current resolver, specify `workspace.resolver = "1"` in the workspace root's manifest
4442+
[NOTE] to use the edition 2021 resolver, specify `workspace.resolver = "2"` in the workspace root's manifest
4443+
[NOTE] for more details see https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions
4444+
[UPDATING] `alternative` index
4445+
[WARNING] manifest has no documentation, homepage or repository.
4446+
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
4447+
[PACKAGING] a v0.1.0 ([ROOT]/foo/a)
4448+
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
4449+
[WARNING] manifest has no documentation, homepage or repository.
4450+
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
4451+
[PACKAGING] b v0.1.0 ([ROOT]/foo/b)
4452+
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
4453+
[WARNING] manifest has no documentation, homepage or repository.
4454+
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
4455+
[PACKAGING] c v0.1.0 ([ROOT]/foo/c)
4456+
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
4457+
[VERIFYING] a v0.1.0 ([ROOT]/foo/a)
4458+
[COMPILING] a v0.1.0 ([ROOT]/foo/target/package/a-0.1.0)
4459+
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
4460+
[VERIFYING] b v0.1.0 ([ROOT]/foo/b)
4461+
[COMPILING] b v0.1.0 ([ROOT]/foo/target/package/b-0.1.0)
4462+
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
4463+
[VERIFYING] c v0.1.0 ([ROOT]/foo/c)
4464+
[COMPILING] c v0.1.0 ([ROOT]/foo/target/package/c-0.1.0)
4465+
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
4466+
[UPLOADING] a v0.1.0 ([ROOT]/foo/a)
4467+
[ERROR] failed to publish `a` v0.1.0; the following crates have not been published yet: b v0.1.0, c v0.1.0
4468+
4469+
Caused by:
4470+
failed to publish to registry at http://127.0.0.1:[..]/
4471+
4472+
Caused by:
4473+
the remote server responded with an error (status 400 Bad Request): crate [email protected] already exists on crates.io index
4474+
4475+
"#]])
44004476
.run();
44014477
}
44024478

0 commit comments

Comments
 (0)