diff --git a/crates/rover-client/src/error.rs b/crates/rover-client/src/error.rs index 1c4f2f1fc..5116e8eac 100644 --- a/crates/rover-client/src/error.rs +++ b/crates/rover-client/src/error.rs @@ -145,11 +145,20 @@ pub enum RoverClientError { check_response: CheckResponse, }, - // While checking the proposed schema, the operations task (and also build task, if run) succeeded, - // but other check tasks failed. - #[error("{}", other_check_task_failure_msg(.has_build_task))] + /// While checking the proposed schema, we encountered changes that would cause checks to fail in + /// blocking downstream variants. + #[error("{}", downstream_check_error_msg(.blocking_downstream_variants))] + DownstreamCheckFailure { + blocking_downstream_variants: Vec, + target_url: String, + }, + + /// While checking the proposed schema, the build, operations, and downstream (if run) tasks succeeded + /// or are pending, but other check tasks failed. + #[error("{}", other_check_task_failure_msg(.has_build_task,.has_downstream_task))] OtherCheckTaskFailure { has_build_task: bool, + has_downstream_task: bool, target_url: String, }, @@ -193,18 +202,6 @@ pub enum RoverClientError { ChecksTimeoutError { url: Option }, } -fn other_check_task_failure_msg(has_build_task: &bool) -> String { - let succeeding_tasks = if *has_build_task { - "The build and operations tasks".to_string() - } else { - "The operations task".to_string() - }; - format!( - "{} succeeded, but other check tasks failed.", - succeeding_tasks - ) -} - fn operation_check_error_msg(check_response: &CheckResponse) -> String { let failure_count = check_response.get_failure_count(); let plural = match failure_count { @@ -217,6 +214,37 @@ fn operation_check_error_msg(check_response: &CheckResponse) -> String { ) } +fn downstream_check_error_msg(downstream_blocking_variants: &Vec) -> String { + let variants = downstream_blocking_variants.join(","); + let plural_this = match downstream_blocking_variants.len() { + 1 => "this", + _ => "these", + }; + let plural = match downstream_blocking_variants.len() { + 1 => "", + _ => "s", + }; + format!( + "The downstream check task has encountered check failures for at least {} blocking downstream variant{}: {}.", + plural_this, + plural, + variants, + ) +} + +fn other_check_task_failure_msg(has_build_task: &bool, has_downstream_task: &bool) -> String { + let succeeding_tasks = match (*has_build_task, *has_downstream_task) { + (false, false) => "The operations task", + (true, false) => "The build and operations tasks", + (true, true) => "The build, operations, and downstream tasks", + (false, true) => unreachable!("Can't have a downstream task without a build task"), + }; + format!( + "{} succeeded or are pending, but other check tasks failed.", + succeeding_tasks + ) +} + impl From for RoverClientError { fn from(e: introspector_gadget::error::RoverClientError) -> Self { match e { diff --git a/crates/rover-client/src/operations/graph/check_workflow/runner.rs b/crates/rover-client/src/operations/graph/check_workflow/runner.rs index 067913314..1a790c937 100644 --- a/crates/rover-client/src/operations/graph/check_workflow/runner.rs +++ b/crates/rover-client/src/operations/graph/check_workflow/runner.rs @@ -7,8 +7,8 @@ use crate::RoverClientError; use graphql_client::*; -use self::graph_check_workflow_query::CheckWorkflowStatus; use self::graph_check_workflow_query::GraphCheckWorkflowQueryGraphCheckWorkflowTasks::OperationsCheckTask; +use self::graph_check_workflow_query::{CheckWorkflowStatus, CheckWorkflowTaskStatus}; use super::types::OperationsResult; @@ -69,14 +69,15 @@ fn get_check_response_from_data( graph_ref: graph_ref.clone(), })?; - let status = check_workflow.status.into(); + let workflow_status = check_workflow.status; + let mut operations_status = None; + let mut operations_target_url = None; let mut operations_result: Option = None; - let mut target_url = None; let mut number_of_checked_operations: u64 = 0; - let mut changes = Vec::new(); for task in check_workflow.tasks { if let OperationsCheckTask(task) = task { - target_url = task.target_url; + operations_status = Some(task.status); + operations_target_url = task.target_url; if let Some(result) = task.result { number_of_checked_operations = result.number_of_checked_operations.try_into().unwrap(); @@ -85,7 +86,13 @@ fn get_check_response_from_data( } } - if let Some(result) = operations_result { + if matches!(operations_status, Some(CheckWorkflowTaskStatus::FAILED)) + || matches!(workflow_status, CheckWorkflowStatus::PASSED) + { + let result = operations_result.ok_or(RoverClientError::MalformedResponse { + null_field: "OperationsCheckTask.result".to_string(), + })?; + let mut changes = Vec::with_capacity(result.changes.len()); for change in result.changes { changes.push(SchemaChange { code: change.code, @@ -93,24 +100,34 @@ fn get_check_response_from_data( description: change.description, }); } - } - // The `graph` check response does not return this field - // only `subgraph` check does. Since `CheckResponse` is shared - // between `graph` and `subgraph` checks, defaulting this - // to false for now since its currently only used in - // `check_response.rs` to format better console messages. - let core_schema_modified = false; + // The `graph` check response does not return this field + // only `subgraph` check does. Since `CheckResponse` is shared + // between `graph` and `subgraph` checks, defaulting this + // to false for now since its currently only used in + // `check_response.rs` to format better console messages. + let core_schema_modified = false; - CheckResponse::try_new( - target_url, - number_of_checked_operations, - changes, - status, - graph_ref, - false, - core_schema_modified, - ) + CheckResponse::try_new( + operations_target_url, + number_of_checked_operations, + changes, + workflow_status.into(), + graph_ref, + core_schema_modified, + ) + } else { + // Note that graph IDs and variants don't need percent-encoding due to their regex restrictions. + let default_target_url = format!( + "https://studio.apollographql.com/graph/{}/checks?variant={}", + graph_ref.name, graph_ref.variant + ); + Err(RoverClientError::OtherCheckTaskFailure { + has_build_task: false, + has_downstream_task: false, + target_url: operations_target_url.unwrap_or(default_target_url), + }) + } } fn get_target_url_from_data(data: QueryResponseData) -> Option { diff --git a/crates/rover-client/src/operations/subgraph/check_workflow/check_workflow_query.graphql b/crates/rover-client/src/operations/subgraph/check_workflow/check_workflow_query.graphql index 62e915970..b5346ceda 100644 --- a/crates/rover-client/src/operations/subgraph/check_workflow/check_workflow_query.graphql +++ b/crates/rover-client/src/operations/subgraph/check_workflow/check_workflow_query.graphql @@ -31,7 +31,13 @@ query SubgraphCheckWorkflowQuery($workflowId: ID!, $graphId: ID!) { } } } + ... on DownstreamCheckTask { + results { + downstreamVariantName + failsUpstreamWorkflow + } + } } } } -} \ No newline at end of file +} diff --git a/crates/rover-client/src/operations/subgraph/check_workflow/runner.rs b/crates/rover-client/src/operations/subgraph/check_workflow/runner.rs index efbaea5f3..c2abc68fe 100644 --- a/crates/rover-client/src/operations/subgraph/check_workflow/runner.rs +++ b/crates/rover-client/src/operations/subgraph/check_workflow/runner.rs @@ -11,9 +11,11 @@ use apollo_federation_types::build::BuildError; use graphql_client::*; use self::subgraph_check_workflow_query::CheckWorkflowStatus; +use self::subgraph_check_workflow_query::CheckWorkflowTaskStatus; use self::subgraph_check_workflow_query::SubgraphCheckWorkflowQueryGraphCheckWorkflowTasksOn::{ - CompositionCheckTask, OperationsCheckTask, + CompositionCheckTask, DownstreamCheckTask, OperationsCheckTask, }; +use self::subgraph_check_workflow_query::SubgraphCheckWorkflowQueryGraphCheckWorkflowTasksOnOperationsCheckTaskResult; #[derive(GraphQLQuery)] // The paths are relative to the directory where your `Cargo.toml` is located. @@ -74,16 +76,21 @@ fn get_check_response_from_data( graph_ref: graph_ref.clone(), })?; - let status = check_workflow.status.into(); + let workflow_status = check_workflow.status; + let mut operations_status = None; + let mut operations_target_url = None; let mut operations_result = None; - let mut target_url = None; let mut number_of_checked_operations: u64 = 0; let mut core_schema_modified = false; let mut composition_errors = Vec::new(); + let mut downstream_status = None; + let mut downstream_target_url = None; + let mut blocking_downstream_variants = Vec::new(); for task in check_workflow.tasks { match task.on { OperationsCheckTask(typed_task) => { - target_url = task.target_url; + operations_status = Some(task.status); + operations_target_url = task.target_url; if let Some(result) = typed_task.result { number_of_checked_operations = result.number_of_checked_operations.try_into().unwrap(); @@ -96,34 +103,22 @@ fn get_check_response_from_data( composition_errors = result.errors; } } + DownstreamCheckTask(typed_task) => { + downstream_status = Some(task.status); + downstream_target_url = task.target_url; + if let Some(results) = typed_task.results { + blocking_downstream_variants = results + .iter() + .filter(|result| result.fails_upstream_workflow.unwrap_or(false)) + .map(|result| result.downstream_variant_name.clone()) + .collect(); + } + } _ => (), } } - if composition_errors.is_empty() { - let result = operations_result.ok_or(RoverClientError::AdhocError { - msg: "No operation was found for this check.".to_string(), - })?; - - let mut changes = Vec::with_capacity(result.changes.len()); - for change in result.changes { - changes.push(SchemaChange { - code: change.code, - severity: change.severity.into(), - description: change.description, - }); - } - - CheckResponse::try_new( - target_url, - number_of_checked_operations, - changes, - status, - graph_ref, - true, - core_schema_modified, - ) - } else { + if !composition_errors.is_empty() { let num_failures = composition_errors.len(); let mut build_errors = Vec::with_capacity(num_failures); @@ -133,10 +128,47 @@ fn get_check_response_from_data( Some(query_composition_error.message), )); } - Err(RoverClientError::SubgraphBuildErrors { + return Err(RoverClientError::SubgraphBuildErrors { subgraph, graph_ref, source: build_errors.into(), + }); + } + + // Note that graph IDs and variants don't need percent-encoding due to their regex restrictions. + let default_target_url = format!( + "https://studio.apollographql.com/graph/{}/checks?variant={}", + graph_ref.name, graph_ref.variant + ); + + if matches!(operations_status, Some(CheckWorkflowTaskStatus::FAILED)) { + get_check_response_from_result( + operations_result, + operations_target_url, + number_of_checked_operations, + workflow_status, + graph_ref, + core_schema_modified, + ) + } else if matches!(downstream_status, Some(CheckWorkflowTaskStatus::FAILED)) { + Err(RoverClientError::DownstreamCheckFailure { + blocking_downstream_variants, + target_url: downstream_target_url.unwrap_or(default_target_url), + }) + } else if matches!(workflow_status, CheckWorkflowStatus::PASSED) { + get_check_response_from_result( + operations_result, + operations_target_url, + number_of_checked_operations, + workflow_status, + graph_ref, + core_schema_modified, + ) + } else { + Err(RoverClientError::OtherCheckTaskFailure { + has_build_task: true, + has_downstream_task: downstream_status.is_some(), + target_url: operations_target_url.unwrap_or(default_target_url), }) } } @@ -152,3 +184,35 @@ fn get_target_url_from_data(data: QueryResponseData) -> Option { } target_url } + +fn get_check_response_from_result( + operations_result: Option< + SubgraphCheckWorkflowQueryGraphCheckWorkflowTasksOnOperationsCheckTaskResult, + >, + operations_target_url: Option, + number_of_checked_operations: u64, + workflow_status: CheckWorkflowStatus, + graph_ref: GraphRef, + core_schema_modified: bool, +) -> Result { + let result = operations_result.ok_or(RoverClientError::AdhocError { + msg: "Operations check task has no result.".to_string(), + })?; + let mut changes = Vec::with_capacity(result.changes.len()); + for change in result.changes { + changes.push(SchemaChange { + code: change.code, + severity: change.severity.into(), + description: change.description, + }); + } + + CheckResponse::try_new( + operations_target_url, + number_of_checked_operations, + changes, + workflow_status.into(), + graph_ref, + core_schema_modified, + ) +} diff --git a/crates/rover-client/src/shared/check_response.rs b/crates/rover-client/src/shared/check_response.rs index b508c704c..04c23f539 100644 --- a/crates/rover-client/src/shared/check_response.rs +++ b/crates/rover-client/src/shared/check_response.rs @@ -30,7 +30,6 @@ impl CheckResponse { changes: Vec, result: ChangeSeverity, graph_ref: GraphRef, - has_build_task: bool, core_schema_modified: bool, ) -> Result { let mut failure_count = 0; @@ -50,20 +49,12 @@ impl CheckResponse { }; if failure_count > 0 { - return Err(RoverClientError::OperationCheckFailure { + Err(RoverClientError::OperationCheckFailure { graph_ref, check_response, - }); - } - match check_response.result { - ChangeSeverity::PASS => Ok(check_response), - ChangeSeverity::FAIL => Err(RoverClientError::OtherCheckTaskFailure { - has_build_task, - target_url: check_response.target_url.unwrap_or_else(|| - // Note that graph IDs and variants don't need percent-encoding due to their regex restrictions. - format!("https://studio.apollographql.com/graph/{}/checks?variant={}", graph_ref.name, graph_ref.variant) - ) - }), + }) + } else { + Ok(check_response) } } diff --git a/src/command/output.rs b/src/command/output.rs index 4f18bdd3e..7a8008fac 100644 --- a/src/command/output.rs +++ b/src/command/output.rs @@ -868,7 +868,6 @@ mod tests { ChangeSeverity::PASS, graph_ref, true, - true, ); if let Ok(mock_check_response) = mock_check_response { let actual_json: JsonOutput = RoverOutput::CheckResponse(mock_check_response).into(); @@ -925,7 +924,6 @@ mod tests { ], ChangeSeverity::FAIL, graph_ref, false, - false, ); if let Err(operation_check_failure) = check_response { diff --git a/src/error/metadata/code.rs b/src/error/metadata/code.rs index 25454a6ee..f1b3f1755 100644 --- a/src/error/metadata/code.rs +++ b/src/error/metadata/code.rs @@ -42,6 +42,7 @@ pub enum Code { E034, E035, E036, + E037, } impl Display for Code { @@ -91,6 +92,7 @@ impl Code { (Code::E034, include_str!("./codes/E034.md").to_string()), (Code::E035, include_str!("./codes/E035.md").to_string()), (Code::E036, include_str!("./codes/E036.md").to_string()), + (Code::E037, include_str!("./codes/E037.md").to_string()), ]; contents.into_iter().collect() } diff --git a/src/error/metadata/codes/E036.md b/src/error/metadata/codes/E036.md index e3ac8c221..8f80b86b1 100644 --- a/src/error/metadata/codes/E036.md +++ b/src/error/metadata/codes/E036.md @@ -1 +1 @@ -This check error occurs when the operations task (and build task, if run) have succeeded, but some other check task has failed. Please view the check in [Apollo Studio](https://studio.apollographql.com/) at the provided link to see the failure reason. You can read more about client checks [here](https://www.apollographql.com/docs/studio/schema-checks/). +This check error occurs when the build task, operations task, and downstream task (if run) have succeeded or are pending, but some other check task has failed. Please view the check in [Apollo Studio](https://studio.apollographql.com/) at the provided link to see the failure reason. You can read more about client checks [here](https://www.apollographql.com/docs/studio/schema-checks/). diff --git a/src/error/metadata/codes/E037.md b/src/error/metadata/codes/E037.md new file mode 100644 index 000000000..d9e6739b9 --- /dev/null +++ b/src/error/metadata/codes/E037.md @@ -0,0 +1 @@ +This error occurs when a downstream check task fails. This means that you proposed a schema for your source variant that causes checks to fail in blocking downstream variants. You can configure which downstream variants are blocking in the Checks -> Configuration view in [Apollo Studio](https://studio.apollographql.com/), and you can read more about client checks [here](https://www.apollographql.com/docs/studio/schema-checks/). diff --git a/src/error/metadata/mod.rs b/src/error/metadata/mod.rs index 887a49cdb..2fc984228 100644 --- a/src/error/metadata/mod.rs +++ b/src/error/metadata/mod.rs @@ -115,8 +115,18 @@ impl From<&mut saucer::Error> for Metadata { }), Some(Code::E030), ), + RoverClientError::DownstreamCheckFailure { + blocking_downstream_variants: _, + target_url, + } => ( + Some(Suggestion::FixDownstreamCheckFailure { + target_url: target_url.clone(), + }), + Some(Code::E037), + ), RoverClientError::OtherCheckTaskFailure { has_build_task: _, + has_downstream_task: _, target_url, } => ( Some(Suggestion::FixOtherCheckTaskFailure { diff --git a/src/error/metadata/suggestion.rs b/src/error/metadata/suggestion.rs index d2f88ac35..ce4de5116 100644 --- a/src/error/metadata/suggestion.rs +++ b/src/error/metadata/suggestion.rs @@ -47,6 +47,9 @@ pub enum Suggestion { FixOperationsInSchema { graph_ref: GraphRef, }, + FixDownstreamCheckFailure { + target_url: String, + }, FixOtherCheckTaskFailure { target_url: String, }, @@ -183,6 +186,7 @@ impl Display for Suggestion { format!("{} See {} for more information on resolving build errors.", prefix, Style::Link.paint("https://www.apollographql.com/docs/federation/errors/")) }, Suggestion::FixOperationsInSchema { graph_ref } => format!("The changes in the schema you proposed are incompatible with graph {}. See {} for more information on resolving operation check errors.", Style::Link.paint(graph_ref.to_string()), Style::Link.paint("https://www.apollographql.com/docs/studio/schema-checks/")), + Suggestion::FixDownstreamCheckFailure { target_url } => format!("The changes in the schema you proposed cause checks to fail for blocking downstream variants. See {} to view the failure reasons for these downstream checks.", Style::Link.paint(target_url)), Suggestion::FixOtherCheckTaskFailure { target_url } => format!("See {} to view the failure reason for the check.", Style::Link.paint(target_url)), Suggestion::IncreaseClientTimeout => "You can try increasing the timeout value by passing a higher value to the --client-timeout option.".to_string(), Suggestion::IncreaseChecksTimeout {url} => format!("You can try increasing the timeout value by setting APOLLO_CHECKS_TIMEOUT_SECONDS to a higher value in your env. The default value is 300 seconds. You can also view the live check progress by visiting {}.", Style::Link.paint(url.clone().unwrap_or_else(|| "https://studio.apollographql.com".to_string()))),