refactor(error): extract exit code logic into HasExitCode trait

Introduce a common HasExitCode trait for PrebakeError, BakeError,
ExecutionError, and FinalizeError. Refactor main.rs to consolidate
CLI command handling and use the trait for unified exit code resolution.
This commit is contained in:
Catty Steve
2026-04-24 21:45:57 +08:00
parent 901005639b
commit 2c9d32e954
6 changed files with 106 additions and 117 deletions
+26 -4
View File
@@ -1,7 +1,14 @@
use os_info::Info; use os_info::Info;
use std::fmt::Debug;
use std::fmt::Display;
use std::path::PathBuf; use std::path::PathBuf;
use thiserror::Error; use thiserror::Error;
// Obsolete?
pub trait HasExitCode: Display + Debug + Send + Sync + Sized + 'static {
fn exit_code(&self) -> i32;
}
pub const EXITCODE_OK: i32 = 0; pub const EXITCODE_OK: i32 = 0;
pub const EXITCODE_GENERAL_ERROR: i32 = 1; pub const EXITCODE_GENERAL_ERROR: i32 = 1;
pub const EXITCODE_INVALID_ARGS: i32 = 2; pub const EXITCODE_INVALID_ARGS: i32 = 2;
@@ -69,8 +76,8 @@ pub enum PrebakeError {
ConfigError(String), ConfigError(String),
} }
impl PrebakeError { impl HasExitCode for PrebakeError {
pub fn exit_code(&self) -> i32 { fn exit_code(&self) -> i32 {
match self { match self {
PrebakeError::PrivDropFailed(_) => EXITCODE_PRIV_DROP_FAILED, PrebakeError::PrivDropFailed(_) => EXITCODE_PRIV_DROP_FAILED,
PrebakeError::UserNotFound(_) => EXITCODE_PRIV_DROP_FAILED, PrebakeError::UserNotFound(_) => EXITCODE_PRIV_DROP_FAILED,
@@ -149,8 +156,23 @@ pub enum ExecutionError {
PackageManagerNotFound, PackageManagerNotFound,
} }
impl ExecutionError { impl HasExitCode for BakeError {
pub fn exit_code(&self) -> i32 { fn exit_code(&self) -> i32 {
match self {
BakeError::ScriptGenerationFailed { .. } => EXITCODE_IO_ERROR,
BakeError::TemplateError(_) => EXITCODE_PARSE_ERROR,
BakeError::CircularDependency(_) => EXITCODE_PARSE_ERROR,
BakeError::UnknownDependency(..) => EXITCODE_PARSE_ERROR,
BakeError::IoError(_) => EXITCODE_IO_ERROR,
BakeError::YamlParseError(_) => EXITCODE_PARSE_ERROR,
BakeError::DecoratorParseError { .. } => EXITCODE_PARSE_ERROR,
BakeError::ScriptExecutionError(e) => e.exit_code(),
}
}
}
impl HasExitCode for ExecutionError {
fn exit_code(&self) -> i32 {
match self { match self {
ExecutionError::InvalidCommand(_) => EXITCODE_EXECUTION_ERROR, ExecutionError::InvalidCommand(_) => EXITCODE_EXECUTION_ERROR,
ExecutionError::ExecutionFailed(_) => EXITCODE_EXECUTION_ERROR, ExecutionError::ExecutionFailed(_) => EXITCODE_EXECUTION_ERROR,
+5 -2
View File
@@ -249,7 +249,6 @@ pub struct ArtifactDef {
/// Compression method: "zstd", "gzip", "none", "dir" /// Compression method: "zstd", "gzip", "none", "dir"
#[serde(default)] #[serde(default)]
pub compression: CompressionMethod, pub compression: CompressionMethod,
// Publishing targets // Publishing targets
// TODO: under heavy refactor // TODO: under heavy refactor
} }
@@ -351,7 +350,11 @@ impl FinalizeConfig {
fn validate_notification(&self, errors: &mut Vec<String>) { fn validate_notification(&self, errors: &mut Vec<String>) {
// Validate templates // Validate templates
for (name, tmpl) in &self.notification.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()) { if tmpl.use_.is_some()
&& (tmpl.on_success.is_some()
|| tmpl.on_failure.is_some()
|| tmpl.on_finish_of.is_some())
{
errors.push(format!( errors.push(format!(
"Template '{}' cannot combine 'use' with 'on_*' fields (MVP restriction)", "Template '{}' cannot combine 'use' with 'on_*' fields (MVP restriction)",
name name
+3 -2
View File
@@ -5,6 +5,7 @@ use crate::{
ExecutionError, ExecutionError,
error::{ error::{
EXITCODE_EXECUTION_ERROR, EXITCODE_GENERAL_ERROR, EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR, EXITCODE_EXECUTION_ERROR, EXITCODE_GENERAL_ERROR, EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR,
HasExitCode,
}, },
}; };
@@ -38,8 +39,8 @@ pub enum FinalizeError {
ExecutionError(#[from] ExecutionError), ExecutionError(#[from] ExecutionError),
} }
impl FinalizeError { impl HasExitCode for FinalizeError {
pub fn exit_code(&self) -> i32 { fn exit_code(&self) -> i32 {
match self { match self {
FinalizeError::YamlParseError(_) => EXITCODE_PARSE_ERROR, FinalizeError::YamlParseError(_) => EXITCODE_PARSE_ERROR,
FinalizeError::ValidateError(_) => EXITCODE_PARSE_ERROR, FinalizeError::ValidateError(_) => EXITCODE_PARSE_ERROR,
+41 -79
View File
@@ -5,7 +5,7 @@ use workshop_baker::{
cli::{BareCommands, Cli, Commands, Parser}, cli::{BareCommands, Cli, Commands, Parser},
daemon, daemon,
engine::{EventSender, ExecutionContext}, engine::{EventSender, ExecutionContext},
error::PrebakeError, error::HasExitCode,
finalize::finalize, finalize::finalize,
prebake::prebake, prebake::prebake,
}; };
@@ -21,108 +21,70 @@ async fn main() {
Some(Commands::Client { config: _ }) => { Some(Commands::Client { config: _ }) => {
unimplemented!("Client mode not yet implemented.") unimplemented!("Client mode not yet implemented.")
} }
Some(Commands::Bare { command }) => match command { Some(Commands::Bare { command }) => {
BareCommands::Prebake { config } => {
log::info!("Running in standalone mode."); log::info!("Running in standalone mode.");
let (stage, privileged) = match command {
BareCommands::Prebake { .. } => ("init", true),
BareCommands::Bake { .. } => ("bake", false),
BareCommands::Finalize { .. } => ("finalize", false),
};
let mut ctx = ExecutionContext { let mut ctx = ExecutionContext {
standalone: true, standalone: true,
task_id: format!("{}-{}-init", cli.pipeline, cli.build_id), task_id: format!("{}-{}-{}", cli.pipeline, cli.build_id, stage),
pipeline_name: cli.pipeline.clone(), pipeline_name: cli.pipeline.clone(),
username: cli.username.clone(), username: cli.username.clone(),
dry_run: cli.dry_run, dry_run: cli.dry_run,
privileged: true, privileged,
..Default::default() ..Default::default()
}; };
let (tx, event_rx) = tokio::sync::mpsc::channel(256); let (tx, event_rx) = tokio::sync::mpsc::channel(256);
let event_tx: EventSender = Some(tx); let event_tx: EventSender = Some(tx);
let pipeline_name = cli.pipeline.clone(); let handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
let build_id = cli.build_id.clone();
let receiver_handle =
tokio::spawn(workshop_baker::prebake::event::event_receiver(
event_rx, event_rx,
pipeline_name, cli.pipeline.clone(),
build_id, cli.build_id.clone(),
)); ));
let result = prebake(config, &cli, None, &mut ctx, event_tx).await;
drop(ctx); let result: anyhow::Result<()> = match command {
let _ = receiver_handle.await; BareCommands::Prebake { config } => {
dbg!(&result); prebake(config, &cli, None, &mut ctx, event_tx).await
if let Err(ref e) = result {
log::error!("Prebake stage failed: {}", e);
std::process::exit(
e.downcast_ref::<PrebakeError>()
.map(|pe| pe.exit_code())
.unwrap_or(1),
);
}
} }
BareCommands::Bake { BareCommands::Bake {
script, script,
bake_base, bake_base,
prebake: prebake_path, prebake: prebake_path,
} => { } => bake(script, bake_base, prebake_path, &cli, &mut ctx, event_tx)
log::info!("Running in standalone mode."); .await
let mut ctx = ExecutionContext { .map_err(|e| e.into()),
standalone: true,
task_id: format!("{}-{}-bake", cli.pipeline, cli.build_id),
pipeline_name: cli.pipeline.clone(),
username: cli.username.clone(),
dry_run: cli.dry_run,
privileged: false,
..Default::default()
};
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
let event_tx: EventSender = Some(tx);
let pipeline_name = cli.pipeline.clone();
let build_id = cli.build_id.clone();
let receiver_handle =
tokio::spawn(workshop_baker::prebake::event::event_receiver(
event_rx,
pipeline_name,
build_id,
));
let result =
bake(script, bake_base, prebake_path, &cli, &mut ctx, event_tx).await;
drop(ctx);
let _ = receiver_handle.await;
dbg!(&result);
if result.is_err() {
log::error!("Bake stage failed! Exiting...");
std::process::exit(1);
}
}
BareCommands::Finalize { config } => { BareCommands::Finalize { config } => {
log::info!("Running in standalone mode."); finalize(Path::new(config), &cli, &mut ctx, event_tx)
let mut ctx = ExecutionContext { .await
standalone: true, .map_err(|e| e.into())
task_id: format!("{}-{}-finalize", cli.pipeline, cli.build_id), }
pipeline_name: cli.pipeline.clone(),
username: cli.username.clone(),
dry_run: cli.dry_run,
privileged: false,
..Default::default()
}; };
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
let event_tx: EventSender = Some(tx);
let pipeline_name = cli.pipeline.clone();
let build_id = cli.build_id.clone();
let receiver_handle =
tokio::spawn(workshop_baker::prebake::event::event_receiver(
event_rx,
pipeline_name,
build_id,
));
let result =
finalize(Path::new(config), &cli, &mut ctx, event_tx).await;
drop(ctx); drop(ctx);
let _ = receiver_handle.await; let _ = handle.await;
dbg!(&result); dbg!(&result);
if let Err(ref e) = result { if let Err(ref e) = result {
log::error!("Finalize stage failed: {}", e); let code = match command {
std::process::exit(1); BareCommands::Prebake { .. } => e
.downcast_ref::<workshop_baker::error::PrebakeError>()
.map(|pe| pe.exit_code())
.unwrap_or(1),
BareCommands::Bake { .. } => e
.downcast_ref::<workshop_baker::error::BakeError>()
.map(|be| be.exit_code())
.unwrap_or(1),
BareCommands::Finalize { .. } => e
.downcast_ref::<workshop_baker::finalize::FinalizeError>()
.map(|fe| fe.exit_code())
.unwrap_or(1),
};
log::error!("{} stage failed: {}", stage, e);
std::process::exit(code);
} }
} }
},
None => { None => {
log::info!("Executor will be running in daemon mode."); log::info!("Executor will be running in daemon mode.");
let _result = daemon::daemon(&cli).await; let _result = daemon::daemon(&cli).await;
+10 -9
View File
@@ -16,7 +16,7 @@ use crate::prebake::config::Bootstrap;
use crate::prebake::config::PrebakeConfig; use crate::prebake::config::PrebakeConfig;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use std::path::{PathBuf,Path}; use std::path::{Path, PathBuf};
use which::which; use which::which;
/// Executes the bootstrap stage of the prebake workflow. /// Executes the bootstrap stage of the prebake workflow.
@@ -55,15 +55,17 @@ pub async fn bootstrap(
if let Some(custom_bootstrap) = bootstrap_config.custom { if let Some(custom_bootstrap) = bootstrap_config.custom {
if ctx.dry_run { if ctx.dry_run {
log::info!("[DRY_RUN] User provided bootstrap script:\n{}", custom_bootstrap); log::info!(
"[DRY_RUN] User provided bootstrap script:\n{}",
custom_bootstrap
);
return Ok(()); return Ok(());
} }
// TODO: Parse custom bootstrap URL and download them // TODO: Parse custom bootstrap URL and download them
log::debug!("User provided bootstrap script:\n{}", custom_bootstrap); log::debug!("User provided bootstrap script:\n{}", custom_bootstrap);
if ctx.standalone { if ctx.standalone {
write_bootstrap_content(&bootstrap_path, custom_bootstrap)?; write_bootstrap_content(&bootstrap_path, custom_bootstrap)?;
} } else {
else {
// Requires PIP(Pipeline Implicit Parameters) to be implemented // Requires PIP(Pipeline Implicit Parameters) to be implemented
unimplemented!("Permission check is not implemented, failing..."); unimplemented!("Permission check is not implemented, failing...");
} }
@@ -147,7 +149,9 @@ fn generate_bootstrap(
// Requires PIP(Pipeline Implicit Parameters) to be implemented // Requires PIP(Pipeline Implicit Parameters) to be implemented
// unimplemented!("Permission check is not implemented, failing..."); // unimplemented!("Permission check is not implemented, failing...");
if standalone { if standalone {
log::warn!("Running the whole pipeline with root could harm your pipeline or cause undefined behavior.") log::warn!(
"Running the whole pipeline with root could harm your pipeline or cause undefined behavior."
)
} else { } else {
unimplemented!("Permission check is not implemented, failing..."); unimplemented!("Permission check is not implemented, failing...");
} }
@@ -221,10 +225,7 @@ fn generate_bootstrap(
// TODO: Introduce permission check // TODO: Introduce permission check
unimplemented!("Permission check is not implemented, failing...") unimplemented!("Permission check is not implemented, failing...")
} }
script.push_str(&format!( script.push_str(&format!("cat >> /etc/doas.conf << 'EOF'\n{}\nEOF\n", doas));
"cat >> /etc/doas.conf << 'EOF'\n{}\nEOF\n",
doas
));
} }
if let Some(pkg_manager) = pkg_manager { if let Some(pkg_manager) = pkg_manager {
let allowed_commands = format!("/usr/bin/{} *", pkg_manager.binary()); let allowed_commands = format!("/usr/bin/{} *", pkg_manager.binary());
+1 -1
View File
@@ -3,7 +3,7 @@ pub mod builderconfig;
pub mod buildstatus; pub mod buildstatus;
pub mod cache; pub mod cache;
pub mod command; pub mod command;
pub mod compression;
pub mod memsize; pub mod memsize;
pub mod repology; pub mod repology;
pub mod time; pub mod time;
pub mod compression;