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 std::fmt::Debug;
use std::fmt::Display;
use std::path::PathBuf;
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_GENERAL_ERROR: i32 = 1;
pub const EXITCODE_INVALID_ARGS: i32 = 2;
@@ -69,8 +76,8 @@ pub enum PrebakeError {
ConfigError(String),
}
impl PrebakeError {
pub fn exit_code(&self) -> i32 {
impl HasExitCode for PrebakeError {
fn exit_code(&self) -> i32 {
match self {
PrebakeError::PrivDropFailed(_) => EXITCODE_PRIV_DROP_FAILED,
PrebakeError::UserNotFound(_) => EXITCODE_PRIV_DROP_FAILED,
@@ -149,8 +156,23 @@ pub enum ExecutionError {
PackageManagerNotFound,
}
impl ExecutionError {
pub fn exit_code(&self) -> i32 {
impl HasExitCode for BakeError {
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 {
ExecutionError::InvalidCommand(_) => 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"
#[serde(default)]
pub compression: CompressionMethod,
// Publishing targets
// TODO: under heavy refactor
}
@@ -351,7 +350,11 @@ impl FinalizeConfig {
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()) {
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
+3 -2
View File
@@ -5,6 +5,7 @@ use crate::{
ExecutionError,
error::{
EXITCODE_EXECUTION_ERROR, EXITCODE_GENERAL_ERROR, EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR,
HasExitCode,
},
};
@@ -38,8 +39,8 @@ pub enum FinalizeError {
ExecutionError(#[from] ExecutionError),
}
impl FinalizeError {
pub fn exit_code(&self) -> i32 {
impl HasExitCode for FinalizeError {
fn exit_code(&self) -> i32 {
match self {
FinalizeError::YamlParseError(_) => EXITCODE_PARSE_ERROR,
FinalizeError::ValidateError(_) => EXITCODE_PARSE_ERROR,
+61 -99
View File
@@ -5,7 +5,7 @@ use workshop_baker::{
cli::{BareCommands, Cli, Commands, Parser},
daemon,
engine::{EventSender, ExecutionContext},
error::PrebakeError,
error::HasExitCode,
finalize::finalize,
prebake::prebake,
};
@@ -21,108 +21,70 @@ async fn main() {
Some(Commands::Client { config: _ }) => {
unimplemented!("Client mode not yet implemented.")
}
Some(Commands::Bare { command }) => match command {
BareCommands::Prebake { config } => {
log::info!("Running in standalone mode.");
let mut ctx = ExecutionContext {
standalone: true,
task_id: format!("{}-{}-init", cli.pipeline, cli.build_id),
pipeline_name: cli.pipeline.clone(),
username: cli.username.clone(),
dry_run: cli.dry_run,
privileged: true,
..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 = prebake(config, &cli, None, &mut ctx, event_tx).await;
drop(ctx);
let _ = receiver_handle.await;
dbg!(&result);
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),
);
Some(Commands::Bare { command }) => {
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 {
standalone: true,
task_id: format!("{}-{}-{}", cli.pipeline, cli.build_id, stage),
pipeline_name: cli.pipeline.clone(),
username: cli.username.clone(),
dry_run: cli.dry_run,
privileged,
..Default::default()
};
let (tx, event_rx) = tokio::sync::mpsc::channel(256);
let event_tx: EventSender = Some(tx);
let handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
event_rx,
cli.pipeline.clone(),
cli.build_id.clone(),
));
let result: anyhow::Result<()> = match command {
BareCommands::Prebake { config } => {
prebake(config, &cli, None, &mut ctx, event_tx).await
}
}
BareCommands::Bake {
script,
bake_base,
prebake: prebake_path,
} => {
log::info!("Running in standalone mode.");
let mut ctx = ExecutionContext {
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::Bake {
script,
bake_base,
prebake: prebake_path,
} => bake(script, bake_base, prebake_path, &cli, &mut ctx, event_tx)
.await
.map_err(|e| e.into()),
BareCommands::Finalize { config } => {
finalize(Path::new(config), &cli, &mut ctx, event_tx)
.await
.map_err(|e| e.into())
}
}
BareCommands::Finalize { config } => {
log::info!("Running in standalone mode.");
let mut ctx = ExecutionContext {
standalone: true,
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()
};
drop(ctx);
let _ = handle.await;
dbg!(&result);
if let Err(ref e) = result {
let code = match command {
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),
};
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);
let _ = receiver_handle.await;
dbg!(&result);
if let Err(ref e) = result {
log::error!("Finalize stage failed: {}", e);
std::process::exit(1);
}
log::error!("{} stage failed: {}", stage, e);
std::process::exit(code);
}
},
}
None => {
log::info!("Executor will be running in daemon mode.");
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 std::fs::File;
use std::io::Write;
use std::path::{PathBuf,Path};
use std::path::{Path, PathBuf};
use which::which;
/// 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 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(());
}
// TODO: Parse custom bootstrap URL and download them
log::debug!("User provided bootstrap script:\n{}", custom_bootstrap);
if ctx.standalone {
write_bootstrap_content(&bootstrap_path, custom_bootstrap)?;
}
else {
} else {
// Requires PIP(Pipeline Implicit Parameters) to be implemented
unimplemented!("Permission check is not implemented, failing...");
}
@@ -147,7 +149,9 @@ fn generate_bootstrap(
// Requires PIP(Pipeline Implicit Parameters) to be implemented
// unimplemented!("Permission check is not implemented, failing...");
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 {
unimplemented!("Permission check is not implemented, failing...");
}
@@ -221,10 +225,7 @@ fn generate_bootstrap(
// TODO: Introduce permission check
unimplemented!("Permission check is not implemented, failing...")
}
script.push_str(&format!(
"cat >> /etc/doas.conf << 'EOF'\n{}\nEOF\n",
doas
));
script.push_str(&format!("cat >> /etc/doas.conf << 'EOF'\n{}\nEOF\n", doas));
}
if let Some(pkg_manager) = pkg_manager {
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 cache;
pub mod command;
pub mod compression;
pub mod memsize;
pub mod repology;
pub mod time;
pub mod compression;