diff --git a/workshop-baker/Cargo.toml b/workshop-baker/Cargo.toml index 4d75ff4..8cda569 100644 --- a/workshop-baker/Cargo.toml +++ b/workshop-baker/Cargo.toml @@ -58,6 +58,14 @@ globset = "0.4.18" [dev-dependencies] criterion = "0.5" +[lints.rust] +dead_code = "allow" +unreachable_code = "allow" + +[lints.clippy] +inherent_to_string = "allow" +non_canonical_partial_ord_impl = "allow" + [[bench]] name = "parser_bench" harness = false diff --git a/workshop-baker/src/bake.rs b/workshop-baker/src/bake.rs index e36e435..ce891ae 100644 --- a/workshop-baker/src/bake.rs +++ b/workshop-baker/src/bake.rs @@ -1,26 +1,26 @@ -use workshop_engine::{Engine, EventSender, ExecutionContext}; -use crate::bake::parser::Function; use crate::bake::constant::{DEFAULT_WORKSPACE, TRIVIAL_SCRIPT_NAME}; use crate::bake::error::BakeError; +use crate::bake::parser::Function; use crate::cli::Cli; +use crate::prebake; use crate::prebake::PrebakeConfig; use crate::prebake::security::get_drop_after; use crate::prebake::stage::PrebakeStage; -use crate::prebake; use std::path::{Path, PathBuf}; +use workshop_engine::{Engine, EventSender, ExecutionContext}; mod builder; pub mod constant; mod decorator; -pub mod parser; pub mod error; +pub mod parser; mod schedule; pub async fn bake( script_path: &Path, bake_base_path: &Path, prebake_path: &Path, - cli: &Cli, + _cli: &Cli, ctx: &mut ExecutionContext, event_tx: EventSender, ) -> Result<(), BakeError> { @@ -43,10 +43,10 @@ pub async fn bake( let use_template = true; let engine = Engine::new(); - if let Some(env) = &prebake.envvars { - if let Some(prebake_env) = &env.bake { - ctx.env_vars.extend(prebake_env.clone()); - } + if let Some(env) = &prebake.envvars + && let Some(prebake_env) = &env.bake + { + ctx.env_vars.extend(prebake_env.clone()); } if !has_pipeline || trivial { @@ -57,7 +57,7 @@ pub async fn bake( }; let script = builder::build_script(&function, bake_base_path, &workspace, None, use_template)?; - let result = engine.execute_script(&script, &ctx, &event_tx).await; + let result = engine.execute_script(&script, ctx, &event_tx).await; result?; } else { let sorted_functions = schedule::sort_function(functions.pipeline_functions)?; @@ -72,7 +72,7 @@ pub async fn bake( None, use_template, )?; - let result = engine.execute_script(&script, &ctx, &event_tx).await; + let result = engine.execute_script(&script, ctx, &event_tx).await; result?; } } @@ -96,10 +96,10 @@ fn get_workspace(prebake_config: &PrebakeConfig) -> PathBuf { } fn privileged(prebake_config: &PrebakeConfig) -> bool { - if let Some(bootstrap) = &prebake_config.bootstrap { - if bootstrap.user == "root" { - return true; - } + if let Some(bootstrap) = &prebake_config.bootstrap + && bootstrap.user == "root" + { + return true; } let drop_after = get_drop_after(&prebake_config.security).unwrap_or_default(); PrebakeStage::Never == drop_after diff --git a/workshop-baker/src/bake/builder.rs b/workshop-baker/src/bake/builder.rs index d3889f7..16cf838 100644 --- a/workshop-baker/src/bake/builder.rs +++ b/workshop-baker/src/bake/builder.rs @@ -1,7 +1,7 @@ -use crate::bake::decorator::Decorator; -use crate::bake::parser::Function; use crate::bake::constant::{PIPELINE_SKIP_ERRORCODE, TRIVIAL_SCRIPT_NAME}; +use crate::bake::decorator::Decorator; use crate::bake::error::BakeError; +use crate::bake::parser::Function; use minijinja::{Environment, context}; use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; diff --git a/workshop-baker/src/bake/error.rs b/workshop-baker/src/bake/error.rs index e8b7e7c..680a5be 100644 --- a/workshop-baker/src/bake/error.rs +++ b/workshop-baker/src/bake/error.rs @@ -2,10 +2,7 @@ use thiserror::Error; use workshop_engine::{ ExecutionError, - error::{ - EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR, - HasExitCode, - }, + error::{EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR, HasExitCode}, }; #[derive(Error, Debug)] diff --git a/workshop-baker/src/finalize.rs b/workshop-baker/src/finalize.rs index 7498d7e..f246e17 100644 --- a/workshop-baker/src/finalize.rs +++ b/workshop-baker/src/finalize.rs @@ -9,12 +9,12 @@ pub mod types; use crate::cli::Cli; use crate::finalize::constant::FINALIZE_STANDALONE_PLUGIN_PATH; -use workshop_engine::EventSender; use crate::finalize::plugin::fetch::{self, FetchArgument}; -use std::path::PathBuf; pub use config::*; pub use error::FinalizeError; use std::path::Path; +use std::path::PathBuf; +use workshop_engine::EventSender; pub fn parse(path: &Path) -> Result { let content = std::fs::read_to_string(path).map_err(FinalizeError::IoError)?; @@ -24,7 +24,7 @@ pub fn parse(path: &Path) -> Result { pub async fn finalize( finalize_path: &Path, - cli: &Cli, + _cli: &Cli, ctx: &mut workshop_engine::ExecutionContext, event_tx: EventSender, ) -> Result<(), FinalizeError> { @@ -55,7 +55,7 @@ pub async fn finalize( checksum: config.checksum.clone(), shallow: config.shallow, }; - fetch::fetch_plugin(&source, &name, &plugin_path, fetch_argument).await?; + fetch::fetch_plugin(source, name, &plugin_path, fetch_argument).await?; config.runtime.fspath = Some(plugin_path.join(name)); } } @@ -75,7 +75,7 @@ pub async fn finalize( // Determine if we should continue with artifacts/latehook // If prior stages failed, we still send notification but skip artifacts - let prior_failed = earlyhook_result.is_err(); + let _prior_failed = earlyhook_result.is_err(); // Prepare artifacts // TODO: artifact stage implementation diff --git a/workshop-baker/src/finalize/config.rs b/workshop-baker/src/finalize/config.rs index 90a9a6b..c03cd15 100644 --- a/workshop-baker/src/finalize/config.rs +++ b/workshop-baker/src/finalize/config.rs @@ -3,13 +3,13 @@ //! This module defines the configuration schema for the finalize stage, //! which handles artifact collection, publishing, and notifications. -use workshop_engine::{CustomCommand, deserialize_duration_ms}; use crate::finalize::types::compression::CompressionMethod; use semver::{Version, VersionReq}; use serde::{Deserialize, Serialize}; use serde_yaml::Value; use std::collections::HashMap; use std::path::PathBuf; +use workshop_engine::{CustomCommand, deserialize_duration_ms}; /// Version requirement for finalize.yml schema compatibility pub const VERSION_REQUIREMENT: &str = "^0.0.1"; @@ -253,7 +253,6 @@ impl Default for ArtifactDef { /// Artifact /// TODO: Under heavy refactor - /// Cleanup configuration #[derive(Debug, Deserialize, Serialize, Clone, Default)] pub struct CleanupConfig { diff --git a/workshop-baker/src/finalize/plugin.rs b/workshop-baker/src/finalize/plugin.rs index 57080fa..842b78c 100644 --- a/workshop-baker/src/finalize/plugin.rs +++ b/workshop-baker/src/finalize/plugin.rs @@ -1,5 +1,5 @@ -use crate::finalize::constant::{FINALIZE_PLUGIN_MANIFEST, FINALIZE_PLUGIN_MANIFEST_EXTENSION}; use crate::finalize::RawPluginMap; +use crate::finalize::constant::{FINALIZE_PLUGIN_MANIFEST, FINALIZE_PLUGIN_MANIFEST_EXTENSION}; use crate::finalize::plugin::metadata::{PluginMetadata, PluginType}; use crate::{EventSender, ExecutionContext, finalize::error::PluginError}; use async_trait::async_trait; @@ -134,7 +134,7 @@ fn load_manifest(path: &Path, name: &str, suffix: &str) -> Result Result { log::error!("Unknown URL scheme in plugin URL: {}", url); - return Err(PluginError::GeneralError(format!( + Err(PluginError::GeneralError(format!( "Unknown URL scheme in plugin URL: {}", url - ))); + ))) } } } @@ -104,7 +101,7 @@ pub async fn fetch_plugins( checksum: config.checksum.clone(), shallow: config.shallow, }; - fetch::fetch_plugin(&source, &name, &plugin_path, fetch_argument).await?; + fetch::fetch_plugin(source, name, plugin_path, fetch_argument).await?; log::debug!("Storing plugin {} to {}", name, plugin_path.display()); config.runtime.fspath = Some(plugin_path.join(name)); } diff --git a/workshop-baker/src/finalize/plugin/fetch/extract.rs b/workshop-baker/src/finalize/plugin/fetch/extract.rs index 21aae97..e8b07fa 100644 --- a/workshop-baker/src/finalize/plugin/fetch/extract.rs +++ b/workshop-baker/src/finalize/plugin/fetch/extract.rs @@ -1,6 +1,6 @@ // Code in this module PARTIALLY OR FULLY utilized AI Coding Agent. use crate::finalize::plugin::PluginError; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use tokio::task; /// Compression type for tar archives. @@ -48,7 +48,7 @@ impl CompressionType { /// Removes existing destination directory if present, creates a fresh directory, /// then unpacks the archive using the appropriate decompression based on `compression`. pub async fn extract_tar( - archive: &PathBuf, + archive: &Path, destdir: &PathBuf, compression: CompressionType, ) -> Result<(), PluginError> { @@ -61,7 +61,7 @@ pub async fn extract_tar( .await .map_err(|e| PluginError::GeneralError(format!("Failed to create directory: {}", e)))?; - let archive = archive.clone(); + let archive = archive.to_path_buf(); let destdir = destdir.clone(); task::spawn_blocking(move || { diff --git a/workshop-baker/src/finalize/plugin/fetch/http.rs b/workshop-baker/src/finalize/plugin/fetch/http.rs index 55e90a0..9f12769 100644 --- a/workshop-baker/src/finalize/plugin/fetch/http.rs +++ b/workshop-baker/src/finalize/plugin/fetch/http.rs @@ -69,7 +69,7 @@ fn extract_archive_extension(url: &str) -> &str { } else if lower.ends_with(".tar") { ".tar" } else { - ".tar" + ".unknown" } } @@ -86,23 +86,21 @@ pub async fn fetch_http_plugin( download_to_file(url, &temp_file, name).await?; - if let Some(checksum) = checksum_str { - if !checksum.is_empty() { - let bytes = tokio::fs::read(&temp_file).await.map_err(|e| { - PluginError::GeneralError(format!("Failed to read downloaded file: {}", e)) - })?; + if let Some(checksum) = checksum_str + && !checksum.is_empty() + { + let bytes = tokio::fs::read(&temp_file).await.map_err(|e| { + PluginError::GeneralError(format!("Failed to read downloaded file: {}", e)) + })?; - let checksum_type = - detect_checksum_type(checksum).map_err(|e| PluginError::GeneralError(e))?; + let checksum_type = detect_checksum_type(checksum).map_err(PluginError::GeneralError)?; - if let Some(ct) = checksum_type { - verify_checksum(&bytes, checksum, ct).map_err(|e| PluginError::GeneralError(e))?; - } + if let Some(ct) = checksum_type { + verify_checksum(&bytes, checksum, ct).map_err(PluginError::GeneralError)?; } } - let compression = - CompressionType::from_path(&temp_file).map_err(|e| PluginError::GeneralError(e))?; + let compression = CompressionType::from_path(&temp_file).map_err(PluginError::GeneralError)?; let destdir = basedir.join(name); extract_tar(&temp_file, &destdir, compression).await?; diff --git a/workshop-baker/src/finalize/plugin/internal.rs b/workshop-baker/src/finalize/plugin/internal.rs index 189126e..2c943c5 100644 --- a/workshop-baker/src/finalize/plugin/internal.rs +++ b/workshop-baker/src/finalize/plugin/internal.rs @@ -10,22 +10,22 @@ pub struct InternalPlugin { } pub fn get_internal_plugin() -> Vec { - let mut internal_plugins: Vec = Vec::new(); - internal_plugins.push(InternalPlugin { - name: "in-site-notify", - entry: Box::new(insitenotify::InsiteNotify), - }); - internal_plugins.push(InternalPlugin { - name: "mail", - entry: Box::new(mail::Mail), - }); - internal_plugins.push(InternalPlugin { - name: "satori", - entry: Box::new(satori::Satori), - }); - internal_plugins.push(InternalPlugin { - name: "webhook", - entry: Box::new(webhook::Webhook), - }); - internal_plugins + vec![ + InternalPlugin { + name: "in-site-notify", + entry: Box::new(insitenotify::InsiteNotify), + }, + InternalPlugin { + name: "mail", + entry: Box::new(mail::Mail), + }, + InternalPlugin { + name: "satori", + entry: Box::new(satori::Satori), + }, + InternalPlugin { + name: "webhook", + entry: Box::new(webhook::Webhook), + }, + ] } diff --git a/workshop-baker/src/finalize/plugin/internal/mail/config.rs b/workshop-baker/src/finalize/plugin/internal/mail/config.rs index cf4bc2a..f450cd5 100644 --- a/workshop-baker/src/finalize/plugin/internal/mail/config.rs +++ b/workshop-baker/src/finalize/plugin/internal/mail/config.rs @@ -1,6 +1,6 @@ // Code in this module PARTIALLY OR FULLY utilized AI Coding Agent. -use workshop_engine::deserialize_duration_ms; use serde::Deserialize; +use workshop_engine::deserialize_duration_ms; /// Email address representation supporting plain string or structured object. #[derive(Debug, Deserialize, Clone)] diff --git a/workshop-baker/src/finalize/plugin/shell.rs b/workshop-baker/src/finalize/plugin/shell.rs index ca5c35d..31004de 100644 --- a/workshop-baker/src/finalize/plugin/shell.rs +++ b/workshop-baker/src/finalize/plugin/shell.rs @@ -94,7 +94,7 @@ fn convert_argument( envvars.insert(prefix.to_string(), value.to_string()); } serde_yaml::Value::String(value) => { - envvars.insert(prefix.to_string(), format!("{}", value)); + envvars.insert(prefix.to_string(), value.to_string()); } serde_yaml::Value::Number(value) => { envvars.insert(prefix.to_string(), value.to_string()); @@ -112,12 +112,12 @@ fn convert_argument( return envvars; } let mut array = String::new(); - array.push_str("("); + array.push('('); let mut has_mapping = false; let mut _envvars: HashMap = HashMap::new(); for (index, item) in seq.into_iter().enumerate() { if index > 0 { - array.push_str(" "); + array.push(' '); } let new_prefix = format!("{}_{}", prefix, index); let subenvvars = convert_argument(new_prefix.as_str(), item.clone(), true); @@ -136,7 +136,7 @@ fn convert_argument( if has_mapping { envvars.extend(_envvars); } else { - array.push_str(")"); + array.push(')'); envvars.insert(prefix.to_string(), array); } } diff --git a/workshop-baker/src/finalize/stage/hook.rs b/workshop-baker/src/finalize/stage/hook.rs index c723e6f..84a3bd7 100644 --- a/workshop-baker/src/finalize/stage/hook.rs +++ b/workshop-baker/src/finalize/stage/hook.rs @@ -1,7 +1,7 @@ use crate::ExecutionContext; -use workshop_engine::{DeltaExecutionContext, Engine, EventSender, ExecutionError, CustomCommand}; use crate::finalize::plugin::{PluginMap, call_named_plugin}; use std::path::PathBuf; +use workshop_engine::{CustomCommand, DeltaExecutionContext, Engine, EventSender, ExecutionError}; /// Executes post-build finalize hooks, supporting both plugin and shell command hooks. /// diff --git a/workshop-baker/src/main.rs b/workshop-baker/src/main.rs index ddee145..8df431f 100644 --- a/workshop-baker/src/main.rs +++ b/workshop-baker/src/main.rs @@ -7,7 +7,7 @@ use workshop_baker::{ finalize::finalize, prebake::prebake, }; -use workshop_engine::{error::HasExitCode, EventSender, ExecutionContext}; +use workshop_engine::{EventSender, ExecutionContext, error::HasExitCode}; #[tokio::main] async fn main() { diff --git a/workshop-baker/src/notify.rs b/workshop-baker/src/notify.rs index 9cfaae0..42255f6 100644 --- a/workshop-baker/src/notify.rs +++ b/workshop-baker/src/notify.rs @@ -1,10 +1,18 @@ -use crate::finalize::constant::FINALIZE_STANDALONE_PLUGIN_PATH; use crate::finalize::FinalizeConfig; use crate::finalize::NotificationConfig; +use crate::finalize::NotificationTemplate; +use crate::finalize::config::TriggerItemRaw; +use crate::finalize::constant::FINALIZE_STANDALONE_PLUGIN_PATH; use crate::finalize::parse; use crate::finalize::plugin; use crate::finalize::plugin::fetch; +use crate::notify::types::Matchable; use crate::notify::types::Method; +use crate::notify::types::NOTIFICATION_DEFAULT_FALLIBLE; +use crate::notify::types::NOTIFICATION_DEFAULT_MAX_LIVES; +use crate::notify::types::NOTIFICATION_DEFAULT_RETRY_BACKOFF; +use crate::notify::types::NOTIFICATION_DEFAULT_TIMEOUT_MS; +use crate::notify::types::NOTIFICATION_NOTREADY_DELAY; use crate::notify::types::NOTIFICATION_TEMPORARY_DELAY_FACTOR; use crate::notify::types::NOTIFICATION_TEMPORARY_DELAY_TIME; use crate::notify::types::NOTIFICATION_TEMPORARY_MAX_TIME; @@ -15,20 +23,14 @@ use crate::notify::types::NotificationPluginMap; use crate::notify::types::NotificationRenderer; use crate::notify::types::NotificationRule; use crate::notify::types::NotificationRulesMap; +use crate::notify::types::ParsedTrigger; use crate::{EventSender, ExecutionContext}; +use chrono::Utc; +use globset::{Glob, GlobSetBuilder}; use std::collections::HashMap; use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; -use crate::notify::types::ParsedTrigger; -use crate::notify::types::Matchable; -use globset::{Glob, GlobSetBuilder}; -use crate::finalize::config::TriggerItemRaw; -use crate::finalize::NotificationTemplate; -use crate::notify::types::NOTIFICATION_DEFAULT_FALLIBLE; -use crate::notify::types::NOTIFICATION_DEFAULT_MAX_LIVES; -use crate::notify::types::NOTIFICATION_DEFAULT_TIMEOUT_MS; -use crate::notify::types::NOTIFICATION_DEFAULT_RETRY_BACKOFF; mod error; pub mod types; @@ -49,8 +51,8 @@ fn extract_plugin(finalize: &FinalizeConfig) -> HashSet { finalize .notification .groups - .iter() - .flat_map(|(_, methods)| methods.keys()) + .values() + .flat_map(|methods| methods.keys()) .cloned() .collect() } @@ -58,7 +60,7 @@ fn extract_plugin(finalize: &FinalizeConfig) -> HashSet { pub async fn notify( finalize_path: &Path, ctx: &mut ExecutionContext, - event_tx: EventSender, + _event_tx: EventSender, notifyevent_rx: &mut NotificationEventReceiver, notifyevent_tx: NotificationEventSender, ) -> anyhow::Result<()> { @@ -84,48 +86,65 @@ pub async fn notify( let notification_rules = parse_rules(&finalize.notification); - // loop { - // tokio::select! { - // Some(events) = notifyevent_rx.recv() => { - // match notify_handler(¬ify_plugins, &finalize.notification, &events){ - // Ok(_) => {}, // Notification succeeded / failed with fallible - // Err(e) => { - // log::debug!("Failed to send notification: {:?}", e); - // match e { - // NotifyError::PluginNotAvailable(_) => { - // // Should keep lives, add static delay and give less priority - // for mut event in events { - // event.not_before = Utc::now() + NOTIFY_NOTREADY_DELAY; - // event.effective_priority = event.effective_priority.saturating_add(1); - // notifyevent_tx.send(event).await?; - // } - // }, - // NotifyError::TemporaryError(_) => { - // // Should cost 1 life, add dynamic (exponential) delay - // for mut event in events { - // event.lives -= 1; - // event.not_before = Utc::now() + notify_delay(&event); - // notifyevent_tx.send(event).await?; - // } - // }, - // NotifyError::PermanentError(_) => { - // // Unexpected! - // // Should be dropped - // log::error!("None of the configured notification plugins works."); - // log::error!("Message will be dropped."); - // // TODO: Send audit/metric information - // } - // error => { - // // Unexpected! - // log::error!("Internal error: notify_handler returned unexpected error: {:?}", error); - // } - // }; - // } - // }; - // } - // } - // } - todo!(); + loop { + tokio::select! { + Some(event) = notifyevent_rx.recv() => { + handle_notification_event( + event, + ¬ify_plugins, + ¬ification_rules, + ¬ifyevent_tx, + ) + .await?; + } + } + } +} + +/// Process a single notification event. +/// +/// Delegates to [`notification_handler`] and handles errors: +/// - [`NotifyError::PluginNotAvailable`] — reschedule with static delay, lower priority +/// - [`NotifyError::TemporaryError`] — consume one life, reschedule with exponential backoff +/// - [`NotifyError::PermanentError`] — drop the event +/// - other — log and drop +async fn handle_notification_event( + event: NotificationEvent, + plugins: &NotificationPluginMap, + rules: &NotificationRulesMap, + notifyevent_tx: &NotificationEventSender, +) -> anyhow::Result<()> { + match notification_handler(plugins, rules, &event) { + Ok(_) => {} + Err(e) => { + log::debug!("Failed to send notification: {:?}", e); + match e { + NotifyError::PluginNotAvailable(_) => { + let mut event = event; + event.not_before = Utc::now() + NOTIFICATION_NOTREADY_DELAY; + event.effective_priority = event.effective_priority.saturating_add(1); + notifyevent_tx.send(event).await?; + } + NotifyError::TemporaryError(_) => { + let mut event = event; + event.lives -= 1; + event.not_before = Utc::now() + notify_delay(&event); + notifyevent_tx.send(event).await?; + } + NotifyError::PermanentError(_) => { + log::error!("None of the configured notification plugins works."); + log::error!("Message will be dropped."); + } + error => { + log::error!( + "Internal error: notify_handler returned unexpected error: {:?}", + error + ); + } + } + } + } + Ok(()) } fn notification_handler( @@ -133,7 +152,7 @@ fn notification_handler( config: &NotificationRulesMap, event: &NotificationEvent, ) -> Result<(), NotifyError> { - for (priority, ruleset) in config.iter() { + for (_priority, ruleset) in config.iter() { for (method, rules) in ruleset.iter() { if !event.target.contains(method) { log::trace!("target does not contain method name: {}", method); @@ -173,23 +192,21 @@ fn notification_handler( fn parse_trigger_item_raw(item: &TriggerItemRaw) -> Vec { match item { - TriggerItemRaw::Simple(s) => { - match s.as_str() { - "on_success" => vec![ParsedTrigger::OnSuccess], - "on_failure" => vec![ParsedTrigger::OnFailure], - s if s.starts_with("on_finish_of:") => { - let pattern = s.trim_start_matches("on_finish_of:").trim(); - let mut builder = GlobSetBuilder::new(); - builder.add(Glob::new(pattern).expect("Invalid glob pattern")); - let glob_set = builder.build().expect("Failed to compile glob set"); - vec![ParsedTrigger::OnFinishOf(Matchable::Glob(glob_set))] - } - _ => { - log::warn!("Unknown trigger string: {}", s); - vec![] - } + TriggerItemRaw::Simple(s) => match s.as_str() { + "on_success" => vec![ParsedTrigger::OnSuccess], + "on_failure" => vec![ParsedTrigger::OnFailure], + s if s.starts_with("on_finish_of:") => { + let pattern = s.trim_start_matches("on_finish_of:").trim(); + let mut builder = GlobSetBuilder::new(); + builder.add(Glob::new(pattern).expect("Invalid glob pattern")); + let glob_set = builder.build().expect("Failed to compile glob set"); + vec![ParsedTrigger::OnFinishOf(Matchable::Glob(glob_set))] } - } + _ => { + log::warn!("Unknown trigger string: {}", s); + vec![] + } + }, TriggerItemRaw::Map(map) => { let mut triggers = Vec::new(); for (key, patterns) in map { @@ -219,22 +236,21 @@ fn merge_templates( policy_template: &Option, ) -> NotificationTemplate { let mut merged = method_template.clone(); - if let Some(policy_tmpl) = policy_template { - if let Ok(policy_template) = + if let Some(policy_tmpl) = policy_template + && let Ok(policy_template) = serde_yaml::from_value::(policy_tmpl.clone()) - { - if policy_template.schema.is_some() { - merged.schema = policy_template.schema; - } - if policy_template.on_success.is_some() { - merged.on_success = policy_template.on_success; - } - if policy_template.on_failure.is_some() { - merged.on_failure = policy_template.on_failure; - } - if policy_template.on_finish_of.is_some() { - merged.on_finish_of = policy_template.on_finish_of; - } + { + if policy_template.schema.is_some() { + merged.schema = policy_template.schema; + } + if policy_template.on_success.is_some() { + merged.on_success = policy_template.on_success; + } + if policy_template.on_failure.is_some() { + merged.on_failure = policy_template.on_failure; + } + if policy_template.on_finish_of.is_some() { + merged.on_finish_of = policy_template.on_finish_of; } } merged @@ -306,7 +322,8 @@ fn parse_rules(config: &NotificationConfig) -> NotificationRulesMap { .flat_map(parse_trigger_item_raw) .collect() }; - let template = merge_templates(&method.template, &Some(policy.template.clone())); + let template = + merge_templates(&method.template, &Some(policy.template.clone())); let method_overrides = extract_overrides(&method.config); let policy_overrides = extract_overrides(&policy.overrides); let fallible = if policy.overrides.get("fallible").is_some() { diff --git a/workshop-baker/src/notify/types.rs b/workshop-baker/src/notify/types.rs index 8aba9ae..1c3aa40 100644 --- a/workshop-baker/src/notify/types.rs +++ b/workshop-baker/src/notify/types.rs @@ -14,7 +14,7 @@ use std::time::Duration; use uuid::Uuid; use crate::finalize::NotificationTemplate; -use crate::finalize::config::Trigger; + /// Final runtime configuration for notification, restored and flattened from yaml pub struct NotificationRule { pub enabled: bool, diff --git a/workshop-baker/src/prebake.rs b/workshop-baker/src/prebake.rs index a170d3a..dcc7ea8 100644 --- a/workshop-baker/src/prebake.rs +++ b/workshop-baker/src/prebake.rs @@ -34,9 +34,9 @@ use crate::{ cli::Cli, prebake::{error::PrebakeError, security::get_drop_after, stage::PrebakeStage}, }; -use workshop_engine::EventSender; use chrono::Utc; use std::path::Path; +use workshop_engine::EventSender; pub use config::PrebakeConfig; pub use security::prebake_drop_privilege; @@ -51,7 +51,6 @@ pub use security::prebake_drop_privilege; /// /// Returns `Ok(PrebakeConfig)` on successful parsing, or `Err(serde_yaml::Error)` /// if the YAML is invalid. - pub fn parse(yaml_content: &str) -> Result { serde_yaml::from_str(yaml_content) } @@ -92,7 +91,7 @@ pub async fn prebake( prebake_path: &Path, cli: &Cli, stage: Option, - mut ctx: &mut ExecutionContext, + ctx: &mut ExecutionContext, event_tx: EventSender, ) -> anyhow::Result<()> { // Initialize @@ -125,26 +124,26 @@ pub async fn prebake( // Bootstrap stage if stage <= PrebakeStage::Bootstrap { - let stage_start = Utc::now(); + let _stage_start = Utc::now(); log::info!("Running PBStage: Bootstrap"); - prebake_drop_privilege(PrebakeStage::Bootstrap, dropafter, target_user, &mut ctx)?; + prebake_drop_privilege(PrebakeStage::Bootstrap, dropafter, target_user, ctx)?; ctx.task_id = format!("{}-{}-bootstrap", cli.pipeline, cli.build_id); let osinfo = os_info::get(); - let result = stage::bootstrap::bootstrap(&prebake, &osinfo, &ctx, &event_tx).await; + let result = stage::bootstrap::bootstrap(&prebake, &osinfo, ctx, &event_tx).await; result?; } // EarlyHook stage if stage <= PrebakeStage::EarlyHook { - let stage_start = Utc::now(); + let _stage_start = Utc::now(); log::info!("Running PBStage: EarlyHook"); - prebake_drop_privilege(PrebakeStage::EarlyHook, dropafter, target_user, &mut ctx)?; + prebake_drop_privilege(PrebakeStage::EarlyHook, dropafter, target_user, ctx)?; ctx.task_id = format!("{}-{}-earlyhook", cli.pipeline, cli.build_id); let earlyhook = match &prebake.hooks { Some(hooks) => hooks.early.clone(), None => None, }; - let result = stage::hook::hook(earlyhook, &ctx, event_tx.clone()).await; + let result = stage::hook::hook(earlyhook, ctx, event_tx.clone()).await; result?; } @@ -160,7 +159,7 @@ pub async fn prebake( let upm_sys_deps = if let Some(user) = &dependencies.user { if stage <= PrebakeStage::DepsSystem { - stage::depsuser::collect_upm_sysdeps(user, &ctx, &endpoint).await? + stage::depsuser::collect_upm_sysdeps(user, ctx, &endpoint).await? } else { stage::depsuser::CollectedUPMdeps::default() } @@ -208,38 +207,37 @@ pub async fn prebake( } if !system_config.is_empty() && stage <= PrebakeStage::DepsSystem { - let stage_start = Utc::now(); + let _stage_start = Utc::now(); log::info!("Running PBStage: DepsSystem"); - prebake_drop_privilege(PrebakeStage::DepsSystem, dropafter, target_user, &mut ctx)?; + prebake_drop_privilege(PrebakeStage::DepsSystem, dropafter, target_user, ctx)?; ctx.task_id = format!("{}-{}-depssystem", cli.pipeline, cli.build_id); - let result = - stage::depssystem::depssystem(&system_config, &ctx, event_tx.clone()).await; + let result = stage::depssystem::depssystem(&system_config, ctx, event_tx.clone()).await; result?; } if let Some(user) = dependencies.user && stage <= PrebakeStage::DepsUser { - let stage_start = Utc::now(); + let _stage_start = Utc::now(); log::info!("Running PBStage: DepsUser"); - prebake_drop_privilege(PrebakeStage::DepsUser, dropafter, target_user, &mut ctx)?; + prebake_drop_privilege(PrebakeStage::DepsUser, dropafter, target_user, ctx)?; ctx.task_id = format!("{}-{}-depsuser", cli.pipeline, cli.build_id); - let result = stage::depsuser::depsuser(&user, &ctx, event_tx.clone(), &endpoint).await; + let result = stage::depsuser::depsuser(&user, ctx, event_tx.clone(), &endpoint).await; result?; } } // LateHook stage if stage <= PrebakeStage::LateHook { - let stage_start = Utc::now(); + let _stage_start = Utc::now(); log::info!("Running PBStage: LateHook"); - prebake_drop_privilege(PrebakeStage::LateHook, dropafter, target_user, &mut ctx)?; + prebake_drop_privilege(PrebakeStage::LateHook, dropafter, target_user, ctx)?; ctx.task_id = format!("{}-{}-latehook", cli.pipeline, cli.build_id); let latehook = match &prebake.hooks { Some(hooks) => hooks.late.clone(), None => None, }; - let result = stage::hook::hook(latehook, &ctx, event_tx.clone()).await; + let result = stage::hook::hook(latehook, ctx, event_tx.clone()).await; result?; } diff --git a/workshop-baker/src/prebake/error.rs b/workshop-baker/src/prebake/error.rs index db6b686..7805ebf 100644 --- a/workshop-baker/src/prebake/error.rs +++ b/workshop-baker/src/prebake/error.rs @@ -3,10 +3,7 @@ use thiserror::Error; use workshop_engine::{ ExecutionError, - error::{ - EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR, EXITCODE_PRIV_DROP_FAILED, - HasExitCode, - }, + error::{EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR, EXITCODE_PRIV_DROP_FAILED, HasExitCode}, }; #[derive(Error, Debug)] diff --git a/workshop-baker/src/prebake/security.rs b/workshop-baker/src/prebake/security.rs index 5695944..e22c6ee 100644 --- a/workshop-baker/src/prebake/security.rs +++ b/workshop-baker/src/prebake/security.rs @@ -1,6 +1,6 @@ use crate::ExecutionContext; -use crate::prebake::error::PrebakeError; use crate::prebake::config::Security; +use crate::prebake::error::PrebakeError; use crate::prebake::stage::PrebakeStage; use privdrop::PrivDrop; use std::ffi::CString; diff --git a/workshop-baker/src/prebake/stage/bootstrap.rs b/workshop-baker/src/prebake/stage/bootstrap.rs index 40dca67..62702cc 100644 --- a/workshop-baker/src/prebake/stage/bootstrap.rs +++ b/workshop-baker/src/prebake/stage/bootstrap.rs @@ -9,15 +9,15 @@ //! and the target operating system, supporting multiple package managers. use crate::ExecutionContext; -use crate::prebake::constant::BOOTSTRAP_SCRIPT_PATH; -use workshop_engine::{Engine, EventSender, pm}; -use crate::prebake::error::BootstrapError; use crate::prebake::config::Bootstrap; use crate::prebake::config::PrebakeConfig; +use crate::prebake::constant::BOOTSTRAP_SCRIPT_PATH; +use crate::prebake::error::BootstrapError; use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; use which::which; +use workshop_engine::{Engine, EventSender, pm}; /// Executes the bootstrap stage of the prebake workflow. /// @@ -96,7 +96,7 @@ fn write_bootstrap_content(bootstrap_path: &Path, content: String) -> Result<(), let mut file = File::create(bootstrap_path)?; file.write_all(content.as_bytes())?; std::fs::set_permissions( - &bootstrap_path, + bootstrap_path, std::os::unix::fs::PermissionsExt::from_mode(0o755), )?; Ok(()) diff --git a/workshop-baker/src/prebake/stage/depssystem.rs b/workshop-baker/src/prebake/stage/depssystem.rs index 214553b..58d0f22 100644 --- a/workshop-baker/src/prebake/stage/depssystem.rs +++ b/workshop-baker/src/prebake/stage/depssystem.rs @@ -1,7 +1,7 @@ use crate::ExecutionContext; -use workshop_engine::{Engine, EventSender, pm, error::DependencyError}; use crate::prebake::config::SystemDependency; use std::collections::HashMap; +use workshop_engine::{Engine, EventSender, error::DependencyError, pm}; /// Handles system dependency installation for the target platform. /// diff --git a/workshop-baker/src/prebake/stage/depsuser.rs b/workshop-baker/src/prebake/stage/depsuser.rs index 801c808..13ecaee 100644 --- a/workshop-baker/src/prebake/stage/depsuser.rs +++ b/workshop-baker/src/prebake/stage/depsuser.rs @@ -1,8 +1,8 @@ use serde_yaml::Value; use crate::ExecutionContext; -use workshop_engine::{EventSender, upm, error::DependencyError, RepologyEndpoint}; use std::collections::HashMap; +use workshop_engine::{EventSender, RepologyEndpoint, error::DependencyError, upm}; #[derive(Default)] pub struct CollectedUPMdeps { diff --git a/workshop-baker/src/prebake/stage/hook.rs b/workshop-baker/src/prebake/stage/hook.rs index ef498e8..e88deb9 100644 --- a/workshop-baker/src/prebake/stage/hook.rs +++ b/workshop-baker/src/prebake/stage/hook.rs @@ -13,8 +13,8 @@ //! and timeout configurations. use crate::ExecutionContext; -use workshop_engine::{DeltaExecutionContext, Engine, EventSender, ExecutionError, CustomCommand}; use std::path::PathBuf; +use workshop_engine::{CustomCommand, DeltaExecutionContext, Engine, EventSender, ExecutionError}; /// Executes a sequence of hook commands. /// @@ -24,7 +24,7 @@ use std::path::PathBuf; /// # Arguments /// /// * `hook` - Optional vector of hook commands to execute. If `None` or empty, -/// the function returns immediately with `Ok(())`. +/// the function returns immediately with `Ok(())`. /// * `ctx` - Base execution context containing pipeline parameters and settings /// * `event_tx` - Event sender for reporting hook execution progress /// diff --git a/workshop-baker/src/types.rs b/workshop-baker/src/types.rs index 5ecc84f..6e81c8a 100644 --- a/workshop-baker/src/types.rs +++ b/workshop-baker/src/types.rs @@ -1 +1 @@ -pub mod buildstatus; \ No newline at end of file +pub mod buildstatus; diff --git a/workshop-baker/tests/finalize_hook_integration.rs b/workshop-baker/tests/finalize_hook_integration.rs index fd243fc..eb614e4 100644 --- a/workshop-baker/tests/finalize_hook_integration.rs +++ b/workshop-baker/tests/finalize_hook_integration.rs @@ -8,7 +8,7 @@ use std::time::Duration; use workshop_baker::ExecutionContext; use workshop_baker::finalize::plugin::PluginMap; use workshop_baker::finalize::stage::hook::hook; -use workshop_engine::{ExecutionEvent, CustomCommand}; +use workshop_engine::{CustomCommand, ExecutionEvent}; fn create_test_context() -> ExecutionContext { ExecutionContext { diff --git a/workshop-baker/tests/finalize_pipeline_integration.rs b/workshop-baker/tests/finalize_pipeline_integration.rs index a8a5fb7..be8ed59 100644 --- a/workshop-baker/tests/finalize_pipeline_integration.rs +++ b/workshop-baker/tests/finalize_pipeline_integration.rs @@ -5,10 +5,10 @@ use std::collections::HashMap; use std::time::Duration; -use workshop_baker::cli::Cli; use workshop_baker::ExecutionContext; -use workshop_engine::ExecutionEvent; +use workshop_baker::cli::Cli; use workshop_baker::finalize::finalize; +use workshop_engine::ExecutionEvent; fn create_test_context() -> ExecutionContext { ExecutionContext { diff --git a/workshop-baker/tests/types_config_integration.rs b/workshop-baker/tests/types_config_integration.rs index 4e5227a..d0b2b29 100644 --- a/workshop-baker/tests/types_config_integration.rs +++ b/workshop-baker/tests/types_config_integration.rs @@ -1,7 +1,7 @@ +use workshop_baker::finalize::types::compression::CompressionMethod; use workshop_baker::prebake::types::architecture::Architecture; use workshop_baker::prebake::types::builderconfig::BuilderConfig; use workshop_baker::prebake::types::cache::{CacheDirectory, CacheMode, CacheStrategyType}; -use workshop_baker::finalize::types::compression::CompressionMethod; use workshop_engine::{CustomCommand, RepologyEndpoint}; #[test] diff --git a/workshop-engine/src/child.rs b/workshop-engine/src/child.rs index 4d7fc10..e2008e0 100644 --- a/workshop-engine/src/child.rs +++ b/workshop-engine/src/child.rs @@ -41,11 +41,17 @@ impl ManagedChild for TokioChild { } fn take_stdout(&mut self) -> Option> { - self.0.stdout.take().map(|s| Box::new(s) as Box) + self.0 + .stdout + .take() + .map(|s| Box::new(s) as Box) } fn take_stderr(&mut self) -> Option> { - self.0.stderr.take().map(|s| Box::new(s) as Box) + self.0 + .stderr + .take() + .map(|s| Box::new(s) as Box) } async fn wait(&mut self) -> io::Result { diff --git a/workshop-engine/src/executor.rs b/workshop-engine/src/executor.rs index 3ff3af5..b27b0e3 100644 --- a/workshop-engine/src/executor.rs +++ b/workshop-engine/src/executor.rs @@ -9,8 +9,8 @@ use tokio::time::timeout; use crate::child::{ManagedChild, TokioChild}; use crate::pm::PackageManager; use crate::types::{ - DeltaExecutionContext, EventSender, ExecutionContext, ExecutionError, - ExecutionEvent, ExecutionResult, StreamType, + DeltaExecutionContext, EventSender, ExecutionContext, ExecutionError, ExecutionEvent, + ExecutionResult, StreamType, }; impl crate::types::Engine { @@ -92,36 +92,36 @@ impl crate::types::Engine { .stderr(Stdio::piped()); } -/// Execute a pre-configured command, managing the full lifecycle. -/// -/// This method: -/// 1. Configures the command with context (env vars, working dir, stdio) -/// 2. Creates a new process group on Unix for process-tree isolation -/// 3. Returns early in dry_run mode -/// 4. Spawns the OS process -/// 5. Delegates to `execute_child` for wait/event/output management -pub async fn execute( - &self, - command: Command, - ctx: &ExecutionContext, - event_tx: &EventSender, -) -> Result { - let mut command = command; - self.construct_command(&mut command, ctx); + /// Execute a pre-configured command, managing the full lifecycle. + /// + /// This method: + /// 1. Configures the command with context (env vars, working dir, stdio) + /// 2. Creates a new process group on Unix for process-tree isolation + /// 3. Returns early in dry_run mode + /// 4. Spawns the OS process + /// 5. Delegates to `execute_child` for wait/event/output management + pub async fn execute( + &self, + command: Command, + ctx: &ExecutionContext, + event_tx: &EventSender, + ) -> Result { + let mut command = command; + self.construct_command(&mut command, ctx); - if ctx.dry_run { - log::info!("[DRY_RUN] {:?}", command); - return Ok(ExecutionResult { - ..Default::default() - }); - } + if ctx.dry_run { + log::info!("[DRY_RUN] {:?}", command); + return Ok(ExecutionResult { + ..Default::default() + }); + } - // Create a new process group so that kill() can terminate the - // entire process tree (bash + its children) on timeout. - #[cfg(unix)] - command.process_group(0); + // Create a new process group so that kill() can terminate the + // entire process tree (bash + its children) on timeout. + #[cfg(unix)] + command.process_group(0); - let child = command.spawn().map_err(|e| { + let child = command.spawn().map_err(|e| { ExecutionError::InvalidCommand(format!("Failed to spawn command: {}", e)) })?; @@ -352,8 +352,8 @@ impl Default for crate::types::Engine { #[cfg(test)] mod tests { use super::*; - use crate::child::MockChild; use crate::Engine; + use crate::child::MockChild; use std::path::PathBuf; use std::time::Duration; @@ -423,7 +423,10 @@ mod tests { let result = engine.execute_child(child, &test_ctx(), &None).await; assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), ExecutionError::ExecutionFailed(_))); + assert!(matches!( + result.unwrap_err(), + ExecutionError::ExecutionFailed(_) + )); } #[tokio::test] @@ -511,11 +514,20 @@ mod tests { let mut found_stdout = false; while let Ok(event) = rx.try_recv() { - if matches!(&event, ExecutionEvent::OutputChunk { stream: StreamType::Stdout, .. }) { + if matches!( + &event, + ExecutionEvent::OutputChunk { + stream: StreamType::Stdout, + .. + } + ) { found_stdout = true; } } - assert!(found_stdout, "Should have received at least one OutputChunk for stdout"); + assert!( + found_stdout, + "Should have received at least one OutputChunk for stdout" + ); } // ------------------------------------------------------------------ @@ -538,7 +550,9 @@ mod tests { // Should have received TaskFailed for timeout let _ = rx.recv().await; // skip TaskStarted let event = rx.recv().await; - assert!(matches!(event, Some(ExecutionEvent::TaskFailed { error, .. }) if error == "Timeout")); + assert!( + matches!(event, Some(ExecutionEvent::TaskFailed { error, .. }) if error == "Timeout") + ); } // ------------------------------------------------------------------ diff --git a/workshop-engine/src/lib.rs b/workshop-engine/src/lib.rs index 267760b..c02349e 100644 --- a/workshop-engine/src/lib.rs +++ b/workshop-engine/src/lib.rs @@ -12,14 +12,17 @@ pub mod upm; pub use child::{ManagedChild, TokioChild}; // Convenience re-exports -pub use error::{ExecutionError, HasExitCode, - EXITCODE_OK, EXITCODE_GENERAL_ERROR, EXITCODE_INVALID_ARGS, - EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR, EXITCODE_EXECUTION_ERROR, - EXITCODE_TIMEOUT, EXITCODE_PRIV_DROP_FAILED}; -pub use types::{Engine, EventSender, ExecutionContext, ExecutionEvent, ExecutionResult, - StreamType, DeltaExecutionContext}; pub use command::CustomCommand; -pub use repology::RepologyEndpoint; -pub use time::{parse_duration_to_ms, deserialize_duration_ms, deserialize_option_duration_ms}; +pub use error::{ + EXITCODE_EXECUTION_ERROR, EXITCODE_GENERAL_ERROR, EXITCODE_INVALID_ARGS, EXITCODE_IO_ERROR, + EXITCODE_OK, EXITCODE_PARSE_ERROR, EXITCODE_PRIV_DROP_FAILED, EXITCODE_TIMEOUT, ExecutionError, + HasExitCode, +}; pub use pm::PackageManager; +pub use repology::RepologyEndpoint; +pub use time::{deserialize_duration_ms, deserialize_option_duration_ms, parse_duration_to_ms}; +pub use types::{ + DeltaExecutionContext, Engine, EventSender, ExecutionContext, ExecutionEvent, ExecutionResult, + StreamType, +}; pub use upm::UserPackageManager; diff --git a/workshop-engine/src/upm.rs b/workshop-engine/src/upm.rs index 1b5b764..eaea08d 100644 --- a/workshop-engine/src/upm.rs +++ b/workshop-engine/src/upm.rs @@ -3,8 +3,8 @@ use async_trait::async_trait; use serde_yaml::Value; mod custom; -use crate::types::ExecutionContext; use crate::error::DependencyError; +use crate::types::ExecutionContext; /// System dependencies required by a user package manager. pub struct UPMSysDeps { diff --git a/workshop-engine/src/upm/custom.rs b/workshop-engine/src/upm/custom.rs index e0f9453..3d05ef3 100644 --- a/workshop-engine/src/upm/custom.rs +++ b/workshop-engine/src/upm/custom.rs @@ -2,11 +2,11 @@ use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -use crate::types::Engine; -use crate::types::DeltaExecutionContext; -use crate::upm::UPMSysDeps; -use crate::error::DependencyError; use crate::command::CustomCommand; +use crate::error::DependencyError; +use crate::types::DeltaExecutionContext; +use crate::types::Engine; +use crate::upm::UPMSysDeps; use super::UserPackageManager; diff --git a/workshop-engine/tests/engine_integration.rs b/workshop-engine/tests/engine_integration.rs index aa7e417..d6aba7e 100644 --- a/workshop-engine/tests/engine_integration.rs +++ b/workshop-engine/tests/engine_integration.rs @@ -213,9 +213,9 @@ async fn test_execute_captures_stderr() { #[tokio::test] async fn test_process_group_kill_kills_subprocesses() { use std::os::unix::process::CommandExt; + use std::process::Stdio; use tokio::io::AsyncBufReadExt; use tokio::process::Command; - use std::process::Stdio; let mut cmd = Command::new("bash"); cmd.args(["-c", "sleep 999 & echo $!"]); @@ -232,7 +232,10 @@ async fn test_process_group_kill_kills_subprocesses() { let stdout = child.stdout.take().expect("stdout pipe"); let mut reader = tokio::io::BufReader::new(stdout); let mut pid_line = String::new(); - reader.read_line(&mut pid_line).await.expect("read pid line"); + reader + .read_line(&mut pid_line) + .await + .expect("read pid line"); let sleep_pid: u32 = pid_line.trim().parse().expect("valid sleep PID"); eprintln!("[test] sleep PID: {}", sleep_pid);