feat(wfe-yaml): optional display name on workflow spec + schema tests
Add an optional `name` field to `WorkflowSpec` so YAML authors can declare a human-friendly display name alongside the existing slug `id`. The compiler copies it through to `WorkflowDefinition.name`, which surfaces in definitions listings, run tables, and JSON output. Slug `id` remains the primary lookup key. Also adds a small smoke test for the schema generators to catch regressions in `generate_json_schema` / `generate_yaml_schema`.
This commit is contained in:
@@ -7,21 +7,23 @@ use wfe_core::models::workflow_definition::{StepOutcome, WorkflowDefinition, Wor
|
||||
use wfe_core::traits::StepBody;
|
||||
|
||||
use crate::error::YamlWorkflowError;
|
||||
use crate::executors::shell::{ShellConfig, ShellStep};
|
||||
#[cfg(feature = "deno")]
|
||||
use crate::executors::deno::{DenoConfig, DenoPermissions, DenoStep};
|
||||
use crate::executors::shell::{ShellConfig, ShellStep};
|
||||
#[cfg(feature = "buildkit")]
|
||||
use wfe_buildkit::{BuildkitConfig, BuildkitStep};
|
||||
#[cfg(feature = "containerd")]
|
||||
use wfe_containerd::{ContainerdConfig, ContainerdStep};
|
||||
use wfe_core::models::condition::{ComparisonOp, FieldComparison, StepCondition};
|
||||
use wfe_core::primitives::sub_workflow::SubWorkflowStep;
|
||||
#[cfg(feature = "kubernetes")]
|
||||
use wfe_kubernetes::{ClusterConfig, KubernetesStep, KubernetesStepConfig};
|
||||
#[cfg(feature = "rustlang")]
|
||||
use wfe_rustlang::{CargoCommand, CargoConfig, CargoStep, RustupCommand, RustupConfig, RustupStep};
|
||||
#[cfg(feature = "kubernetes")]
|
||||
use wfe_kubernetes::{ClusterConfig, KubernetesStepConfig, KubernetesStep};
|
||||
use wfe_core::primitives::sub_workflow::SubWorkflowStep;
|
||||
use wfe_core::models::condition::{ComparisonOp, FieldComparison, StepCondition};
|
||||
|
||||
use crate::schema::{WorkflowSpec, YamlCombinator, YamlComparison, YamlCondition, YamlErrorBehavior, YamlStep};
|
||||
use crate::schema::{
|
||||
WorkflowSpec, YamlCombinator, YamlComparison, YamlCondition, YamlErrorBehavior, YamlStep,
|
||||
};
|
||||
|
||||
/// Configuration for a sub-workflow step.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -43,6 +45,7 @@ pub struct CompiledWorkflow {
|
||||
/// Compile a parsed WorkflowSpec into a CompiledWorkflow.
|
||||
pub fn compile(spec: &WorkflowSpec) -> Result<CompiledWorkflow, YamlWorkflowError> {
|
||||
let mut definition = WorkflowDefinition::new(&spec.id, spec.version);
|
||||
definition.name = spec.name.clone();
|
||||
definition.description = spec.description.clone();
|
||||
|
||||
if let Some(ref eb) = spec.error_behavior {
|
||||
@@ -77,10 +80,8 @@ fn compile_steps(
|
||||
let container_id = *next_id;
|
||||
*next_id += 1;
|
||||
|
||||
let mut container = WorkflowStep::new(
|
||||
container_id,
|
||||
"wfe_core::primitives::sequence::SequenceStep",
|
||||
);
|
||||
let mut container =
|
||||
WorkflowStep::new(container_id, "wfe_core::primitives::sequence::SequenceStep");
|
||||
container.name = Some(yaml_step.name.clone());
|
||||
|
||||
if let Some(ref eb) = yaml_step.error_behavior {
|
||||
@@ -88,8 +89,7 @@ fn compile_steps(
|
||||
}
|
||||
|
||||
// Compile children.
|
||||
let child_ids =
|
||||
compile_steps(parallel_children, definition, factories, next_id)?;
|
||||
let child_ids = compile_steps(parallel_children, definition, factories, next_id)?;
|
||||
container.children = child_ids;
|
||||
|
||||
// Compile condition if present.
|
||||
@@ -104,10 +104,7 @@ fn compile_steps(
|
||||
let step_id = *next_id;
|
||||
*next_id += 1;
|
||||
|
||||
let step_type = yaml_step
|
||||
.step_type
|
||||
.as_deref()
|
||||
.unwrap_or("shell");
|
||||
let step_type = yaml_step.step_type.as_deref().unwrap_or("shell");
|
||||
|
||||
let (step_type_key, step_config_value, factory): (
|
||||
String,
|
||||
@@ -133,10 +130,7 @@ fn compile_steps(
|
||||
let comp_id = *next_id;
|
||||
*next_id += 1;
|
||||
|
||||
let on_failure_type = on_failure
|
||||
.step_type
|
||||
.as_deref()
|
||||
.unwrap_or("shell");
|
||||
let on_failure_type = on_failure.step_type.as_deref().unwrap_or("shell");
|
||||
let (comp_key, comp_config_value, comp_factory) =
|
||||
build_step_config_and_factory(on_failure, on_failure_type)?;
|
||||
|
||||
@@ -156,10 +150,7 @@ fn compile_steps(
|
||||
let success_id = *next_id;
|
||||
*next_id += 1;
|
||||
|
||||
let on_success_type = on_success
|
||||
.step_type
|
||||
.as_deref()
|
||||
.unwrap_or("shell");
|
||||
let on_success_type = on_success.step_type.as_deref().unwrap_or("shell");
|
||||
let (success_key, success_config_value, success_factory) =
|
||||
build_step_config_and_factory(on_success, on_success_type)?;
|
||||
|
||||
@@ -183,10 +174,7 @@ fn compile_steps(
|
||||
let ensure_id = *next_id;
|
||||
*next_id += 1;
|
||||
|
||||
let ensure_type = ensure
|
||||
.step_type
|
||||
.as_deref()
|
||||
.unwrap_or("shell");
|
||||
let ensure_type = ensure.step_type.as_deref().unwrap_or("shell");
|
||||
let (ensure_key, ensure_config_value, ensure_factory) =
|
||||
build_step_config_and_factory(ensure, ensure_type)?;
|
||||
|
||||
@@ -407,9 +395,7 @@ fn build_step_config_and_factory(
|
||||
let config = build_shell_config(step)?;
|
||||
let key = format!("wfe_yaml::shell::{}", step.name);
|
||||
let value = serde_json::to_value(&config).map_err(|e| {
|
||||
YamlWorkflowError::Compilation(format!(
|
||||
"Failed to serialize shell config: {e}"
|
||||
))
|
||||
YamlWorkflowError::Compilation(format!("Failed to serialize shell config: {e}"))
|
||||
})?;
|
||||
let config_clone = config.clone();
|
||||
let factory: StepFactory = Box::new(move || {
|
||||
@@ -422,9 +408,7 @@ fn build_step_config_and_factory(
|
||||
let config = build_deno_config(step)?;
|
||||
let key = format!("wfe_yaml::deno::{}", step.name);
|
||||
let value = serde_json::to_value(&config).map_err(|e| {
|
||||
YamlWorkflowError::Compilation(format!(
|
||||
"Failed to serialize deno config: {e}"
|
||||
))
|
||||
YamlWorkflowError::Compilation(format!("Failed to serialize deno config: {e}"))
|
||||
})?;
|
||||
let config_clone = config.clone();
|
||||
let factory: StepFactory = Box::new(move || {
|
||||
@@ -437,9 +421,7 @@ fn build_step_config_and_factory(
|
||||
let config = build_buildkit_config(step)?;
|
||||
let key = format!("wfe_yaml::buildkit::{}", step.name);
|
||||
let value = serde_json::to_value(&config).map_err(|e| {
|
||||
YamlWorkflowError::Compilation(format!(
|
||||
"Failed to serialize buildkit config: {e}"
|
||||
))
|
||||
YamlWorkflowError::Compilation(format!("Failed to serialize buildkit config: {e}"))
|
||||
})?;
|
||||
let config_clone = config.clone();
|
||||
let factory: StepFactory = Box::new(move || {
|
||||
@@ -474,8 +456,10 @@ fn build_step_config_and_factory(
|
||||
let step_config = config.0;
|
||||
let cluster_config = config.1;
|
||||
let factory: StepFactory = Box::new(move || {
|
||||
Box::new(KubernetesStep::lazy(step_config.clone(), cluster_config.clone()))
|
||||
as Box<dyn StepBody>
|
||||
Box::new(KubernetesStep::lazy(
|
||||
step_config.clone(),
|
||||
cluster_config.clone(),
|
||||
)) as Box<dyn StepBody>
|
||||
});
|
||||
Ok((key, value, factory))
|
||||
}
|
||||
@@ -486,9 +470,7 @@ fn build_step_config_and_factory(
|
||||
let config = build_cargo_config(step, step_type)?;
|
||||
let key = format!("wfe_yaml::cargo::{}", step.name);
|
||||
let value = serde_json::to_value(&config).map_err(|e| {
|
||||
YamlWorkflowError::Compilation(format!(
|
||||
"Failed to serialize cargo config: {e}"
|
||||
))
|
||||
YamlWorkflowError::Compilation(format!("Failed to serialize cargo config: {e}"))
|
||||
})?;
|
||||
let config_clone = config.clone();
|
||||
let factory: StepFactory = Box::new(move || {
|
||||
@@ -501,9 +483,7 @@ fn build_step_config_and_factory(
|
||||
let config = build_rustup_config(step, step_type)?;
|
||||
let key = format!("wfe_yaml::rustup::{}", step.name);
|
||||
let value = serde_json::to_value(&config).map_err(|e| {
|
||||
YamlWorkflowError::Compilation(format!(
|
||||
"Failed to serialize rustup config: {e}"
|
||||
))
|
||||
YamlWorkflowError::Compilation(format!("Failed to serialize rustup config: {e}"))
|
||||
})?;
|
||||
let config_clone = config.clone();
|
||||
let factory: StepFactory = Box::new(move || {
|
||||
@@ -534,9 +514,7 @@ fn build_step_config_and_factory(
|
||||
|
||||
let key = format!("wfe_yaml::workflow::{}", step.name);
|
||||
let value = serde_json::to_value(&sub_config).map_err(|e| {
|
||||
YamlWorkflowError::Compilation(format!(
|
||||
"Failed to serialize workflow config: {e}"
|
||||
))
|
||||
YamlWorkflowError::Compilation(format!("Failed to serialize workflow config: {e}"))
|
||||
})?;
|
||||
let config_clone = sub_config.clone();
|
||||
let factory: StepFactory = Box::new(move || {
|
||||
@@ -603,10 +581,7 @@ fn build_deno_config(step: &YamlStep) -> Result<DenoConfig, YamlWorkflowError> {
|
||||
|
||||
fn build_shell_config(step: &YamlStep) -> Result<ShellConfig, YamlWorkflowError> {
|
||||
let config = step.config.as_ref().ok_or_else(|| {
|
||||
YamlWorkflowError::Compilation(format!(
|
||||
"Step '{}' is missing 'config' section",
|
||||
step.name
|
||||
))
|
||||
YamlWorkflowError::Compilation(format!("Step '{}' is missing 'config' section", step.name))
|
||||
})?;
|
||||
|
||||
let run = config
|
||||
@@ -634,10 +609,7 @@ fn build_shell_config(step: &YamlStep) -> Result<ShellConfig, YamlWorkflowError>
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustlang")]
|
||||
fn build_cargo_config(
|
||||
step: &YamlStep,
|
||||
step_type: &str,
|
||||
) -> Result<CargoConfig, YamlWorkflowError> {
|
||||
fn build_cargo_config(step: &YamlStep, step_type: &str) -> Result<CargoConfig, YamlWorkflowError> {
|
||||
let command = match step_type {
|
||||
"cargo-build" => CargoCommand::Build,
|
||||
"cargo-test" => CargoCommand::Test,
|
||||
@@ -730,9 +702,7 @@ fn parse_duration_ms(s: &str) -> Option<u64> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "buildkit")]
|
||||
fn build_buildkit_config(
|
||||
step: &YamlStep,
|
||||
) -> Result<BuildkitConfig, YamlWorkflowError> {
|
||||
fn build_buildkit_config(step: &YamlStep) -> Result<BuildkitConfig, YamlWorkflowError> {
|
||||
let config = step.config.as_ref().ok_or_else(|| {
|
||||
YamlWorkflowError::Compilation(format!(
|
||||
"BuildKit step '{}' is missing 'config' section",
|
||||
@@ -805,9 +775,7 @@ fn build_buildkit_config(
|
||||
}
|
||||
|
||||
#[cfg(feature = "containerd")]
|
||||
fn build_containerd_config(
|
||||
step: &YamlStep,
|
||||
) -> Result<ContainerdConfig, YamlWorkflowError> {
|
||||
fn build_containerd_config(step: &YamlStep) -> Result<ContainerdConfig, YamlWorkflowError> {
|
||||
let config = step.config.as_ref().ok_or_else(|| {
|
||||
YamlWorkflowError::Compilation(format!(
|
||||
"Containerd step '{}' is missing 'config' section",
|
||||
@@ -869,11 +837,17 @@ fn build_containerd_config(
|
||||
env: config.env.clone(),
|
||||
volumes,
|
||||
working_dir: config.working_dir.clone(),
|
||||
user: config.user.clone().unwrap_or_else(|| "65534:65534".to_string()),
|
||||
user: config
|
||||
.user
|
||||
.clone()
|
||||
.unwrap_or_else(|| "65534:65534".to_string()),
|
||||
network: config.network.clone().unwrap_or_else(|| "none".to_string()),
|
||||
memory: config.memory.clone(),
|
||||
cpu: config.cpu.clone(),
|
||||
pull: config.pull.clone().unwrap_or_else(|| "if-not-present".to_string()),
|
||||
pull: config
|
||||
.pull
|
||||
.clone()
|
||||
.unwrap_or_else(|| "if-not-present".to_string()),
|
||||
containerd_addr: config
|
||||
.containerd_addr
|
||||
.clone()
|
||||
@@ -944,9 +918,7 @@ fn compile_services(
|
||||
}
|
||||
} else {
|
||||
// Default: TCP check on first port.
|
||||
ReadinessCheck::TcpSocket(
|
||||
yaml_svc.ports.first().copied().unwrap_or(0),
|
||||
)
|
||||
ReadinessCheck::TcpSocket(yaml_svc.ports.first().copied().unwrap_or(0))
|
||||
};
|
||||
|
||||
let interval_ms = r
|
||||
|
||||
@@ -82,8 +82,12 @@ pub struct YamlWorkflow {
|
||||
/// A complete workflow definition.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct WorkflowSpec {
|
||||
/// Unique workflow identifier.
|
||||
/// Unique workflow identifier (slug, e.g. "ci"). Primary lookup key.
|
||||
pub id: String,
|
||||
/// Optional human-friendly display name shown in UIs and listings
|
||||
/// (e.g. "Continuous Integration"). Defaults to the slug `id` when unset.
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
/// Workflow version number.
|
||||
pub version: u32,
|
||||
/// Optional human-readable description.
|
||||
@@ -433,3 +437,33 @@ pub fn generate_yaml_schema() -> String {
|
||||
let schema = generate_json_schema();
|
||||
serde_yaml::to_string(&schema).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn json_schema_is_non_empty() {
|
||||
let schema = generate_json_schema();
|
||||
assert!(schema.is_object(), "schema should be a JSON object");
|
||||
let obj = schema.as_object().unwrap();
|
||||
assert!(
|
||||
obj.contains_key("$schema")
|
||||
|| obj.contains_key("type")
|
||||
|| obj.contains_key("properties"),
|
||||
"schema missing standard JSON Schema keys: {:?}",
|
||||
obj.keys().collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yaml_schema_is_non_empty() {
|
||||
let yaml = generate_yaml_schema();
|
||||
assert!(!yaml.is_empty(), "yaml schema should not be empty");
|
||||
assert!(
|
||||
yaml.len() > 100,
|
||||
"yaml schema suspiciously short: {} chars",
|
||||
yaml.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user