Files
honey-biscuit-workshop/workshop-baker/src/finalize/config.rs
T
Catty Steve 5ce8b72900 refactor(notify): extract types to separate module and redesign
NotificationRule

- Move NotifyEvent, NotifyPluginMap, and related types to
  notify/types.rs
- Introduce NotificationRule struct replacing NotificationMethod fields
- Add NotificationRulesMap for priority-based notification configuration
- Comment out main event loop and add todo!() placeholders for WIP
2026-04-24 23:01:40 +08:00

354 lines
10 KiB
Rust

//! Finalize stage configuration types
//!
//! This module defines the configuration schema for the finalize stage,
//! which handles artifact collection, publishing, and notifications.
use crate::types::command::CustomCommand;
use crate::types::compression::CompressionMethod;
use crate::types::time::deserialize_duration_ms;
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use std::collections::HashMap;
use std::path::PathBuf;
/// Version requirement for finalize.yml schema compatibility
pub const VERSION_REQUIREMENT: &str = "^0.0.1";
pub type RawPluginMap = HashMap<String, HashMap<String, PluginConfig>>;
/// Root configuration for finalize stage
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct FinalizeConfig {
/// Schema version for compatibility checking
pub version: String,
/// Plugin definitions
/// Map of logical plugin name -> plugin source URI -> plugin config
#[serde(default)]
pub plugin: RawPluginMap,
/// Notification configuration (templates + priority groups)
#[serde(default)]
pub notification: NotificationConfig,
/// Artifact definitions
#[serde(default)]
pub artifact: Vec<ArtifactDef>,
/// Cleanup configuration
#[serde(default)]
pub cleanup: CleanupConfig,
/// Hooks configuration
#[serde(default)]
pub hooks: Option<FinalizeHooks>,
}
/// Plugin configuration
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct PluginConfig {
/// Whether plugin failure should not fail the build.
/// Same as @fallible decorator
#[serde(default)]
pub fallible: bool,
/// Number of retries on failure (0 = no retry, -1 = infinite retries)
#[serde(default)]
pub retry: i8,
/// Timeout for plugin execution in milliseconds (0 = no timeout)
#[serde(default)]
pub timeout_ms: u64,
/// Whether the plugin should be resolved after bake stage
#[serde(default)]
pub late_binding: bool,
/// (Git) Enable shallow clone
#[serde(default)]
pub shallow: Option<bool>,
/// (Http) Checksum of the plugin package
#[serde(default)]
pub checksum: Option<String>,
/// Baker runtime configuration
#[serde(skip)]
pub runtime: PluginRuntimeConfig,
/// Plugin-specific configuration parameters.
/// These are passed to the plugin via according method.
#[serde(flatten)]
pub config: Value,
}
#[derive(Debug, Clone, Default)]
pub struct PluginRuntimeConfig {
/// Error description if plugin is unavailable at runtime.
/// None means available
pub error: Option<String>,
/// Path of plugin directory in the environment
pub fspath: Option<PathBuf>,
/// manifest.yml
pub manifest: Option<Value>,
}
/// Notification method configuration
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct NotificationMethod {
/// Template definition
#[serde(default)]
pub template: NotificationTemplate,
/// Trigger definition
#[serde(default)]
pub trigger: Vec<TriggerItemRaw>,
/// Notification-specific configuration (url, token, to, from, etc.)
#[serde(flatten)]
pub config: Value,
/// Policy list for flexible trigger/template/config combinations.
#[serde(default)]
pub policy: Vec<NotifyPolicy>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct NotificationTemplate {
/// Payload schema: "default" (plugin-defined) or "custom" (requires on_* templates)
#[serde(default)]
pub schema: Option<String>,
/// Content template for build success notification
#[serde(default)]
pub on_success: Option<String>,
/// Content template for build failure notification
#[serde(default)]
pub on_failure: Option<String>,
/// Stage-triggered notification configuration
#[serde(default)]
pub on_finish_of: Option<OnFinishOf>,
}
/// Notification configuration container
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct NotificationConfig {
/// Template definitions for notification content
#[serde(default)]
pub templates: HashMap<String, NotificationTemplateDef>,
#[serde(flatten)]
pub groups: HashMap<u32, HashMap<String, NotificationMethod>>,
}
/// Stage-triggered notification configuration
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct OnFinishOf {
/// Stage name to trigger on
/// Format: "prebake.ready", "bake.build", "finalize.artifact", etc.
pub stage: String,
/// Content template for stage completion notification
pub content: String,
}
/// Notification template definition.
/// Supports inline content (on_success/on_failure/on_finish_of) or external reference (use).
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct NotificationTemplateDef {
/// External template reference (git URL).
/// Cannot be combined with on_* fields in MVP (validation will reject).
#[serde(default, rename = "use")]
pub use_: Option<String>,
/// Content template for build success notification
#[serde(default)]
pub on_success: Option<String>,
/// Content template for build failure notification
#[serde(default)]
pub on_failure: Option<String>,
/// Content template for stage completion notification
#[serde(default)]
pub on_finish_of: Option<String>,
}
/// Notification trigger type.
#[derive(Debug, Clone)]
pub enum Trigger {
OnSuccess,
OnFailure,
OnFinishOf(String),
}
/// Raw trigger item for deserialization.
/// Supports `- on_success` (string) and `- on_finish_of: [...]` (map).
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(untagged)]
pub enum TriggerItemRaw {
Simple(String),
Map(std::collections::HashMap<String, Vec<String>>),
}
/// Notification policy: a set of triggers with optional template/config overrides.
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct NotifyPolicy {
/// Trigger conditions for this policy.
/// Empty means inheriting method-level triggers.
#[serde(default)]
pub trigger: Vec<TriggerItemRaw>,
/// Template configuration (schema, on_success, on_failure, on_finish_of, subject, body...).
/// Empty/null means inheriting method-level templates.
#[serde(default)]
pub template: Value,
/// Override fields: to, url, token, fallible, retry, timeout_ms, etc.
#[serde(flatten)]
pub overrides: Value,
}
/// Artifact definition
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ArtifactDef {
/// Artifact ID (optional, auto-generated as MD5(PUID + PATH) if not specified)
#[serde(default)]
pub id: Option<String>,
/// Artifact path (supports glob patterns, empty for non-filesystem artifacts like Docker images)
#[serde(default)]
pub path: String,
/// Retention duration in milliseconds
/// Formats: "7d", "168h", "604800" (seconds), 604800 (number=seconds)
/// 0 = permanent retention
#[serde(
default = "default_retention_ms",
deserialize_with = "deserialize_duration_ms"
)]
pub retention_ms: u64,
/// Compression method: "zstd", "gzip", "none", "dir"
#[serde(default)]
pub compression: CompressionMethod,
// Publishing targets
// TODO: under heavy refactor
}
impl Default for ArtifactDef {
fn default() -> Self {
Self {
id: None,
path: String::new(),
retention_ms: default_retention_ms(),
compression: CompressionMethod::default(),
}
}
}
/// Artifact
/// TODO: Under heavy refactor
/// Cleanup configuration
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct CleanupConfig {
/// Cleanup policy: "auto", "manual", "on_failure" (WIP: manual, on_failure)
#[serde(default)]
pub policy: CleanupPolicy,
}
/// Cleanup policy
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
#[serde(rename_all = "lowercase")]
pub enum CleanupPolicy {
#[default]
Auto,
Manual,
OnSuccess,
Never,
}
/// Hooks configuration for finalize stage
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct FinalizeHooks {
/// Hooks executed before artifact collection
#[serde(default)]
pub early: Option<Vec<CustomCommand>>,
/// Hooks executed after all operations complete
#[serde(default)]
pub late: Option<Vec<CustomCommand>>,
}
// Default value functions
fn default_retention_ms() -> u64 {
7 * 24 * 3600 * 1000
}
impl FinalizeConfig {
/// Validate the finalize configuration
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
// Validate version
let version_requirement =
VersionReq::parse(VERSION_REQUIREMENT).expect("Invalid version requirement");
let version = &self.version;
let version = match version.matches('.').count() {
0 => format!("{}.0.0", version),
1 => format!("{}.0", version),
_ => version.clone(),
};
match Version::parse(&version) {
Ok(version) if version_requirement.matches(&version) => {}
Ok(version) => errors.push(format!(
"Version {} does not satisfy requirement {}",
version, VERSION_REQUIREMENT
)),
Err(e) => errors.push(format!("Invalid version format: {}", e)),
}
// Validate plugin definitions: each plugin must have exactly one URL
for (plugin_name, urls) in &self.plugin {
match urls.len() {
0 => errors.push(format!("Plugin '{}' has no URL defined", plugin_name)),
1 => {}
n => errors.push(format!(
"Plugin '{}' has {} URL definitions, expected exactly 1",
plugin_name, n
)),
}
}
self.validate_notification(&mut errors);
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn validate_notification(&self, errors: &mut Vec<String>) {
// Validate templates
for (name, tmpl) in &self.notification.templates {
if tmpl.use_.is_some()
&& (tmpl.on_success.is_some()
|| tmpl.on_failure.is_some()
|| tmpl.on_finish_of.is_some())
{
errors.push(format!(
"Template '{}' cannot combine 'use' with 'on_*' fields (MVP restriction)",
name
));
}
}
}
}