feat(wfe-server): gRPC reflection, auto-generated schema endpoints, Dockerfile
- tonic-reflection for gRPC service discovery - /schema/workflow.json (JSON Schema from schemars derives) - /schema/workflow.yaml (same schema in YAML) - /schema/workflow.proto (raw proto file) - Multi-stage alpine Dockerfile with all executor features - Comprehensive configuration reference (wfe-server/README.md) - Release script (scripts/release.sh) - Bumped to 1.8.1
This commit is contained in:
18
CHANGELOG.md
18
CHANGELOG.md
@@ -2,6 +2,24 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
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
|
## [1.8.0] - 2026-04-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
22
Cargo.toml
22
Cargo.toml
@@ -3,7 +3,7 @@ members = ["wfe-core", "wfe-sqlite", "wfe-postgres", "wfe-opensearch", "wfe-valk
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.8.0"
|
version = "1.8.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://src.sunbeam.pt/studio/wfe"
|
repository = "https://src.sunbeam.pt/studio/wfe"
|
||||||
@@ -38,16 +38,16 @@ redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] }
|
|||||||
opensearch = "2"
|
opensearch = "2"
|
||||||
|
|
||||||
# Internal crates
|
# Internal crates
|
||||||
wfe-core = { version = "1.8.0", path = "wfe-core", registry = "sunbeam" }
|
wfe-core = { version = "1.8.1", path = "wfe-core", registry = "sunbeam" }
|
||||||
wfe-sqlite = { version = "1.8.0", path = "wfe-sqlite", registry = "sunbeam" }
|
wfe-sqlite = { version = "1.8.1", path = "wfe-sqlite", registry = "sunbeam" }
|
||||||
wfe-postgres = { version = "1.8.0", path = "wfe-postgres", registry = "sunbeam" }
|
wfe-postgres = { version = "1.8.1", path = "wfe-postgres", registry = "sunbeam" }
|
||||||
wfe-opensearch = { version = "1.8.0", path = "wfe-opensearch", registry = "sunbeam" }
|
wfe-opensearch = { version = "1.8.1", path = "wfe-opensearch", registry = "sunbeam" }
|
||||||
wfe-valkey = { version = "1.8.0", path = "wfe-valkey", registry = "sunbeam" }
|
wfe-valkey = { version = "1.8.1", path = "wfe-valkey", registry = "sunbeam" }
|
||||||
wfe-yaml = { version = "1.8.0", path = "wfe-yaml", registry = "sunbeam" }
|
wfe-yaml = { version = "1.8.1", path = "wfe-yaml", registry = "sunbeam" }
|
||||||
wfe-buildkit = { version = "1.8.0", path = "wfe-buildkit", registry = "sunbeam" }
|
wfe-buildkit = { version = "1.8.1", path = "wfe-buildkit", registry = "sunbeam" }
|
||||||
wfe-containerd = { version = "1.8.0", path = "wfe-containerd", registry = "sunbeam" }
|
wfe-containerd = { version = "1.8.1", path = "wfe-containerd", registry = "sunbeam" }
|
||||||
wfe-rustlang = { version = "1.8.0", path = "wfe-rustlang", registry = "sunbeam" }
|
wfe-rustlang = { version = "1.8.1", path = "wfe-rustlang", registry = "sunbeam" }
|
||||||
wfe-kubernetes = { version = "1.8.0", path = "wfe-kubernetes", registry = "sunbeam" }
|
wfe-kubernetes = { version = "1.8.1", path = "wfe-kubernetes", registry = "sunbeam" }
|
||||||
|
|
||||||
# YAML
|
# YAML
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
|
|||||||
53
scripts/release.sh
Executable file
53
scripts/release.sh
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION="${1:?Usage: scripts/release.sh <version> [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 ==="
|
||||||
@@ -16,7 +16,7 @@ async-trait = { workspace = true }
|
|||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
regex = { 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"
|
tonic = "0.14"
|
||||||
tower = { version = "0.4", features = ["util"] }
|
tower = { version = "0.4", features = ["util"] }
|
||||||
hyper-util = { version = "0.1", features = ["tokio"] }
|
hyper-util = { version = "0.1", features = ["tokio"] }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ description = "containerd container runner executor for WFE"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wfe-core = { workspace = true }
|
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 }
|
tokio = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ description = "Deno bindings for the WFE workflow engine"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wfe-core = { workspace = true, features = ["test-support"] }
|
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_core = { workspace = true }
|
||||||
deno_error = { workspace = true }
|
deno_error = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let proto_files = vec!["proto/wfe/v1/wfe.proto"];
|
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();
|
let mut prost_config = prost_build::Config::new();
|
||||||
prost_config.include_file("mod.rs");
|
prost_config.include_file("mod.rs");
|
||||||
|
|
||||||
tonic_prost_build::configure()
|
tonic_prost_build::configure()
|
||||||
.build_server(true)
|
.build_server(true)
|
||||||
.build_client(true)
|
.build_client(true)
|
||||||
|
.file_descriptor_set_path(&descriptor_path)
|
||||||
.compile_with_config(
|
.compile_with_config(
|
||||||
prost_config,
|
prost_config,
|
||||||
&proto_files,
|
&proto_files,
|
||||||
|
|||||||
@@ -15,3 +15,6 @@ include!(concat!(env!("OUT_DIR"), "/mod.rs"));
|
|||||||
pub use prost;
|
pub use prost;
|
||||||
pub use prost_types;
|
pub use prost_types;
|
||||||
pub use tonic;
|
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"));
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
# Internal
|
# Internal
|
||||||
wfe-core = { workspace = true, features = ["test-support"] }
|
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" }
|
||||||
wfe-yaml = { version = "1.8.0", path = "../wfe-yaml", registry = "sunbeam", features = ["rustlang", "buildkit", "containerd"] }
|
wfe-yaml = { version = "1.8.1", path = "../wfe-yaml", registry = "sunbeam", features = ["rustlang", "buildkit", "containerd"] }
|
||||||
wfe-server-protos = { version = "1.8.0", path = "../wfe-server-protos", registry = "sunbeam" }
|
wfe-server-protos = { version = "1.8.1", path = "../wfe-server-protos", registry = "sunbeam" }
|
||||||
wfe-sqlite = { workspace = true }
|
wfe-sqlite = { workspace = true }
|
||||||
wfe-postgres = { workspace = true }
|
wfe-postgres = { workspace = true }
|
||||||
wfe-valkey = { workspace = true }
|
wfe-valkey = { workspace = true }
|
||||||
@@ -26,6 +26,7 @@ opensearch = { workspace = true }
|
|||||||
# gRPC
|
# gRPC
|
||||||
tonic = "0.14"
|
tonic = "0.14"
|
||||||
tonic-health = "0.14"
|
tonic-health = "0.14"
|
||||||
|
tonic-reflection = "0.14"
|
||||||
prost-types = "0.14"
|
prost-types = "0.14"
|
||||||
|
|
||||||
# HTTP (webhooks)
|
# HTTP (webhooks)
|
||||||
|
|||||||
@@ -172,6 +172,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.route("/webhooks/github", axum::routing::post(webhook::handle_github_webhook))
|
.route("/webhooks/github", axum::routing::post(webhook::handle_github_webhook))
|
||||||
.route("/webhooks/gitea", axum::routing::post(webhook::handle_gitea_webhook))
|
.route("/webhooks/gitea", axum::routing::post(webhook::handle_gitea_webhook))
|
||||||
.route("/healthz", axum::routing::get(webhook::health_check))
|
.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))
|
.layer(axum::extract::DefaultBodyLimit::max(2 * 1024 * 1024))
|
||||||
.with_state(webhook_state);
|
.with_state(webhook_state);
|
||||||
|
|
||||||
@@ -180,8 +183,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let http_addr = config.http_addr;
|
let http_addr = config.http_addr;
|
||||||
tracing::info!(%grpc_addr, %http_addr, "servers listening");
|
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()
|
let grpc_server = Server::builder()
|
||||||
.add_service(health_service)
|
.add_service(health_service)
|
||||||
|
.add_service(reflection_service)
|
||||||
.add_service(WfeServer::with_interceptor(wfe_service, auth_interceptor))
|
.add_service(WfeServer::with_interceptor(wfe_service, auth_interceptor))
|
||||||
.serve(grpc_addr);
|
.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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ thiserror = { workspace = true }
|
|||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
|
schemars = { version = "1", features = ["derive"] }
|
||||||
deno_core = { workspace = true, optional = true }
|
deno_core = { workspace = true, optional = true }
|
||||||
deno_error = { workspace = true, optional = true }
|
deno_error = { workspace = true, optional = true }
|
||||||
url = { workspace = true, optional = true }
|
url = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// A condition in YAML that determines whether a step executes.
|
/// A condition in YAML that determines whether a step executes.
|
||||||
///
|
///
|
||||||
/// Uses `#[serde(untagged)]` so serde tries each variant in order.
|
/// 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:`.
|
/// 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`).
|
/// Comparison is listed first because it is more specific (requires `field`).
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum YamlCondition {
|
pub enum YamlCondition {
|
||||||
/// Leaf comparison (has a `field:` key).
|
/// Leaf comparison (has a `field:` key).
|
||||||
@@ -17,7 +18,7 @@ pub enum YamlCondition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A combinator condition containing sub-conditions.
|
/// A combinator condition containing sub-conditions.
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
|
||||||
pub struct YamlCombinator {
|
pub struct YamlCombinator {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub all: Option<Vec<YamlCondition>>,
|
pub all: Option<Vec<YamlCondition>>,
|
||||||
@@ -32,22 +33,29 @@ pub struct YamlCombinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A leaf comparison condition that compares a field value.
|
/// A leaf comparison condition that compares a field value.
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
|
||||||
pub struct YamlComparison {
|
pub struct YamlComparison {
|
||||||
pub field: String,
|
pub field: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[schemars(with = "Option<serde_json::Value>")]
|
||||||
pub equals: Option<serde_yaml::Value>,
|
pub equals: Option<serde_yaml::Value>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[schemars(with = "Option<serde_json::Value>")]
|
||||||
pub not_equals: Option<serde_yaml::Value>,
|
pub not_equals: Option<serde_yaml::Value>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[schemars(with = "Option<serde_json::Value>")]
|
||||||
pub gt: Option<serde_yaml::Value>,
|
pub gt: Option<serde_yaml::Value>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[schemars(with = "Option<serde_json::Value>")]
|
||||||
pub gte: Option<serde_yaml::Value>,
|
pub gte: Option<serde_yaml::Value>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[schemars(with = "Option<serde_json::Value>")]
|
||||||
pub lt: Option<serde_yaml::Value>,
|
pub lt: Option<serde_yaml::Value>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[schemars(with = "Option<serde_json::Value>")]
|
||||||
pub lte: Option<serde_yaml::Value>,
|
pub lte: Option<serde_yaml::Value>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[schemars(with = "Option<serde_json::Value>")]
|
||||||
pub contains: Option<serde_yaml::Value>,
|
pub contains: Option<serde_yaml::Value>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub is_null: Option<bool>,
|
pub is_null: Option<bool>,
|
||||||
@@ -56,7 +64,7 @@ pub struct YamlComparison {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Top-level YAML file structure supporting both single and multi-workflow files.
|
/// Top-level YAML file structure supporting both single and multi-workflow files.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct YamlWorkflowFile {
|
pub struct YamlWorkflowFile {
|
||||||
/// Single workflow (backward compatible).
|
/// Single workflow (backward compatible).
|
||||||
pub workflow: Option<WorkflowSpec>,
|
pub workflow: Option<WorkflowSpec>,
|
||||||
@@ -66,19 +74,25 @@ pub struct YamlWorkflowFile {
|
|||||||
|
|
||||||
/// Legacy single-workflow top-level structure. Kept for backward compatibility
|
/// Legacy single-workflow top-level structure. Kept for backward compatibility
|
||||||
/// with code that deserializes `YamlWorkflow` directly.
|
/// with code that deserializes `YamlWorkflow` directly.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct YamlWorkflow {
|
pub struct YamlWorkflow {
|
||||||
pub workflow: WorkflowSpec,
|
pub workflow: WorkflowSpec,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
/// A complete workflow definition.
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct WorkflowSpec {
|
pub struct WorkflowSpec {
|
||||||
|
/// Unique workflow identifier.
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
/// Workflow version number.
|
||||||
pub version: u32,
|
pub version: u32,
|
||||||
|
/// Optional human-readable description.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
/// Default error handling behavior for all steps.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub error_behavior: Option<YamlErrorBehavior>,
|
pub error_behavior: Option<YamlErrorBehavior>,
|
||||||
|
/// The steps that make up this workflow.
|
||||||
pub steps: Vec<YamlStep>,
|
pub steps: Vec<YamlStep>,
|
||||||
/// Typed input schema: { field_name: type_string }.
|
/// Typed input schema: { field_name: type_string }.
|
||||||
/// Example: `"repo_url": "string"`, `"tags": "list<string>"`.
|
/// Example: `"repo_url": "string"`, `"tags": "list<string>"`.
|
||||||
@@ -87,36 +101,45 @@ pub struct WorkflowSpec {
|
|||||||
/// Typed output schema: { field_name: type_string }.
|
/// Typed output schema: { field_name: type_string }.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub outputs: HashMap<String, String>,
|
pub outputs: HashMap<String, String>,
|
||||||
/// Infrastructure services required by this workflow.
|
/// Infrastructure services required by this workflow (databases, caches, etc.).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub services: HashMap<String, YamlService>,
|
pub services: HashMap<String, YamlService>,
|
||||||
/// Allow unknown top-level keys (e.g. `_templates`) for YAML anchors.
|
/// Allow unknown top-level keys (e.g. `_templates`) for YAML anchors.
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
|
#[schemars(skip)]
|
||||||
pub _extra: HashMap<String, serde_yaml::Value>,
|
pub _extra: HashMap<String, serde_yaml::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A service definition in YAML format.
|
/// A service definition in YAML format.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct YamlService {
|
pub struct YamlService {
|
||||||
|
/// Container image to run (e.g., "postgres:15").
|
||||||
pub image: String,
|
pub image: String,
|
||||||
|
/// Ports to expose (container ports).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ports: Vec<u16>,
|
pub ports: Vec<u16>,
|
||||||
|
/// Environment variables for the service container.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub env: HashMap<String, String>,
|
pub env: HashMap<String, String>,
|
||||||
|
/// Readiness probe configuration.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub readiness: Option<YamlReadiness>,
|
pub readiness: Option<YamlReadiness>,
|
||||||
|
/// Memory limit (e.g., "512Mi").
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub memory: Option<String>,
|
pub memory: Option<String>,
|
||||||
|
/// CPU limit (e.g., "500m").
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub cpu: Option<String>,
|
pub cpu: Option<String>,
|
||||||
|
/// Override container entrypoint.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub command: Option<Vec<String>>,
|
pub command: Option<Vec<String>>,
|
||||||
|
/// Override container args.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub args: Option<Vec<String>>,
|
pub args: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Readiness probe configuration in YAML format.
|
/// Readiness probe configuration in YAML format.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct YamlReadiness {
|
pub struct YamlReadiness {
|
||||||
/// Execute a command to check readiness.
|
/// Execute a command to check readiness.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -139,9 +162,11 @@ pub struct YamlReadiness {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// HTTP GET readiness check.
|
/// HTTP GET readiness check.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct YamlHttpGet {
|
pub struct YamlHttpGet {
|
||||||
|
/// Port to check.
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
|
/// HTTP path (default: "/").
|
||||||
#[serde(default = "default_health_path")]
|
#[serde(default = "default_health_path")]
|
||||||
pub path: String,
|
pub path: String,
|
||||||
}
|
}
|
||||||
@@ -150,179 +175,261 @@ fn default_health_path() -> String {
|
|||||||
"/".into()
|
"/".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
/// A single step in a workflow.
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct YamlStep {
|
pub struct YamlStep {
|
||||||
|
/// Step identifier (must be unique within the workflow).
|
||||||
pub name: String,
|
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")]
|
#[serde(rename = "type")]
|
||||||
pub step_type: Option<String>,
|
pub step_type: Option<String>,
|
||||||
|
/// Type-specific configuration.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub config: Option<StepConfig>,
|
pub config: Option<StepConfig>,
|
||||||
|
/// Input data references.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub inputs: Vec<DataRef>,
|
pub inputs: Vec<DataRef>,
|
||||||
|
/// Output data references.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub outputs: Vec<DataRef>,
|
pub outputs: Vec<DataRef>,
|
||||||
|
/// Steps to run in parallel.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub parallel: Option<Vec<YamlStep>>,
|
pub parallel: Option<Vec<YamlStep>>,
|
||||||
|
/// Error handling override for this step.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub error_behavior: Option<YamlErrorBehavior>,
|
pub error_behavior: Option<YamlErrorBehavior>,
|
||||||
|
/// Hook step to run on success.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub on_success: Option<Box<YamlStep>>,
|
pub on_success: Option<Box<YamlStep>>,
|
||||||
|
/// Compensation step to run on failure.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub on_failure: Option<Box<YamlStep>>,
|
pub on_failure: Option<Box<YamlStep>>,
|
||||||
|
/// Cleanup step that always runs.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ensure: Option<Box<YamlStep>>,
|
pub ensure: Option<Box<YamlStep>>,
|
||||||
/// Optional condition that must be true for this step to execute.
|
/// Condition that must be true for this step to execute.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub when: Option<YamlCondition>,
|
pub when: Option<YamlCondition>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
/// Step configuration (fields are type-specific).
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
|
||||||
pub struct StepConfig {
|
pub struct StepConfig {
|
||||||
|
// --- Shell ---
|
||||||
|
/// Shell command to run (shorthand for shell steps).
|
||||||
pub run: Option<String>,
|
pub run: Option<String>,
|
||||||
|
/// Path to a script file (deno steps).
|
||||||
pub file: Option<String>,
|
pub file: Option<String>,
|
||||||
|
/// Inline script source (deno steps).
|
||||||
pub script: Option<String>,
|
pub script: Option<String>,
|
||||||
|
/// Shell binary to use (default: "sh").
|
||||||
pub shell: Option<String>,
|
pub shell: Option<String>,
|
||||||
|
/// Environment variables.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub env: HashMap<String, String>,
|
pub env: HashMap<String, String>,
|
||||||
|
/// Execution timeout (e.g., "5m", "30s").
|
||||||
pub timeout: Option<String>,
|
pub timeout: Option<String>,
|
||||||
|
/// Working directory.
|
||||||
pub working_dir: Option<String>,
|
pub working_dir: Option<String>,
|
||||||
|
|
||||||
|
// --- Deno ---
|
||||||
|
/// Deno sandbox permissions.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub permissions: Option<DenoPermissionsYaml>,
|
pub permissions: Option<DenoPermissionsYaml>,
|
||||||
|
/// ES modules to import (e.g., "npm:lodash@4").
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub modules: Vec<String>,
|
pub modules: Vec<String>,
|
||||||
// BuildKit fields
|
|
||||||
|
// --- BuildKit ---
|
||||||
|
/// Dockerfile path.
|
||||||
pub dockerfile: Option<String>,
|
pub dockerfile: Option<String>,
|
||||||
|
/// Build context path.
|
||||||
pub context: Option<String>,
|
pub context: Option<String>,
|
||||||
|
/// Multi-stage build target.
|
||||||
pub target: Option<String>,
|
pub target: Option<String>,
|
||||||
|
/// Image tags.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
|
/// Build arguments.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub build_args: HashMap<String, String>,
|
pub build_args: HashMap<String, String>,
|
||||||
|
/// Cache import sources.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub cache_from: Vec<String>,
|
pub cache_from: Vec<String>,
|
||||||
|
/// Cache export destinations.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub cache_to: Vec<String>,
|
pub cache_to: Vec<String>,
|
||||||
|
/// Push built image to registry.
|
||||||
pub push: Option<bool>,
|
pub push: Option<bool>,
|
||||||
|
/// BuildKit daemon address.
|
||||||
pub buildkit_addr: Option<String>,
|
pub buildkit_addr: Option<String>,
|
||||||
|
/// TLS configuration.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tls: Option<TlsConfigYaml>,
|
pub tls: Option<TlsConfigYaml>,
|
||||||
|
/// Registry authentication per registry hostname.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub registry_auth: Option<HashMap<String, RegistryAuthYaml>>,
|
pub registry_auth: Option<HashMap<String, RegistryAuthYaml>>,
|
||||||
// Containerd fields
|
|
||||||
|
// --- Containerd ---
|
||||||
|
/// Container image (required for containerd/kubernetes steps).
|
||||||
pub image: Option<String>,
|
pub image: Option<String>,
|
||||||
|
/// Container command override.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub command: Option<Vec<String>>,
|
pub command: Option<Vec<String>>,
|
||||||
|
/// Volume mounts.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub volumes: Vec<VolumeMountYaml>,
|
pub volumes: Vec<VolumeMountYaml>,
|
||||||
|
/// User:group (e.g., "1000:1000").
|
||||||
pub user: Option<String>,
|
pub user: Option<String>,
|
||||||
|
/// Network mode: none, host, bridge.
|
||||||
pub network: Option<String>,
|
pub network: Option<String>,
|
||||||
|
/// Memory limit (e.g., "512m").
|
||||||
pub memory: Option<String>,
|
pub memory: Option<String>,
|
||||||
|
/// CPU limit (e.g., "1.0").
|
||||||
pub cpu: Option<String>,
|
pub cpu: Option<String>,
|
||||||
|
/// Image pull policy: always, if-not-present, never.
|
||||||
pub pull: Option<String>,
|
pub pull: Option<String>,
|
||||||
|
/// Containerd daemon address.
|
||||||
pub containerd_addr: Option<String>,
|
pub containerd_addr: Option<String>,
|
||||||
/// CLI binary name for containerd steps: "nerdctl" (default) or "docker".
|
/// CLI binary name: "nerdctl" (default) or "docker".
|
||||||
pub cli: Option<String>,
|
pub cli: Option<String>,
|
||||||
// Kubernetes fields
|
|
||||||
/// Kubeconfig path for kubernetes steps.
|
// --- Kubernetes ---
|
||||||
|
/// Kubeconfig path.
|
||||||
pub kubeconfig: Option<String>,
|
pub kubeconfig: Option<String>,
|
||||||
/// Namespace override for kubernetes steps.
|
/// Namespace override.
|
||||||
pub namespace: Option<String>,
|
pub namespace: Option<String>,
|
||||||
/// Image pull policy for kubernetes steps: Always, IfNotPresent, Never.
|
/// Image pull policy: Always, IfNotPresent, Never.
|
||||||
pub pull_policy: Option<String>,
|
pub pull_policy: Option<String>,
|
||||||
// Cargo fields
|
|
||||||
|
// --- Cargo ---
|
||||||
/// Target package for cargo steps (`-p`).
|
/// Target package for cargo steps (`-p`).
|
||||||
pub package: Option<String>,
|
pub package: Option<String>,
|
||||||
/// Features to enable for cargo steps.
|
/// Features to enable.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub features: Vec<String>,
|
pub features: Vec<String>,
|
||||||
/// Enable all features for cargo steps.
|
/// Enable all features.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub all_features: Option<bool>,
|
pub all_features: Option<bool>,
|
||||||
/// Disable default features for cargo steps.
|
/// Disable default features.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub no_default_features: Option<bool>,
|
pub no_default_features: Option<bool>,
|
||||||
/// Build in release mode for cargo steps.
|
/// Build in release mode.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub release: Option<bool>,
|
pub release: Option<bool>,
|
||||||
/// Build profile for cargo steps (`--profile`).
|
/// Build profile (--profile).
|
||||||
pub profile: Option<String>,
|
pub profile: Option<String>,
|
||||||
/// Rust toolchain override for cargo steps (e.g. "nightly").
|
/// Rust toolchain override (e.g., "nightly").
|
||||||
pub toolchain: Option<String>,
|
pub toolchain: Option<String>,
|
||||||
/// Additional arguments for cargo/rustup steps.
|
/// Additional CLI arguments.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub extra_args: Vec<String>,
|
pub extra_args: Vec<String>,
|
||||||
/// Output directory for generated files (e.g., MDX docs).
|
/// Output directory for generated files.
|
||||||
pub output_dir: Option<String>,
|
pub output_dir: Option<String>,
|
||||||
// Rustup fields
|
|
||||||
/// Components to add for rustup steps (e.g. ["clippy", "rustfmt"]).
|
// --- Rustup ---
|
||||||
|
/// Components to add (e.g., ["clippy", "rustfmt"]).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub components: Vec<String>,
|
pub components: Vec<String>,
|
||||||
/// Compilation targets to add for rustup steps (e.g. ["wasm32-unknown-unknown"]).
|
/// Compilation targets to add.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub targets: Vec<String>,
|
pub targets: Vec<String>,
|
||||||
/// Default toolchain for rust-install steps.
|
/// Default toolchain for rust-install steps.
|
||||||
pub default_toolchain: Option<String>,
|
pub default_toolchain: Option<String>,
|
||||||
// Workflow (sub-workflow) fields
|
|
||||||
/// Child workflow ID (for `type: workflow` steps).
|
// --- Sub-workflow ---
|
||||||
|
/// Child workflow ID.
|
||||||
#[serde(rename = "workflow")]
|
#[serde(rename = "workflow")]
|
||||||
pub child_workflow: Option<String>,
|
pub child_workflow: Option<String>,
|
||||||
/// Child workflow version (for `type: workflow` steps).
|
/// Child workflow version.
|
||||||
#[serde(rename = "workflow_version")]
|
#[serde(rename = "workflow_version")]
|
||||||
pub child_version: Option<u32>,
|
pub child_version: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// YAML-level permission configuration for Deno steps.
|
/// Deno sandbox permission configuration.
|
||||||
#[derive(Debug, Deserialize, Clone, Default)]
|
#[derive(Debug, Deserialize, Serialize, Clone, Default, JsonSchema)]
|
||||||
pub struct DenoPermissionsYaml {
|
pub struct DenoPermissionsYaml {
|
||||||
|
/// Allowed network hosts.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub net: Vec<String>,
|
pub net: Vec<String>,
|
||||||
|
/// Allowed read paths.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub read: Vec<String>,
|
pub read: Vec<String>,
|
||||||
|
/// Allowed write paths.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub write: Vec<String>,
|
pub write: Vec<String>,
|
||||||
|
/// Allowed environment variable names.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub env: Vec<String>,
|
pub env: Vec<String>,
|
||||||
|
/// Allow subprocess execution.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub run: bool,
|
pub run: bool,
|
||||||
|
/// Allow dynamic imports.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dynamic_import: bool,
|
pub dynamic_import: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
/// Data reference for step inputs/outputs.
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct DataRef {
|
pub struct DataRef {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub path: Option<String>,
|
pub path: Option<String>,
|
||||||
pub json_path: Option<String>,
|
pub json_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// YAML-level TLS configuration for BuildKit steps.
|
/// TLS configuration for BuildKit connections.
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
|
||||||
pub struct TlsConfigYaml {
|
pub struct TlsConfigYaml {
|
||||||
pub ca: Option<String>,
|
pub ca: Option<String>,
|
||||||
pub cert: Option<String>,
|
pub cert: Option<String>,
|
||||||
pub key: Option<String>,
|
pub key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// YAML-level registry auth configuration for BuildKit steps.
|
/// Registry authentication credentials.
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
|
||||||
pub struct RegistryAuthYaml {
|
pub struct RegistryAuthYaml {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// YAML-level volume mount configuration for containerd steps.
|
/// Volume mount configuration for containerd steps.
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone, JsonSchema)]
|
||||||
pub struct VolumeMountYaml {
|
pub struct VolumeMountYaml {
|
||||||
|
/// Host path.
|
||||||
pub source: String,
|
pub source: String,
|
||||||
|
/// Container path.
|
||||||
pub target: String,
|
pub target: String,
|
||||||
|
/// Mount as read-only.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub readonly: bool,
|
pub readonly: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
/// Error handling behavior configuration.
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct YamlErrorBehavior {
|
pub struct YamlErrorBehavior {
|
||||||
|
/// Behavior type: retry, suspend, terminate, compensate.
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
pub behavior_type: String,
|
pub behavior_type: String,
|
||||||
|
/// Retry interval (e.g., "60s").
|
||||||
pub interval: Option<String>,
|
pub interval: Option<String>,
|
||||||
|
/// Maximum retry attempts.
|
||||||
pub max_retries: Option<u32>,
|
pub max_retries: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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::<YamlWorkflowFile>();
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user