Concourse-CI-inspired YAML format for defining workflows. Compiles to standard WorkflowDefinition + step factories. Features: - Schema parsing with serde_yaml (YamlWorkflow, YamlStep, StepConfig) - ((var.path)) interpolation from config maps at load time - YAML anchors (&anchor/*alias) fully supported - Validation at load time (no runtime surprises) - Shell executor: runs commands via tokio::process, captures stdout, parses ##wfe[output name=value] annotations for structured outputs - Compiler: sequential wiring, parallel blocks, on_failure/on_success/ ensure hooks, error behavior mapping - Public API: load_workflow(), load_workflow_from_str() - 23 tests (schema, interpolation, compiler, e2e)
226 lines
5.2 KiB
Rust
226 lines
5.2 KiB
Rust
use std::collections::HashMap;
|
|
use std::time::Duration;
|
|
|
|
use wfe_core::models::error_behavior::ErrorBehavior;
|
|
use wfe_yaml::load_workflow_from_str;
|
|
|
|
#[test]
|
|
fn single_step_produces_one_workflow_step() {
|
|
let yaml = r#"
|
|
workflow:
|
|
id: single
|
|
version: 1
|
|
steps:
|
|
- name: hello
|
|
type: shell
|
|
config:
|
|
run: echo hello
|
|
"#;
|
|
let compiled = load_workflow_from_str(yaml, &HashMap::new()).unwrap();
|
|
// The definition should have exactly 1 main step.
|
|
let main_steps: Vec<_> = compiled
|
|
.definition
|
|
.steps
|
|
.iter()
|
|
.filter(|s| s.name.as_deref() == Some("hello"))
|
|
.collect();
|
|
assert_eq!(main_steps.len(), 1);
|
|
assert_eq!(main_steps[0].id, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn two_sequential_steps_wired_correctly() {
|
|
let yaml = r#"
|
|
workflow:
|
|
id: sequential
|
|
version: 1
|
|
steps:
|
|
- name: step-a
|
|
type: shell
|
|
config:
|
|
run: echo a
|
|
- name: step-b
|
|
type: shell
|
|
config:
|
|
run: echo b
|
|
"#;
|
|
let compiled = load_workflow_from_str(yaml, &HashMap::new()).unwrap();
|
|
|
|
let step_a = compiled
|
|
.definition
|
|
.steps
|
|
.iter()
|
|
.find(|s| s.name.as_deref() == Some("step-a"))
|
|
.unwrap();
|
|
let step_b = compiled
|
|
.definition
|
|
.steps
|
|
.iter()
|
|
.find(|s| s.name.as_deref() == Some("step-b"))
|
|
.unwrap();
|
|
|
|
// step-a should have an outcome pointing to step-b.
|
|
assert_eq!(step_a.outcomes.len(), 1);
|
|
assert_eq!(step_a.outcomes[0].next_step, step_b.id);
|
|
}
|
|
|
|
#[test]
|
|
fn parallel_block_produces_container_with_children() {
|
|
let yaml = r#"
|
|
workflow:
|
|
id: parallel-wf
|
|
version: 1
|
|
steps:
|
|
- name: parallel-group
|
|
parallel:
|
|
- name: task-a
|
|
type: shell
|
|
config:
|
|
run: echo a
|
|
- name: task-b
|
|
type: shell
|
|
config:
|
|
run: echo b
|
|
"#;
|
|
let compiled = load_workflow_from_str(yaml, &HashMap::new()).unwrap();
|
|
|
|
let container = compiled
|
|
.definition
|
|
.steps
|
|
.iter()
|
|
.find(|s| s.name.as_deref() == Some("parallel-group"))
|
|
.unwrap();
|
|
|
|
assert!(
|
|
container.step_type.contains("SequenceStep"),
|
|
"Container should be a SequenceStep, got: {}",
|
|
container.step_type
|
|
);
|
|
assert_eq!(container.children.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn on_failure_creates_compensation_step() {
|
|
let yaml = r#"
|
|
workflow:
|
|
id: compensation-wf
|
|
version: 1
|
|
steps:
|
|
- name: deploy
|
|
type: shell
|
|
config:
|
|
run: deploy.sh
|
|
on_failure:
|
|
name: rollback
|
|
type: shell
|
|
config:
|
|
run: rollback.sh
|
|
"#;
|
|
let compiled = load_workflow_from_str(yaml, &HashMap::new()).unwrap();
|
|
|
|
let deploy = compiled
|
|
.definition
|
|
.steps
|
|
.iter()
|
|
.find(|s| s.name.as_deref() == Some("deploy"))
|
|
.unwrap();
|
|
|
|
assert!(deploy.compensation_step_id.is_some());
|
|
assert_eq!(deploy.error_behavior, Some(ErrorBehavior::Compensate));
|
|
|
|
let rollback = compiled
|
|
.definition
|
|
.steps
|
|
.iter()
|
|
.find(|s| s.name.as_deref() == Some("rollback"))
|
|
.unwrap();
|
|
|
|
assert_eq!(deploy.compensation_step_id, Some(rollback.id));
|
|
}
|
|
|
|
#[test]
|
|
fn error_behavior_maps_correctly() {
|
|
let yaml = r#"
|
|
workflow:
|
|
id: retry-wf
|
|
version: 1
|
|
error_behavior:
|
|
type: retry
|
|
interval: 5s
|
|
max_retries: 10
|
|
steps:
|
|
- name: step1
|
|
type: shell
|
|
config:
|
|
run: echo hi
|
|
error_behavior:
|
|
type: suspend
|
|
"#;
|
|
let compiled = load_workflow_from_str(yaml, &HashMap::new()).unwrap();
|
|
|
|
assert_eq!(
|
|
compiled.definition.default_error_behavior,
|
|
ErrorBehavior::Retry {
|
|
interval: Duration::from_secs(5),
|
|
max_retries: 10,
|
|
}
|
|
);
|
|
|
|
let step = compiled
|
|
.definition
|
|
.steps
|
|
.iter()
|
|
.find(|s| s.name.as_deref() == Some("step1"))
|
|
.unwrap();
|
|
assert_eq!(step.error_behavior, Some(ErrorBehavior::Suspend));
|
|
}
|
|
|
|
#[test]
|
|
fn anchors_compile_correctly() {
|
|
let yaml = r#"
|
|
workflow:
|
|
id: anchor-wf
|
|
version: 1
|
|
steps:
|
|
- name: build
|
|
type: shell
|
|
config: &default_config
|
|
shell: bash
|
|
timeout: 5m
|
|
run: cargo build
|
|
|
|
- name: test
|
|
type: shell
|
|
config: *default_config
|
|
"#;
|
|
let compiled = load_workflow_from_str(yaml, &HashMap::new()).unwrap();
|
|
|
|
// Should have 2 main steps + factories.
|
|
let build_step = compiled
|
|
.definition
|
|
.steps
|
|
.iter()
|
|
.find(|s| s.name.as_deref() == Some("build"))
|
|
.unwrap();
|
|
let test_step = compiled
|
|
.definition
|
|
.steps
|
|
.iter()
|
|
.find(|s| s.name.as_deref() == Some("test"))
|
|
.unwrap();
|
|
|
|
// Both should have step_config.
|
|
assert!(build_step.step_config.is_some());
|
|
assert!(test_step.step_config.is_some());
|
|
|
|
// Build should wire to test.
|
|
assert_eq!(build_step.outcomes.len(), 1);
|
|
assert_eq!(build_step.outcomes[0].next_step, test_step.id);
|
|
|
|
// Test uses the same config via alias - shell should be bash.
|
|
let test_config: wfe_yaml::executors::shell::ShellConfig =
|
|
serde_json::from_value(test_step.step_config.clone().unwrap()).unwrap();
|
|
assert_eq!(test_config.run, "cargo build");
|
|
assert_eq!(test_config.shell, "bash", "shell should be inherited from YAML anchor alias");
|
|
}
|