diff --git a/cli/tools/publish/mod.rs b/cli/tools/publish/mod.rs index fea6dd06a4a326..7ce262d9edcbc5 100644 --- a/cli/tools/publish/mod.rs +++ b/cli/tools/publish/mod.rs @@ -914,6 +914,10 @@ async fn perform_publish( assert_eq!(prepared_package_by_name.len(), authorizations.len()); let mut futures: FuturesUnordered>> = Default::default(); + // Collect the errors of any packages that failed to publish so that we can + // keep publishing the remaining (independent) packages instead of aborting on + // the first failure, then report all of them at the end. + let mut errors: Vec = Vec::new(); loop { let next_batch = publish_order_graph.next(); @@ -959,16 +963,68 @@ async fn perform_publish( } let Some(result) = futures.next().await else { - // done, ensure no circular dependency + // Done. This `?` is reached with packages still pending only when a + // package failed above and its dependents stayed blocked (we don't + // `finish_package` a failure). That's safe and won't swallow the + // collected errors: `ensure_no_pending` runs the cycle check on the + // static package graph, so blocked-but-acyclic dependents return `Ok` and + // a genuine circular dependency still surfaces here. publish_order_graph.ensure_no_pending()?; break; }; - let package_name = result?; - publish_order_graph.finish_package(&package_name); + match result { + Ok(package_name) => publish_order_graph.finish_package(&package_name), + // Record the failure (preserving its context chain) and keep going so + // that the other in-flight and queued packages are still published. We + // intentionally don't call `finish_package` here: packages that depend on + // the one that failed must not be published against a missing version. + Err(err) => errors.push(err), + } } - Ok(()) + if errors.is_empty() { + Ok(()) + } else { + // Any packages still left in `prepared_package_by_name` were never + // attempted because they depended (directly or transitively) on a package + // that failed to publish, so their dependency version never became + // available. Surface them so the skip isn't silent. + if !prepared_package_by_name.is_empty() { + let mut skipped = prepared_package_by_name + .values() + .map(|p| p.display_name()) + .collect::>(); + skipped.sort(); + log::warn!( + "{} Skipped publishing {} package(s) that depended on a package that failed to publish:\n{}", + colors::yellow("Warning"), + skipped.len(), + skipped + .iter() + .map(|name| format!(" {}", name)) + .collect::>() + .join("\n"), + ); + } + Err(combine_publish_errors(errors)) + } +} + +/// Combines the errors collected while publishing multiple packages into a +/// single error, preserving each error's context chain. +fn combine_publish_errors(mut errors: Vec) -> AnyError { + if errors.len() == 1 { + return errors.remove(0); + } + + let mut message = format!("Failed to publish {} packages:", errors.len()); + for err in &errors { + // `{:#}` renders the full anyhow context chain (e.g. + // "Failed to publish @scope/name: "). + message.push_str(&format!("\n\n* {:#}", err)); + } + deno_core::anyhow::anyhow!(message) } async fn publish_package( diff --git a/tests/specs/publish/workspace_continue_blocked_dependent/__test__.jsonc b/tests/specs/publish/workspace_continue_blocked_dependent/__test__.jsonc new file mode 100644 index 00000000000000..241bb87e044023 --- /dev/null +++ b/tests/specs/publish/workspace_continue_blocked_dependent/__test__.jsonc @@ -0,0 +1,5 @@ +{ + "args": "publish --token 'sadfasdf'", + "output": "publish.out", + "exitCode": 1 +} diff --git a/tests/specs/publish/workspace_continue_blocked_dependent/deno.json b/tests/specs/publish/workspace_continue_blocked_dependent/deno.json new file mode 100644 index 00000000000000..36d7b80d7f94ee --- /dev/null +++ b/tests/specs/publish/workspace_continue_blocked_dependent/deno.json @@ -0,0 +1,3 @@ +{ + "workspace": ["./publish_fails", "./dependent"] +} diff --git a/tests/specs/publish/workspace_continue_blocked_dependent/dependent/LICENSE b/tests/specs/publish/workspace_continue_blocked_dependent/dependent/LICENSE new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/tests/specs/publish/workspace_continue_blocked_dependent/dependent/deno.json b/tests/specs/publish/workspace_continue_blocked_dependent/dependent/deno.json new file mode 100644 index 00000000000000..108c0fd44dd9ee --- /dev/null +++ b/tests/specs/publish/workspace_continue_blocked_dependent/dependent/deno.json @@ -0,0 +1,8 @@ +{ + "name": "@foo/dependent", + "version": "1.0.0", + "exports": "./mod.ts", + "imports": { + "publish-fails": "jsr:@foo/publish-fails@1" + } +} diff --git a/tests/specs/publish/workspace_continue_blocked_dependent/dependent/mod.ts b/tests/specs/publish/workspace_continue_blocked_dependent/dependent/mod.ts new file mode 100644 index 00000000000000..a7755db0a4fc42 --- /dev/null +++ b/tests/specs/publish/workspace_continue_blocked_dependent/dependent/mod.ts @@ -0,0 +1,5 @@ +import { a } from "publish-fails"; + +export function dependent(): string { + return a(); +} diff --git a/tests/specs/publish/workspace_continue_blocked_dependent/publish.out b/tests/specs/publish/workspace_continue_blocked_dependent/publish.out new file mode 100644 index 00000000000000..4b4dc8cd5a5652 --- /dev/null +++ b/tests/specs/publish/workspace_continue_blocked_dependent/publish.out @@ -0,0 +1,17 @@ +Publishing a workspace... +[UNORDERED_START] +Check [WILDLINE] +Check [WILDLINE] +[UNORDERED_END] +Checking for slow types in the public API... +[UNORDERED_START] +Check [WILDLINE] +Check [WILDLINE] +[UNORDERED_END] +Publishing @foo/publish-fails@1.0.0 ... +Warning Skipped publishing 1 package(s) that depended on a package that failed to publish: + @foo/dependent@1.0.0 +error: Failed to publish @foo/publish-fails@1.0.0 + +Caused by: + Failed to publish @foo/publish-fails at 1.0.0: Simulated publish failure diff --git a/tests/specs/publish/workspace_continue_blocked_dependent/publish_fails/LICENSE b/tests/specs/publish/workspace_continue_blocked_dependent/publish_fails/LICENSE new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/tests/specs/publish/workspace_continue_blocked_dependent/publish_fails/deno.json b/tests/specs/publish/workspace_continue_blocked_dependent/publish_fails/deno.json new file mode 100644 index 00000000000000..05c6a0ba67447f --- /dev/null +++ b/tests/specs/publish/workspace_continue_blocked_dependent/publish_fails/deno.json @@ -0,0 +1,5 @@ +{ + "name": "@foo/publish-fails", + "version": "1.0.0", + "exports": "./mod.ts" +} diff --git a/tests/specs/publish/workspace_continue_blocked_dependent/publish_fails/mod.ts b/tests/specs/publish/workspace_continue_blocked_dependent/publish_fails/mod.ts new file mode 100644 index 00000000000000..11436944054f5f --- /dev/null +++ b/tests/specs/publish/workspace_continue_blocked_dependent/publish_fails/mod.ts @@ -0,0 +1,3 @@ +export function a(): string { + return "a"; +} diff --git a/tests/specs/publish/workspace_continue_on_error/__test__.jsonc b/tests/specs/publish/workspace_continue_on_error/__test__.jsonc new file mode 100644 index 00000000000000..241bb87e044023 --- /dev/null +++ b/tests/specs/publish/workspace_continue_on_error/__test__.jsonc @@ -0,0 +1,5 @@ +{ + "args": "publish --token 'sadfasdf'", + "output": "publish.out", + "exitCode": 1 +} diff --git a/tests/specs/publish/workspace_continue_on_error/a/LICENSE b/tests/specs/publish/workspace_continue_on_error/a/LICENSE new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/tests/specs/publish/workspace_continue_on_error/a/deno.json b/tests/specs/publish/workspace_continue_on_error/a/deno.json new file mode 100644 index 00000000000000..49b2310320a1b1 --- /dev/null +++ b/tests/specs/publish/workspace_continue_on_error/a/deno.json @@ -0,0 +1,5 @@ +{ + "name": "@foo/a", + "version": "1.0.0", + "exports": "./mod.ts" +} diff --git a/tests/specs/publish/workspace_continue_on_error/a/mod.ts b/tests/specs/publish/workspace_continue_on_error/a/mod.ts new file mode 100644 index 00000000000000..11436944054f5f --- /dev/null +++ b/tests/specs/publish/workspace_continue_on_error/a/mod.ts @@ -0,0 +1,3 @@ +export function a(): string { + return "a"; +} diff --git a/tests/specs/publish/workspace_continue_on_error/deno.json b/tests/specs/publish/workspace_continue_on_error/deno.json new file mode 100644 index 00000000000000..093833df7509b3 --- /dev/null +++ b/tests/specs/publish/workspace_continue_on_error/deno.json @@ -0,0 +1,3 @@ +{ + "workspace": ["./a", "./publish_fails", "./publish_fails2"] +} diff --git a/tests/specs/publish/workspace_continue_on_error/publish.out b/tests/specs/publish/workspace_continue_on_error/publish.out new file mode 100644 index 00000000000000..31d8997804b091 --- /dev/null +++ b/tests/specs/publish/workspace_continue_on_error/publish.out @@ -0,0 +1,26 @@ +Publishing a workspace... +[UNORDERED_START] +Check [WILDLINE] +Check [WILDLINE] +Check [WILDLINE] +[UNORDERED_END] +Checking for slow types in the public API... +[UNORDERED_START] +Check [WILDLINE] +Check [WILDLINE] +Check [WILDLINE] +[UNORDERED_END] +[UNORDERED_START] +Publishing @foo/a@1.0.0 ... +Publishing @foo/publish-fails@1.0.0 ... +Publishing @foo/publish-fails2@1.0.0 ... +[UNORDERED_END] +Successfully published @foo/a@1.0.0 +Visit http://127.0.0.1:4250/@foo/a@1.0.0 for details +error: Failed to publish 2 packages: +[UNORDERED_START] + +* Failed to publish @foo/publish-fails@1.0.0: Failed to publish @foo/publish-fails at 1.0.0: Simulated publish failure + +* Failed to publish @foo/publish-fails2@1.0.0: Failed to publish @foo/publish-fails2 at 1.0.0: Simulated publish failure +[UNORDERED_END] diff --git a/tests/specs/publish/workspace_continue_on_error/publish_fails/LICENSE b/tests/specs/publish/workspace_continue_on_error/publish_fails/LICENSE new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/tests/specs/publish/workspace_continue_on_error/publish_fails/deno.json b/tests/specs/publish/workspace_continue_on_error/publish_fails/deno.json new file mode 100644 index 00000000000000..05c6a0ba67447f --- /dev/null +++ b/tests/specs/publish/workspace_continue_on_error/publish_fails/deno.json @@ -0,0 +1,5 @@ +{ + "name": "@foo/publish-fails", + "version": "1.0.0", + "exports": "./mod.ts" +} diff --git a/tests/specs/publish/workspace_continue_on_error/publish_fails/mod.ts b/tests/specs/publish/workspace_continue_on_error/publish_fails/mod.ts new file mode 100644 index 00000000000000..6e6cb044face1b --- /dev/null +++ b/tests/specs/publish/workspace_continue_on_error/publish_fails/mod.ts @@ -0,0 +1,3 @@ +export function b(): string { + return "b"; +} diff --git a/tests/specs/publish/workspace_continue_on_error/publish_fails2/LICENSE b/tests/specs/publish/workspace_continue_on_error/publish_fails2/LICENSE new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/tests/specs/publish/workspace_continue_on_error/publish_fails2/deno.json b/tests/specs/publish/workspace_continue_on_error/publish_fails2/deno.json new file mode 100644 index 00000000000000..62b565f2cfe618 --- /dev/null +++ b/tests/specs/publish/workspace_continue_on_error/publish_fails2/deno.json @@ -0,0 +1,5 @@ +{ + "name": "@foo/publish-fails2", + "version": "1.0.0", + "exports": "./mod.ts" +} diff --git a/tests/specs/publish/workspace_continue_on_error/publish_fails2/mod.ts b/tests/specs/publish/workspace_continue_on_error/publish_fails2/mod.ts new file mode 100644 index 00000000000000..bcf8729250d2b2 --- /dev/null +++ b/tests/specs/publish/workspace_continue_on_error/publish_fails2/mod.ts @@ -0,0 +1,3 @@ +export function c(): string { + return "c"; +} diff --git a/tests/specs/serve/request_signal_streaming/main.out b/tests/specs/serve/request_signal_streaming/main.out index 289f53a216a603..6a206b2fd408fa 100644 --- a/tests/specs/serve/request_signal_streaming/main.out +++ b/tests/specs/serve/request_signal_streaming/main.out @@ -1,3 +1,4 @@ +Deno.serve: request.signal aborts on successful responses (legacy behavior, see https://github.com/denoland/deno/issues/29111). Move cleanup to the handler's return path, or opt in to the new behavior with --unstable-no-legacy-abort. See https://docs.deno.com/runtime/reference/migrate-deprecations/ body: chunk1;chunk2;chunk3;chunk4;chunk5; aborted during stream: false aborted after completion: true diff --git a/tests/util/server/servers/jsr_registry.rs b/tests/util/server/servers/jsr_registry.rs index a591c4a69e0374..c255ed822e4b99 100644 --- a/tests/util/server/servers/jsr_registry.rs +++ b/tests/util/server/servers/jsr_registry.rs @@ -144,12 +144,27 @@ async fn registry_server_handler( .body(UnsyncBoxBody::new(Empty::new()))?; return Ok(res); } else if path.starts_with("/api/scopes/") { - let body = serde_json::to_string_pretty(&json!({ - "id": "sdfwqer-sffg-qwerasdf", - "status": "success", - "error": null - })) - .unwrap(); + // Allow tests to simulate a publish failure for an individual package by + // naming the package "publish-fails" (or "publish-fails"), used to + // exercise that publishing a workspace keeps going after a package fails. + let body = if path.contains("/packages/publish-fails") { + serde_json::to_string_pretty(&json!({ + "id": "sdfwqer-sffg-qwerasdf", + "status": "failure", + "error": { + "code": "internalServerError", + "message": "Simulated publish failure" + } + })) + .unwrap() + } else { + serde_json::to_string_pretty(&json!({ + "id": "sdfwqer-sffg-qwerasdf", + "status": "success", + "error": null + })) + .unwrap() + }; let res = Response::new(UnsyncBoxBody::new(Full::from(body))); return Ok(res); } else if path.starts_with("/api/publish_status/") {