feat(baker/finalize): Implement plugin manifest support and typed

metadata

- Rename env vars WS_* to HBW_* for consistency
- Add PluginMetadata struct and PluginType enum for typed plugin info
- Support manifest.yml files for external plugin discovery
- Add shell/rhai/dylib plugin stubs with proper type signatures
- Implement shallow argument merge for plugin runtime arguments
This commit is contained in:
Catty Steve
2026-04-18 23:58:33 +08:00
parent a8ed935f6a
commit a1da093176
12 changed files with 254 additions and 23 deletions
+1 -1
View File
@@ -3,4 +3,4 @@ pub const BOOTSTRAP_SCRIPT_PATH: &str = "/bootstrap.sh";
pub const BAKE_SCRIPT_PATH: &str = "bake.sh";
pub const PIPELINE_SKIP_ERRORCODE: u8 = 233;
pub const TRIVIAL_SCRIPT_NAME: &str = "BAKE_TRIVIAL";
pub const BAKER_INTERNAL_CONFIG_PREFIX: &str = "_bakerinternal_";
pub const FINALIZE_PLUGIN_MANIFEST: &str = "manifest.yml";
+3 -3
View File
@@ -116,9 +116,9 @@ impl super::types::Engine {
command
.current_dir(&ctx.working_dir)
.envs(&ctx.env_vars)
.env("WS_TASKID", &ctx.task_id)
.env("WS_PIPELINE", &ctx.pipeline_name)
.env("WS_USERNAME", &ctx.username)
.env("HBW_TASKID", &ctx.task_id)
.env("HBW_PIPELINE", &ctx.pipeline_name)
.env("HBW_USERNAME", &ctx.username)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
+141 -14
View File
@@ -1,12 +1,19 @@
use crate::constant::FINALIZE_PLUGIN_MANIFEST;
use crate::finalize::RawPluginMap;
use crate::finalize::plugin::metadata::{PluginMetadata, PluginType};
use crate::{EventSender, ExecutionContext, finalize::error::PluginError};
use async_trait::async_trait;
use std::collections::HashMap;
use std::path::PathBuf;
mod dylib;
mod internal;
mod metadata;
mod rhai;
mod shell;
pub struct FinalizePlugin {
metadata: serde_yaml::Value,
metadata: PluginMetadata,
argument: serde_yaml::Value,
pub entry: Box<dyn AsyncPluginFn>,
}
@@ -14,13 +21,13 @@ pub struct FinalizePlugin {
impl FinalizePlugin {
pub fn new(entry: Box<dyn AsyncPluginFn>) -> Self {
Self {
metadata: serde_yaml::Value::Null,
argument: serde_yaml::Value::Null,
metadata: PluginMetadata::default(),
argument: serde_yaml::Value::Mapping(Default::default()),
entry,
}
}
pub fn with_metadata(mut self, metadata: serde_yaml::Value) -> Self {
pub fn with_metadata(mut self, metadata: PluginMetadata) -> Self {
self.metadata = metadata;
self
}
@@ -30,9 +37,25 @@ impl FinalizePlugin {
self
}
pub async fn call(&self, ctx: &ExecutionContext, event_tx: &EventSender) -> PluginResult {
pub async fn call(
&self,
runtime_argument: serde_yaml::Value,
ctx: &ExecutionContext,
event_tx: &EventSender,
) -> PluginResult {
// shallow merge argument
let _argument = match (&self.argument, runtime_argument) {
(serde_yaml::Value::Mapping(base), serde_yaml::Value::Mapping(runtime)) => {
let mut merged = base.clone();
for (k, v) in runtime {
merged.insert(k.clone(), v.clone());
}
serde_yaml::Value::Mapping(merged)
}
_ => self.argument.clone(), // If not both are mappings, just use the original argument
};
self.entry
.call(self.metadata.clone(), self.argument.clone(), ctx, event_tx)
.call(&self.metadata, _argument, ctx, event_tx)
.await
}
}
@@ -44,13 +67,116 @@ type PluginResult = Result<(), PluginError>;
pub trait AsyncPluginFn: Send + Sync {
async fn call(
&self,
metadata: serde_yaml::Value,
metadata: &PluginMetadata,
argument: serde_yaml::Value,
ctx: &ExecutionContext,
event_tx: &EventSender,
) -> PluginResult;
}
/// Reads and parses a plugin's manifest file.
///
/// # Arguments
///
/// * `path` - Optional filesystem path to the plugin directory
/// * `name` - Name of the plugin (used for error reporting)
///
/// # Returns
///
/// Returns parsed [`PluginMetadata`] on success, or a [`PluginError`] if the manifest
/// cannot be read or parsed.
fn read_manifest(path: Option<PathBuf>, name: &str) -> Result<PluginMetadata, PluginError> {
let path = path.ok_or_else(|| PluginError::PluginUnavailable {
name: name.to_string(),
reason: "Plugin not present in filesystem.".to_string(),
})?;
let manifest_content =
std::fs::read_to_string(path.join(FINALIZE_PLUGIN_MANIFEST)).map_err(|e| {
PluginError::PluginUnavailable {
name: name.to_string(),
reason: format!("Failed to read plugin manifest: {}", e),
}
})?;
let mut metadata: PluginMetadata = serde_yaml::from_str(manifest_content.as_str()).map_err(|e| PluginError::PluginUnavailable {
name: name.to_string(),
reason: format!("Failed to parse plugin manifest: {}", e),
})?;
metadata.fspath = path;
Ok(metadata)
}
/// Determines the plugin type based on the manifest type or file extension.
///
/// The function first returns `original` if it's not [`PluginType::Unknown`].
/// Otherwise, it infers the type from the entrypoint file extension:
/// - `.sh` / `.bash` → [`PluginType::Shell`]
/// - `.rhai` → [`PluginType::Rhai`]
/// - `.so` / `.dll` / `.dylib` → [`PluginType::Dylib`]
///
/// # Arguments
///
/// * `original` - Plugin type as specified in the manifest
/// * `entrypoint` - Path or filename of the plugin entrypoint
///
/// # Returns
///
/// The inferred [`PluginType`].
fn get_plugin_type(original: PluginType, entrypoint: &str) -> PluginType {
if original != PluginType::Unknown {
return original;
}
let _suffix = entrypoint.rsplit('.').next().unwrap_or("");
match _suffix {
"sh" | "bash" => PluginType::Shell,
"rhai" => PluginType::Rhai,
"so" | "dll" | "dylib" => PluginType::Dylib,
_ => PluginType::Unknown,
}
}
/// Creates a plugin entry function instance based on the plugin type.
///
/// # Arguments
///
/// * `plugin_type` - The type of plugin to instantiate
/// * `name` - Name of the plugin (used for error reporting)
///
/// # Returns
///
/// Returns a boxed [`AsyncPluginFn`] on success, or a [`PluginError`] if the plugin type
/// is [`PluginType::Unknown`] and cannot be inferred.
fn get_entry(plugin_type: PluginType, name: &str) -> Result<Box<dyn AsyncPluginFn>, PluginError> {
match plugin_type {
PluginType::Shell => Ok(Box::new(shell::Shell)),
PluginType::Rhai => Ok(Box::new(rhai::Rhai)),
PluginType::Dylib => Ok(Box::new(dylib::Dylib)),
PluginType::Unknown => {
log::error!(
"Plugin {} has unknown plugin type and cannot be inferred from entrypoint.",
name
);
return Err(PluginError::PluginUnavailable {
name: name.to_string(),
reason: "Unknown plugin type and cannot be inferred from entrypoint".to_string(),
});
}
}
}
/// Registers all available plugins (internal and external) into a plugin map.
///
/// Internal plugins are registered first, followed by external plugins loaded from
/// the provided [`RawPluginMap`]. Plugins that are unavailable (e.g., missing runtime)
/// are logged and skipped.
///
/// # Arguments
///
/// * `external_plugins` - Map of external plugin configurations
///
/// # Returns
///
/// Returns a [`PluginMap`] containing all registered plugins, or a [`PluginError`] if
/// a plugin manifest cannot be read or parsed.
pub fn register(external_plugins: RawPluginMap) -> Result<PluginMap, PluginError> {
let mut plugins: PluginMap = HashMap::new();
@@ -72,12 +198,13 @@ pub fn register(external_plugins: RawPluginMap) -> Result<PluginMap, PluginError
log::warn!("Plugin {} is not available: {}", name, reason);
continue;
}
let metadata = read_manifest(config.runtime.fspath, &name)?;
let plugin_type = get_plugin_type(metadata.plugin_type.clone(), &metadata.entrypoint);
let entry = get_entry(plugin_type, &name)?;
plugins.insert(
name,
// TODO: Implement function for Shell & Rhai
// Use dummy function to please compiler
FinalizePlugin::new(Box::new(internal::dummy::Dummy))
.with_metadata(config.runtime.manifest.unwrap_or_default())
FinalizePlugin::new(entry)
.with_metadata(metadata)
.with_argument(config.config),
);
}
@@ -85,12 +212,13 @@ pub fn register(external_plugins: RawPluginMap) -> Result<PluginMap, PluginError
Ok(plugins)
}
/// Calls a registered plugin by name, passing the execution context and event sender.
/// Calls a registered plugin by name, passing the configuration, execution context and event sender.
///
/// # Arguments
///
/// * `plugins` - Map of registered plugin names to their [`FinalizePlugin`] instances
/// * `name` - Name of the plugin to invoke
/// * `config` - Configuration value passed to the plugin
/// * `ctx` - Execution context for the plugin
/// * `event_tx` - Event sender for plugin lifecycle events
///
@@ -108,6 +236,5 @@ pub async fn call_named_plugin(
let plugin = plugins
.get(&name)
.ok_or_else(|| PluginError::UnknownPlugin(name))?;
// TODO: Shallow argument merge
plugin.with_argument(config).call(ctx, event_tx).await
plugin.call(config, ctx, event_tx).await
}
@@ -0,0 +1,19 @@
use crate::finalize::plugin::PluginMetadata;
use crate::finalize::plugin::PluginError;
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
use async_trait::async_trait;
pub struct Dylib;
#[async_trait]
impl AsyncPluginFn for Dylib {
async fn call(
&self,
metadata: &PluginMetadata,
argument: serde_yaml::Value,
ctx: &ExecutionContext,
event_tx: &EventSender,
) -> Result<(), PluginError> {
unimplemented!("Dylib plugin is not implemented yet");
}
}
@@ -1,4 +1,5 @@
use crate::finalize::plugin::PluginError;
use crate::finalize::plugin::PluginMetadata;
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
use async_trait::async_trait;
@@ -8,7 +9,7 @@ pub struct Dummy;
impl AsyncPluginFn for Dummy {
async fn call(
&self,
_metadata: serde_yaml::Value,
_metadata: &PluginMetadata,
_argument: serde_yaml::Value,
_ctx: &ExecutionContext,
_event_tx: &EventSender,
@@ -1,3 +1,4 @@
use crate::finalize::plugin::PluginMetadata;
use crate::{EventSender, ExecutionContext, finalize::{error::PluginError, plugin::AsyncPluginFn}};
use async_trait::async_trait;
@@ -7,7 +8,7 @@ pub struct InsiteNotify;
impl AsyncPluginFn for InsiteNotify {
async fn call(
&self,
_metadata: serde_yaml::Value,
_metadata: &PluginMetadata,
_argument: serde_yaml::Value,
_ctx: &ExecutionContext,
_event_tx: &EventSender,
@@ -1,3 +1,4 @@
use crate::finalize::plugin::PluginMetadata;
use crate::finalize::plugin::PluginError;
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
use async_trait::async_trait;
@@ -8,7 +9,7 @@ pub struct Mail;
impl AsyncPluginFn for Mail {
async fn call(
&self,
_metadata: serde_yaml::Value,
_metadata: &PluginMetadata,
_argument: serde_yaml::Value,
_ctx: &ExecutionContext,
_event_tx: &EventSender,
@@ -1,3 +1,4 @@
use crate::finalize::plugin::PluginMetadata;
use crate::finalize::plugin::PluginError;
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
use async_trait::async_trait;
@@ -8,7 +9,7 @@ pub struct Satori;
impl AsyncPluginFn for Satori {
async fn call(
&self,
_metadata: serde_yaml::Value,
_metadata: &PluginMetadata,
_argument: serde_yaml::Value,
_ctx: &ExecutionContext,
_event_tx: &EventSender,
@@ -1,3 +1,4 @@
use crate::finalize::plugin::PluginMetadata;
use crate::finalize::plugin::PluginError;
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
use async_trait::async_trait;
@@ -8,7 +9,7 @@ pub struct Webhook;
impl AsyncPluginFn for Webhook {
async fn call(
&self,
_metadata: serde_yaml::Value,
_metadata: &PluginMetadata,
_argument: serde_yaml::Value,
_ctx: &ExecutionContext,
_event_tx: &EventSender,
@@ -0,0 +1,41 @@
use std::path::PathBuf;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct PluginMetadata {
pub name: String,
pub version: String,
pub entrypoint: String,
#[serde(default)]
pub plugin_type: PluginType,
#[serde(flatten)]
pub value: serde_yaml::Value,
#[serde(skip)]
pub fspath: PathBuf,
}
impl Default for PluginMetadata {
fn default() -> Self {
Self {
name: String::new(),
version: String::new(),
entrypoint: String::new(),
plugin_type: PluginType::Unknown,
value: serde_yaml::Value::Null,
fspath: PathBuf::new(),
}
}
}
#[derive(Deserialize, Default, PartialEq, Eq, Clone)]
pub enum PluginType {
#[default]
Unknown,
Shell,
Rhai,
Dylib,
}
@@ -0,0 +1,19 @@
use crate::finalize::plugin::PluginMetadata;
use crate::finalize::plugin::PluginError;
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
use async_trait::async_trait;
pub struct Rhai;
#[async_trait]
impl AsyncPluginFn for Rhai {
async fn call(
&self,
_metadata: &PluginMetadata,
_argument: serde_yaml::Value,
_ctx: &ExecutionContext,
_event_tx: &EventSender,
) -> Result<(), PluginError> {
todo!("Rhai plugin is not implemented yet");
}
}
@@ -0,0 +1,20 @@
use crate::finalize::plugin::PluginMetadata;
use crate::finalize::plugin::PluginError;
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
use async_trait::async_trait;
pub struct Shell;
#[async_trait]
impl AsyncPluginFn for Shell {
async fn call(
&self,
metadata: &PluginMetadata,
argument: serde_yaml::Value,
ctx: &ExecutionContext,
event_tx: &EventSender,
) -> Result<(), PluginError> {
todo!();
}
}