Skip to content
Merged
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
37 changes: 33 additions & 4 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ on:
- 'mkdocs.yml'
- 'scripts/generate_supported_formats_doc.py'
- 'scripts/generate_tool_registry_doc.py'
- 'scripts/generate_spec_v2_reference.py'
- 'schemas/renderflow-v2.schema.json'
- 'examples/renderflow-v2.yaml'
- 'crates/renderflow-core/data/tool-registry.yaml'
- 'crates/renderflow-core/src/audio/format.rs'
- 'crates/renderflow-core/src/graph/format.rs'
- 'crates/renderflow-core/src/image/format.rs'
- 'crates/renderflow-core/src/input_format.rs'
- 'crates/renderflow-core/src/spec.rs'
- 'crates/renderflow-core/src/commands/spec.rs'
- 'crates/renderflow-core/src/toolchain.rs'
push:
branches:
Expand All @@ -27,11 +32,16 @@ on:
- 'mkdocs.yml'
- 'scripts/generate_supported_formats_doc.py'
- 'scripts/generate_tool_registry_doc.py'
- 'scripts/generate_spec_v2_reference.py'
- 'schemas/renderflow-v2.schema.json'
- 'examples/renderflow-v2.yaml'
- 'crates/renderflow-core/data/tool-registry.yaml'
- 'crates/renderflow-core/src/audio/format.rs'
- 'crates/renderflow-core/src/graph/format.rs'
- 'crates/renderflow-core/src/image/format.rs'
- 'crates/renderflow-core/src/input_format.rs'
- 'crates/renderflow-core/src/spec.rs'
- 'crates/renderflow-core/src/commands/spec.rs'
- 'crates/renderflow-core/src/toolchain.rs'
workflow_dispatch:

Expand All @@ -53,6 +63,11 @@ jobs:
with:
fetch-depth: 0

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.94"

- name: Setup Python
uses: actions/setup-python@v5
with:
Expand All @@ -63,13 +78,20 @@ jobs:
python -m pip install --upgrade pip
pip install mkdocs-material mike pyyaml

- name: Regenerate generated docs
- name: Regenerate generated docs and schemas
run: |
python scripts/generate_supported_formats_doc.py
python scripts/generate_tool_registry_doc.py
cargo run --quiet -p renderflow-cli -- spec schema --format json --output schemas/renderflow-v2.schema.json
python scripts/generate_spec_v2_reference.py

- name: Verify generated docs are committed
run: git diff --exit-code -- docs/user-guide/supported-formats.md docs/user-guide/tool-registry.md
- name: Verify generated docs and schemas are committed
run: |
git diff --exit-code -- \
docs/user-guide/supported-formats.md \
docs/user-guide/tool-registry.md \
docs/user-guide/spec-v2-reference.md \
schemas/renderflow-v2.schema.json

- name: Build docs
run: mkdocs build --strict
Expand All @@ -91,6 +113,11 @@ jobs:
with:
fetch-depth: 0

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.94"

- name: Setup Python
uses: actions/setup-python@v5
with:
Expand All @@ -101,10 +128,12 @@ jobs:
python -m pip install --upgrade pip
pip install mkdocs-material mike pyyaml

- name: Regenerate generated docs
- name: Regenerate generated docs and schemas
run: |
python scripts/generate_supported_formats_doc.py
python scripts/generate_tool_registry_doc.py
cargo run --quiet -p renderflow-cli -- spec schema --format json --output schemas/renderflow-v2.schema.json
python scripts/generate_spec_v2_reference.py

- name: Configure Pages
uses: actions/configure-pages@v5
Expand Down
38 changes: 22 additions & 16 deletions crates/renderflow-core/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use anyhow::{bail, Result};
use clap::Parser;
use tracing::info;

