feat(wfe-yaml): wire rustlang step types and containerd integration tests

Add rustlang feature flag to wfe-yaml with support for all cargo and
rustup step types (15 total), including cargo-doc-mdx.

Schema additions: output_dir, package, features, all_features,
no_default_features, release, profile, toolchain, extra_args,
components, targets, default_toolchain fields on StepConfig.

Integration tests for compiling all step types from YAML, and
containerd-based end-to-end tests for running Rust toolchain
inside containers from bare Debian images.
This commit is contained in:
2026-03-29 16:57:50 +01:00
parent 272ddf17c2
commit 60e8c7f9a8
5 changed files with 1403 additions and 0 deletions

View File

@@ -13,6 +13,8 @@ use crate::executors::deno::{DenoConfig, DenoPermissions, DenoStep};
use wfe_buildkit::{BuildkitConfig, BuildkitStep};
#[cfg(feature = "containerd")]
use wfe_containerd::{ContainerdConfig, ContainerdStep};
#[cfg(feature = "rustlang")]
use wfe_rustlang::{CargoCommand, CargoConfig, CargoStep, RustupCommand, RustupConfig, RustupStep};
use wfe_core::primitives::sub_workflow::SubWorkflowStep;
use wfe_core::models::condition::{ComparisonOp, FieldComparison, StepCondition};
@@ -454,6 +456,38 @@ fn build_step_config_and_factory(
});
Ok((key, value, factory))
}
#[cfg(feature = "rustlang")]
"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" => {
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}"
))
})?;
let config_clone = config.clone();
let factory: StepFactory = Box::new(move || {
Box::new(CargoStep::new(config_clone.clone())) as Box<dyn StepBody>
});
Ok((key, value, factory))
}
#[cfg(feature = "rustlang")]
"rust-install" | "rustup-toolchain" | "rustup-component" | "rustup-target" => {
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}"
))
})?;
let config_clone = config.clone();
let factory: StepFactory = Box::new(move || {
Box::new(RustupStep::new(config_clone.clone())) as Box<dyn StepBody>
});
Ok((key, value, factory))
}
"workflow" => {
let config = step.config.as_ref().ok_or_else(|| {
YamlWorkflowError::Compilation(format!(
@@ -576,6 +610,88 @@ fn build_shell_config(step: &YamlStep) -> Result<ShellConfig, YamlWorkflowError>
})
}
#[cfg(feature = "rustlang")]
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,
"cargo-check" => CargoCommand::Check,
"cargo-clippy" => CargoCommand::Clippy,
"cargo-fmt" => CargoCommand::Fmt,
"cargo-doc" => CargoCommand::Doc,
"cargo-publish" => CargoCommand::Publish,
"cargo-audit" => CargoCommand::Audit,
"cargo-deny" => CargoCommand::Deny,
"cargo-nextest" => CargoCommand::Nextest,
"cargo-llvm-cov" => CargoCommand::LlvmCov,
"cargo-doc-mdx" => CargoCommand::DocMdx,
_ => {
return Err(YamlWorkflowError::Compilation(format!(
"Unknown cargo step type: '{step_type}'"
)));
}
};
let config = step.config.as_ref();
let timeout_ms = config
.and_then(|c| c.timeout.as_ref())
.and_then(|t| parse_duration_ms(t));
Ok(CargoConfig {
command,
toolchain: config.and_then(|c| c.toolchain.clone()),
package: config.and_then(|c| c.package.clone()),
features: config.map(|c| c.features.clone()).unwrap_or_default(),
all_features: config.and_then(|c| c.all_features).unwrap_or(false),
no_default_features: config.and_then(|c| c.no_default_features).unwrap_or(false),
release: config.and_then(|c| c.release).unwrap_or(false),
target: config.and_then(|c| c.target.clone()),
profile: config.and_then(|c| c.profile.clone()),
extra_args: config.map(|c| c.extra_args.clone()).unwrap_or_default(),
env: config.map(|c| c.env.clone()).unwrap_or_default(),
working_dir: config.and_then(|c| c.working_dir.clone()),
timeout_ms,
output_dir: config.and_then(|c| c.output_dir.clone()),
})
}
#[cfg(feature = "rustlang")]
fn build_rustup_config(
step: &YamlStep,
step_type: &str,
) -> Result<RustupConfig, YamlWorkflowError> {
let command = match step_type {
"rust-install" => RustupCommand::Install,
"rustup-toolchain" => RustupCommand::ToolchainInstall,
"rustup-component" => RustupCommand::ComponentAdd,
"rustup-target" => RustupCommand::TargetAdd,
_ => {
return Err(YamlWorkflowError::Compilation(format!(
"Unknown rustup step type: '{step_type}'"
)));
}
};
let config = step.config.as_ref();
let timeout_ms = config
.and_then(|c| c.timeout.as_ref())
.and_then(|t| parse_duration_ms(t));
Ok(RustupConfig {
command,
toolchain: config.and_then(|c| c.toolchain.clone()),
components: config.map(|c| c.components.clone()).unwrap_or_default(),
targets: config.map(|c| c.targets.clone()).unwrap_or_default(),
profile: config.and_then(|c| c.profile.clone()),
default_toolchain: config.and_then(|c| c.default_toolchain.clone()),
extra_args: config.map(|c| c.extra_args.clone()).unwrap_or_default(),
timeout_ms,
})
}
fn parse_duration_ms(s: &str) -> Option<u64> {
let s = s.trim();
// Check "ms" before "s" since strip_suffix('s') would also match "500ms"

View File

@@ -164,6 +164,39 @@ pub struct StepConfig {
pub containerd_addr: Option<String>,
/// CLI binary name for containerd steps: "nerdctl" (default) or "docker".
pub cli: Option<String>,
// Cargo fields
/// Target package for cargo steps (`-p`).
pub package: Option<String>,
/// Features to enable for cargo steps.
#[serde(default)]
pub features: Vec<String>,
/// Enable all features for cargo steps.
#[serde(default)]
pub all_features: Option<bool>,
/// Disable default features for cargo steps.
#[serde(default)]
pub no_default_features: Option<bool>,
/// Build in release mode for cargo steps.
#[serde(default)]
pub release: Option<bool>,
/// Build profile for cargo steps (`--profile`).
pub profile: Option<String>,
/// Rust toolchain override for cargo steps (e.g. "nightly").
pub toolchain: Option<String>,
/// Additional arguments for cargo/rustup steps.
#[serde(default)]
pub extra_args: Vec<String>,
/// Output directory for generated files (e.g., MDX docs).
pub output_dir: Option<String>,
// Rustup fields
/// Components to add for rustup steps (e.g. ["clippy", "rustfmt"]).
#[serde(default)]
pub components: Vec<String>,
/// Compilation targets to add for rustup steps (e.g. ["wasm32-unknown-unknown"]).
#[serde(default)]
pub targets: Vec<String>,
/// Default toolchain for rust-install steps.
pub default_toolchain: Option<String>,
// Workflow (sub-workflow) fields
/// Child workflow ID (for `type: workflow` steps).
#[serde(rename = "workflow")]