Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 60 additions & 4 deletions cli/tools/publish/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,10 @@ async fn perform_publish(
assert_eq!(prepared_package_by_name.len(), authorizations.len());
let mut futures: FuturesUnordered<LocalBoxFuture<Result<String, AnyError>>> =
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<AnyError> = Vec::new();
loop {
let next_batch = publish_order_graph.next();

Expand Down Expand Up @@ -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::<Vec<_>>();
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::<Vec<_>>()
.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>) -> 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: <cause>").
message.push_str(&format!("\n\n* {:#}", err));
}
deno_core::anyhow::anyhow!(message)
}

async fn publish_package(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"args": "publish --token 'sadfasdf'",
"output": "publish.out",
"exitCode": 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"workspace": ["./publish_fails", "./dependent"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@foo/dependent",
"version": "1.0.0",
"exports": "./mod.ts",
"imports": {
"publish-fails": "jsr:@foo/publish-fails@1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { a } from "publish-fails";

export function dependent(): string {
return a();
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "@foo/publish-fails",
"version": "1.0.0",
"exports": "./mod.ts"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function a(): string {
return "a";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"args": "publish --token 'sadfasdf'",
"output": "publish.out",
"exitCode": 1
}
Empty file.
5 changes: 5 additions & 0 deletions tests/specs/publish/workspace_continue_on_error/a/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "@foo/a",
"version": "1.0.0",
"exports": "./mod.ts"
}
3 changes: 3 additions & 0 deletions tests/specs/publish/workspace_continue_on_error/a/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function a(): string {
return "a";
}
3 changes: 3 additions & 0 deletions tests/specs/publish/workspace_continue_on_error/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"workspace": ["./a", "./publish_fails", "./publish_fails2"]
}
26 changes: 26 additions & 0 deletions tests/specs/publish/workspace_continue_on_error/publish.out
Original file line number Diff line number Diff line change
@@ -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]
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "@foo/publish-fails",
"version": "1.0.0",
"exports": "./mod.ts"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function b(): string {
return "b";
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "@foo/publish-fails2",
"version": "1.0.0",
"exports": "./mod.ts"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function c(): string {
return "c";
}
1 change: 1 addition & 0 deletions tests/specs/serve/request_signal_streaming/main.out
Original file line number Diff line number Diff line change
@@ -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
27 changes: 21 additions & 6 deletions tests/util/server/servers/jsr_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<n>"), 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/") {
Expand Down
Loading