5ce8b72900
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
354 lines
10 KiB
Rust
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
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|