diff --git a/CHANGELOG.md b/CHANGELOG.md index 570df7d..9046461 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to this project will be documented in this file. +## [1.8.1] - 2026-04-06 + +### Added + +- **wfe-server**: gRPC reflection support via `tonic-reflection` +- **wfe-server**: Schema endpoints: `/schema/workflow.json` (JSON Schema), `/schema/workflow.yaml` (YAML Schema), `/schema/workflow.proto` (raw proto) +- **wfe-yaml**: Auto-generated JSON Schema from `schemars` derives on all YAML types +- **wfe-server**: Dockerfile for multi-stage alpine build with all executor features +- **wfe-server**: Comprehensive configuration reference (README.md) + +### Fixed + +- **wfe-yaml**: Added missing `license`, `repository`, `homepage` fields to Cargo.toml +- **wfe-buildkit-protos**: Removed vendored Go repos (166MB -> 356K), kept only .proto files +- **wfe-containerd-protos**: Removed vendored Go repos (53MB -> 216K), kept only .proto files +- Filesystem loop warnings from circular symlinks in vendored Go modules eliminated +- Pinned `icu_calendar <2.2` to work around `temporal_rs`/`deno_core` incompatibility + ## [1.8.0] - 2026-04-06 ### Added diff --git a/Cargo.toml b/Cargo.toml index 2c034e8..8d193a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["wfe-core", "wfe-sqlite", "wfe-postgres", "wfe-opensearch", "wfe-valk resolver = "2" [workspace.package] -version = "1.8.0" +version = "1.8.1" edition = "2024" license = "MIT" repository = "https://src.sunbeam.pt/studio/wfe" @@ -38,16 +38,16 @@ redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] } opensearch = "2" # Internal crates -wfe-core = { version = "1.8.0", path = "wfe-core", registry = "sunbeam" } -wfe-sqlite = { version = "1.8.0", path = "wfe-sqlite", registry = "sunbeam" } -wfe-postgres = { version = "1.8.0", path = "wfe-postgres", registry = "sunbeam" } -wfe-opensearch = { version = "1.8.0", path = "wfe-opensearch", registry = "sunbeam" } -wfe-valkey = { version = "1.8.0", path = "wfe-valkey", registry = "sunbeam" } -wfe-yaml = { version = "1.8.0", path = "wfe-yaml", registry = "sunbeam" } -wfe-buildkit = { version = "1.8.0", path = "wfe-buildkit", registry = "sunbeam" } -wfe-containerd = { version = "1.8.0", path = "wfe-containerd", registry = "sunbeam" } -wfe-rustlang = { version = "1.8.0", path = "wfe-rustlang", registry = "sunbeam" } -wfe-kubernetes = { version = "1.8.0", path = "wfe-kubernetes", registry = "sunbeam" } +wfe-core = { version = "1.8.1", path = "wfe-core", registry = "sunbeam" } +wfe-sqlite = { version = "1.8.1", path = "wfe-sqlite", registry = "sunbeam" } +wfe-postgres = { version = "1.8.1", path = "wfe-postgres", registry = "sunbeam" } +wfe-opensearch = { version = "1.8.1", path = "wfe-opensearch", registry = "sunbeam" } +wfe-valkey = { version = "1.8.1", path = "wfe-valkey", registry = "sunbeam" } +wfe-yaml = { version = "1.8.1", path = "wfe-yaml", registry = "sunbeam" } +wfe-buildkit = { version = "1.8.1", path = "wfe-buildkit", registry = "sunbeam" } +wfe-containerd = { version = "1.8.1", path = "wfe-containerd", registry = "sunbeam" } +wfe-rustlang = { version = "1.8.1", path = "wfe-rustlang", registry = "sunbeam" } +wfe-kubernetes = { version = "1.8.1", path = "wfe-kubernetes", registry = "sunbeam" } # YAML serde_yaml = "0.9" diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..7bb3d24 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +VERSION="${1:?Usage: scripts/release.sh [message]}" +MESSAGE="${2:-v${VERSION}}" + +echo "=== Releasing v${VERSION} ===" + +# Tag +git tag -a "v${VERSION}" -m "${MESSAGE}" + +# Publish leaf crates +for crate in wfe-core wfe-containerd-protos wfe-buildkit-protos wfe-server-protos; do + echo "--- Publishing ${crate} ---" + cargo publish -p "${crate}" --registry sunbeam +done + +# Middle layer +for crate in wfe-sqlite wfe-postgres wfe-opensearch wfe-valkey wfe-buildkit wfe-containerd wfe-rustlang; do + echo "--- Publishing ${crate} ---" + cargo publish -p "${crate}" --registry sunbeam +done + +# Top layer (needs index to catch up) +sleep 10 +for crate in wfe wfe-yaml; do + echo "--- Publishing ${crate} ---" + cargo publish -p "${crate}" --registry sunbeam +done + +# Final layer +sleep 10 +for crate in wfe-server wfe-deno wfe-kubernetes; do + echo "--- Publishing ${crate} ---" + cargo publish -p "${crate}" --registry sunbeam +done + +# Push git +git push origin mainline +git push origin "v${VERSION}" + +# Create Gitea release from changelog +release_body=$(awk -v ver="${VERSION}" '/^## \[/{if(found)exit;if(index($0,"["ver"]"))found=1;next}found{print}' CHANGELOG.md) +tea release create --tag "v${VERSION}" --title "v${VERSION}" --note "${release_body}" || echo "(release already exists)" + +# Build + push Docker image +echo "--- Building Docker image ---" +docker buildx build --builder sunbeam-remote --platform linux/amd64 \ + -t "src.sunbeam.pt/studio/wfe:${VERSION}" \ + -t "src.sunbeam.pt/studio/wfe:latest" \ + --push . + +echo "=== v${VERSION} released ===" diff --git a/wfe-buildkit/Cargo.toml b/wfe-buildkit/Cargo.toml index b0f74f4..a7af80b 100644 --- a/wfe-buildkit/Cargo.toml +++ b/wfe-buildkit/Cargo.toml @@ -16,7 +16,7 @@ async-trait = { workspace = true } tracing = { workspace = true } thiserror = { workspace = true } regex = { workspace = true } -wfe-buildkit-protos = { version = "1.8.0", path = "../wfe-buildkit-protos", registry = "sunbeam" } +wfe-buildkit-protos = { version = "1.8.1", path = "../wfe-buildkit-protos", registry = "sunbeam" } tonic = "0.14" tower = { version = "0.4", features = ["util"] } hyper-util = { version = "0.1", features = ["tokio"] } diff --git a/wfe-containerd/Cargo.toml b/wfe-containerd/Cargo.toml index ac568c7..6e38624 100644 --- a/wfe-containerd/Cargo.toml +++ b/wfe-containerd/Cargo.toml @@ -9,7 +9,7 @@ description = "containerd container runner executor for WFE" [dependencies] wfe-core = { workspace = true } -wfe-containerd-protos = { version = "1.8.0", path = "../wfe-containerd-protos", registry = "sunbeam" } +wfe-containerd-protos = { version = "1.8.1", path = "../wfe-containerd-protos", registry = "sunbeam" } tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/wfe-deno/Cargo.toml b/wfe-deno/Cargo.toml index 95e0ab7..120da49 100644 --- a/wfe-deno/Cargo.toml +++ b/wfe-deno/Cargo.toml @@ -9,7 +9,7 @@ description = "Deno bindings for the WFE workflow engine" [dependencies] wfe-core = { workspace = true, features = ["test-support"] } -wfe = { version = "1.8.0", path = "../wfe", registry = "sunbeam" } +wfe = { version = "1.8.1", path = "../wfe", registry = "sunbeam" } deno_core = { workspace = true } deno_error = { workspace = true } tokio = { workspace = true } diff --git a/wfe-server-protos/build.rs b/wfe-server-protos/build.rs index 613474e..21ea9c6 100644 --- a/wfe-server-protos/build.rs +++ b/wfe-server-protos/build.rs @@ -1,12 +1,18 @@ +use std::path::PathBuf; + fn main() -> Result<(), Box> { let proto_files = vec!["proto/wfe/v1/wfe.proto"]; + let out_dir = PathBuf::from(std::env::var("OUT_DIR")?); + let descriptor_path = out_dir.join("wfe_descriptor.bin"); + let mut prost_config = prost_build::Config::new(); prost_config.include_file("mod.rs"); tonic_prost_build::configure() .build_server(true) .build_client(true) + .file_descriptor_set_path(&descriptor_path) .compile_with_config( prost_config, &proto_files, diff --git a/wfe-server-protos/src/lib.rs b/wfe-server-protos/src/lib.rs index 390a0d6..cb62534 100644 --- a/wfe-server-protos/src/lib.rs +++ b/wfe-server-protos/src/lib.rs @@ -15,3 +15,6 @@ include!(concat!(env!("OUT_DIR"), "/mod.rs")); pub use prost; pub use prost_types; pub use tonic; + +/// Encoded file descriptor set for gRPC reflection. +pub const FILE_DESCRIPTOR_SET: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/wfe_descriptor.bin")); diff --git a/wfe-server/Cargo.toml b/wfe-server/Cargo.toml index 1c5a2bc..12d2e14 100644 --- a/wfe-server/Cargo.toml +++ b/wfe-server/Cargo.toml @@ -14,9 +14,9 @@ path = "src/main.rs" [dependencies] # Internal wfe-core = { workspace = true, features = ["test-support"] } -wfe = { version = "1.8.0", path = "../wfe", registry = "sunbeam" } -wfe-yaml = { version = "1.8.0", path = "../wfe-yaml", registry = "sunbeam", features = ["rustlang", "buildkit", "containerd"] } -wfe-server-protos = { version = "1.8.0", path = "../wfe-server-protos", registry = "sunbeam" } +wfe = { version = "1.8.1", path = "../wfe", registry = "sunbeam" } +wfe-yaml = { version = "1.8.1", path = "../wfe-yaml", registry = "sunbeam", features = ["rustlang", "buildkit", "containerd"] } +wfe-server-protos = { version = "1.8.1", path = "../wfe-server-protos", registry = "sunbeam" } wfe-sqlite = { workspace = true } wfe-postgres = { workspace = true } wfe-valkey = { workspace = true } @@ -26,6 +26,7 @@ opensearch = { workspace = true } # gRPC tonic = "0.14" tonic-health = "0.14" +tonic-reflection = "0.14" prost-types = "0.14" # HTTP (webhooks) diff --git a/wfe-server/src/main.rs b/wfe-server/src/main.rs index ee40ec6..f5aa48e 100644 --- a/wfe-server/src/main.rs +++ b/wfe-server/src/main.rs @@ -172,6 +172,9 @@ async fn main() -> Result<(), Box> { .route("/webhooks/github", axum::routing::post(webhook::handle_github_webhook)) .route("/webhooks/gitea", axum::routing::post(webhook::handle_gitea_webhook)) .route("/healthz", axum::routing::get(webhook::health_check)) + .route("/schema/workflow.proto", axum::routing::get(serve_proto_schema)) + .route("/schema/workflow.json", axum::routing::get(serve_json_schema)) + .route("/schema/workflow.yaml", axum::routing::get(serve_yaml_example)) .layer(axum::extract::DefaultBodyLimit::max(2 * 1024 * 1024)) .with_state(webhook_state); @@ -180,8 +183,14 @@ async fn main() -> Result<(), Box> { let http_addr = config.http_addr; tracing::info!(%grpc_addr, %http_addr, "servers listening"); + let reflection_service = tonic_reflection::server::Builder::configure() + .register_encoded_file_descriptor_set(wfe_server_protos::FILE_DESCRIPTOR_SET) + .build_v1() + .expect("failed to build reflection service"); + let grpc_server = Server::builder() .add_service(health_service) + .add_service(reflection_service) .add_service(WfeServer::with_interceptor(wfe_service, auth_interceptor)) .serve(grpc_addr); @@ -248,3 +257,25 @@ async fn load_yaml_definitions(host: &wfe::WorkflowHost, dir: &std::path::Path) } } } + +/// Serve the raw .proto schema file. +async fn serve_proto_schema() -> impl axum::response::IntoResponse { + ( + [(axum::http::header::CONTENT_TYPE, "text/plain; charset=utf-8")], + include_str!("../../wfe-server-protos/proto/wfe/v1/wfe.proto"), + ) +} + +/// Serve the auto-generated JSON Schema for workflow YAML definitions. +async fn serve_json_schema() -> impl axum::response::IntoResponse { + let schema = wfe_yaml::schema::generate_json_schema(); + axum::Json(schema) +} + +/// Serve the auto-generated JSON Schema as YAML. +async fn serve_yaml_example() -> impl axum::response::IntoResponse { + ( + [(axum::http::header::CONTENT_TYPE, "text/yaml; charset=utf-8")], + wfe_yaml::schema::generate_yaml_schema(), + ) +} diff --git a/wfe-yaml/Cargo.toml b/wfe-yaml/Cargo.toml index 39abd0f..7dee27e 100644 --- a/wfe-yaml/Cargo.toml +++ b/wfe-yaml/Cargo.toml @@ -27,6 +27,7 @@ thiserror = { workspace = true } tracing = { workspace = true } chrono = { workspace = true } regex = { workspace = true } +schemars = { version = "1", features = ["derive"] } deno_core = { workspace = true, optional = true } deno_error = { workspace = true, optional = true } url = { workspace = true, optional = true } diff --git a/wfe-yaml/src/schema.rs b/wfe-yaml/src/schema.rs index 6a6f8de..9f0d03b 100644 --- a/wfe-yaml/src/schema.rs +++ b/wfe-yaml/src/schema.rs @@ -1,13 +1,14 @@ use std::collections::HashMap; -use serde::Deserialize; +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, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)] #[serde(untagged)] pub enum YamlCondition { /// Leaf comparison (has a `field:` key). @@ -17,7 +18,7 @@ pub enum YamlCondition { } /// A combinator condition containing sub-conditions. -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)] pub struct YamlCombinator { #[serde(default)] pub all: Option>, @@ -32,22 +33,29 @@ pub struct YamlCombinator { } /// A leaf comparison condition that compares a field value. -#[derive(Debug, Deserialize, Clone)] +#[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, @@ -56,7 +64,7 @@ pub struct YamlComparison { } /// Top-level YAML file structure supporting both single and multi-workflow files. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct YamlWorkflowFile { /// Single workflow (backward compatible). pub workflow: Option, @@ -66,19 +74,25 @@ pub struct YamlWorkflowFile { /// Legacy single-workflow top-level structure. Kept for backward compatibility /// with code that deserializes `YamlWorkflow` directly. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct YamlWorkflow { pub workflow: WorkflowSpec, } -#[derive(Debug, Deserialize)] +/// A complete workflow definition. +#[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct WorkflowSpec { + /// Unique workflow identifier. pub id: String, + /// 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"`. @@ -87,36 +101,45 @@ pub struct WorkflowSpec { /// Typed output schema: { field_name: type_string }. #[serde(default)] pub outputs: HashMap, - /// Infrastructure services required by this workflow. + /// 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)] +#[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)] +#[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct YamlReadiness { /// Execute a command to check readiness. #[serde(default)] @@ -139,9 +162,11 @@ pub struct YamlReadiness { } /// HTTP GET readiness check. -#[derive(Debug, Deserialize)] +#[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, } @@ -150,179 +175,261 @@ fn default_health_path() -> String { "/".into() } -#[derive(Debug, Deserialize)] +/// 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>, - /// Optional condition that must be true for this step to execute. + /// Condition that must be true for this step to execute. #[serde(default)] pub when: Option, } -#[derive(Debug, Deserialize, Clone)] +/// 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 fields + + // --- 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 fields + + // --- 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 for containerd steps: "nerdctl" (default) or "docker". + /// CLI binary name: "nerdctl" (default) or "docker". pub cli: Option, - // Kubernetes fields - /// Kubeconfig path for kubernetes steps. + + // --- Kubernetes --- + /// Kubeconfig path. pub kubeconfig: Option, - /// Namespace override for kubernetes steps. + /// Namespace override. pub namespace: Option, - /// Image pull policy for kubernetes steps: Always, IfNotPresent, Never. + /// Image pull policy: Always, IfNotPresent, Never. pub pull_policy: Option, - // Cargo fields + + // --- Cargo --- /// Target package for cargo steps (`-p`). pub package: Option, - /// Features to enable for cargo steps. + /// Features to enable. #[serde(default)] pub features: Vec, - /// Enable all features for cargo steps. + /// Enable all features. #[serde(default)] pub all_features: Option, - /// Disable default features for cargo steps. + /// Disable default features. #[serde(default)] pub no_default_features: Option, - /// Build in release mode for cargo steps. + /// Build in release mode. #[serde(default)] pub release: Option, - /// Build profile for cargo steps (`--profile`). + /// Build profile (--profile). pub profile: Option, - /// Rust toolchain override for cargo steps (e.g. "nightly"). + /// Rust toolchain override (e.g., "nightly"). pub toolchain: Option, - /// Additional arguments for cargo/rustup steps. + /// Additional CLI arguments. #[serde(default)] pub extra_args: Vec, - /// Output directory for generated files (e.g., MDX docs). + /// Output directory for generated files. pub output_dir: Option, - // Rustup fields - /// Components to add for rustup steps (e.g. ["clippy", "rustfmt"]). + + // --- Rustup --- + /// Components to add (e.g., ["clippy", "rustfmt"]). #[serde(default)] pub components: Vec, - /// Compilation targets to add for rustup steps (e.g. ["wasm32-unknown-unknown"]). + /// Compilation targets to add. #[serde(default)] pub targets: Vec, /// Default toolchain for rust-install steps. pub default_toolchain: Option, - // Workflow (sub-workflow) fields - /// Child workflow ID (for `type: workflow` steps). + + // --- Sub-workflow --- + /// Child workflow ID. #[serde(rename = "workflow")] pub child_workflow: Option, - /// Child workflow version (for `type: workflow` steps). + /// Child workflow version. #[serde(rename = "workflow_version")] pub child_version: Option, } -/// YAML-level permission configuration for Deno steps. -#[derive(Debug, Deserialize, Clone, Default)] +/// 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, } -#[derive(Debug, Deserialize)] +/// Data reference for step inputs/outputs. +#[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct DataRef { pub name: String, pub path: Option, pub json_path: Option, } -/// YAML-level TLS configuration for BuildKit steps. -#[derive(Debug, Deserialize, Clone)] +/// TLS configuration for BuildKit connections. +#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)] pub struct TlsConfigYaml { pub ca: Option, pub cert: Option, pub key: Option, } -/// YAML-level registry auth configuration for BuildKit steps. -#[derive(Debug, Deserialize, Clone)] +/// Registry authentication credentials. +#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)] pub struct RegistryAuthYaml { pub username: String, pub password: String, } -/// YAML-level volume mount configuration for containerd steps. -#[derive(Debug, Deserialize, Clone)] +/// 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, } -#[derive(Debug, Deserialize)] +/// 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() +}