use crate::cli::{AiCommands, Cli, Commands, GraphCommands, PluginCommands, ToolCommands};
use crate::cli::{
AiCommands, Cli, Commands, GraphCommands, PluginCommands, SpecCommands, ToolCommands,
};
use crate::{commands, transforms};

/// Initialize logging for a Renderflow CLI run.
Expand Down Expand Up @@ -47,22 +49,15 @@ pub fn run_cli(cli: Cli) -> Result<()> {
target,
all,
export,
}) => {
commands::inspect::run(
&config,
&output_format,
target.as_deref(),
all,
export.as_deref(),
None, // optimization: use the mode from the config file
)?
}
}) => commands::inspect::run(
&config,
&output_format,
target.as_deref(),
all,
export.as_deref(),
None,
)?,
Some(Commands::Plugin { subcommand }) => {
// The plugin registry is empty at the top-level CLI entry point.
// Third-party plugins are registered programmatically before
// calling renderflow as a library. The CLI commands are
// primarily useful when renderflow is embedded in a larger
// application that populates the registry before dispatching.
let registry = transforms::plugin::PluginRegistry::new();
match subcommand {
PluginCommands::List => commands::plugin::run_list(&registry)?,
Expand Down Expand Up @@ -146,6 +141,17 @@ pub fn run_cli(cli: Cli) -> Result<()> {
Some(Commands::Capabilities { format, transforms }) => {
commands::tools::run_capabilities(transforms.as_deref(), &format)?
}
Some(Commands::Spec { subcommand }) => match subcommand {
SpecCommands::Validate { config, format } => {
commands::spec::run_validate(&config, &format)?
}
SpecCommands::Migrate { config, output } => {
commands::spec::run_migrate(&config, output.as_deref())?
}
SpecCommands::Schema { format, output } => {
commands::spec::run_schema(&format, output.as_deref())?
}
},
Some(Commands::Version) => commands::system::run_version(),
Some(Commands::Env) => commands::system::run_env(),
Some(Commands::Doctor { strict }) => commands::system::run_doctor(strict)?,
Expand Down
37 changes: 37 additions & 0 deletions crates/renderflow-core/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ use crate::optimization::OptimizationMode;
renderflow tools list List runtime tool providers\n \
renderflow tools inspect tool.ffmpeg Inspect one runtime provider\n \
renderflow capabilities List provider capability IDs\n \
renderflow spec validate Validate v1/v2 execution specifications\n \
renderflow spec migrate Migrate unversioned v1 config to v2\n \
renderflow my-project.yaml Shorthand: run build on the given config"
)]
pub struct Cli {
Expand Down Expand Up @@ -218,6 +220,13 @@ pub enum Commands {
transforms: Option<String>,
},

/// Validate, migrate, and export the Renderflow execution specification.
#[command(subcommand_required = true, arg_required_else_help = true)]
Spec {
#[command(subcommand)]
subcommand: SpecCommands,
},

/// Print the installed Renderflow version
Version,

Expand All @@ -232,6 +241,34 @@ pub enum Commands {
},
}

/// Subcommands for `renderflow spec`.
#[derive(Subcommand)]
pub enum SpecCommands {
/// Validate an unversioned v1 config or a versioned v2 spec.
Validate {
#[arg(long, default_value = "renderflow.yaml", value_name = "FILE")]
config: String,
#[arg(long, default_value = "text", value_name = "FORMAT")]
format: String,
},

/// Migrate an unversioned v1 config to the v2 execution specification.
Migrate {
#[arg(long, default_value = "renderflow.yaml", value_name = "FILE")]
config: String,
#[arg(long, short = 'o', value_name = "FILE")]
output: Option<String>,
},

/// Emit the canonical v2 JSON Schema used by the runtime.
Schema {
#[arg(long, default_value = "json", value_name = "FORMAT")]
format: String,
#[arg(long, short = 'o', value_name = "FILE")]
output: Option<String>,
},
}

/// Subcommands for `renderflow plugin`.
#[derive(Subcommand)]
pub enum PluginCommands {
Expand Down
1 change: 1 addition & 0 deletions crates/renderflow-core/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod graph;
pub mod graph_build;
pub mod inspect;
pub mod plugin;
pub mod spec;
pub mod system;
pub mod tools;
pub mod watch;
116 changes: 116 additions & 0 deletions crates/renderflow-core/src/commands/spec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use std::fs;

use anyhow::{Context, Result};
use serde::Serialize;

use crate::spec::{
json_schema_pretty, migrate_v1_file, validate_spec_file, SourceSpecVersion, SPEC_V2_ID,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OutputFormat {
Text,
Json,
Yaml,
}

impl OutputFormat {
fn parse(value: &str) -> Result<Self> {
match value.to_ascii_lowercase().as_str() {
"text" => Ok(Self::Text),
"json" => Ok(Self::Json),
"yaml" | "yml" => Ok(Self::Yaml),
other => {
anyhow::bail!("unknown spec output format '{other}'; supported: text, json, yaml")
}
}
}
}

fn write_output(content: &str, output: Option<&str>) -> Result<()> {
if let Some(path) = output {
fs::write(path, content).with_context(|| format!("failed to write '{path}'"))?;
} else {
print!("{content}");
}
Ok(())
}

fn serialize<T: Serialize>(value: &T, format: OutputFormat) -> Result<String> {
match format {
OutputFormat::Text => anyhow::bail!("text output requires a dedicated renderer"),
OutputFormat::Json => Ok(format!("{}\n", serde_json::to_string_pretty(value)?)),
OutputFormat::Yaml => Ok(serde_yaml_ng::to_string(value)?),
}
}

pub fn run_validate(config: &str, format: &str) -> Result<()> {
let report = validate_spec_file(config);
let format = OutputFormat::parse(format)?;

match format {
OutputFormat::Text => {
if report.valid {
let version = match report.source_version {
Some(SourceSpecVersion::V1) => "v1 compatibility",
Some(SourceSpecVersion::V2) => SPEC_V2_ID,
None => "unknown",
};
println!("Renderflow spec: valid ({version})");
} else {
eprintln!("Renderflow spec: invalid");
for diagnostic in &report.diagnostics {
eprintln!(
" {} [{}] {}",
diagnostic.path, diagnostic.code, diagnostic.message
);
}
}
}
OutputFormat::Json | OutputFormat::Yaml => {
print!("{}", serialize(&report, format)?);
}
}

if !report.valid {
anyhow::bail!(
"spec validation failed with {} diagnostic(s)",
report.diagnostics.len()
);
}
Ok(())
}

pub fn run_migrate(config: &str, output: Option<&str>) -> Result<()> {
let migrated = migrate_v1_file(config)?;
let yaml =
serde_yaml_ng::to_string(&migrated).context("failed to serialize migrated v2 spec")?;
write_output(&yaml, output)
}

pub fn run_schema(format: &str, output: Option<&str>) -> Result<()> {
let format = OutputFormat::parse(format)?;
let schema = crate::spec::json_schema();
let content = match format {
OutputFormat::Text | OutputFormat::Json => json_schema_pretty()?,
OutputFormat::Yaml => serde_yaml_ng::to_string(&schema)?,
};
write_output(&content, output)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn output_format_rejects_unknown_value() {
assert!(OutputFormat::parse("toml").is_err());
}

#[test]
fn schema_command_serialization_is_machine_readable() {
let json = json_schema_pretty().expect("schema serializes");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("schema is JSON");
assert_eq!(parsed["properties"]["schema"]["const"], SPEC_V2_ID);
}
}
1 change: 1 addition & 0 deletions crates/renderflow-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub mod optimization;
mod pipeline;
pub mod process;
mod sdk;
pub mod spec;
pub mod strategies;
mod template;
pub mod toolchain;
Expand Down
Loading
Loading