This commit is contained in:
@@ -30,7 +30,7 @@ jobs:
|
|||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
components: clippy
|
components: clippy, rustfmt
|
||||||
|
|
||||||
- name: Cache cargo dependencies
|
- name: Cache cargo dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -43,8 +43,14 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-cargo-
|
${{ runner.os }}-cargo-
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: cargo fmt -- --check
|
||||||
|
|
||||||
- name: Check code (cargo check)
|
- name: Check code (cargo check)
|
||||||
run: cargo check --all-features
|
run: cargo check --all-features
|
||||||
|
|
||||||
- name: Run Clippy lints
|
- name: Run Clippy lints
|
||||||
run: cargo clippy --all-features -- -D warnings
|
run: cargo clippy --all-features -- -D warnings
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --all-features
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{Criterion, black_box, criterion_group, criterion_main};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
|||||||
+22
-17
@@ -1,16 +1,16 @@
|
|||||||
|
use crate::ExecutionContext;
|
||||||
use crate::bake::parser::Function;
|
use crate::bake::parser::Function;
|
||||||
use crate::cli::Cli;
|
use crate::cli::Cli;
|
||||||
use crate::constant::{DEFAULT_WORKSPACE, STANDALONE_STATUS_DIR, TRIVIAL_SCRIPT_NAME};
|
use crate::constant::{DEFAULT_WORKSPACE, STANDALONE_STATUS_DIR, TRIVIAL_SCRIPT_NAME};
|
||||||
use crate::engine::EventSender;
|
use crate::engine::EventSender;
|
||||||
use crate::error::BakeError;
|
use crate::error::BakeError;
|
||||||
use crate::ExecutionContext;
|
|
||||||
use crate::prebake::PrebakeConfig;
|
use crate::prebake::PrebakeConfig;
|
||||||
use crate::prebake::security::get_drop_after;
|
use crate::prebake::security::get_drop_after;
|
||||||
use crate::prebake::stage::PrebakeStage;
|
use crate::prebake::stage::PrebakeStage;
|
||||||
use crate::types::buildstatus::{write_stage_status, StageInfo, StagePhase, StageResult};
|
use crate::types::buildstatus::{StageInfo, StagePhase, StageResult, write_stage_status};
|
||||||
use crate::{Engine, prebake};
|
use crate::{Engine, prebake};
|
||||||
use std::path::{PathBuf, Path};
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
mod builder;
|
mod builder;
|
||||||
mod decorator;
|
mod decorator;
|
||||||
@@ -52,20 +52,19 @@ pub async fn bake(
|
|||||||
|
|
||||||
if !has_pipeline || trivial {
|
if !has_pipeline || trivial {
|
||||||
let stage_start = Utc::now();
|
let stage_start = Utc::now();
|
||||||
let function = Function{
|
let function = Function {
|
||||||
name: TRIVIAL_SCRIPT_NAME.to_string(),
|
name: TRIVIAL_SCRIPT_NAME.to_string(),
|
||||||
decorators: vec![],
|
decorators: vec![],
|
||||||
body: script_content,
|
body: script_content,
|
||||||
};
|
};
|
||||||
let script = builder::build_script(
|
let script =
|
||||||
&function,
|
builder::build_script(&function, bake_base_path, &workspace, None, use_template)?;
|
||||||
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;
|
||||||
let stage_result = if result.is_ok() { StageResult::Success } else { StageResult::Failure };
|
let stage_result = if result.is_ok() {
|
||||||
|
StageResult::Success
|
||||||
|
} else {
|
||||||
|
StageResult::Failure
|
||||||
|
};
|
||||||
let _ = write_stage_status(
|
let _ = write_stage_status(
|
||||||
STANDALONE_STATUS_DIR,
|
STANDALONE_STATUS_DIR,
|
||||||
StageInfo {
|
StageInfo {
|
||||||
@@ -73,7 +72,9 @@ pub async fn bake(
|
|||||||
phase: StagePhase::Bake,
|
phase: StagePhase::Bake,
|
||||||
substage: "trivial".to_string(),
|
substage: "trivial".to_string(),
|
||||||
result: stage_result,
|
result: stage_result,
|
||||||
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
duration_ms: Utc::now()
|
||||||
|
.signed_duration_since(stage_start)
|
||||||
|
.num_milliseconds() as u64,
|
||||||
started_at: stage_start,
|
started_at: stage_start,
|
||||||
finished_at: Utc::now(),
|
finished_at: Utc::now(),
|
||||||
error_message: result.as_ref().err().map(|e| e.to_string()),
|
error_message: result.as_ref().err().map(|e| e.to_string()),
|
||||||
@@ -95,7 +96,11 @@ pub async fn bake(
|
|||||||
use_template,
|
use_template,
|
||||||
)?;
|
)?;
|
||||||
let result = engine.execute_script(&script, &ctx, &event_tx).await;
|
let result = engine.execute_script(&script, &ctx, &event_tx).await;
|
||||||
let stage_result = if result.is_ok() { StageResult::Success } else { StageResult::Failure };
|
let stage_result = if result.is_ok() {
|
||||||
|
StageResult::Success
|
||||||
|
} else {
|
||||||
|
StageResult::Failure
|
||||||
|
};
|
||||||
let _ = write_stage_status(
|
let _ = write_stage_status(
|
||||||
STANDALONE_STATUS_DIR,
|
STANDALONE_STATUS_DIR,
|
||||||
StageInfo {
|
StageInfo {
|
||||||
@@ -103,7 +108,9 @@ pub async fn bake(
|
|||||||
phase: StagePhase::Bake,
|
phase: StagePhase::Bake,
|
||||||
substage: func.name.clone(),
|
substage: func.name.clone(),
|
||||||
result: stage_result,
|
result: stage_result,
|
||||||
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
duration_ms: Utc::now()
|
||||||
|
.signed_duration_since(stage_start)
|
||||||
|
.num_milliseconds() as u64,
|
||||||
started_at: stage_start,
|
started_at: stage_start,
|
||||||
finished_at: Utc::now(),
|
finished_at: Utc::now(),
|
||||||
error_message: result.as_ref().err().map(|e| e.to_string()),
|
error_message: result.as_ref().err().map(|e| e.to_string()),
|
||||||
@@ -141,7 +148,6 @@ fn privileged(prebake_config: &PrebakeConfig) -> bool {
|
|||||||
PrebakeStage::Never == drop_after
|
PrebakeStage::Never == drop_after
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type TaskResult = Result<(), BakeError>;
|
type TaskResult = Result<(), BakeError>;
|
||||||
|
|
||||||
type TaskFn = Box<dyn FnOnce() -> TaskResult + Send>;
|
type TaskFn = Box<dyn FnOnce() -> TaskResult + Send>;
|
||||||
@@ -154,5 +160,4 @@ type TaskFn = Box<dyn FnOnce() -> TaskResult + Send>;
|
|||||||
|
|
||||||
// fn with_timeout<F>(f: F, duration: i32) -> TaskFn {
|
// fn with_timeout<F>(f: F, duration: i32) -> TaskFn {
|
||||||
|
|
||||||
|
|
||||||
// }
|
// }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::decorator::{parse_decorator, Decorator};
|
use super::decorator::{Decorator, parse_decorator};
|
||||||
use crate::bake::BakeError;
|
use crate::bake::BakeError;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|||||||
@@ -28,10 +28,8 @@ pub fn sort_function(functions: Vec<Function>) -> Result<Vec<Function>, BakeErro
|
|||||||
|
|
||||||
// Add implicit @after for pipeline functions that don't have explicit @after
|
// Add implicit @after for pipeline functions that don't have explicit @after
|
||||||
if is_pipeline {
|
if is_pipeline {
|
||||||
if !has_after {
|
if !has_after && let Some(ref last) = last_pipeline {
|
||||||
if let Some(ref last) = last_pipeline {
|
ts.add_dependency(last, function.name.clone());
|
||||||
ts.add_dependency(last, function.name.clone());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
last_pipeline = Some(function.name.clone());
|
last_pipeline = Some(function.name.clone());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use crate::cli::{Cli};
|
use crate::cli::Cli;
|
||||||
// use serde::Serialize;
|
// use serde::Serialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
// use std::path::PathBuf;
|
// use std::path::PathBuf;
|
||||||
// use std::sync::Arc;
|
// use std::sync::Arc;
|
||||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||||
use tokio::net::{UnixStream};
|
use tokio::net::UnixStream;
|
||||||
// use tokio::process::{Child, Command};
|
// use tokio::process::{Child, Command};
|
||||||
// use tokio::signal;
|
// use tokio::signal;
|
||||||
// use tokio::sync::RwLock;
|
// use tokio::sync::RwLock;
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use tokio::time::timeout;
|
|||||||
|
|
||||||
use super::pm::PackageManager;
|
use super::pm::PackageManager;
|
||||||
use super::types::{
|
use super::types::{
|
||||||
CgroupManager, EventSender, ExecutionContext, ExecutionError, ExecutionEvent, ExecutionResult,
|
CgroupManager, DeltaExecutionContext, EventSender, ExecutionContext, ExecutionError,
|
||||||
ResourceLimits, DeltaExecutionContext, StreamType
|
ExecutionEvent, ExecutionResult, ResourceLimits, StreamType,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl super::types::Engine {
|
impl super::types::Engine {
|
||||||
@@ -68,7 +68,7 @@ impl super::types::Engine {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !abs_path.exists() {
|
if !abs_path.exists() {
|
||||||
return Err(ExecutionError::ScriptNotFound(abs_path))
|
return Err(ExecutionError::ScriptNotFound(abs_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut command = Command::new(ctx.shell.as_str());
|
let mut command = Command::new(ctx.shell.as_str());
|
||||||
@@ -94,8 +94,8 @@ impl super::types::Engine {
|
|||||||
cmd: &str,
|
cmd: &str,
|
||||||
ctx: &ExecutionContext,
|
ctx: &ExecutionContext,
|
||||||
event_tx: &EventSender,
|
event_tx: &EventSender,
|
||||||
delta_ctx: &DeltaExecutionContext
|
delta_ctx: &DeltaExecutionContext,
|
||||||
) -> Result<ExecutionResult,ExecutionError> {
|
) -> Result<ExecutionResult, ExecutionError> {
|
||||||
let mut local_ctx = ctx.clone();
|
let mut local_ctx = ctx.clone();
|
||||||
if let Some(working_dir) = &delta_ctx.working_dir {
|
if let Some(working_dir) = &delta_ctx.working_dir {
|
||||||
log::debug!("Updating working directory to {}", working_dir.display());
|
log::debug!("Updating working directory to {}", working_dir.display());
|
||||||
@@ -222,7 +222,7 @@ impl super::types::Engine {
|
|||||||
return Err(ExecutionError::IoError(format!(
|
return Err(ExecutionError::IoError(format!(
|
||||||
"Failed to wait for process: {}",
|
"Failed to wait for process: {}",
|
||||||
e
|
e
|
||||||
)))
|
)));
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
@@ -267,21 +267,25 @@ impl super::types::Engine {
|
|||||||
while let Some(data) = stdout_rx.recv().await {
|
while let Some(data) = stdout_rx.recv().await {
|
||||||
stdout_buf.push_str(&data);
|
stdout_buf.push_str(&data);
|
||||||
if let Some(tx) = event_tx {
|
if let Some(tx) = event_tx {
|
||||||
let _ = tx.send(ExecutionEvent::OutputChunk {
|
let _ = tx
|
||||||
task_id: task_id.clone(),
|
.send(ExecutionEvent::OutputChunk {
|
||||||
stream: StreamType::Stdout,
|
task_id: task_id.clone(),
|
||||||
data: data.clone(),
|
stream: StreamType::Stdout,
|
||||||
}).await;
|
data: data.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while let Some(data) = stderr_rx.recv().await {
|
while let Some(data) = stderr_rx.recv().await {
|
||||||
stderr_buf.push_str(&data);
|
stderr_buf.push_str(&data);
|
||||||
if let Some(tx) = event_tx {
|
if let Some(tx) = event_tx {
|
||||||
let _ = tx.send(ExecutionEvent::OutputChunk {
|
let _ = tx
|
||||||
task_id: task_id.clone(),
|
.send(ExecutionEvent::OutputChunk {
|
||||||
stream: StreamType::Stderr,
|
task_id: task_id.clone(),
|
||||||
data: data.clone(),
|
stream: StreamType::Stderr,
|
||||||
}).await;
|
data: data.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,7 +332,7 @@ impl super::types::Engine {
|
|||||||
package: &[String],
|
package: &[String],
|
||||||
pkgman: &dyn PackageManager,
|
pkgman: &dyn PackageManager,
|
||||||
ctx: &ExecutionContext,
|
ctx: &ExecutionContext,
|
||||||
event_tx: &EventSender
|
event_tx: &EventSender,
|
||||||
) -> Result<ExecutionResult, ExecutionError> {
|
) -> Result<ExecutionResult, ExecutionError> {
|
||||||
let cmds = pkgman.install(package);
|
let cmds = pkgman.install(package);
|
||||||
let mut combined_result = ExecutionResult {
|
let mut combined_result = ExecutionResult {
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ impl PackageManager for Pacman {
|
|||||||
));
|
));
|
||||||
|
|
||||||
let mut keyring_cmd = Command::new("sh");
|
let mut keyring_cmd = Command::new("sh");
|
||||||
keyring_cmd.arg("-c").arg("pacman-key --init");
|
keyring_cmd.arg("-c").arg("pacman-key --init");
|
||||||
vec![cmd, keyring_cmd]
|
vec![cmd, keyring_cmd]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,18 +446,22 @@ mod tests {
|
|||||||
.map(|a| a.to_string_lossy().into_owned())
|
.map(|a| a.to_string_lossy().into_owned())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
assert!(recv_args
|
assert!(
|
||||||
.join(" ")
|
recv_args
|
||||||
.contains("pacman-key --recv-keys ABCDEF123456"));
|
.join(" ")
|
||||||
|
.contains("pacman-key --recv-keys ABCDEF123456")
|
||||||
|
);
|
||||||
|
|
||||||
let lsign_cmd = cmds[2].as_std();
|
let lsign_cmd = cmds[2].as_std();
|
||||||
let lsign_args: Vec<String> = lsign_cmd
|
let lsign_args: Vec<String> = lsign_cmd
|
||||||
.get_args()
|
.get_args()
|
||||||
.map(|a| a.to_string_lossy().into_owned())
|
.map(|a| a.to_string_lossy().into_owned())
|
||||||
.collect();
|
.collect();
|
||||||
assert!(lsign_args
|
assert!(
|
||||||
.join(" ")
|
lsign_args
|
||||||
.contains("pacman-key --lsign-key ABCDEF123456"));
|
.join(" ")
|
||||||
|
.contains("pacman-key --lsign-key ABCDEF123456")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -493,9 +497,11 @@ mod tests {
|
|||||||
.get_args()
|
.get_args()
|
||||||
.map(|a| a.to_string_lossy().into_owned())
|
.map(|a| a.to_string_lossy().into_owned())
|
||||||
.collect();
|
.collect();
|
||||||
assert!(install_args
|
assert!(
|
||||||
.join(" ")
|
install_args
|
||||||
.contains("pacman -Sy gnupg --needed --noconfirm"));
|
.join(" ")
|
||||||
|
.contains("pacman -Sy gnupg --needed --noconfirm")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -81,10 +81,7 @@ lazy_static::lazy_static! {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn resolve(
|
pub async fn resolve(packages: &[String], target_pm: &str) -> Vec<String> {
|
||||||
packages: &[String],
|
|
||||||
target_pm: &str,
|
|
||||||
) -> Vec<String> {
|
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
for pkg in packages {
|
for pkg in packages {
|
||||||
if let Some(mappings) = MAPPING.get(pkg.as_str()) {
|
if let Some(mappings) = MAPPING.get(pkg.as_str()) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::time::Duration;
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
pub use super::cgroups::CgroupManager;
|
pub use super::cgroups::CgroupManager;
|
||||||
pub use crate::error::{ExecutionError, EXITCODE_EXECUTION_ERROR, EXITCODE_IO_ERROR};
|
pub use crate::error::{EXITCODE_EXECUTION_ERROR, EXITCODE_IO_ERROR, ExecutionError};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ResourceUsage {
|
pub struct ResourceUsage {
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ pub trait UserPackageManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn all_managers() -> Vec<Box<dyn UserPackageManager>> {
|
pub fn all_managers() -> Vec<Box<dyn UserPackageManager>> {
|
||||||
vec![
|
vec![Box::new(custom::Custom)]
|
||||||
Box::new(custom::Custom),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select(manager: &str) -> Option<Box<dyn UserPackageManager>> {
|
pub fn select(manager: &str) -> Option<Box<dyn UserPackageManager>> {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::Engine;
|
use crate::Engine;
|
||||||
use crate::engine::types::DeltaExecutionContext;
|
use crate::engine::types::DeltaExecutionContext;
|
||||||
@@ -55,7 +55,7 @@ impl UserPackageManager for Custom {
|
|||||||
log::error!("Custom command {} failed: {:?}", index, e);
|
log::error!("Custom command {} failed: {:?}", index, e);
|
||||||
return Err(DependencyError::CustomFailed(e));
|
return Err(DependencyError::CustomFailed(e));
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ pub const EXITCODE_PRIV_DROP_FAILED: i32 = 201;
|
|||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum BakeError {
|
pub enum BakeError {
|
||||||
#[error("Failed to generate staged script {script}: {reason}")]
|
#[error("Failed to generate staged script {script}: {reason}")]
|
||||||
ScriptGenerationFailed{script: String, reason: String},
|
ScriptGenerationFailed { script: String, reason: String },
|
||||||
|
|
||||||
#[error("Template error: {0}")]
|
#[error("Template error: {0}")]
|
||||||
TemplateError(#[from] minijinja::Error),
|
TemplateError(#[from] minijinja::Error),
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ pub mod plugin;
|
|||||||
pub mod stage;
|
pub mod stage;
|
||||||
pub mod template;
|
pub mod template;
|
||||||
|
|
||||||
|
use crate::cli::Cli;
|
||||||
use crate::constant::FINALIZE_STANDALONE_PLUGIN_PATH;
|
use crate::constant::FINALIZE_STANDALONE_PLUGIN_PATH;
|
||||||
use crate::constant::STANDALONE_STATUS_DIR;
|
use crate::constant::STANDALONE_STATUS_DIR;
|
||||||
use std::path::PathBuf;
|
|
||||||
use crate::finalize::plugin::fetch::{self, FetchArgument};
|
|
||||||
use crate::cli::Cli;
|
|
||||||
use crate::engine::EventSender;
|
use crate::engine::EventSender;
|
||||||
use crate::types::buildstatus::{read_build_status, BuildStatus, StageResult};
|
use crate::finalize::plugin::fetch::{self, FetchArgument};
|
||||||
|
use crate::types::buildstatus::{BuildStatus, StageResult, read_build_status};
|
||||||
use crate::types::buildstatus::{StageInfo, StagePhase};
|
use crate::types::buildstatus::{StageInfo, StagePhase};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use std::path::PathBuf;
|
||||||
// use crate::finalize::error::PluginError;
|
// use crate::finalize::error::PluginError;
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
pub use error::FinalizeError;
|
pub use error::FinalizeError;
|
||||||
@@ -77,8 +77,7 @@ pub async fn finalize(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Read build status from prior stages
|
// Read build status from prior stages
|
||||||
let build_status = read_build_status(STANDALONE_STATUS_DIR)
|
let build_status = read_build_status(STANDALONE_STATUS_DIR).unwrap_or_default();
|
||||||
.unwrap_or_default();
|
|
||||||
let final_result = build_status.final_result();
|
let final_result = build_status.final_result();
|
||||||
|
|
||||||
// Determine if we should continue with artifacts/latehook
|
// Determine if we should continue with artifacts/latehook
|
||||||
|
|||||||
@@ -406,9 +406,11 @@ plugin:
|
|||||||
let result = config.validate();
|
let result = config.validate();
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
let errors = result.unwrap_err();
|
let errors = result.unwrap_err();
|
||||||
assert!(errors
|
assert!(
|
||||||
.iter()
|
errors
|
||||||
.any(|e| e.contains("show_file_list") && e.contains("no URL")));
|
.iter()
|
||||||
|
.any(|e| e.contains("show_file_list") && e.contains("no URL"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -444,8 +446,10 @@ plugin:
|
|||||||
let result = config.validate();
|
let result = config.validate();
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
let errors = result.unwrap_err();
|
let errors = result.unwrap_err();
|
||||||
assert!(errors
|
assert!(
|
||||||
.iter()
|
errors
|
||||||
.any(|e| e.contains("show_file_list") && e.contains("2 URL definitions")));
|
.iter()
|
||||||
|
.any(|e| e.contains("show_file_list") && e.contains("2 URL definitions"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{ExecutionError, error::{
|
use crate::{
|
||||||
EXITCODE_EXECUTION_ERROR, EXITCODE_GENERAL_ERROR, EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR,
|
ExecutionError,
|
||||||
}};
|
error::{
|
||||||
|
EXITCODE_EXECUTION_ERROR, EXITCODE_GENERAL_ERROR, EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum FinalizeError {
|
pub enum FinalizeError {
|
||||||
@@ -60,16 +63,13 @@ pub enum PluginError {
|
|||||||
GeneralError(String),
|
GeneralError(String),
|
||||||
|
|
||||||
#[error("Plugin {name} is not available: {reason}")]
|
#[error("Plugin {name} is not available: {reason}")]
|
||||||
PluginUnavailable{
|
PluginUnavailable { name: String, reason: String },
|
||||||
name: String,
|
|
||||||
reason: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[error("Plugin execution failed: {0}")]
|
#[error("Plugin execution failed: {0}")]
|
||||||
ExecutionError(#[from] ExecutionError),
|
ExecutionError(#[from] ExecutionError),
|
||||||
|
|
||||||
#[error("Failed to fetch plugin({name}:{url}): {reason}")]
|
#[error("Failed to fetch plugin({name}:{url}): {reason}")]
|
||||||
FetchError{
|
FetchError {
|
||||||
name: String,
|
name: String,
|
||||||
url: String,
|
url: String,
|
||||||
reason: String,
|
reason: String,
|
||||||
|
|||||||
@@ -10,25 +10,39 @@ pub async fn event_receiver(
|
|||||||
ExecutionEvent::TaskStarted { task_id, timestamp } => {
|
ExecutionEvent::TaskStarted { task_id, timestamp } => {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"[{}] [{}] [{}] Task started: {}",
|
"[{}] [{}] [{}] Task started: {}",
|
||||||
pipeline_name, build_id, timestamp, task_id
|
pipeline_name,
|
||||||
|
build_id,
|
||||||
|
timestamp,
|
||||||
|
task_id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ExecutionEvent::OutputChunk { task_id, stream, data } => {
|
ExecutionEvent::OutputChunk {
|
||||||
match stream {
|
task_id,
|
||||||
StreamType::Stdout => print!("[{}] [{}] [stdout] {}", pipeline_name, task_id, data),
|
stream,
|
||||||
StreamType::Stderr => eprint!("[{}] [{}] [stderr] {}", pipeline_name, task_id, data),
|
data,
|
||||||
|
} => match stream {
|
||||||
|
StreamType::Stdout => print!("[{}] [{}] [stdout] {}", pipeline_name, task_id, data),
|
||||||
|
StreamType::Stderr => {
|
||||||
|
eprint!("[{}] [{}] [stderr] {}", pipeline_name, task_id, data)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
ExecutionEvent::TaskCompleted { task_id, result } => {
|
ExecutionEvent::TaskCompleted { task_id, result } => {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"[{}] [{}] Task completed: {} (exit_code={}, duration={:?})",
|
"[{}] [{}] Task completed: {} (exit_code={}, duration={:?})",
|
||||||
pipeline_name, build_id, task_id, result.exit_code, result.duration
|
pipeline_name,
|
||||||
|
build_id,
|
||||||
|
task_id,
|
||||||
|
result.exit_code,
|
||||||
|
result.duration
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ExecutionEvent::TaskFailed { task_id, error } => {
|
ExecutionEvent::TaskFailed { task_id, error } => {
|
||||||
log::error!(
|
log::error!(
|
||||||
"[{}] [{}] Task failed: {} (error={})",
|
"[{}] [{}] Task failed: {} (error={})",
|
||||||
pipeline_name, build_id, task_id, error
|
pipeline_name,
|
||||||
|
build_id,
|
||||||
|
task_id,
|
||||||
|
error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::finalize::plugin::PluginMetadata;
|
|
||||||
use crate::finalize::plugin::PluginError;
|
use crate::finalize::plugin::PluginError;
|
||||||
|
use crate::finalize::plugin::PluginMetadata;
|
||||||
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// Code in this module PARTIALLY OR FULLY utilized AI Coding Agent.
|
// Code in this module PARTIALLY OR FULLY utilized AI Coding Agent.
|
||||||
use std::collections::HashSet;
|
|
||||||
use crate::finalize::fetch;
|
use crate::finalize::fetch;
|
||||||
use crate::finalize::{
|
use crate::finalize::{
|
||||||
PluginConfig,
|
PluginConfig,
|
||||||
plugin::{PluginError, RawPluginMap},
|
plugin::{PluginError, RawPluginMap},
|
||||||
};
|
};
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
mod checksum;
|
mod checksum;
|
||||||
@@ -93,12 +93,13 @@ pub async fn fetch_plugins(
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
log::debug!("Downloading plugin {}", name);
|
log::debug!("Downloading plugin {}", name);
|
||||||
let (source, config) = data.iter_mut().next().ok_or_else(|| {
|
let (source, config) =
|
||||||
PluginError::PluginUnavailable {
|
data.iter_mut()
|
||||||
name: name.clone(),
|
.next()
|
||||||
reason: "no source configured".to_string(),
|
.ok_or_else(|| PluginError::PluginUnavailable {
|
||||||
}
|
name: name.clone(),
|
||||||
})?;
|
reason: "no source configured".to_string(),
|
||||||
|
})?;
|
||||||
let fetch_argument = FetchArgument {
|
let fetch_argument = FetchArgument {
|
||||||
checksum: config.checksum.clone(),
|
checksum: config.checksum.clone(),
|
||||||
shallow: config.shallow,
|
shallow: config.shallow,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code in this module PARTIALLY OR FULLY utilized AI Coding Agent.
|
// Code in this module PARTIALLY OR FULLY utilized AI Coding Agent.
|
||||||
use std::path::PathBuf;
|
|
||||||
use crate::finalize::plugin::PluginError;
|
use crate::finalize::plugin::PluginError;
|
||||||
|
use std::path::PathBuf;
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -12,14 +12,18 @@ pub enum CompressionType {
|
|||||||
|
|
||||||
impl CompressionType {
|
impl CompressionType {
|
||||||
pub fn from_path(path: &std::path::Path) -> Result<Self, String> {
|
pub fn from_path(path: &std::path::Path) -> Result<Self, String> {
|
||||||
let name = path.file_name()
|
let name = path
|
||||||
|
.file_name()
|
||||||
.and_then(|n| n.to_str())
|
.and_then(|n| n.to_str())
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
|
|
||||||
if name.ends_with(".tar.gz") || name.ends_with(".tgz") {
|
if name.ends_with(".tar.gz") || name.ends_with(".tgz") {
|
||||||
Ok(CompressionType::Gzip)
|
Ok(CompressionType::Gzip)
|
||||||
} else if name.ends_with(".tar.zst") || name.ends_with(".tar.zstd") || name.ends_with(".tzst") {
|
} else if name.ends_with(".tar.zst")
|
||||||
|
|| name.ends_with(".tar.zstd")
|
||||||
|
|| name.ends_with(".tzst")
|
||||||
|
{
|
||||||
Ok(CompressionType::Zstd)
|
Ok(CompressionType::Zstd)
|
||||||
} else if name.ends_with(".tar") {
|
} else if name.ends_with(".tar") {
|
||||||
Ok(CompressionType::None)
|
Ok(CompressionType::None)
|
||||||
@@ -38,51 +42,50 @@ pub async fn extract_tar(
|
|||||||
compression: CompressionType,
|
compression: CompressionType,
|
||||||
) -> Result<(), PluginError> {
|
) -> Result<(), PluginError> {
|
||||||
if destdir.exists() {
|
if destdir.exists() {
|
||||||
tokio::fs::remove_dir_all(destdir).await.map_err(|e|
|
tokio::fs::remove_dir_all(destdir).await.map_err(|e| {
|
||||||
PluginError::GeneralError(format!("Failed to remove existing directory: {}", e))
|
PluginError::GeneralError(format!("Failed to remove existing directory: {}", e))
|
||||||
)?;
|
})?;
|
||||||
}
|
}
|
||||||
tokio::fs::create_dir_all(destdir).await.map_err(|e|
|
tokio::fs::create_dir_all(destdir)
|
||||||
PluginError::GeneralError(format!("Failed to create directory: {}", e))
|
.await
|
||||||
)?;
|
.map_err(|e| PluginError::GeneralError(format!("Failed to create directory: {}", e)))?;
|
||||||
|
|
||||||
let archive = archive.clone();
|
let archive = archive.clone();
|
||||||
let destdir = destdir.clone();
|
let destdir = destdir.clone();
|
||||||
|
|
||||||
task::spawn_blocking(move || {
|
task::spawn_blocking(move || {
|
||||||
let file = std::fs::File::open(&archive).map_err(|e|
|
let file = std::fs::File::open(&archive)
|
||||||
PluginError::GeneralError(format!("Failed to open archive: {}", e))
|
.map_err(|e| PluginError::GeneralError(format!("Failed to open archive: {}", e)))?;
|
||||||
)?;
|
|
||||||
|
|
||||||
match compression {
|
match compression {
|
||||||
CompressionType::Gzip => {
|
CompressionType::Gzip => {
|
||||||
let decoder = flate2::read::GzDecoder::new(file);
|
let decoder = flate2::read::GzDecoder::new(file);
|
||||||
let mut archive = tar::Archive::new(decoder);
|
let mut archive = tar::Archive::new(decoder);
|
||||||
archive.unpack(&destdir).map_err(|e|
|
archive.unpack(&destdir).map_err(|e| {
|
||||||
PluginError::GeneralError(format!("Failed to unpack archive: {}", e))
|
PluginError::GeneralError(format!("Failed to unpack archive: {}", e))
|
||||||
)?;
|
})?;
|
||||||
}
|
}
|
||||||
CompressionType::Zstd => {
|
CompressionType::Zstd => {
|
||||||
let decoder = zstd::stream::read::Decoder::new(file).map_err(|e|
|
let decoder = zstd::stream::read::Decoder::new(file).map_err(|e| {
|
||||||
PluginError::GeneralError(format!("Failed to create zstd decoder: {}", e))
|
PluginError::GeneralError(format!("Failed to create zstd decoder: {}", e))
|
||||||
)?;
|
})?;
|
||||||
let mut archive = tar::Archive::new(decoder);
|
let mut archive = tar::Archive::new(decoder);
|
||||||
archive.unpack(&destdir).map_err(|e|
|
archive.unpack(&destdir).map_err(|e| {
|
||||||
PluginError::GeneralError(format!("Failed to unpack archive: {}", e))
|
PluginError::GeneralError(format!("Failed to unpack archive: {}", e))
|
||||||
)?;
|
})?;
|
||||||
}
|
}
|
||||||
CompressionType::None => {
|
CompressionType::None => {
|
||||||
let mut archive = tar::Archive::new(file);
|
let mut archive = tar::Archive::new(file);
|
||||||
archive.unpack(&destdir).map_err(|e|
|
archive.unpack(&destdir).map_err(|e| {
|
||||||
PluginError::GeneralError(format!("Failed to unpack archive: {}", e))
|
PluginError::GeneralError(format!("Failed to unpack archive: {}", e))
|
||||||
)?;
|
})?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok::<(), PluginError>(())
|
Ok::<(), PluginError>(())
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| PluginError::GeneralError(format!("Task join error: {}", e)))??;
|
.map_err(|e| PluginError::GeneralError(format!("Task join error: {}", e)))??;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Code in this module PARTIALLY OR FULLY utilized AI Coding Agent.
|
// Code in this module PARTIALLY OR FULLY utilized AI Coding Agent.
|
||||||
|
use crate::finalize::plugin::PluginError;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use crate::finalize::plugin::PluginError;
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub enum GitVersion {
|
pub enum GitVersion {
|
||||||
@@ -106,24 +106,16 @@ pub async fn fetch_git_plugin(
|
|||||||
GitVersion::Mixed(tag, commit) => {
|
GitVersion::Mixed(tag, commit) => {
|
||||||
log::debug!("Fetching git plugin: {}@{}/{}", name, tag, commit);
|
log::debug!("Fetching git plugin: {}@{}/{}", name, tag, commit);
|
||||||
let shallow_flag = shallow.unwrap_or(false);
|
let shallow_flag = shallow.unwrap_or(false);
|
||||||
let result = git_clone_and_checkout(
|
let result =
|
||||||
&baseurl,
|
git_clone_and_checkout(&baseurl, &tag, &basedir.join(name), name, shallow_flag)
|
||||||
&tag,
|
.await;
|
||||||
&basedir.join(name),
|
|
||||||
name,
|
|
||||||
shallow_flag,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
if result.is_err() {
|
if result.is_err() {
|
||||||
log::warn!("Failed to clone with tag, trying commit hash. URL: {}", path);
|
log::warn!(
|
||||||
return git_clone_and_checkout(
|
"Failed to clone with tag, trying commit hash. URL: {}",
|
||||||
&baseurl,
|
path
|
||||||
&commit,
|
);
|
||||||
&basedir.join(name),
|
return git_clone_and_checkout(&baseurl, &commit, &basedir.join(name), name, false)
|
||||||
name,
|
.await;
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
// Code in this module PARTIALLY OR FULLY utilized AI Coding Agent.
|
// Code in this module PARTIALLY OR FULLY utilized AI Coding Agent.
|
||||||
use std::path::Path;
|
|
||||||
use crate::finalize::plugin::PluginError;
|
|
||||||
use super::checksum::{detect_checksum_type, verify_checksum};
|
use super::checksum::{detect_checksum_type, verify_checksum};
|
||||||
use super::extract::{extract_tar, CompressionType};
|
use super::extract::{CompressionType, extract_tar};
|
||||||
|
use crate::finalize::plugin::PluginError;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
pub async fn download_to_file(
|
pub async fn download_to_file(url: &str, dest: &Path, name: &str) -> Result<(), PluginError> {
|
||||||
url: &str,
|
|
||||||
dest: &Path,
|
|
||||||
name: &str,
|
|
||||||
) -> Result<(), PluginError> {
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(300))
|
.timeout(std::time::Duration::from_secs(300))
|
||||||
.build()
|
.build()
|
||||||
@@ -33,28 +29,42 @@ pub async fn download_to_file(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let bytes = response.bytes().await.map_err(|e| PluginError::FetchError {
|
let bytes = response
|
||||||
name: name.to_string(),
|
.bytes()
|
||||||
url: url.to_string(),
|
.await
|
||||||
reason: format!("Failed to read response body: {}", e),
|
.map_err(|e| PluginError::FetchError {
|
||||||
})?;
|
name: name.to_string(),
|
||||||
|
url: url.to_string(),
|
||||||
|
reason: format!("Failed to read response body: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
tokio::fs::write(dest, &bytes).await.map_err(|e| PluginError::FetchError {
|
tokio::fs::write(dest, &bytes)
|
||||||
name: name.to_string(),
|
.await
|
||||||
url: url.to_string(),
|
.map_err(|e| PluginError::FetchError {
|
||||||
reason: format!("Failed to write file: {}", e),
|
name: name.to_string(),
|
||||||
})?;
|
url: url.to_string(),
|
||||||
|
reason: format!("Failed to write file: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_archive_extension(url: &str) -> &str {
|
fn extract_archive_extension(url: &str) -> &str {
|
||||||
let path_part = url.split('?').next().unwrap_or(url).split('#').next().unwrap_or(url);
|
let path_part = url
|
||||||
|
.split('?')
|
||||||
|
.next()
|
||||||
|
.unwrap_or(url)
|
||||||
|
.split('#')
|
||||||
|
.next()
|
||||||
|
.unwrap_or(url);
|
||||||
let file_part = path_part.rsplit('/').next().unwrap_or("");
|
let file_part = path_part.rsplit('/').next().unwrap_or("");
|
||||||
let lower = file_part.to_lowercase();
|
let lower = file_part.to_lowercase();
|
||||||
if lower.ends_with(".tar.gz") || lower.ends_with(".tgz") {
|
if lower.ends_with(".tar.gz") || lower.ends_with(".tgz") {
|
||||||
".tar.gz"
|
".tar.gz"
|
||||||
} else if lower.ends_with(".tar.zst") || lower.ends_with(".tar.zstd") || lower.ends_with(".tzst") {
|
} else if lower.ends_with(".tar.zst")
|
||||||
|
|| lower.ends_with(".tar.zstd")
|
||||||
|
|| lower.ends_with(".tzst")
|
||||||
|
{
|
||||||
".tar.zst"
|
".tar.zst"
|
||||||
} else if lower.ends_with(".tar") {
|
} else if lower.ends_with(".tar") {
|
||||||
".tar"
|
".tar"
|
||||||
@@ -82,19 +92,17 @@ pub async fn fetch_http_plugin(
|
|||||||
PluginError::GeneralError(format!("Failed to read downloaded file: {}", e))
|
PluginError::GeneralError(format!("Failed to read downloaded file: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let checksum_type = detect_checksum_type(checksum)
|
let checksum_type =
|
||||||
.map_err(|e| PluginError::GeneralError(e))?;
|
detect_checksum_type(checksum).map_err(|e| PluginError::GeneralError(e))?;
|
||||||
|
|
||||||
if let Some(ct) = checksum_type {
|
if let Some(ct) = checksum_type {
|
||||||
verify_checksum(&bytes, checksum, ct).map_err(|e| {
|
verify_checksum(&bytes, checksum, ct).map_err(|e| PluginError::GeneralError(e))?;
|
||||||
PluginError::GeneralError(e)
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let compression = CompressionType::from_path(&temp_file)
|
let compression =
|
||||||
.map_err(|e| PluginError::GeneralError(e))?;
|
CompressionType::from_path(&temp_file).map_err(|e| PluginError::GeneralError(e))?;
|
||||||
let destdir = basedir.join(name);
|
let destdir = basedir.join(name);
|
||||||
extract_tar(&temp_file, &destdir, compression).await?;
|
extract_tar(&temp_file, &destdir, compression).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use crate::finalize::plugin::AsyncPluginFn;
|
use crate::finalize::plugin::AsyncPluginFn;
|
||||||
|
pub mod dummy;
|
||||||
mod insitenotify;
|
mod insitenotify;
|
||||||
mod mail;
|
mod mail;
|
||||||
mod satori;
|
mod satori;
|
||||||
mod webhook;
|
mod webhook;
|
||||||
pub mod dummy;
|
|
||||||
pub struct InternalPlugin {
|
pub struct InternalPlugin {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub entry: Box<dyn AsyncPluginFn>,
|
pub entry: Box<dyn AsyncPluginFn>,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use crate::finalize::plugin::PluginMetadata;
|
use crate::finalize::plugin::PluginMetadata;
|
||||||
use crate::{EventSender, ExecutionContext, finalize::{error::PluginError, plugin::AsyncPluginFn}};
|
use crate::{
|
||||||
|
EventSender, ExecutionContext,
|
||||||
|
finalize::{error::PluginError, plugin::AsyncPluginFn},
|
||||||
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
pub struct InsiteNotify;
|
pub struct InsiteNotify;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// Code in this module PARTIALLY OR FULLY utilized AI Coding Agent.
|
// Code in this module PARTIALLY OR FULLY utilized AI Coding Agent.
|
||||||
use crate::finalize::plugin::PluginMetadata;
|
|
||||||
use crate::finalize::plugin::PluginError;
|
use crate::finalize::plugin::PluginError;
|
||||||
|
use crate::finalize::plugin::PluginMetadata;
|
||||||
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use std::time::Duration;
|
use lettre::message::{MultiPart, SinglePart, header};
|
||||||
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
|
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
|
||||||
use lettre::message::{header, MultiPart, SinglePart};
|
use std::time::Duration;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
@@ -29,20 +29,29 @@ impl AsyncPluginFn for Mail {
|
|||||||
.map_err(|e| PluginError::GeneralError(format!("Invalid mail config: {}", e)))?;
|
.map_err(|e| PluginError::GeneralError(format!("Invalid mail config: {}", e)))?;
|
||||||
|
|
||||||
if let Err(errors) = config.validate() {
|
if let Err(errors) = config.validate() {
|
||||||
return Err(PluginError::GeneralError(
|
return Err(PluginError::GeneralError(format!(
|
||||||
format!("Mail config validation failed: {:?}", errors)
|
"Mail config validation failed: {:?}",
|
||||||
));
|
errors
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let template_data = prepare_template_data(ctx);
|
let template_data = prepare_template_data(ctx);
|
||||||
|
|
||||||
let (subject_template, body_template_html, body_template_text) = match config.schema {
|
let (subject_template, body_template_html, body_template_text) = match config.schema {
|
||||||
Schema::Default => {
|
Schema::Default => {
|
||||||
let subject = config.subject.as_deref()
|
let subject = config
|
||||||
|
.subject
|
||||||
|
.as_deref()
|
||||||
.unwrap_or(DefaultTemplates::SUBJECT);
|
.unwrap_or(DefaultTemplates::SUBJECT);
|
||||||
let body_html = config.body.as_deref()
|
let body_html = config
|
||||||
|
.body
|
||||||
|
.as_deref()
|
||||||
.unwrap_or(DefaultTemplates::BODY_HTML);
|
.unwrap_or(DefaultTemplates::BODY_HTML);
|
||||||
(subject.to_string(), body_html.to_string(), DefaultTemplates::BODY_TEXT.to_string())
|
(
|
||||||
|
subject.to_string(),
|
||||||
|
body_html.to_string(),
|
||||||
|
DefaultTemplates::BODY_TEXT.to_string(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Schema::Custom => {
|
Schema::Custom => {
|
||||||
let subject = config.subject.clone().unwrap();
|
let subject = config.subject.clone().unwrap();
|
||||||
@@ -55,8 +64,9 @@ impl AsyncPluginFn for Mail {
|
|||||||
.map_err(|e| PluginError::GeneralError(format!("Failed to render subject: {}", e)))?;
|
.map_err(|e| PluginError::GeneralError(format!("Failed to render subject: {}", e)))?;
|
||||||
let body_html = render_template(&body_template_html, &template_data)
|
let body_html = render_template(&body_template_html, &template_data)
|
||||||
.map_err(|e| PluginError::GeneralError(format!("Failed to render body_html: {}", e)))?;
|
.map_err(|e| PluginError::GeneralError(format!("Failed to render body_html: {}", e)))?;
|
||||||
let body_text = render_template(&body_template_text, &template_data)
|
let body_text = render_template(&body_template_text, &template_data).map_err(|e| {
|
||||||
.map_err(|e| PluginError::GeneralError(format!("Failed to render body_text(?): {}", e)))?;
|
PluginError::GeneralError(format!("Failed to render body_text(?): {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
let email = build_email(&config, subject, body_html, body_text)?;
|
let email = build_email(&config, subject, body_html, body_text)?;
|
||||||
|
|
||||||
@@ -71,43 +81,48 @@ fn build_email(
|
|||||||
body_text: String,
|
body_text: String,
|
||||||
) -> Result<Message, PluginError> {
|
) -> Result<Message, PluginError> {
|
||||||
let from_str = config.from.to_string();
|
let from_str = config.from.to_string();
|
||||||
let from: lettre::message::Mailbox = from_str.parse()
|
let from: lettre::message::Mailbox = from_str.parse().map_err(|e| {
|
||||||
.map_err(|e| PluginError::GeneralError(format!("Invalid from address '{}': {}", from_str, e)))?;
|
PluginError::GeneralError(format!("Invalid from address '{}': {}", from_str, e))
|
||||||
|
})?;
|
||||||
|
|
||||||
let to_addrs: Vec<String> = config.to.to_vec();
|
let to_addrs: Vec<String> = config.to.to_vec();
|
||||||
if to_addrs.is_empty() {
|
if to_addrs.is_empty() {
|
||||||
return Err(PluginError::GeneralError("No recipients specified".to_string()));
|
return Err(PluginError::GeneralError(
|
||||||
|
"No recipients specified".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut message_builder = Message::builder()
|
let mut message_builder = Message::builder().from(from).subject(subject);
|
||||||
.from(from)
|
|
||||||
.subject(subject);
|
|
||||||
|
|
||||||
for addr in &to_addrs {
|
for addr in &to_addrs {
|
||||||
let mailbox: lettre::message::Mailbox = addr.parse()
|
let mailbox: lettre::message::Mailbox = addr.parse().map_err(|e| {
|
||||||
.map_err(|e| PluginError::GeneralError(format!("Invalid to address '{}': {}", addr, e)))?;
|
PluginError::GeneralError(format!("Invalid to address '{}': {}", addr, e))
|
||||||
|
})?;
|
||||||
message_builder = message_builder.to(mailbox);
|
message_builder = message_builder.to(mailbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(cc) = &config.cc {
|
if let Some(cc) = &config.cc {
|
||||||
for addr in cc.to_vec() {
|
for addr in cc.to_vec() {
|
||||||
let mailbox: lettre::message::Mailbox = addr.parse()
|
let mailbox: lettre::message::Mailbox = addr.parse().map_err(|e| {
|
||||||
.map_err(|e| PluginError::GeneralError(format!("Invalid cc address '{}': {}", addr, e)))?;
|
PluginError::GeneralError(format!("Invalid cc address '{}': {}", addr, e))
|
||||||
|
})?;
|
||||||
message_builder = message_builder.cc(mailbox);
|
message_builder = message_builder.cc(mailbox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(bcc) = &config.bcc {
|
if let Some(bcc) = &config.bcc {
|
||||||
for addr in bcc.to_vec() {
|
for addr in bcc.to_vec() {
|
||||||
let mailbox: lettre::message::Mailbox = addr.parse()
|
let mailbox: lettre::message::Mailbox = addr.parse().map_err(|e| {
|
||||||
.map_err(|e| PluginError::GeneralError(format!("Invalid bcc address '{}': {}", addr, e)))?;
|
PluginError::GeneralError(format!("Invalid bcc address '{}': {}", addr, e))
|
||||||
|
})?;
|
||||||
message_builder = message_builder.bcc(mailbox);
|
message_builder = message_builder.bcc(mailbox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(reply_to) = &config.reply_to {
|
if let Some(reply_to) = &config.reply_to {
|
||||||
let mailbox: lettre::message::Mailbox = reply_to.parse()
|
let mailbox: lettre::message::Mailbox = reply_to.parse().map_err(|e| {
|
||||||
.map_err(|e| PluginError::GeneralError(format!("Invalid reply-to address '{}': {}", reply_to, e)))?;
|
PluginError::GeneralError(format!("Invalid reply-to address '{}': {}", reply_to, e))
|
||||||
|
})?;
|
||||||
message_builder = message_builder.reply_to(mailbox);
|
message_builder = message_builder.reply_to(mailbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,23 +132,20 @@ fn build_email(
|
|||||||
.singlepart(
|
.singlepart(
|
||||||
SinglePart::builder()
|
SinglePart::builder()
|
||||||
.header(header::ContentType::TEXT_PLAIN)
|
.header(header::ContentType::TEXT_PLAIN)
|
||||||
.body(body_text)
|
.body(body_text),
|
||||||
)
|
)
|
||||||
.singlepart(
|
.singlepart(
|
||||||
SinglePart::builder()
|
SinglePart::builder()
|
||||||
.header(header::ContentType::TEXT_HTML)
|
.header(header::ContentType::TEXT_HTML)
|
||||||
.body(body_html)
|
.body(body_html),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
.map_err(|e| PluginError::GeneralError(format!("Failed to build email: {}", e)))?;
|
.map_err(|e| PluginError::GeneralError(format!("Failed to build email: {}", e)))?;
|
||||||
|
|
||||||
Ok(message)
|
Ok(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_email(
|
async fn send_email(config: &MailConfig, email: Message) -> Result<(), PluginError> {
|
||||||
config: &MailConfig,
|
|
||||||
email: Message,
|
|
||||||
) -> Result<(), PluginError> {
|
|
||||||
let connect_timeout = if config.smtp.connect_timeout_ms == 0 {
|
let connect_timeout = if config.smtp.connect_timeout_ms == 0 {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@@ -162,10 +174,13 @@ async fn send_email(
|
|||||||
match config.smtp.auth.auth_type {
|
match config.smtp.auth.auth_type {
|
||||||
AuthType::Password => {
|
AuthType::Password => {
|
||||||
log::debug!("Using password auth");
|
log::debug!("Using password auth");
|
||||||
transport_builder = transport_builder.credentials((
|
transport_builder = transport_builder.credentials(
|
||||||
config.smtp.auth.username.clone(),
|
(
|
||||||
config.smtp.auth.password.clone()
|
config.smtp.auth.username.clone(),
|
||||||
).into());
|
config.smtp.auth.password.clone(),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
AuthType::None => {
|
AuthType::None => {
|
||||||
log::debug!("Using no auth");
|
log::debug!("Using no auth");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::finalize::plugin::PluginMetadata;
|
|
||||||
use crate::finalize::plugin::PluginError;
|
use crate::finalize::plugin::PluginError;
|
||||||
|
use crate::finalize::plugin::PluginMetadata;
|
||||||
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::finalize::plugin::PluginMetadata;
|
|
||||||
use crate::finalize::plugin::PluginError;
|
use crate::finalize::plugin::PluginError;
|
||||||
|
use crate::finalize::plugin::PluginMetadata;
|
||||||
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::finalize::plugin::PluginMetadata;
|
|
||||||
use crate::finalize::plugin::PluginError;
|
use crate::finalize::plugin::PluginError;
|
||||||
|
use crate::finalize::plugin::PluginMetadata;
|
||||||
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::constant::FINALIZE_SHELL_ENVPREFIX;
|
|
||||||
use crate::Engine;
|
use crate::Engine;
|
||||||
|
use crate::constant::FINALIZE_SHELL_ENVPREFIX;
|
||||||
use crate::finalize::plugin::PluginError;
|
use crate::finalize::plugin::PluginError;
|
||||||
use crate::finalize::plugin::PluginMetadata;
|
use crate::finalize::plugin::PluginMetadata;
|
||||||
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
||||||
@@ -8,7 +8,6 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
pub struct Shell;
|
pub struct Shell;
|
||||||
|
|
||||||
|
|
||||||
/// Parses a YAML value used as a mapping key into a string.
|
/// Parses a YAML value used as a mapping key into a string.
|
||||||
///
|
///
|
||||||
/// Since YAML keys can be strings, numbers, or booleans, this function
|
/// Since YAML keys can be strings, numbers, or booleans, this function
|
||||||
@@ -72,7 +71,11 @@ fn parse_key(key: serde_yaml::Value) -> String {
|
|||||||
/// let result = convert_argument("PLUGIN", yaml, false);
|
/// let result = convert_argument("PLUGIN", yaml, false);
|
||||||
/// // Yields: {"PLUGIN_HOST" => "\"docker.io\"", "PLUGIN_USER" => "\"admin\""}
|
/// // Yields: {"PLUGIN_HOST" => "\"docker.io\"", "PLUGIN_USER" => "\"admin\""}
|
||||||
/// ```
|
/// ```
|
||||||
fn convert_argument(prefix: &str, argument: serde_yaml::Value, in_sequence: bool) -> HashMap<String, String> {
|
fn convert_argument(
|
||||||
|
prefix: &str,
|
||||||
|
argument: serde_yaml::Value,
|
||||||
|
in_sequence: bool,
|
||||||
|
) -> HashMap<String, String> {
|
||||||
let mut envvars: HashMap<String, String> = HashMap::new();
|
let mut envvars: HashMap<String, String> = HashMap::new();
|
||||||
match argument {
|
match argument {
|
||||||
serde_yaml::Value::Mapping(mapping) => {
|
serde_yaml::Value::Mapping(mapping) => {
|
||||||
@@ -95,14 +98,17 @@ fn convert_argument(prefix: &str, argument: serde_yaml::Value, in_sequence: bool
|
|||||||
}
|
}
|
||||||
serde_yaml::Value::Number(value) => {
|
serde_yaml::Value::Number(value) => {
|
||||||
envvars.insert(prefix.to_string(), value.to_string());
|
envvars.insert(prefix.to_string(), value.to_string());
|
||||||
},
|
}
|
||||||
serde_yaml::Value::Tagged(tagged_value) => {
|
serde_yaml::Value::Tagged(tagged_value) => {
|
||||||
let subenvvars = convert_argument(prefix, tagged_value.value, in_sequence);
|
let subenvvars = convert_argument(prefix, tagged_value.value, in_sequence);
|
||||||
envvars.extend(subenvvars);
|
envvars.extend(subenvvars);
|
||||||
}
|
}
|
||||||
serde_yaml::Value::Sequence(seq) => {
|
serde_yaml::Value::Sequence(seq) => {
|
||||||
if in_sequence {
|
if in_sequence {
|
||||||
log::warn!("Nested sequence in sequence is not supported, skipping: {}", prefix);
|
log::warn!(
|
||||||
|
"Nested sequence in sequence is not supported, skipping: {}",
|
||||||
|
prefix
|
||||||
|
);
|
||||||
return envvars;
|
return envvars;
|
||||||
}
|
}
|
||||||
let mut array = String::new();
|
let mut array = String::new();
|
||||||
@@ -117,16 +123,19 @@ fn convert_argument(prefix: &str, argument: serde_yaml::Value, in_sequence: bool
|
|||||||
let subenvvars = convert_argument(new_prefix.as_str(), item.clone(), true);
|
let subenvvars = convert_argument(new_prefix.as_str(), item.clone(), true);
|
||||||
if item.is_mapping() {
|
if item.is_mapping() {
|
||||||
has_mapping = true;
|
has_mapping = true;
|
||||||
}
|
} else {
|
||||||
else {
|
array.push_str(
|
||||||
array.push_str(subenvvars.get(new_prefix.as_str()).map(|v| v.as_str()).unwrap_or_default());
|
subenvvars
|
||||||
|
.get(new_prefix.as_str())
|
||||||
|
.map(|v| v.as_str())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_envvars.extend(subenvvars);
|
_envvars.extend(subenvvars);
|
||||||
}
|
}
|
||||||
if has_mapping {
|
if has_mapping {
|
||||||
envvars.extend(_envvars);
|
envvars.extend(_envvars);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
array.push_str(")");
|
array.push_str(")");
|
||||||
envvars.insert(prefix.to_string(), array);
|
envvars.insert(prefix.to_string(), array);
|
||||||
}
|
}
|
||||||
@@ -148,7 +157,13 @@ impl AsyncPluginFn for Shell {
|
|||||||
let envvars = convert_argument(FINALIZE_SHELL_ENVPREFIX, argument, false);
|
let envvars = convert_argument(FINALIZE_SHELL_ENVPREFIX, argument, false);
|
||||||
let mut local_ctx = ctx.clone();
|
let mut local_ctx = ctx.clone();
|
||||||
local_ctx.env_vars.extend(envvars);
|
local_ctx.env_vars.extend(envvars);
|
||||||
engine.execute_script(&metadata.fspath.join(&metadata.entrypoint), &local_ctx, event_tx).await?;
|
engine
|
||||||
|
.execute_script(
|
||||||
|
&metadata.fspath.join(&metadata.entrypoint),
|
||||||
|
&local_ctx,
|
||||||
|
event_tx,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,27 +220,36 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_convert_argument_nested_mapping() {
|
fn test_convert_argument_nested_mapping() {
|
||||||
let yaml = serde_yaml::from_str("
|
let yaml = serde_yaml::from_str(
|
||||||
|
"
|
||||||
from:
|
from:
|
||||||
host: smtp.example.com
|
host: smtp.example.com
|
||||||
port: 587
|
port: 587
|
||||||
").unwrap();
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let result = convert_argument("CFG", yaml, false);
|
let result = convert_argument("CFG", yaml, false);
|
||||||
|
|
||||||
// Nested keys use uppercase format: CFG_FROM_HOST
|
// Nested keys use uppercase format: CFG_FROM_HOST
|
||||||
assert_eq!(result.get("CFG_FROM_HOST"), Some(&"smtp.example.com".to_string()));
|
assert_eq!(
|
||||||
|
result.get("CFG_FROM_HOST"),
|
||||||
|
Some(&"smtp.example.com".to_string())
|
||||||
|
);
|
||||||
assert_eq!(result.get("CFG_FROM_PORT"), Some(&"587".to_string()));
|
assert_eq!(result.get("CFG_FROM_PORT"), Some(&"587".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_convert_argument_different_types() {
|
fn test_convert_argument_different_types() {
|
||||||
let yaml = serde_yaml::from_str("
|
let yaml = serde_yaml::from_str(
|
||||||
|
"
|
||||||
enabled: true
|
enabled: true
|
||||||
count: 42
|
count: 42
|
||||||
ratio: 3.14
|
ratio: 3.14
|
||||||
name: test
|
name: test
|
||||||
empty: ~
|
empty: ~
|
||||||
").unwrap();
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let result = convert_argument("VAR", yaml, false);
|
let result = convert_argument("VAR", yaml, false);
|
||||||
|
|
||||||
assert_eq!(result.get("VAR_ENABLED"), Some(&"true".to_string()));
|
assert_eq!(result.get("VAR_ENABLED"), Some(&"true".to_string()));
|
||||||
@@ -240,18 +264,24 @@ empty: ~
|
|||||||
let yaml = serde_yaml::from_str("tags:\n - latest\n - v1.0\n - stable").unwrap();
|
let yaml = serde_yaml::from_str("tags:\n - latest\n - v1.0\n - stable").unwrap();
|
||||||
let result = convert_argument("CFG", yaml, false);
|
let result = convert_argument("CFG", yaml, false);
|
||||||
|
|
||||||
assert_eq!(result.get("CFG_TAGS"), Some(&"(latest v1.0 stable)".to_string()));
|
assert_eq!(
|
||||||
|
result.get("CFG_TAGS"),
|
||||||
|
Some(&"(latest v1.0 stable)".to_string())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_convert_argument_sequence_with_mapping() {
|
fn test_convert_argument_sequence_with_mapping() {
|
||||||
let yaml = serde_yaml::from_str("
|
let yaml = serde_yaml::from_str(
|
||||||
|
"
|
||||||
env:
|
env:
|
||||||
- name: KEY1
|
- name: KEY1
|
||||||
value: val1
|
value: val1
|
||||||
- name: KEY2
|
- name: KEY2
|
||||||
value: val2
|
value: val2
|
||||||
").unwrap();
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let result = convert_argument("CFG", yaml, false);
|
let result = convert_argument("CFG", yaml, false);
|
||||||
|
|
||||||
assert_eq!(result.get("CFG_ENV_0_NAME"), Some(&"KEY1".to_string()));
|
assert_eq!(result.get("CFG_ENV_0_NAME"), Some(&"KEY1".to_string()));
|
||||||
@@ -263,13 +293,16 @@ env:
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_convert_argument_nested_sequence() {
|
fn test_convert_argument_nested_sequence() {
|
||||||
let yaml = serde_yaml::from_str("
|
let yaml = serde_yaml::from_str(
|
||||||
|
"
|
||||||
matrix:
|
matrix:
|
||||||
- - a
|
- - a
|
||||||
- b
|
- b
|
||||||
- - c
|
- - c
|
||||||
- d
|
- d
|
||||||
").unwrap();
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let result = convert_argument("CFG", yaml, false);
|
let result = convert_argument("CFG", yaml, false);
|
||||||
|
|
||||||
// Nested sequences are not supported (line 50-52 logs warning and skips)
|
// Nested sequences are not supported (line 50-52 logs warning and skips)
|
||||||
@@ -297,10 +330,13 @@ matrix:
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_convert_argument_null_value() {
|
fn test_convert_argument_null_value() {
|
||||||
let yaml = serde_yaml::from_str("
|
let yaml = serde_yaml::from_str(
|
||||||
|
"
|
||||||
host: docker.io
|
host: docker.io
|
||||||
token: ~
|
token: ~
|
||||||
").unwrap();
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let result = convert_argument("CFG", yaml, false);
|
let result = convert_argument("CFG", yaml, false);
|
||||||
|
|
||||||
// Keys are uppercased: CFG_HOST
|
// Keys are uppercased: CFG_HOST
|
||||||
@@ -311,10 +347,13 @@ token: ~
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_convert_argument_number_keys() {
|
fn test_convert_argument_number_keys() {
|
||||||
let yaml = serde_yaml::from_str("
|
let yaml = serde_yaml::from_str(
|
||||||
|
"
|
||||||
0: first
|
0: first
|
||||||
1: second
|
1: second
|
||||||
").unwrap();
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let result = convert_argument("CFG", yaml, false);
|
let result = convert_argument("CFG", yaml, false);
|
||||||
|
|
||||||
// Number keys are preserved but prefixed: CFG_0, CFG_1
|
// Number keys are preserved but prefixed: CFG_0, CFG_1
|
||||||
@@ -324,7 +363,8 @@ token: ~
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_convert_argument_complex_nested() {
|
fn test_convert_argument_complex_nested() {
|
||||||
let yaml = serde_yaml::from_str("
|
let yaml = serde_yaml::from_str(
|
||||||
|
"
|
||||||
plugin:
|
plugin:
|
||||||
docker-push:
|
docker-push:
|
||||||
host: docker.io
|
host: docker.io
|
||||||
@@ -335,38 +375,67 @@ plugin:
|
|||||||
tags:
|
tags:
|
||||||
- latest
|
- latest
|
||||||
- v1.0
|
- v1.0
|
||||||
").unwrap();
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let result = convert_argument("CFG", yaml, false);
|
let result = convert_argument("CFG", yaml, false);
|
||||||
|
|
||||||
// All keys uppercased for shell environment variables
|
// All keys uppercased for shell environment variables
|
||||||
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_HOST"), Some(&"docker.io".to_string()));
|
assert_eq!(
|
||||||
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_IMAGE"), Some(&"user/app:tag".to_string()));
|
result.get("CFG_PLUGIN_DOCKER-PUSH_HOST"),
|
||||||
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_OPTIONS_FORCE"), Some(&"true".to_string()));
|
Some(&"docker.io".to_string())
|
||||||
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_OPTIONS_RETRIES"), Some(&"3".to_string()));
|
);
|
||||||
assert_eq!(result.get("CFG_PLUGIN_DOCKER-PUSH_TAGS"), Some(&"(latest v1.0)".to_string()));
|
assert_eq!(
|
||||||
|
result.get("CFG_PLUGIN_DOCKER-PUSH_IMAGE"),
|
||||||
|
Some(&"user/app:tag".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
result.get("CFG_PLUGIN_DOCKER-PUSH_OPTIONS_FORCE"),
|
||||||
|
Some(&"true".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
result.get("CFG_PLUGIN_DOCKER-PUSH_OPTIONS_RETRIES"),
|
||||||
|
Some(&"3".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
result.get("CFG_PLUGIN_DOCKER-PUSH_TAGS"),
|
||||||
|
Some(&"(latest v1.0)".to_string())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_convert_argument_special_chars_in_values() {
|
fn test_convert_argument_special_chars_in_values() {
|
||||||
let yaml = serde_yaml::from_str("
|
let yaml = serde_yaml::from_str(
|
||||||
|
"
|
||||||
url: https://example.com/path?query=value
|
url: https://example.com/path?query=value
|
||||||
path: /workspace/output.tar.gz
|
path: /workspace/output.tar.gz
|
||||||
token: 'Bearer abc123'
|
token: 'Bearer abc123'
|
||||||
").unwrap();
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let result = convert_argument("CFG", yaml, false);
|
let result = convert_argument("CFG", yaml, false);
|
||||||
|
|
||||||
// Keys uppercased: CFG_URL, CFG_PATH, CFG_TOKEN
|
// Keys uppercased: CFG_URL, CFG_PATH, CFG_TOKEN
|
||||||
assert_eq!(result.get("CFG_URL"), Some(&"https://example.com/path?query=value".to_string()));
|
assert_eq!(
|
||||||
assert_eq!(result.get("CFG_PATH"), Some(&"/workspace/output.tar.gz".to_string()));
|
result.get("CFG_URL"),
|
||||||
|
Some(&"https://example.com/path?query=value".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
result.get("CFG_PATH"),
|
||||||
|
Some(&"/workspace/output.tar.gz".to_string())
|
||||||
|
);
|
||||||
assert_eq!(result.get("CFG_TOKEN"), Some(&"Bearer abc123".to_string()));
|
assert_eq!(result.get("CFG_TOKEN"), Some(&"Bearer abc123".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_convert_argument_quoted_strings_preserved() {
|
fn test_convert_argument_quoted_strings_preserved() {
|
||||||
let yaml = serde_yaml::from_str("
|
let yaml = serde_yaml::from_str(
|
||||||
|
"
|
||||||
msg: 'Hello World'
|
msg: 'Hello World'
|
||||||
cmd: \"echo 'test'\"
|
cmd: \"echo 'test'\"
|
||||||
").unwrap();
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let result = convert_argument("CFG", yaml, false);
|
let result = convert_argument("CFG", yaml, false);
|
||||||
|
|
||||||
// Keys uppercased: CFG_MSG, CFG_CMD
|
// Keys uppercased: CFG_MSG, CFG_CMD
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ pub async fn hook(
|
|||||||
hook_cmd.argument.clone(),
|
hook_cmd.argument.clone(),
|
||||||
&plugin_ctx,
|
&plugin_ctx,
|
||||||
&event_tx,
|
&event_tx,
|
||||||
).await.map_err(|e| {
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
log::error!("Plugin hook {} failed: {:?}", index, e);
|
log::error!("Plugin hook {} failed: {:?}", index, e);
|
||||||
ExecutionError::ExecutionFailed(e.to_string())
|
ExecutionError::ExecutionFailed(e.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
// Library crate for workshop-executor
|
// Library crate for workshop-executor
|
||||||
// Selective exports: only modules used by external callers or tests
|
// Selective exports: only modules used by external callers or tests
|
||||||
|
|
||||||
|
pub mod bake;
|
||||||
|
pub mod constant;
|
||||||
pub mod daemon;
|
pub mod daemon;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod finalize;
|
pub mod finalize;
|
||||||
pub mod monitor;
|
pub mod monitor;
|
||||||
|
pub mod notify;
|
||||||
pub mod prebake;
|
pub mod prebake;
|
||||||
pub mod bake;
|
|
||||||
pub mod socket;
|
pub mod socket;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod constant;
|
|
||||||
pub mod notify;
|
|
||||||
|
|
||||||
// External crates re-exports
|
// External crates re-exports
|
||||||
pub use config;
|
pub use config;
|
||||||
|
|||||||
+22
-15
@@ -1,14 +1,13 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use env_logger;
|
|
||||||
use workshop_baker::{
|
use workshop_baker::{
|
||||||
|
bake::bake,
|
||||||
cli::{Cli, Commands, Parser},
|
cli::{Cli, Commands, Parser},
|
||||||
daemon,
|
daemon,
|
||||||
engine::{EventSender, ExecutionContext},
|
engine::{EventSender, ExecutionContext},
|
||||||
error::PrebakeError,
|
error::PrebakeError,
|
||||||
prebake::prebake,
|
|
||||||
bake::bake,
|
|
||||||
finalize::finalize,
|
finalize::finalize,
|
||||||
|
prebake::prebake,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -22,7 +21,7 @@ async fn main() {
|
|||||||
Some(Commands::Monitor { group: _ }) => {
|
Some(Commands::Monitor { group: _ }) => {
|
||||||
unimplemented!("executor-monitor should be moved to daemon instead.")
|
unimplemented!("executor-monitor should be moved to daemon instead.")
|
||||||
}
|
}
|
||||||
Some(Commands::Client { config:_ }) => {
|
Some(Commands::Client { config: _ }) => {
|
||||||
unimplemented!("Client mode not yet implemented.")
|
unimplemented!("Client mode not yet implemented.")
|
||||||
}
|
}
|
||||||
Some(Commands::Prebake { config }) => {
|
Some(Commands::Prebake { config }) => {
|
||||||
@@ -40,9 +39,11 @@ async fn main() {
|
|||||||
let event_tx: EventSender = Some(tx);
|
let event_tx: EventSender = Some(tx);
|
||||||
let pipeline_name = cli.pipeline.clone();
|
let pipeline_name = cli.pipeline.clone();
|
||||||
let build_id = cli.build_id.clone();
|
let build_id = cli.build_id.clone();
|
||||||
let receiver_handle = tokio::spawn(
|
let receiver_handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
||||||
workshop_baker::prebake::event::event_receiver(event_rx, pipeline_name, build_id)
|
event_rx,
|
||||||
);
|
pipeline_name,
|
||||||
|
build_id,
|
||||||
|
));
|
||||||
let result = prebake(config, &cli, None, &mut ctx, event_tx).await;
|
let result = prebake(config, &cli, None, &mut ctx, event_tx).await;
|
||||||
drop(ctx);
|
drop(ctx);
|
||||||
let _ = receiver_handle.await;
|
let _ = receiver_handle.await;
|
||||||
@@ -52,12 +53,14 @@ async fn main() {
|
|||||||
std::process::exit(
|
std::process::exit(
|
||||||
e.downcast_ref::<PrebakeError>()
|
e.downcast_ref::<PrebakeError>()
|
||||||
.map(|pe| pe.exit_code())
|
.map(|pe| pe.exit_code())
|
||||||
.unwrap_or(1)
|
.unwrap_or(1),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Commands::Bake {
|
Some(Commands::Bake {
|
||||||
script, bake_base, prebake
|
script,
|
||||||
|
bake_base,
|
||||||
|
prebake,
|
||||||
}) => {
|
}) => {
|
||||||
log::info!("Running in standalone mode.");
|
log::info!("Running in standalone mode.");
|
||||||
let mut ctx = ExecutionContext {
|
let mut ctx = ExecutionContext {
|
||||||
@@ -73,9 +76,11 @@ async fn main() {
|
|||||||
let event_tx: EventSender = Some(tx);
|
let event_tx: EventSender = Some(tx);
|
||||||
let pipeline_name = cli.pipeline.clone();
|
let pipeline_name = cli.pipeline.clone();
|
||||||
let build_id = cli.build_id.clone();
|
let build_id = cli.build_id.clone();
|
||||||
let receiver_handle = tokio::spawn(
|
let receiver_handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
||||||
workshop_baker::prebake::event::event_receiver(event_rx, pipeline_name, build_id)
|
event_rx,
|
||||||
);
|
pipeline_name,
|
||||||
|
build_id,
|
||||||
|
));
|
||||||
let result = bake(script, bake_base, prebake, &cli, &mut ctx, event_tx).await;
|
let result = bake(script, bake_base, prebake, &cli, &mut ctx, event_tx).await;
|
||||||
drop(ctx);
|
drop(ctx);
|
||||||
let _ = receiver_handle.await;
|
let _ = receiver_handle.await;
|
||||||
@@ -100,9 +105,11 @@ async fn main() {
|
|||||||
let event_tx: EventSender = Some(tx);
|
let event_tx: EventSender = Some(tx);
|
||||||
let pipeline_name = cli.pipeline.clone();
|
let pipeline_name = cli.pipeline.clone();
|
||||||
let build_id = cli.build_id.clone();
|
let build_id = cli.build_id.clone();
|
||||||
let receiver_handle = tokio::spawn(
|
let receiver_handle = tokio::spawn(workshop_baker::prebake::event::event_receiver(
|
||||||
workshop_baker::prebake::event::event_receiver(event_rx, pipeline_name, build_id)
|
event_rx,
|
||||||
);
|
pipeline_name,
|
||||||
|
build_id,
|
||||||
|
));
|
||||||
let result = finalize(Path::new(config), &cli, &mut ctx, event_tx).await;
|
let result = finalize(Path::new(config), &cli, &mut ctx, event_tx).await;
|
||||||
drop(ctx);
|
drop(ctx);
|
||||||
let _ = receiver_handle.await;
|
let _ = receiver_handle.await;
|
||||||
|
|||||||
@@ -190,13 +190,20 @@ fn wait_time(group_period: u32, group_watermark: u32, count: u32) -> f32 {
|
|||||||
fn notify_delay(event: &NotifyEvent) -> std::time::Duration {
|
fn notify_delay(event: &NotifyEvent) -> std::time::Duration {
|
||||||
let exponent = (event.max_lives - event.lives) as i32;
|
let exponent = (event.max_lives - event.lives) as i32;
|
||||||
if exponent < 0 {
|
if exponent < 0 {
|
||||||
log::warn!("Unexpected lives in notification: lives({}) > max_lives({})", event.lives, event.max_lives);
|
log::warn!(
|
||||||
|
"Unexpected lives in notification: lives({}) > max_lives({})",
|
||||||
|
event.lives,
|
||||||
|
event.max_lives
|
||||||
|
);
|
||||||
return std::time::Duration::from_secs_f64(NOTIFY_TEMPORARY_MAX_TIME);
|
return std::time::Duration::from_secs_f64(NOTIFY_TEMPORARY_MAX_TIME);
|
||||||
}
|
}
|
||||||
let multiplier = NOTIFY_TEMPORARY_DELAY_FACTOR.powi(exponent);
|
let multiplier = NOTIFY_TEMPORARY_DELAY_FACTOR.powi(exponent);
|
||||||
let delay = multiplier * NOTIFY_TEMPORARY_DELAY_TIME;
|
let delay = multiplier * NOTIFY_TEMPORARY_DELAY_TIME;
|
||||||
if !delay.is_finite() || delay < 0.0 {
|
if !delay.is_finite() || delay < 0.0 {
|
||||||
log::warn!("Unexpected calculated delay: {}, treating as infinite", delay);
|
log::warn!(
|
||||||
|
"Unexpected calculated delay: {}, treating as infinite",
|
||||||
|
delay
|
||||||
|
);
|
||||||
return std::time::Duration::from_secs_f64(NOTIFY_TEMPORARY_MAX_TIME);
|
return std::time::Duration::from_secs_f64(NOTIFY_TEMPORARY_MAX_TIME);
|
||||||
}
|
}
|
||||||
std::time::Duration::from_secs_f64(delay)
|
std::time::Duration::from_secs_f64(delay)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum NotifyError{
|
pub enum NotifyError {
|
||||||
#[error("Finalize config validation failed: {0:?}")]
|
#[error("Finalize config validation failed: {0:?}")]
|
||||||
FinalizeValidationError(Vec<String>),
|
FinalizeValidationError(Vec<String>),
|
||||||
|
|
||||||
|
|||||||
@@ -27,16 +27,16 @@ pub mod security;
|
|||||||
pub mod stage;
|
pub mod stage;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
ExecutionContext,
|
||||||
cli::Cli,
|
cli::Cli,
|
||||||
|
constant::STANDALONE_STATUS_DIR,
|
||||||
engine::EventSender,
|
engine::EventSender,
|
||||||
error::PrebakeError,
|
error::PrebakeError,
|
||||||
ExecutionContext,
|
|
||||||
prebake::{security::get_drop_after, stage::PrebakeStage},
|
prebake::{security::get_drop_after, stage::PrebakeStage},
|
||||||
types::buildstatus::{write_stage_status, StageInfo, StagePhase, StageResult},
|
types::buildstatus::{StageInfo, StagePhase, StageResult, write_stage_status},
|
||||||
constant::STANDALONE_STATUS_DIR,
|
|
||||||
};
|
};
|
||||||
use std::path::Path;
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
pub use config::PrebakeConfig;
|
pub use config::PrebakeConfig;
|
||||||
pub use security::prebake_drop_privilege;
|
pub use security::prebake_drop_privilege;
|
||||||
@@ -92,7 +92,7 @@ pub async fn prebake(
|
|||||||
prebake_path: &Path,
|
prebake_path: &Path,
|
||||||
cli: &Cli,
|
cli: &Cli,
|
||||||
stage: Option<PrebakeStage>,
|
stage: Option<PrebakeStage>,
|
||||||
mut ctx: &mut ExecutionContext,
|
mut ctx: &mut ExecutionContext,
|
||||||
event_tx: EventSender,
|
event_tx: EventSender,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// Initialize
|
// Initialize
|
||||||
@@ -131,7 +131,11 @@ mut ctx: &mut ExecutionContext,
|
|||||||
ctx.task_id = format!("{}-{}-bootstrap", cli.pipeline, cli.build_id);
|
ctx.task_id = format!("{}-{}-bootstrap", cli.pipeline, cli.build_id);
|
||||||
let osinfo = os_info::get();
|
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;
|
||||||
let stage_result = if result.is_ok() { StageResult::Success } else { StageResult::Failure };
|
let stage_result = if result.is_ok() {
|
||||||
|
StageResult::Success
|
||||||
|
} else {
|
||||||
|
StageResult::Failure
|
||||||
|
};
|
||||||
let _ = write_stage_status(
|
let _ = write_stage_status(
|
||||||
STANDALONE_STATUS_DIR,
|
STANDALONE_STATUS_DIR,
|
||||||
StageInfo {
|
StageInfo {
|
||||||
@@ -139,7 +143,9 @@ mut ctx: &mut ExecutionContext,
|
|||||||
phase: StagePhase::Prebake,
|
phase: StagePhase::Prebake,
|
||||||
substage: "bootstrap".to_string(),
|
substage: "bootstrap".to_string(),
|
||||||
result: stage_result,
|
result: stage_result,
|
||||||
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
duration_ms: Utc::now()
|
||||||
|
.signed_duration_since(stage_start)
|
||||||
|
.num_milliseconds() as u64,
|
||||||
started_at: stage_start,
|
started_at: stage_start,
|
||||||
finished_at: Utc::now(),
|
finished_at: Utc::now(),
|
||||||
error_message: result.as_ref().err().map(|e| e.to_string()),
|
error_message: result.as_ref().err().map(|e| e.to_string()),
|
||||||
@@ -159,7 +165,11 @@ mut ctx: &mut ExecutionContext,
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
let result = stage::hook::hook(earlyhook, &ctx, event_tx.clone()).await;
|
let result = stage::hook::hook(earlyhook, &ctx, event_tx.clone()).await;
|
||||||
let stage_result = if result.is_ok() { StageResult::Success } else { StageResult::Failure };
|
let stage_result = if result.is_ok() {
|
||||||
|
StageResult::Success
|
||||||
|
} else {
|
||||||
|
StageResult::Failure
|
||||||
|
};
|
||||||
let _ = write_stage_status(
|
let _ = write_stage_status(
|
||||||
STANDALONE_STATUS_DIR,
|
STANDALONE_STATUS_DIR,
|
||||||
StageInfo {
|
StageInfo {
|
||||||
@@ -167,7 +177,9 @@ mut ctx: &mut ExecutionContext,
|
|||||||
phase: StagePhase::Prebake,
|
phase: StagePhase::Prebake,
|
||||||
substage: "earlyhook".to_string(),
|
substage: "earlyhook".to_string(),
|
||||||
result: stage_result,
|
result: stage_result,
|
||||||
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
duration_ms: Utc::now()
|
||||||
|
.signed_duration_since(stage_start)
|
||||||
|
.num_milliseconds() as u64,
|
||||||
started_at: stage_start,
|
started_at: stage_start,
|
||||||
finished_at: Utc::now(),
|
finished_at: Utc::now(),
|
||||||
error_message: result.as_ref().err().map(|e| e.to_string()),
|
error_message: result.as_ref().err().map(|e| e.to_string()),
|
||||||
@@ -240,8 +252,13 @@ mut ctx: &mut ExecutionContext,
|
|||||||
log::info!("Running PBStage: DepsSystem");
|
log::info!("Running PBStage: DepsSystem");
|
||||||
prebake_drop_privilege(PrebakeStage::DepsSystem, dropafter, target_user, &mut ctx)?;
|
prebake_drop_privilege(PrebakeStage::DepsSystem, dropafter, target_user, &mut ctx)?;
|
||||||
ctx.task_id = format!("{}-{}-depssystem", cli.pipeline, cli.build_id);
|
ctx.task_id = format!("{}-{}-depssystem", cli.pipeline, cli.build_id);
|
||||||
let result = stage::depssystem::depssystem(&system_config, &ctx, event_tx.clone()).await;
|
let result =
|
||||||
let stage_result = if result.is_ok() { StageResult::Success } else { StageResult::Failure };
|
stage::depssystem::depssystem(&system_config, &ctx, event_tx.clone()).await;
|
||||||
|
let stage_result = if result.is_ok() {
|
||||||
|
StageResult::Success
|
||||||
|
} else {
|
||||||
|
StageResult::Failure
|
||||||
|
};
|
||||||
let _ = write_stage_status(
|
let _ = write_stage_status(
|
||||||
STANDALONE_STATUS_DIR,
|
STANDALONE_STATUS_DIR,
|
||||||
StageInfo {
|
StageInfo {
|
||||||
@@ -249,7 +266,9 @@ mut ctx: &mut ExecutionContext,
|
|||||||
phase: StagePhase::Prebake,
|
phase: StagePhase::Prebake,
|
||||||
substage: "depssystem".to_string(),
|
substage: "depssystem".to_string(),
|
||||||
result: stage_result,
|
result: stage_result,
|
||||||
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
duration_ms: Utc::now()
|
||||||
|
.signed_duration_since(stage_start)
|
||||||
|
.num_milliseconds() as u64,
|
||||||
started_at: stage_start,
|
started_at: stage_start,
|
||||||
finished_at: Utc::now(),
|
finished_at: Utc::now(),
|
||||||
error_message: result.as_ref().err().map(|e| e.to_string()),
|
error_message: result.as_ref().err().map(|e| e.to_string()),
|
||||||
@@ -266,7 +285,11 @@ mut ctx: &mut ExecutionContext,
|
|||||||
prebake_drop_privilege(PrebakeStage::DepsUser, dropafter, target_user, &mut ctx)?;
|
prebake_drop_privilege(PrebakeStage::DepsUser, dropafter, target_user, &mut ctx)?;
|
||||||
ctx.task_id = format!("{}-{}-depsuser", cli.pipeline, cli.build_id);
|
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;
|
||||||
let stage_result = if result.is_ok() { StageResult::Success } else { StageResult::Failure };
|
let stage_result = if result.is_ok() {
|
||||||
|
StageResult::Success
|
||||||
|
} else {
|
||||||
|
StageResult::Failure
|
||||||
|
};
|
||||||
let _ = write_stage_status(
|
let _ = write_stage_status(
|
||||||
STANDALONE_STATUS_DIR,
|
STANDALONE_STATUS_DIR,
|
||||||
StageInfo {
|
StageInfo {
|
||||||
@@ -274,7 +297,9 @@ mut ctx: &mut ExecutionContext,
|
|||||||
phase: StagePhase::Prebake,
|
phase: StagePhase::Prebake,
|
||||||
substage: "depsuser".to_string(),
|
substage: "depsuser".to_string(),
|
||||||
result: stage_result,
|
result: stage_result,
|
||||||
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
duration_ms: Utc::now()
|
||||||
|
.signed_duration_since(stage_start)
|
||||||
|
.num_milliseconds() as u64,
|
||||||
started_at: stage_start,
|
started_at: stage_start,
|
||||||
finished_at: Utc::now(),
|
finished_at: Utc::now(),
|
||||||
error_message: result.as_ref().err().map(|e| e.to_string()),
|
error_message: result.as_ref().err().map(|e| e.to_string()),
|
||||||
@@ -295,7 +320,11 @@ mut ctx: &mut ExecutionContext,
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
let result = stage::hook::hook(latehook, &ctx, event_tx.clone()).await;
|
let result = stage::hook::hook(latehook, &ctx, event_tx.clone()).await;
|
||||||
let stage_result = if result.is_ok() { StageResult::Success } else { StageResult::Failure };
|
let stage_result = if result.is_ok() {
|
||||||
|
StageResult::Success
|
||||||
|
} else {
|
||||||
|
StageResult::Failure
|
||||||
|
};
|
||||||
let _ = write_stage_status(
|
let _ = write_stage_status(
|
||||||
STANDALONE_STATUS_DIR,
|
STANDALONE_STATUS_DIR,
|
||||||
StageInfo {
|
StageInfo {
|
||||||
@@ -303,7 +332,9 @@ mut ctx: &mut ExecutionContext,
|
|||||||
phase: StagePhase::Prebake,
|
phase: StagePhase::Prebake,
|
||||||
substage: "latehook".to_string(),
|
substage: "latehook".to_string(),
|
||||||
result: stage_result,
|
result: stage_result,
|
||||||
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
duration_ms: Utc::now()
|
||||||
|
.signed_duration_since(stage_start)
|
||||||
|
.num_milliseconds() as u64,
|
||||||
started_at: stage_start,
|
started_at: stage_start,
|
||||||
finished_at: Utc::now(),
|
finished_at: Utc::now(),
|
||||||
error_message: result.as_ref().err().map(|e| e.to_string()),
|
error_message: result.as_ref().err().map(|e| e.to_string()),
|
||||||
|
|||||||
@@ -10,25 +10,39 @@ pub async fn event_receiver(
|
|||||||
ExecutionEvent::TaskStarted { task_id, timestamp } => {
|
ExecutionEvent::TaskStarted { task_id, timestamp } => {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"[{}] [{}] [{}] Task started: {}",
|
"[{}] [{}] [{}] Task started: {}",
|
||||||
pipeline_name, build_id, timestamp, task_id
|
pipeline_name,
|
||||||
|
build_id,
|
||||||
|
timestamp,
|
||||||
|
task_id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ExecutionEvent::OutputChunk { task_id, stream, data } => {
|
ExecutionEvent::OutputChunk {
|
||||||
match stream {
|
task_id,
|
||||||
StreamType::Stdout => print!("[{}] [{}] [stdout] {}", pipeline_name, task_id, data),
|
stream,
|
||||||
StreamType::Stderr => eprint!("[{}] [{}] [stderr] {}", pipeline_name, task_id, data),
|
data,
|
||||||
|
} => match stream {
|
||||||
|
StreamType::Stdout => print!("[{}] [{}] [stdout] {}", pipeline_name, task_id, data),
|
||||||
|
StreamType::Stderr => {
|
||||||
|
eprint!("[{}] [{}] [stderr] {}", pipeline_name, task_id, data)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
ExecutionEvent::TaskCompleted { task_id, result } => {
|
ExecutionEvent::TaskCompleted { task_id, result } => {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"[{}] [{}] Task completed: {} (exit_code={}, duration={:?})",
|
"[{}] [{}] Task completed: {} (exit_code={}, duration={:?})",
|
||||||
pipeline_name, build_id, task_id, result.exit_code, result.duration
|
pipeline_name,
|
||||||
|
build_id,
|
||||||
|
task_id,
|
||||||
|
result.exit_code,
|
||||||
|
result.duration
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ExecutionEvent::TaskFailed { task_id, error } => {
|
ExecutionEvent::TaskFailed { task_id, error } => {
|
||||||
log::error!(
|
log::error!(
|
||||||
"[{}] [{}] Task failed: {} (error={})",
|
"[{}] [{}] Task failed: {} (error={})",
|
||||||
pipeline_name, build_id, task_id, error
|
pipeline_name,
|
||||||
|
build_id,
|
||||||
|
task_id,
|
||||||
|
error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
pub mod bootstrap;
|
pub mod bootstrap;
|
||||||
pub mod environment;
|
|
||||||
pub mod hook;
|
|
||||||
pub mod depssystem;
|
pub mod depssystem;
|
||||||
pub mod depsuser;
|
pub mod depsuser;
|
||||||
|
pub mod environment;
|
||||||
|
pub mod hook;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||||
pub enum PrebakeStage {
|
pub enum PrebakeStage {
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ pub async fn bootstrap(
|
|||||||
log::debug!("Generated bootstrap script:\n{}", script);
|
log::debug!("Generated bootstrap script:\n{}", script);
|
||||||
let mut file = File::create(&bootstrap_path)?;
|
let mut file = File::create(&bootstrap_path)?;
|
||||||
file.write_all(script.as_bytes())?;
|
file.write_all(script.as_bytes())?;
|
||||||
std::fs::set_permissions(&bootstrap_path, std::os::unix::fs::PermissionsExt::from_mode(0o755))?;
|
std::fs::set_permissions(
|
||||||
|
&bootstrap_path,
|
||||||
|
std::os::unix::fs::PermissionsExt::from_mode(0o755),
|
||||||
|
)?;
|
||||||
} else {
|
} else {
|
||||||
// Requires PIP(Pipeline Implicit Parameters) to be implemented
|
// Requires PIP(Pipeline Implicit Parameters) to be implemented
|
||||||
unimplemented!("Not supporting custom bootstrap.sh at this time");
|
unimplemented!("Not supporting custom bootstrap.sh at this time");
|
||||||
|
|||||||
@@ -41,13 +41,12 @@ pub async fn depssystem(
|
|||||||
log::info!("Changing mirror to: {}", mirror);
|
log::info!("Changing mirror to: {}", mirror);
|
||||||
let cmds = package_manager.change_mirror(mirror, Some(&osinfo));
|
let cmds = package_manager.change_mirror(mirror, Some(&osinfo));
|
||||||
for cmd in cmds {
|
for cmd in cmds {
|
||||||
log::debug!("Executing: {:?}", &cmd);
|
log::debug!("Executing: {:?}", &cmd);
|
||||||
engine
|
engine.execute(cmd, ctx, &event_tx).await.map_err(|e| {
|
||||||
.execute(cmd, ctx, &event_tx)
|
DependencyError::StageFailed {
|
||||||
.await
|
stage: "MirrorChange",
|
||||||
.map_err(|e| DependencyError::StageFailed {
|
source: e,
|
||||||
stage: "MirrorChange",
|
}
|
||||||
source: e,
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ pub async fn collect_upm_sysdeps(
|
|||||||
let package_manager = match upm::select(name) {
|
let package_manager = match upm::select(name) {
|
||||||
Some(pm) => pm,
|
Some(pm) => pm,
|
||||||
None => {
|
None => {
|
||||||
log::warn!("Unknown user package manager: {}, skipping sysdep collection", name);
|
log::warn!(
|
||||||
|
"Unknown user package manager: {}, skipping sysdep collection",
|
||||||
|
name
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::prebake::config::{PrebakeConfig};
|
use crate::prebake::config::PrebakeConfig;
|
||||||
use crate::types::builderconfig::BuilderType;
|
use crate::types::builderconfig::BuilderType;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
//! Each hook command supports custom working directories, environment variables,
|
//! Each hook command supports custom working directories, environment variables,
|
||||||
//! and timeout configurations.
|
//! and timeout configurations.
|
||||||
|
|
||||||
use crate::engine::types::DeltaExecutionContext;
|
|
||||||
use crate::ExecutionContext;
|
use crate::ExecutionContext;
|
||||||
|
use crate::engine::types::DeltaExecutionContext;
|
||||||
use crate::engine::{Engine, EventSender, ExecutionError};
|
use crate::engine::{Engine, EventSender, ExecutionError};
|
||||||
use crate::types::command::CustomCommand;
|
use crate::types::command::CustomCommand;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pub mod architecture;
|
pub mod architecture;
|
||||||
pub mod buildstatus;
|
|
||||||
pub mod builderconfig;
|
pub mod builderconfig;
|
||||||
|
pub mod buildstatus;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod command;
|
pub mod command;
|
||||||
pub mod memsize;
|
pub mod memsize;
|
||||||
|
|||||||
@@ -153,11 +153,11 @@ pub fn parse_mem_size(s: &str) -> Result<MemSize, String> {
|
|||||||
"k" | "K" => 1000,
|
"k" | "K" => 1000,
|
||||||
"m" | "M" => 1000 * 1000,
|
"m" | "M" => 1000 * 1000,
|
||||||
"g" | "G" => 1000 * 1000 * 1000,
|
"g" | "G" => 1000 * 1000 * 1000,
|
||||||
"t" | "T" => 1000 as u64 * 1000 * 1000 * 1000,
|
"t" | "T" => 1000_u64 * 1000 * 1000 * 1000,
|
||||||
"ki" | "kI" | "Ki" | "KI" => 1024,
|
"ki" | "kI" | "Ki" | "KI" => 1024,
|
||||||
"mi" | "mI" | "Mi" | "MI" => 1024 * 1024,
|
"mi" | "mI" | "Mi" | "MI" => 1024 * 1024,
|
||||||
"gi" | "gI" | "Gi" | "GI" => 1024 * 1024 * 1024,
|
"gi" | "gI" | "Gi" | "GI" => 1024 * 1024 * 1024,
|
||||||
"ti" | "tI" | "Ti" | "TI" => 1024 as u64 * 1024 * 1024 * 1024,
|
"ti" | "tI" | "Ti" | "TI" => 1024_u64 * 1024 * 1024 * 1024,
|
||||||
_ => return Err(format!("unknown unit: {}", unit_raw)),
|
_ => return Err(format!("unknown unit: {}", unit_raw)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use serde::{de::Visitor, Deserializer};
|
use serde::{Deserializer, de::Visitor};
|
||||||
|
|
||||||
pub const INT_MAX_MS: u64 = i32::MAX as u64;
|
pub const INT_MAX_MS: u64 = i32::MAX as u64;
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use workshop_baker::engine::{ExecutionContext, ExecutionEvent};
|
use workshop_baker::engine::{ExecutionContext, ExecutionEvent};
|
||||||
|
use workshop_baker::finalize::plugin::PluginMap;
|
||||||
use workshop_baker::finalize::stage::hook::hook;
|
use workshop_baker::finalize::stage::hook::hook;
|
||||||
use workshop_baker::finalize::plugin::{PluginMap};
|
|
||||||
use workshop_baker::types::command::CustomCommand;
|
use workshop_baker::types::command::CustomCommand;
|
||||||
|
|
||||||
fn create_test_context() -> ExecutionContext {
|
fn create_test_context() -> ExecutionContext {
|
||||||
@@ -26,7 +26,10 @@ fn create_test_context() -> ExecutionContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_test_event_channel() -> (Option<tokio::sync::mpsc::Sender<ExecutionEvent>>, tokio::sync::mpsc::Receiver<ExecutionEvent>) {
|
fn create_test_event_channel() -> (
|
||||||
|
Option<tokio::sync::mpsc::Sender<ExecutionEvent>>,
|
||||||
|
tokio::sync::mpsc::Receiver<ExecutionEvent>,
|
||||||
|
) {
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
||||||
(Some(tx), rx)
|
(Some(tx), rx)
|
||||||
}
|
}
|
||||||
@@ -146,7 +149,10 @@ async fn test_multiple_hooks_execute_sequentially() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let result = hook(Some(&cmds), &ctx, event_tx, &plugins).await;
|
let result = hook(Some(&cmds), &ctx, event_tx, &plugins).await;
|
||||||
assert!(result.is_ok(), "Multiple hooks should execute sequentially and succeed");
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Multiple hooks should execute sequentially and succeed"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use workshop_baker::cli::Cli;
|
||||||
use workshop_baker::engine::{ExecutionContext, ExecutionEvent};
|
use workshop_baker::engine::{ExecutionContext, ExecutionEvent};
|
||||||
use workshop_baker::finalize::finalize;
|
use workshop_baker::finalize::finalize;
|
||||||
use workshop_baker::cli::Cli;
|
|
||||||
|
|
||||||
fn create_test_context() -> ExecutionContext {
|
fn create_test_context() -> ExecutionContext {
|
||||||
ExecutionContext {
|
ExecutionContext {
|
||||||
@@ -35,7 +35,10 @@ fn create_test_cli() -> Cli {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_test_event_channel() -> (Option<tokio::sync::mpsc::Sender<ExecutionEvent>>, tokio::sync::mpsc::Receiver<ExecutionEvent>) {
|
fn create_test_event_channel() -> (
|
||||||
|
Option<tokio::sync::mpsc::Sender<ExecutionEvent>>,
|
||||||
|
tokio::sync::mpsc::Receiver<ExecutionEvent>,
|
||||||
|
) {
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
||||||
(Some(tx), rx)
|
(Some(tx), rx)
|
||||||
}
|
}
|
||||||
@@ -76,7 +79,11 @@ hooks:
|
|||||||
let _ = consumer_handle.await;
|
let _ = consumer_handle.await;
|
||||||
std::fs::remove_file(&finalize_path).ok();
|
std::fs::remove_file(&finalize_path).ok();
|
||||||
|
|
||||||
assert!(result.is_ok(), "Full finalize pipeline should succeed: {:?}", result);
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Full finalize pipeline should succeed: {:?}",
|
||||||
|
result
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -94,16 +101,19 @@ version: "0.0.1"
|
|||||||
let mut ctx = create_test_context();
|
let mut ctx = create_test_context();
|
||||||
let (event_tx, mut event_rx) = create_test_event_channel();
|
let (event_tx, mut event_rx) = create_test_event_channel();
|
||||||
|
|
||||||
let consumer_handle = tokio::spawn(async move {
|
let consumer_handle =
|
||||||
while let Some(_event) = event_rx.recv().await {}
|
tokio::spawn(async move { while let Some(_event) = event_rx.recv().await {} });
|
||||||
});
|
|
||||||
|
|
||||||
let result = finalize(&finalize_path, &cli, &mut ctx, event_tx).await;
|
let result = finalize(&finalize_path, &cli, &mut ctx, event_tx).await;
|
||||||
|
|
||||||
let _ = consumer_handle.await;
|
let _ = consumer_handle.await;
|
||||||
std::fs::remove_file(&finalize_path).ok();
|
std::fs::remove_file(&finalize_path).ok();
|
||||||
|
|
||||||
assert!(result.is_ok(), "Finalize with empty hooks should succeed: {:?}", result);
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Finalize with empty hooks should succeed: {:?}",
|
||||||
|
result
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -130,9 +140,8 @@ hooks:
|
|||||||
let mut ctx = create_test_context();
|
let mut ctx = create_test_context();
|
||||||
let (event_tx, mut event_rx) = create_test_event_channel();
|
let (event_tx, mut event_rx) = create_test_event_channel();
|
||||||
|
|
||||||
let consumer_handle = tokio::spawn(async move {
|
let consumer_handle =
|
||||||
while let Some(_event) = event_rx.recv().await {}
|
tokio::spawn(async move { while let Some(_event) = event_rx.recv().await {} });
|
||||||
});
|
|
||||||
|
|
||||||
let result = finalize(&finalize_path, &cli, &mut ctx, event_tx).await;
|
let result = finalize(&finalize_path, &cli, &mut ctx, event_tx).await;
|
||||||
|
|
||||||
@@ -141,5 +150,8 @@ hooks:
|
|||||||
|
|
||||||
// With the notification subsystem, prior stage failures cause finalize to
|
// With the notification subsystem, prior stage failures cause finalize to
|
||||||
// send a failure notification and then return Ok (skipping artifacts/latehook)
|
// send a failure notification and then return Ok (skipping artifacts/latehook)
|
||||||
assert!(result.is_ok(), "Finalize with failing early hook should return Ok after sending failure notification");
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Finalize with failing early hook should return Ok after sending failure notification"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user