use std::collections::HashMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// A condition in YAML that determines whether a step executes. /// /// Uses `#[serde(untagged)]` so serde tries each variant in order. /// A comparison has a `field:` key; a combinator has `all:/any:/none:/one_of:/not:`. /// Comparison is listed first because it is more specific (requires `field`). #[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)] #[serde(untagged)] pub enum YamlCondition { /// Leaf comparison (has a `field:` key). Comparison(Box), /// Combinator with sub-conditions. Combinator(YamlCombinator), } /// A combinator condition containing sub-conditions. #[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)] pub struct YamlCombinator { #[serde(default)] pub all: Option>, #[serde(default)] pub any: Option>, #[serde(default)] pub none: Option>, #[serde(default)] pub one_of: Option>, #[serde(default)] pub not: Option>, } /// A leaf comparison condition that compares a field value. #[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)] pub struct YamlComparison { pub field: String, #[serde(default)] #[schemars(with = "Option")] pub equals: Option, #[serde(default)] #[schemars(with = "Option")] pub not_equals: Option, #[serde(default)] #[schemars(with = "Option")] pub gt: Option, #[serde(default)] #[schemars(with = "Option")] pub gte: Option, #[serde(default)] #[schemars(with = "Option")] pub lt: Option, #[serde(default)] #[schemars(with = "Option")] pub lte: Option, #[serde(default)] #[schemars(with = "Option")] pub contains: Option, #[serde(default)] pub is_null: Option, #[serde(default)] pub is_not_null: Option, } /// Top-level YAML file structure supporting both single and multi-workflow files. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct YamlWorkflowFile { /// Single workflow (backward compatible). pub workflow: Option, /// Multiple workflows in one file. pub workflows: Option>, } /// Legacy single-workflow top-level structure. Kept for backward compatibility /// with code that deserializes `YamlWorkflow` directly. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct YamlWorkflow { pub workflow: WorkflowSpec, } /// A complete workflow definition. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct WorkflowSpec { /// 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, /// Workflow version number. pub version: u32, /// Optional human-readable description. #[serde(default)] pub description: Option, /// Default error handling behavior for all steps. #[serde(default)] pub error_behavior: Option, /// The steps that make up this workflow. pub steps: Vec, /// Typed input schema: { field_name: type_string }. /// Example: `"repo_url": "string"`, `"tags": "list"`. #[serde(default)] pub inputs: HashMap, /// Typed output schema: { field_name: type_string }. #[serde(default)] pub outputs: HashMap, /// Infrastructure services required by this workflow (databases, caches, etc.). #[serde(default)] pub services: HashMap, /// Allow unknown top-level keys (e.g. `_templates`) for YAML anchors. #[serde(flatten)] #[schemars(skip)] pub _extra: HashMap, } /// A service definition in YAML format. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct YamlService { /// Container image to run (e.g., "postgres:15"). pub image: String, /// Ports to expose (container ports). #[serde(default)] pub ports: Vec, /// Environment variables for the service container. #[serde(default)] pub env: HashMap, /// Readiness probe configuration. #[serde(default)] pub readiness: Option, /// Memory limit (e.g., "512Mi"). #[serde(default)] pub memory: Option, /// CPU limit (e.g., "500m"). #[serde(default)] pub cpu: Option, /// Override container entrypoint. #[serde(default)] pub command: Option>, /// Override container args. #[serde(default)] pub args: Option>, } /// Readiness probe configuration in YAML format. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct YamlReadiness { /// Execute a command to check readiness. #[serde(default)] pub exec: Option>, /// Check if a TCP port is accepting connections. #[serde(default)] pub tcp: Option, /// HTTP GET health check. #[serde(default)] pub http: Option, /// Poll interval (e.g., "5s", "2s"). #[serde(default)] pub interval: Option, /// Total timeout (e.g., "60s", "30s"). #[serde(default)] pub timeout: Option, /// Max retries before giving up. #[serde(default)] pub retries: Option, } /// HTTP GET readiness check. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct YamlHttpGet { /// Port to check. pub port: u16, /// HTTP path (default: "/"). #[serde(default = "default_health_path")] pub path: String, } fn default_health_path() -> String { "/".into() } /// A single step in a workflow. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct YamlStep { /// Step identifier (must be unique within the workflow). pub name: String, /// Executor type. One of: shell, deno, containerd, buildkit, kubernetes, k8s, /// cargo-build, cargo-test, cargo-check, cargo-clippy, cargo-fmt, cargo-doc, /// cargo-publish, cargo-audit, cargo-deny, cargo-nextest, cargo-llvm-cov, /// cargo-doc-mdx, rust-install, rustup-toolchain, rustup-component, /// rustup-target, workflow. Default: "shell". #[serde(rename = "type")] pub step_type: Option, /// Type-specific configuration. #[serde(default)] pub config: Option, /// Input data references. #[serde(default)] pub inputs: Vec, /// Output data references. #[serde(default)] pub outputs: Vec, /// Steps to run in parallel. #[serde(default)] pub parallel: Option>, /// Error handling override for this step. #[serde(default)] pub error_behavior: Option, /// Hook step to run on success. #[serde(default)] pub on_success: Option>, /// Compensation step to run on failure. #[serde(default)] pub on_failure: Option>, /// Cleanup step that always runs. #[serde(default)] pub ensure: Option>, /// Condition that must be true for this step to execute. #[serde(default)] pub when: Option, } /// Step configuration (fields are type-specific). #[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)] pub struct StepConfig { // --- Shell --- /// Shell command to run (shorthand for shell steps). pub run: Option, /// Path to a script file (deno steps). pub file: Option, /// Inline script source (deno steps). pub script: Option, /// Shell binary to use (default: "sh"). pub shell: Option, /// Environment variables. #[serde(default)] pub env: HashMap, /// Execution timeout (e.g., "5m", "30s"). pub timeout: Option, /// Working directory. pub working_dir: Option, // --- Deno --- /// Deno sandbox permissions. #[serde(default)] pub permissions: Option, /// ES modules to import (e.g., "npm:lodash@4"). #[serde(default)] pub modules: Vec, // --- BuildKit --- /// Dockerfile path. pub dockerfile: Option, /// Build context path. pub context: Option, /// Multi-stage build target. pub target: Option, /// Image tags. #[serde(default)] pub tags: Vec, /// Build arguments. #[serde(default)] pub build_args: HashMap, /// Cache import sources. #[serde(default)] pub cache_from: Vec, /// Cache export destinations. #[serde(default)] pub cache_to: Vec, /// Push built image to registry. pub push: Option, /// BuildKit daemon address. pub buildkit_addr: Option, /// TLS configuration. #[serde(default)] pub tls: Option, /// Registry authentication per registry hostname. #[serde(default)] pub registry_auth: Option>, // --- Containerd --- /// Container image (required for containerd/kubernetes steps). pub image: Option, /// Container command override. #[serde(default)] pub command: Option>, /// Volume mounts. #[serde(default)] pub volumes: Vec, /// User:group (e.g., "1000:1000"). pub user: Option, /// Network mode: none, host, bridge. pub network: Option, /// Memory limit (e.g., "512m"). pub memory: Option, /// CPU limit (e.g., "1.0"). pub cpu: Option, /// Image pull policy: always, if-not-present, never. pub pull: Option, /// Containerd daemon address. pub containerd_addr: Option, /// CLI binary name: "nerdctl" (default) or "docker". pub cli: Option, // --- Kubernetes --- /// Kubeconfig path. pub kubeconfig: Option, /// Namespace override. pub namespace: Option, /// Image pull policy: Always, IfNotPresent, Never. pub pull_policy: Option, // --- Cargo --- /// Target package for cargo steps (`-p`). pub package: Option, /// Features to enable. #[serde(default)] pub features: Vec, /// Enable all features. #[serde(default)] pub all_features: Option, /// Disable default features. #[serde(default)] pub no_default_features: Option, /// Build in release mode. #[serde(default)] pub release: Option, /// Build profile (--profile). pub profile: Option, /// Rust toolchain override (e.g., "nightly"). pub toolchain: Option, /// Additional CLI arguments. #[serde(default)] pub extra_args: Vec, /// Output directory for generated files. pub output_dir: Option, // --- Rustup --- /// Components to add (e.g., ["clippy", "rustfmt"]). #[serde(default)] pub components: Vec, /// Compilation targets to add. #[serde(default)] pub targets: Vec, /// Default toolchain for rust-install steps. pub default_toolchain: Option, // --- Sub-workflow --- /// Child workflow ID. #[serde(rename = "workflow")] pub child_workflow: Option, /// Child workflow version. #[serde(rename = "workflow_version")] pub child_version: Option, } /// Deno sandbox permission configuration. #[derive(Debug, Deserialize, Serialize, Clone, Default, JsonSchema)] pub struct DenoPermissionsYaml { /// Allowed network hosts. #[serde(default)] pub net: Vec, /// Allowed read paths. #[serde(default)] pub read: Vec, /// Allowed write paths. #[serde(default)] pub write: Vec, /// Allowed environment variable names. #[serde(default)] pub env: Vec, /// Allow subprocess execution. #[serde(default)] pub run: bool, /// Allow dynamic imports. #[serde(default)] pub dynamic_import: bool, } /// Data reference for step inputs/outputs. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct DataRef { pub name: String, pub path: Option, pub json_path: Option, } /// TLS configuration for BuildKit connections. #[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)] pub struct TlsConfigYaml { pub ca: Option, pub cert: Option, pub key: Option, } /// Registry authentication credentials. #[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)] pub struct RegistryAuthYaml { pub username: String, pub password: String, } /// Volume mount configuration for containerd steps. #[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)] pub struct VolumeMountYaml { /// Host path. pub source: String, /// Container path. pub target: String, /// Mount as read-only. #[serde(default)] pub readonly: bool, } /// Error handling behavior configuration. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct YamlErrorBehavior { /// Behavior type: retry, suspend, terminate, compensate. #[serde(rename = "type")] pub behavior_type: String, /// Retry interval (e.g., "60s"). pub interval: Option, /// Maximum retry attempts. pub max_retries: Option, } /// Generate the JSON Schema for the WFE workflow YAML format. pub fn generate_json_schema() -> serde_json::Value { let schema = schemars::generate::SchemaSettings::default() .into_generator() .into_root_schema_for::(); serde_json::to_value(schema).unwrap_or_default() } /// Generate the JSON Schema as YAML for human consumption. 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::>() ); } #[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() ); } }