From d6579c5b42956474866d8d523b97b80c7bfc9694 Mon Sep 17 00:00:00 2001 From: divybot Date: Thu, 11 Jun 2026 12:01:20 +0530 Subject: [PATCH 1/3] fix(publish): continue publishing workspace after a package fails When publishing a workspace with multiple packages, a failure while publishing one package would fast-fail the whole process via the `?` operator in `perform_publish`, aborting before the remaining in-flight and queued packages (and steps like provenance attestation) were handled. Collect publish errors instead of propagating the first one: keep publishing the other independent packages, preserve each error's context chain, and exit non-zero at the end reporting all collected failures. Packages depending on a failed package are still skipped (we don't call `finish_package` for a failure) so they're never published against a missing version. Co-Authored-By: Divy Srivastava --- cli/tools/publish/mod.rs | 36 +++++++++++++++++-- .../__test__.jsonc | 5 +++ .../workspace_continue_on_error/a/LICENSE | 0 .../workspace_continue_on_error/a/deno.json | 5 +++ .../workspace_continue_on_error/a/mod.ts | 3 ++ .../workspace_continue_on_error/deno.json | 3 ++ .../workspace_continue_on_error/publish.out | 26 ++++++++++++++ .../publish_fails/LICENSE | 0 .../publish_fails/deno.json | 5 +++ .../publish_fails/mod.ts | 3 ++ .../publish_fails2/LICENSE | 0 .../publish_fails2/deno.json | 5 +++ .../publish_fails2/mod.ts | 3 ++ tests/util/server/servers/jsr_registry.rs | 27 ++++++++++---- 14 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 tests/specs/publish/workspace_continue_on_error/__test__.jsonc create mode 100644 tests/specs/publish/workspace_continue_on_error/a/LICENSE create mode 100644 tests/specs/publish/workspace_continue_on_error/a/deno.json create mode 100644 tests/specs/publish/workspace_continue_on_error/a/mod.ts create mode 100644 tests/specs/publish/workspace_continue_on_error/deno.json create mode 100644 tests/specs/publish/workspace_continue_on_error/publish.out create mode 100644 tests/specs/publish/workspace_continue_on_error/publish_fails/LICENSE create mode 100644 tests/specs/publish/workspace_continue_on_error/publish_fails/deno.json create mode 100644 tests/specs/publish/workspace_continue_on_error/publish_fails/mod.ts create mode 100644 tests/specs/publish/workspace_continue_on_error/publish_fails2/LICENSE create mode 100644 tests/specs/publish/workspace_continue_on_error/publish_fails2/deno.json create mode 100644 tests/specs/publish/workspace_continue_on_error/publish_fails2/mod.ts diff --git a/cli/tools/publish/mod.rs b/cli/tools/publish/mod.rs index 8fcd2a98b3ce56..dd0ef5f98af8be 100644 --- a/cli/tools/publish/mod.rs +++ b/cli/tools/publish/mod.rs @@ -852,6 +852,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(); @@ -902,11 +906,37 @@ async fn perform_publish( 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 { + 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_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/util/server/servers/jsr_registry.rs b/tests/util/server/servers/jsr_registry.rs index be15a185fe810e..4ad4a0761d0dce 100644 --- a/tests/util/server/servers/jsr_registry.rs +++ b/tests/util/server/servers/jsr_registry.rs @@ -121,12 +121,27 @@ async fn registry_server_handler( let res = Response::new(UnsyncBoxBody::new(Full::from(body))); 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/") { From 50df891ae85a1b1b3d33278725492d045ffeda5f Mon Sep 17 00:00:00 2001 From: divybot Date: Sat, 13 Jun 2026 13:53:06 +0530 Subject: [PATCH 2/3] test: cover blocked-dependent case and report skipped packages Address review feedback on #35133: - Add a spec test where a package depends on a failing one, locking in that blocked dependents stay unpublished and ensure_no_pending() does not swallow the collected errors. - Report skipped (blocked) dependents instead of silently dropping them. - Document why the retained ensure_no_pending()? is safe. --- cli/tools/publish/mod.rs | 28 ++++++++++++++++++- .../__test__.jsonc | 5 ++++ .../deno.json | 3 ++ .../dependent/LICENSE | 0 .../dependent/deno.json | 8 ++++++ .../dependent/mod.ts | 5 ++++ .../publish.out | 17 +++++++++++ .../publish_fails/LICENSE | 0 .../publish_fails/deno.json | 5 ++++ .../publish_fails/mod.ts | 3 ++ 10 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 tests/specs/publish/workspace_continue_blocked_dependent/__test__.jsonc create mode 100644 tests/specs/publish/workspace_continue_blocked_dependent/deno.json create mode 100644 tests/specs/publish/workspace_continue_blocked_dependent/dependent/LICENSE create mode 100644 tests/specs/publish/workspace_continue_blocked_dependent/dependent/deno.json create mode 100644 tests/specs/publish/workspace_continue_blocked_dependent/dependent/mod.ts create mode 100644 tests/specs/publish/workspace_continue_blocked_dependent/publish.out create mode 100644 tests/specs/publish/workspace_continue_blocked_dependent/publish_fails/LICENSE create mode 100644 tests/specs/publish/workspace_continue_blocked_dependent/publish_fails/deno.json create mode 100644 tests/specs/publish/workspace_continue_blocked_dependent/publish_fails/mod.ts diff --git a/cli/tools/publish/mod.rs b/cli/tools/publish/mod.rs index dd0ef5f98af8be..dc310e7176bf68 100644 --- a/cli/tools/publish/mod.rs +++ b/cli/tools/publish/mod.rs @@ -901,7 +901,12 @@ 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; }; @@ -919,6 +924,27 @@ async fn perform_publish( 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)) } } 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"; +} From 1dc97f99934d98a56a97404aaa5679c65f44ccf4 Mon Sep 17 00:00:00 2001 From: divybot Date: Sat, 13 Jun 2026 15:13:56 +0530 Subject: [PATCH 3/3] fix(test): expect legacy-abort deprecation warning in request_signal_streaming This test (added in #35049) and the legacy-abort deprecation warning (added in #34397) landed in parallel and collided on main: the test exercises legacy abort with a handler that accesses request.signal and returns successfully, which is exactly what #34397 warns about, but its main.out wasn't updated. main has been failing this spec test on every commit since #34397, which blocks CI for this PR (and every other) once merged with main. The warning is correct for this scenario, so expect it in the test output. Folded in here (rather than the standalone #35191) to make this PR's CI self-sufficient; happy to split back out if preferred. --- tests/specs/serve/request_signal_streaming/main.out | 1 + 1 file changed, 1 insertion(+) 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