mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-08 03:10:26 +00:00
feat(cli): allow rendering invalid graphs
Keep graph validation diagnostics visible, but let users opt into rendering DOT workflows that fail semantic validation with --allow-invalid.
This commit is contained in:
parent
8c6b187cb7
commit
245052db38
4 changed files with 84 additions and 14 deletions
|
|
@ -455,6 +455,7 @@ fabro graph [OPTIONS] <WORKFLOW>
|
|||
| Option | Description |
|
||||
| --- | --- |
|
||||
| `-d, --direction <direction>` | Graph layout direction (overrides the DOT file's rankdir)<br />Values: `lr`, `tb` |
|
||||
| `--allow-invalid` | Render even when workflow validation reports errors |
|
||||
| `--format <format>` | Output format<br />Values: `svg`<br />Default: `svg` |
|
||||
| `-o, --output <output>` | Output file path (defaults to stdout) |
|
||||
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
|
||||
|
|
|
|||
|
|
@ -483,6 +483,10 @@ pub(crate) struct GraphArgs {
|
|||
/// Graph layout direction (overrides the DOT file's rankdir)
|
||||
#[arg(short = 'd', long)]
|
||||
pub(crate) direction: Option<GraphDirection>,
|
||||
|
||||
/// Render even when workflow validation reports errors
|
||||
#[arg(long)]
|
||||
pub(crate) allow_invalid: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use std::io::Write;
|
|||
use anyhow::{Context, bail};
|
||||
use fabro_api::types;
|
||||
use fabro_config::user::active_settings_path;
|
||||
use fabro_graphviz::render;
|
||||
use fabro_manifest::{ManifestBuildInput, build_run_manifest};
|
||||
use fabro_util::terminal::Styles;
|
||||
use tracing::debug;
|
||||
|
|
@ -43,23 +44,27 @@ pub(crate) async fn run(
|
|||
let diagnostics = api_diagnostics_to_local(&preflight.workflow.diagnostics);
|
||||
|
||||
print_diagnostics(&diagnostics, styles, printer);
|
||||
if diagnostics
|
||||
let has_errors = diagnostics
|
||||
.iter()
|
||||
.any(|diagnostic| diagnostic.severity == fabro_validate::Severity::Error)
|
||||
{
|
||||
.any(|diagnostic| diagnostic.severity == fabro_validate::Severity::Error);
|
||||
if has_errors && !args.allow_invalid {
|
||||
bail!("Validation failed");
|
||||
}
|
||||
|
||||
let rendered = client
|
||||
.render_workflow_graph(types::RenderWorkflowGraphRequest {
|
||||
manifest: built.manifest,
|
||||
format: Some(types::RenderWorkflowGraphFormat::Svg),
|
||||
direction: args.direction.map(|direction| match direction {
|
||||
GraphDirection::Lr => types::RenderWorkflowGraphDirection::Lr,
|
||||
GraphDirection::Tb => types::RenderWorkflowGraphDirection::Tb,
|
||||
}),
|
||||
})
|
||||
.await?;
|
||||
let rendered = if has_errors && args.allow_invalid {
|
||||
render_manifest_graph_locally(&built.manifest, args.direction)?
|
||||
} else {
|
||||
client
|
||||
.render_workflow_graph(types::RenderWorkflowGraphRequest {
|
||||
manifest: built.manifest,
|
||||
format: Some(types::RenderWorkflowGraphFormat::Svg),
|
||||
direction: args.direction.map(|direction| match direction {
|
||||
GraphDirection::Lr => types::RenderWorkflowGraphDirection::Lr,
|
||||
GraphDirection::Tb => types::RenderWorkflowGraphDirection::Tb,
|
||||
}),
|
||||
})
|
||||
.await?
|
||||
};
|
||||
|
||||
if let Some(ref output_path) = args.output {
|
||||
std::fs::write(output_path, &rendered)
|
||||
|
|
@ -80,6 +85,32 @@ pub(crate) async fn run(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn render_manifest_graph_locally(
|
||||
manifest: &types::RunManifest,
|
||||
direction: Option<GraphDirection>,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
let source = manifest_root_source(manifest)?;
|
||||
let source = match direction {
|
||||
Some(direction) => render::apply_direction(source, &direction.to_string()).into_owned(),
|
||||
None => source.to_string(),
|
||||
};
|
||||
|
||||
render::render_dot(&source).context("rendering workflow graph")
|
||||
}
|
||||
|
||||
fn manifest_root_source(manifest: &types::RunManifest) -> anyhow::Result<&str> {
|
||||
manifest
|
||||
.workflows
|
||||
.get(&manifest.target.path)
|
||||
.map(|workflow| workflow.source.as_str())
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"manifest target path is missing from workflows map: {}",
|
||||
manifest.target.path
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn output_file_json(output_path: &std::path::Path, format: GraphOutputFormat) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"path": absolute_or_current(output_path),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use fabro_test::{fabro_snapshot, test_context};
|
||||
|
||||
use super::support::fixture;
|
||||
use super::support::{fixture, read_text};
|
||||
|
||||
#[test]
|
||||
fn help() {
|
||||
|
|
@ -61,6 +61,9 @@ fn help() {
|
|||
|
||||
[env: FABRO_QUIET=]
|
||||
|
||||
--allow-invalid
|
||||
Render even when workflow validation reports errors
|
||||
|
||||
--verbose
|
||||
Enable verbose output
|
||||
|
||||
|
|
@ -72,6 +75,37 @@ fn help() {
|
|||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graph_allow_invalid_renders_after_diagnostics() {
|
||||
let context = test_context!();
|
||||
let workflow = fixture("invalid.fabro");
|
||||
let output_path = context.temp_dir.join("invalid.svg");
|
||||
let mut cmd = context.command();
|
||||
cmd.args([
|
||||
"graph",
|
||||
"--allow-invalid",
|
||||
"-o",
|
||||
output_path.to_str().unwrap(),
|
||||
workflow.to_str().unwrap(),
|
||||
]);
|
||||
|
||||
fabro_snapshot!(context.filters(), cmd, @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
----- stderr -----
|
||||
error: Pipeline must have exactly one start node (shape=Mdiamond or id start/Start) (start_node)
|
||||
error [node: exit]: Exit node 'exit' has 1 outgoing edge(s) but must have none (exit_no_outgoing)
|
||||
");
|
||||
|
||||
let svg = read_text(&output_path);
|
||||
assert!(
|
||||
svg.contains("<svg") && svg.contains("Invalid"),
|
||||
"expected invalid workflow to render as SVG, got: {}",
|
||||
&svg[..svg.len().min(200)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graph_invalid_workflow_fails_after_diagnostics() {
|
||||
let context = test_context!();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue